diff --git a/README.md b/README.md
index c8b55dd..e71f591 100644
--- a/README.md
+++ b/README.md
@@ -9,10 +9,6 @@
> Your Self-Hosted Knowledge Base
-🌐 **[Visit the official website](https://www.notediscovery.com)**
-
-🚀 **[Try the Live Demo](https://gamosoft-notediscovery-demo.hf.space)** — *Contents reset daily, for demonstration purposes only*
-
## What is NoteDiscovery?
NoteDiscovery is a **lightweight, self-hosted note-taking application** that puts you in complete control of your knowledge base. Write, organize, and discover your notes with a beautiful, modern interface—all running on your own server.
@@ -27,13 +23,20 @@ NoteDiscovery is a **lightweight, self-hosted note-taking application** that put
- **Teams** looking for a self-hosted alternative to commercial apps
- **Anyone** who values simplicity, speed, and ownership
-
-## 💖 Thanks for using NoteDiscovery!
-If this project has been useful to you, consider supporting its development, it truly makes a difference!
+---
-
+
+
+
+
+
+
+
+
+
+---
## ✨ Why NoteDiscovery?
diff --git a/docs/demo-button.svg b/docs/demo-button.svg
new file mode 100644
index 0000000..5880cb8
--- /dev/null
+++ b/docs/demo-button.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+ 🚀 Try Live Demo
+
diff --git a/docs/index.html b/docs/index.html
index fc519be..615e5ad 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -695,6 +695,12 @@
Get Started on GitHub →
✨ Demo resets daily · For demonstration purposes only
+
+
☁️ Or deploy your own instance in one click:
+
+
+
+
@@ -972,6 +978,14 @@
+
+
🔍
+
+ Quick Switcher
+ Jump to any note with a shortcut
+
+
+
🔐
@@ -1007,6 +1021,12 @@
View on GitHub →
✨ Demo resets daily · For demonstration purposes only
+
+
☁️ Don't want to self-host? Deploy on PikaPods:
+
+
+
+
diff --git a/docs/website-button.svg b/docs/website-button.svg
new file mode 100644
index 0000000..ded8c18
--- /dev/null
+++ b/docs/website-button.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+ 🌐 Official Website
+
diff --git a/documentation/FEATURES.md b/documentation/FEATURES.md
index 0c52d73..2a3e674 100644
--- a/documentation/FEATURES.md
+++ b/documentation/FEATURES.md
@@ -286,6 +286,7 @@ date: {{date}}
| Windows/Linux | Mac | Action |
|---------------|-----|--------|
+| `Ctrl+Alt+P` | `Cmd+Option+P` | Quick Switcher (jump to any note) |
| `Ctrl+S` | `Cmd+S` | Save note |
| `Ctrl+Alt+N` | `Cmd+Option+N` | New note |
| `Ctrl+Alt+F` | `Cmd+Option+F` | New folder |
@@ -304,9 +305,11 @@ date: {{date}}
|---------------|-----|--------|--------|
| `Ctrl+B` | `Cmd+B` | Bold | `**text**` |
| `Ctrl+I` | `Cmd+I` | Italic | `*text*` |
-| `Ctrl+K` | `Cmd+K` | Insert link | `[text](url)` |
+| `Ctrl+K` | `Cmd+K` | Insert link (in editor) | `[text](url)` |
| `Ctrl+Alt+T` | `Cmd+Option+T` | Insert table | 3x3 table placeholder |
+> **Tip:** Use `Ctrl+Alt+P` to quickly jump to any note from anywhere in the app.
+
## 🧘 Zen Mode
Full immersive distraction-free writing experience:
diff --git a/frontend/app.js b/frontend/app.js
index 592fd2f..4820983 100644
--- a/frontend/app.js
+++ b/frontend/app.js
@@ -262,6 +262,12 @@ function noteApp() {
shareLinkCopied: false,
_sharedNotePaths: new Set(), // O(1) lookup for shared note indicators
+ // Quick Switcher state (Ctrl+Alt+P)
+ showQuickSwitcher: false,
+ quickSwitcherQuery: '',
+ quickSwitcherIndex: 0,
+ quickSwitcherResults: [],
+
// Homepage state
selectedHomepageFolder: '',
_homepageCache: {
@@ -569,6 +575,13 @@ function noteApp() {
this.saveNote();
}
+ // Ctrl/Cmd + Alt + P for Quick Switcher
+ if ((e.ctrlKey || e.metaKey) && e.altKey && e.code === 'KeyP') {
+ e.preventDefault();
+ this.openQuickSwitcher();
+ return;
+ }
+
// Ctrl/Cmd + Alt/Option + N for new note
if ((e.ctrlKey || e.metaKey) && e.altKey && e.code === 'KeyN') {
e.preventDefault();
@@ -1985,23 +1998,54 @@ function noteApp() {
event.preventDefault();
this.dropTarget = 'editor';
- // Update cursor position as user drags over text
+ // Focus the textarea
const textarea = event.target;
- const textLength = textarea.value.length;
+ if (textarea.tagName !== 'TEXTAREA') return;
- // Calculate approximate cursor position based on mouse position
- // This gives a rough idea of where the link will be inserted
textarea.focus();
- // Try to set cursor at click position (works in most browsers)
- if (textarea.setSelectionRange && document.caretPositionFromPoint) {
- const pos = document.caretPositionFromPoint(event.clientX, event.clientY);
- if (pos && pos.offsetNode === textarea) {
- textarea.setSelectionRange(pos.offset, pos.offset);
- }
+ // Calculate cursor position from mouse coordinates
+ const pos = this.getTextareaCursorFromPoint(textarea, event.clientX, event.clientY);
+ if (pos >= 0) {
+ textarea.setSelectionRange(pos, pos);
}
},
+ // Calculate textarea cursor position from mouse coordinates
+ getTextareaCursorFromPoint(textarea, x, y) {
+ const rect = textarea.getBoundingClientRect();
+ const style = window.getComputedStyle(textarea);
+ const lineHeight = parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.2;
+ const paddingTop = parseFloat(style.paddingTop) || 0;
+ const paddingLeft = parseFloat(style.paddingLeft) || 0;
+
+ // Calculate which line we're on
+ const relativeY = y - rect.top - paddingTop + textarea.scrollTop;
+ const lineIndex = Math.max(0, Math.floor(relativeY / lineHeight));
+
+ // Split content into lines
+ const lines = textarea.value.split('\n');
+
+ // Find the character position at the start of this line
+ let charPos = 0;
+ for (let i = 0; i < Math.min(lineIndex, lines.length); i++) {
+ charPos += lines[i].length + 1; // +1 for newline
+ }
+
+ // If we're beyond the last line, position at end
+ if (lineIndex >= lines.length) {
+ return textarea.value.length;
+ }
+
+ // Approximate character position within the line based on X coordinate
+ const relativeX = x - rect.left - paddingLeft;
+ const charWidth = parseFloat(style.fontSize) * 0.6; // Approximate for monospace
+ const charInLine = Math.max(0, Math.floor(relativeX / charWidth));
+ const lineLength = lines[lineIndex]?.length || 0;
+
+ return charPos + Math.min(charInLine, lineLength);
+ },
+
// Handle dragenter on editor
onEditorDragEnter(event) {
if (!this.draggedItem) return;
@@ -2047,9 +2091,11 @@ function noteApp() {
link = `[${noteName}](${encodedPath})`;
}
- // Insert at cursor position
+ // Insert at drop position
const textarea = event.target;
- const cursorPos = textarea.selectionStart || 0;
+ // Recalculate position from drop coordinates for accuracy
+ let cursorPos = this.getTextareaCursorFromPoint(textarea, event.clientX, event.clientY);
+ if (cursorPos < 0) cursorPos = textarea.selectionStart || 0;
const textBefore = this.noteContent.substring(0, cursorPos);
const textAfter = this.noteContent.substring(cursorPos);
@@ -2087,14 +2133,16 @@ function noteApp() {
}
const textarea = event.target;
- const cursorPos = textarea.selectionStart || 0;
+ // Calculate cursor position from drop coordinates
+ let cursorPos = this.getTextareaCursorFromPoint(textarea, event.clientX, event.clientY);
+ if (cursorPos < 0) cursorPos = textarea.selectionStart || 0;
// Upload each image
for (const file of imageFiles) {
try {
const imagePath = await this.uploadImage(file, this.currentNote);
if (imagePath) {
- this.insertImageMarkdown(imagePath, file.name, cursorPos);
+ await this.insertImageMarkdown(imagePath, file.name, cursorPos);
}
} catch (error) {
ErrorHandler.handle(`upload image ${file.name}`, error);
@@ -2128,7 +2176,7 @@ function noteApp() {
// Insert image markdown at cursor position using wiki-style syntax
// This ensures image links don't break when notes are moved
- insertImageMarkdown(imagePath, altText, cursorPos) {
+ async insertImageMarkdown(imagePath, altText, cursorPos) {
// Extract just the filename from the path (e.g., "folder/_attachments/image.png" -> "image.png")
const filename = imagePath.split('/').pop();
@@ -2142,6 +2190,9 @@ function noteApp() {
? `![[${filename}|${altWithoutExt}]]`
: `![[${filename}]]`;
+ // Reload notes FIRST to update image lookup maps before preview renders
+ await this.loadNotes();
+
const textBefore = this.noteContent.substring(0, cursorPos);
const textAfter = this.noteContent.substring(cursorPos);
@@ -2149,9 +2200,6 @@ function noteApp() {
// Trigger autosave
this.autoSave();
-
- // Reload notes to show the new image in sidebar and update lookup maps
- this.loadNotes();
},
// Handle paste event for clipboard images
@@ -2180,7 +2228,7 @@ function noteApp() {
const imagePath = await this.uploadImage(file, this.currentNote);
if (imagePath) {
- this.insertImageMarkdown(imagePath, filename, cursorPos);
+ await this.insertImageMarkdown(imagePath, filename, cursorPos);
}
} catch (error) {
ErrorHandler.handle('paste image', error);
@@ -5040,6 +5088,87 @@ function noteApp() {
return this._sharedNotePaths.has(notePath);
},
+ // ============================================
+ // Quick Switcher (Ctrl+Alt+P)
+ // ============================================
+
+ openQuickSwitcher() {
+ this.showQuickSwitcher = true;
+ this.quickSwitcherQuery = '';
+ this.quickSwitcherIndex = 0;
+ // Populate initial results
+ this.quickSwitcherResults = (this.allNotes || []).slice(0, 10);
+ // Focus the input after the modal renders
+ this.$nextTick(() => {
+ const input = document.getElementById('quickSwitcherInput');
+ if (input) input.focus();
+ });
+ },
+
+ closeQuickSwitcher() {
+ this.showQuickSwitcher = false;
+ this.quickSwitcherQuery = '';
+ this.quickSwitcherIndex = 0;
+ },
+
+ // Filter notes for quick switcher based on query
+ filterQuickSwitcher(query) {
+ // Only include actual notes, not images
+ const notes = (this.notes || []).filter(n => n.type === 'note');
+ if (!query || !query.trim()) {
+ // Show recent notes when no query
+ return notes.slice(0, 10);
+ }
+ const q = query.toLowerCase();
+ return notes
+ .filter(n =>
+ n.name.toLowerCase().includes(q) ||
+ n.path.toLowerCase().includes(q)
+ )
+ .slice(0, 10);
+ },
+
+ // Handle keyboard navigation in quick switcher
+ handleQuickSwitcherKeydown(e) {
+ const results = this.quickSwitcherResults;
+
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ this.quickSwitcherIndex = Math.min(this.quickSwitcherIndex + 1, results.length - 1);
+ this.scrollQuickSwitcherIntoView();
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ this.quickSwitcherIndex = Math.max(this.quickSwitcherIndex - 1, 0);
+ this.scrollQuickSwitcherIntoView();
+ } else if (e.key === 'Enter') {
+ e.preventDefault();
+ const note = results[this.quickSwitcherIndex];
+ if (note) {
+ this.loadNote(note.path);
+ this.closeQuickSwitcher();
+ }
+ } else if (e.key === 'Escape') {
+ e.preventDefault();
+ this.closeQuickSwitcher();
+ }
+ },
+
+ // Scroll selected item into view in quick switcher
+ scrollQuickSwitcherIntoView() {
+ this.$nextTick(() => {
+ const items = document.querySelectorAll('[data-quick-switcher-item]');
+ if (items[this.quickSwitcherIndex]) {
+ items[this.quickSwitcherIndex].scrollIntoView({ block: 'nearest' });
+ }
+ });
+ },
+
+ // Select note from quick switcher by click
+ selectQuickSwitcherNote(note) {
+ this.loadNote(note.path);
+ this.closeQuickSwitcher();
+ },
+
// Open share modal and fetch current share status
async openShareModal() {
if (!this.currentNote) return;
diff --git a/frontend/index.html b/frontend/index.html
index d30edf3..f5b7509 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1795,7 +1795,7 @@
-
@@ -2784,6 +2793,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ↑↓
+ ↵
+ esc
+
+
+
+