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! +---

- Buy Me a Coffee at ko-fi.com + Official Website +    + Try Live Demo

+

+ Run on PikaPods +    + Buy Me a Coffee at ko-fi.com +

+ +--- ## ✨ 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:

+ + Run on PikaPods + +
@@ -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:

+ + Run 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 +
+
+
+