Merge pull request #151 from gamosoft/features/quick-search
Features/quick search
This commit is contained in:
commit
1948bfb1b8
19
README.md
19
README.md
|
|
@ -9,10 +9,6 @@
|
||||||
|
|
||||||
> Your Self-Hosted Knowledge Base
|
> 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?
|
## 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.
|
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
|
- **Teams** looking for a self-hosted alternative to commercial apps
|
||||||
- **Anyone** who values simplicity, speed, and ownership
|
- **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!
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://ko-fi.com/gamosoft" target="_blank" rel="noopener noreferrer"><img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="Buy Me a Coffee at ko-fi.com"></a>
|
<a href="https://www.notediscovery.com"><img src="docs/website-button.svg" alt="Official Website"></a>
|
||||||
|
|
||||||
|
<a href="https://gamosoft-notediscovery-demo.hf.space"><img src="docs/demo-button.svg" alt="Try Live Demo"></a>
|
||||||
</p>
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.pikapods.com/pods?run=notediscovery"><img src="https://www.pikapods.com/static/run-button.svg" alt="Run on PikaPods"></a>
|
||||||
|
|
||||||
|
<a href="https://ko-fi.com/gamosoft"><img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="Buy Me a Coffee at ko-fi.com"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ✨ Why NoteDiscovery?
|
## ✨ Why NoteDiscovery?
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="180" height="40" viewBox="0 0 180 40">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="demoGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" style="stop-color:#10b981"/>
|
||||||
|
<stop offset="100%" style="stop-color:#059669"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="180" height="40" rx="8" fill="url(#demoGrad)"/>
|
||||||
|
<text x="90" y="25" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="600" fill="white" text-anchor="middle">🚀 Try Live Demo</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 540 B |
|
|
@ -695,6 +695,12 @@
|
||||||
<a href="https://github.com/gamosoft/NoteDiscovery" class="cta-button" target="_blank" rel="noopener">Get Started on GitHub →</a>
|
<a href="https://github.com/gamosoft/NoteDiscovery" class="cta-button" target="_blank" rel="noopener">Get Started on GitHub →</a>
|
||||||
</div>
|
</div>
|
||||||
<p class="demo-notice">✨ Demo resets daily · For demonstration purposes only</p>
|
<p class="demo-notice">✨ Demo resets daily · For demonstration purposes only</p>
|
||||||
|
<div class="pikapods-deploy" style="margin-top: 1.5rem;">
|
||||||
|
<p style="margin-bottom: 0.75rem; color: rgba(255,255,255,0.8); font-size: 0.95rem;">☁️ Or deploy your own instance in one click:</p>
|
||||||
|
<a href="https://www.pikapods.com/pods?run=notediscovery" target="_blank" rel="noopener">
|
||||||
|
<img src="https://www.pikapods.com/static/run-button.svg" alt="Run on PikaPods" style="height: 40px; transition: transform 0.2s;" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -972,6 +978,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="benefit-item">
|
||||||
|
<span class="emoji">🔍</span>
|
||||||
|
<div class="text">
|
||||||
|
<strong>Quick Switcher</strong>
|
||||||
|
Jump to any note with a shortcut
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="benefit-item">
|
<div class="benefit-item">
|
||||||
<span class="emoji">🔐</span>
|
<span class="emoji">🔐</span>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
|
|
@ -1007,6 +1021,12 @@
|
||||||
<a href="https://github.com/gamosoft/NoteDiscovery" class="cta-button" target="_blank" rel="noopener">View on GitHub →</a>
|
<a href="https://github.com/gamosoft/NoteDiscovery" class="cta-button" target="_blank" rel="noopener">View on GitHub →</a>
|
||||||
</div>
|
</div>
|
||||||
<p class="demo-notice">✨ Demo resets daily · For demonstration purposes only</p>
|
<p class="demo-notice">✨ Demo resets daily · For demonstration purposes only</p>
|
||||||
|
<div class="pikapods-deploy" style="margin-top: 1.5rem;">
|
||||||
|
<p style="margin-bottom: 0.75rem; color: rgba(255,255,255,0.8); font-size: 0.95rem;">☁️ Don't want to self-host? Deploy on PikaPods:</p>
|
||||||
|
<a href="https://www.pikapods.com/pods?run=notediscovery" target="_blank" rel="noopener">
|
||||||
|
<img src="https://www.pikapods.com/static/run-button.svg" alt="Run on PikaPods" style="height: 40px; transition: transform 0.2s;" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="180" height="40" viewBox="0 0 180 40">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="webGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" style="stop-color:#6366f1"/>
|
||||||
|
<stop offset="100%" style="stop-color:#8b5cf6"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="180" height="40" rx="8" fill="url(#webGrad)"/>
|
||||||
|
<text x="90" y="25" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="600" fill="white" text-anchor="middle">🌐 Official Website</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 541 B |
|
|
@ -286,6 +286,7 @@ date: {{date}}
|
||||||
|
|
||||||
| Windows/Linux | Mac | Action |
|
| Windows/Linux | Mac | Action |
|
||||||
|---------------|-----|--------|
|
|---------------|-----|--------|
|
||||||
|
| `Ctrl+Alt+P` | `Cmd+Option+P` | Quick Switcher (jump to any note) |
|
||||||
| `Ctrl+S` | `Cmd+S` | Save note |
|
| `Ctrl+S` | `Cmd+S` | Save note |
|
||||||
| `Ctrl+Alt+N` | `Cmd+Option+N` | New note |
|
| `Ctrl+Alt+N` | `Cmd+Option+N` | New note |
|
||||||
| `Ctrl+Alt+F` | `Cmd+Option+F` | New folder |
|
| `Ctrl+Alt+F` | `Cmd+Option+F` | New folder |
|
||||||
|
|
@ -304,9 +305,11 @@ date: {{date}}
|
||||||
|---------------|-----|--------|--------|
|
|---------------|-----|--------|--------|
|
||||||
| `Ctrl+B` | `Cmd+B` | Bold | `**text**` |
|
| `Ctrl+B` | `Cmd+B` | Bold | `**text**` |
|
||||||
| `Ctrl+I` | `Cmd+I` | Italic | `*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 |
|
| `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
|
## 🧘 Zen Mode
|
||||||
|
|
||||||
Full immersive distraction-free writing experience:
|
Full immersive distraction-free writing experience:
|
||||||
|
|
|
||||||
165
frontend/app.js
165
frontend/app.js
|
|
@ -262,6 +262,12 @@ function noteApp() {
|
||||||
shareLinkCopied: false,
|
shareLinkCopied: false,
|
||||||
_sharedNotePaths: new Set(), // O(1) lookup for shared note indicators
|
_sharedNotePaths: new Set(), // O(1) lookup for shared note indicators
|
||||||
|
|
||||||
|
// Quick Switcher state (Ctrl+Alt+P)
|
||||||
|
showQuickSwitcher: false,
|
||||||
|
quickSwitcherQuery: '',
|
||||||
|
quickSwitcherIndex: 0,
|
||||||
|
quickSwitcherResults: [],
|
||||||
|
|
||||||
// Homepage state
|
// Homepage state
|
||||||
selectedHomepageFolder: '',
|
selectedHomepageFolder: '',
|
||||||
_homepageCache: {
|
_homepageCache: {
|
||||||
|
|
@ -569,6 +575,13 @@ function noteApp() {
|
||||||
this.saveNote();
|
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
|
// Ctrl/Cmd + Alt/Option + N for new note
|
||||||
if ((e.ctrlKey || e.metaKey) && e.altKey && e.code === 'KeyN') {
|
if ((e.ctrlKey || e.metaKey) && e.altKey && e.code === 'KeyN') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -1985,21 +1998,52 @@ function noteApp() {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.dropTarget = 'editor';
|
this.dropTarget = 'editor';
|
||||||
|
|
||||||
// Update cursor position as user drags over text
|
// Focus the textarea
|
||||||
const textarea = event.target;
|
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();
|
textarea.focus();
|
||||||
|
|
||||||
// Try to set cursor at click position (works in most browsers)
|
// Calculate cursor position from mouse coordinates
|
||||||
if (textarea.setSelectionRange && document.caretPositionFromPoint) {
|
const pos = this.getTextareaCursorFromPoint(textarea, event.clientX, event.clientY);
|
||||||
const pos = document.caretPositionFromPoint(event.clientX, event.clientY);
|
if (pos >= 0) {
|
||||||
if (pos && pos.offsetNode === textarea) {
|
textarea.setSelectionRange(pos, pos);
|
||||||
textarea.setSelectionRange(pos.offset, pos.offset);
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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
|
// Handle dragenter on editor
|
||||||
|
|
@ -2047,9 +2091,11 @@ function noteApp() {
|
||||||
link = `[${noteName}](${encodedPath})`;
|
link = `[${noteName}](${encodedPath})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert at cursor position
|
// Insert at drop position
|
||||||
const textarea = event.target;
|
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 textBefore = this.noteContent.substring(0, cursorPos);
|
||||||
const textAfter = this.noteContent.substring(cursorPos);
|
const textAfter = this.noteContent.substring(cursorPos);
|
||||||
|
|
||||||
|
|
@ -2087,14 +2133,16 @@ function noteApp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const textarea = event.target;
|
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
|
// Upload each image
|
||||||
for (const file of imageFiles) {
|
for (const file of imageFiles) {
|
||||||
try {
|
try {
|
||||||
const imagePath = await this.uploadImage(file, this.currentNote);
|
const imagePath = await this.uploadImage(file, this.currentNote);
|
||||||
if (imagePath) {
|
if (imagePath) {
|
||||||
this.insertImageMarkdown(imagePath, file.name, cursorPos);
|
await this.insertImageMarkdown(imagePath, file.name, cursorPos);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorHandler.handle(`upload image ${file.name}`, error);
|
ErrorHandler.handle(`upload image ${file.name}`, error);
|
||||||
|
|
@ -2128,7 +2176,7 @@ function noteApp() {
|
||||||
|
|
||||||
// Insert image markdown at cursor position using wiki-style syntax
|
// Insert image markdown at cursor position using wiki-style syntax
|
||||||
// This ensures image links don't break when notes are moved
|
// 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")
|
// Extract just the filename from the path (e.g., "folder/_attachments/image.png" -> "image.png")
|
||||||
const filename = imagePath.split('/').pop();
|
const filename = imagePath.split('/').pop();
|
||||||
|
|
||||||
|
|
@ -2142,6 +2190,9 @@ function noteApp() {
|
||||||
? `![[${filename}|${altWithoutExt}]]`
|
? `![[${filename}|${altWithoutExt}]]`
|
||||||
: `![[${filename}]]`;
|
: `![[${filename}]]`;
|
||||||
|
|
||||||
|
// Reload notes FIRST to update image lookup maps before preview renders
|
||||||
|
await this.loadNotes();
|
||||||
|
|
||||||
const textBefore = this.noteContent.substring(0, cursorPos);
|
const textBefore = this.noteContent.substring(0, cursorPos);
|
||||||
const textAfter = this.noteContent.substring(cursorPos);
|
const textAfter = this.noteContent.substring(cursorPos);
|
||||||
|
|
||||||
|
|
@ -2149,9 +2200,6 @@ function noteApp() {
|
||||||
|
|
||||||
// Trigger autosave
|
// Trigger autosave
|
||||||
this.autoSave();
|
this.autoSave();
|
||||||
|
|
||||||
// Reload notes to show the new image in sidebar and update lookup maps
|
|
||||||
this.loadNotes();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Handle paste event for clipboard images
|
// Handle paste event for clipboard images
|
||||||
|
|
@ -2180,7 +2228,7 @@ function noteApp() {
|
||||||
|
|
||||||
const imagePath = await this.uploadImage(file, this.currentNote);
|
const imagePath = await this.uploadImage(file, this.currentNote);
|
||||||
if (imagePath) {
|
if (imagePath) {
|
||||||
this.insertImageMarkdown(imagePath, filename, cursorPos);
|
await this.insertImageMarkdown(imagePath, filename, cursorPos);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorHandler.handle('paste image', error);
|
ErrorHandler.handle('paste image', error);
|
||||||
|
|
@ -5040,6 +5088,87 @@ function noteApp() {
|
||||||
return this._sharedNotePaths.has(notePath);
|
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
|
// Open share modal and fetch current share status
|
||||||
async openShareModal() {
|
async openShareModal() {
|
||||||
if (!this.currentNote) return;
|
if (!this.currentNote) return;
|
||||||
|
|
|
||||||
|
|
@ -1795,7 +1795,7 @@
|
||||||
</a>
|
</a>
|
||||||
<!-- Ko-fi support link (demo mode only) -->
|
<!-- Ko-fi support link (demo mode only) -->
|
||||||
<template x-if="demoMode">
|
<template x-if="demoMode">
|
||||||
<div class="mt-3">
|
<div class="mt-3 flex flex-col gap-2 items-center">
|
||||||
<a
|
<a
|
||||||
href="https://ko-fi.com/gamosoft"
|
href="https://ko-fi.com/gamosoft"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|
@ -1806,6 +1806,15 @@
|
||||||
<span>☕</span>
|
<span>☕</span>
|
||||||
<span x-text="t('support.enjoying_demo')"></span>
|
<span x-text="t('support.enjoying_demo')"></span>
|
||||||
</a>
|
</a>
|
||||||
|
<!-- PikaPods deploy button -->
|
||||||
|
<a
|
||||||
|
href="https://www.pikapods.com/pods?run=notediscovery"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="inline-flex items-center transition-all hover:scale-105"
|
||||||
|
>
|
||||||
|
<img src="https://www.pikapods.com/static/run-button.svg" alt="Run on PikaPods" style="height: 28px;">
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2784,6 +2793,72 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Switcher Modal (Ctrl+K) -->
|
||||||
|
<div x-show="showQuickSwitcher"
|
||||||
|
@click.self="closeQuickSwitcher()"
|
||||||
|
@keydown.escape.window="showQuickSwitcher && closeQuickSwitcher()"
|
||||||
|
x-effect="if (showQuickSwitcher) { quickSwitcherResults = filterQuickSwitcher(quickSwitcherQuery); quickSwitcherIndex = 0; }"
|
||||||
|
class="fixed inset-0 flex items-start justify-center pt-24"
|
||||||
|
style="background-color: rgba(0, 0, 0, 0.5); z-index: 1200;"
|
||||||
|
x-cloak>
|
||||||
|
<div class="rounded-lg shadow-2xl w-full max-w-lg mx-4 overflow-hidden"
|
||||||
|
style="background-color: var(--bg-primary); border: 1px solid var(--border-primary);"
|
||||||
|
@click.stop>
|
||||||
|
|
||||||
|
<!-- Search Input -->
|
||||||
|
<div class="p-3 border-b" style="border-color: var(--border-primary);">
|
||||||
|
<input id="quickSwitcherInput"
|
||||||
|
type="text"
|
||||||
|
x-model="quickSwitcherQuery"
|
||||||
|
@keydown="handleQuickSwitcherKeydown($event)"
|
||||||
|
:placeholder="t('quick_switcher.placeholder')"
|
||||||
|
class="w-full px-3 py-2 rounded text-base outline-none"
|
||||||
|
style="background-color: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-primary);"
|
||||||
|
autocomplete="off">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Results List -->
|
||||||
|
<div id="quickSwitcherResults" class="max-h-80 overflow-y-auto">
|
||||||
|
<template x-if="quickSwitcherResults.length === 0">
|
||||||
|
<div class="p-4 text-center text-sm" style="color: var(--text-secondary);">
|
||||||
|
<span x-text="t('quick_switcher.no_results')"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template x-for="(note, index) in quickSwitcherResults" :key="note.path">
|
||||||
|
<div @click="selectQuickSwitcherNote(note)"
|
||||||
|
data-quick-switcher-item
|
||||||
|
:class="index === quickSwitcherIndex ? 'quick-switcher-selected' : ''"
|
||||||
|
class="px-4 py-3 cursor-pointer flex items-center gap-3 transition-colors"
|
||||||
|
:style="index === quickSwitcherIndex ? 'background-color: var(--bg-tertiary);' : ''"
|
||||||
|
@mouseenter="quickSwitcherIndex = index">
|
||||||
|
<!-- Note icon -->
|
||||||
|
<svg class="w-4 h-4 flex-shrink-0" style="color: var(--text-secondary);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="font-medium truncate" style="color: var(--text-primary);" x-text="note.name"></div>
|
||||||
|
<div class="text-xs truncate" style="color: var(--text-secondary);" x-show="note.folder" x-text="note.folder"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Recent notes hint (only when no query typed) -->
|
||||||
|
<div x-show="!quickSwitcherQuery.trim() && quickSwitcherResults.length > 0"
|
||||||
|
class="px-4 py-2 text-xs text-center italic"
|
||||||
|
style="color: var(--text-secondary);">
|
||||||
|
<span x-text="t('quick_switcher.recent_hint')"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer hint -->
|
||||||
|
<div class="px-4 py-2 text-xs border-t flex gap-4" style="border-color: var(--border-primary); color: var(--text-secondary); background-color: var(--bg-secondary);">
|
||||||
|
<span>↑↓ <span x-text="t('quick_switcher.navigate')"></span></span>
|
||||||
|
<span>↵ <span x-text="t('quick_switcher.open')"></span></span>
|
||||||
|
<span>esc <span x-text="t('quick_switcher.close')"></span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Share Modal -->
|
<!-- Share Modal -->
|
||||||
<div x-show="showShareModal"
|
<div x-show="showShareModal"
|
||||||
@click.self="showShareModal = false"
|
@click.self="showShareModal = false"
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,15 @@
|
||||||
"error_revoking": "Fehler beim Widerrufen des Freigabelinks: {{error}}"
|
"error_revoking": "Fehler beim Widerrufen des Freigabelinks: {{error}}"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"quick_switcher": {
|
||||||
|
"placeholder": "Notizen suchen...",
|
||||||
|
"no_results": "Keine Ergebnisse",
|
||||||
|
"recent_hint": "Zeigt aktuelle Notizen. Tippen zum Suchen.",
|
||||||
|
"navigate": "navigieren",
|
||||||
|
"open": "öffnen",
|
||||||
|
"close": "schließen"
|
||||||
|
},
|
||||||
|
|
||||||
"tags": {
|
"tags": {
|
||||||
"title": "Tags",
|
"title": "Tags",
|
||||||
"no_tags": "Keine Tags gefunden",
|
"no_tags": "Keine Tags gefunden",
|
||||||
|
|
@ -263,7 +272,8 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"support": {
|
"support": {
|
||||||
"enjoying_demo": "Unterstütze dieses Projekt"
|
"enjoying_demo": "Unterstütze dieses Projekt",
|
||||||
|
"deploy_own": "Eigene Instanz bereitstellen"
|
||||||
},
|
},
|
||||||
|
|
||||||
"demo": {
|
"demo": {
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,15 @@
|
||||||
"error_revoking": "Failed to revoke share link: {{error}}"
|
"error_revoking": "Failed to revoke share link: {{error}}"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"quick_switcher": {
|
||||||
|
"placeholder": "Type to search notes...",
|
||||||
|
"no_results": "No matching notes",
|
||||||
|
"recent_hint": "Showing recent notes. Type to search all.",
|
||||||
|
"navigate": "navigate",
|
||||||
|
"open": "open",
|
||||||
|
"close": "close"
|
||||||
|
},
|
||||||
|
|
||||||
"tags": {
|
"tags": {
|
||||||
"title": "Tags",
|
"title": "Tags",
|
||||||
"no_tags": "No tags found",
|
"no_tags": "No tags found",
|
||||||
|
|
@ -263,7 +272,8 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"support": {
|
"support": {
|
||||||
"enjoying_demo": "Support this project"
|
"enjoying_demo": "Support this project",
|
||||||
|
"deploy_own": "Deploy your own instance"
|
||||||
},
|
},
|
||||||
|
|
||||||
"demo": {
|
"demo": {
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,15 @@
|
||||||
"error_revoking": "Failed to revoke share link: {{error}}"
|
"error_revoking": "Failed to revoke share link: {{error}}"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"quick_switcher": {
|
||||||
|
"placeholder": "Type to search notes...",
|
||||||
|
"no_results": "No matching notes",
|
||||||
|
"recent_hint": "Showing recent notes. Type to search all.",
|
||||||
|
"navigate": "navigate",
|
||||||
|
"open": "open",
|
||||||
|
"close": "close"
|
||||||
|
},
|
||||||
|
|
||||||
"tags": {
|
"tags": {
|
||||||
"title": "Tags",
|
"title": "Tags",
|
||||||
"no_tags": "No tags found",
|
"no_tags": "No tags found",
|
||||||
|
|
@ -263,7 +272,8 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"support": {
|
"support": {
|
||||||
"enjoying_demo": "Support this project"
|
"enjoying_demo": "Support this project",
|
||||||
|
"deploy_own": "Deploy your own instance"
|
||||||
},
|
},
|
||||||
|
|
||||||
"demo": {
|
"demo": {
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,15 @@
|
||||||
"error_revoking": "Error al revocar el enlace: {{error}}"
|
"error_revoking": "Error al revocar el enlace: {{error}}"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"quick_switcher": {
|
||||||
|
"placeholder": "Escribe para buscar notas...",
|
||||||
|
"no_results": "Sin resultados",
|
||||||
|
"recent_hint": "Mostrando notas recientes. Escribe para buscar todas.",
|
||||||
|
"navigate": "navegar",
|
||||||
|
"open": "abrir",
|
||||||
|
"close": "cerrar"
|
||||||
|
},
|
||||||
|
|
||||||
"tags": {
|
"tags": {
|
||||||
"title": "Etiquetas",
|
"title": "Etiquetas",
|
||||||
"no_tags": "No se encontraron etiquetas",
|
"no_tags": "No se encontraron etiquetas",
|
||||||
|
|
@ -263,7 +272,8 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"support": {
|
"support": {
|
||||||
"enjoying_demo": "Apoya este proyecto"
|
"enjoying_demo": "Apoya este proyecto",
|
||||||
|
"deploy_own": "Despliega tu propia instancia"
|
||||||
},
|
},
|
||||||
|
|
||||||
"demo": {
|
"demo": {
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,15 @@
|
||||||
"error_revoking": "Échec de la révocation du lien : {{error}}"
|
"error_revoking": "Échec de la révocation du lien : {{error}}"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"quick_switcher": {
|
||||||
|
"placeholder": "Rechercher des notes...",
|
||||||
|
"no_results": "Aucun résultat",
|
||||||
|
"recent_hint": "Notes récentes. Tapez pour tout rechercher.",
|
||||||
|
"navigate": "naviguer",
|
||||||
|
"open": "ouvrir",
|
||||||
|
"close": "fermer"
|
||||||
|
},
|
||||||
|
|
||||||
"tags": {
|
"tags": {
|
||||||
"title": "Étiquettes",
|
"title": "Étiquettes",
|
||||||
"no_tags": "Aucune étiquette trouvée",
|
"no_tags": "Aucune étiquette trouvée",
|
||||||
|
|
@ -263,7 +272,8 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"support": {
|
"support": {
|
||||||
"enjoying_demo": "Soutenez ce projet"
|
"enjoying_demo": "Soutenez ce projet",
|
||||||
|
"deploy_own": "Déployez votre propre instance"
|
||||||
},
|
},
|
||||||
|
|
||||||
"demo": {
|
"demo": {
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,15 @@
|
||||||
"error_revoking": "Impossibile revocare il link: {{error}}"
|
"error_revoking": "Impossibile revocare il link: {{error}}"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"quick_switcher": {
|
||||||
|
"placeholder": "Cerca note...",
|
||||||
|
"no_results": "Nessun risultato",
|
||||||
|
"recent_hint": "Note recenti. Digita per cercare tutte.",
|
||||||
|
"navigate": "naviga",
|
||||||
|
"open": "apri",
|
||||||
|
"close": "chiudi"
|
||||||
|
},
|
||||||
|
|
||||||
"tags": {
|
"tags": {
|
||||||
"title": "Tag",
|
"title": "Tag",
|
||||||
"no_tags": "Nessun tag trovato",
|
"no_tags": "Nessun tag trovato",
|
||||||
|
|
@ -263,7 +272,8 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"support": {
|
"support": {
|
||||||
"enjoying_demo": "Supporta questo progetto"
|
"enjoying_demo": "Supporta questo progetto",
|
||||||
|
"deploy_own": "Crea la tua istanza"
|
||||||
},
|
},
|
||||||
|
|
||||||
"demo": {
|
"demo": {
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,15 @@
|
||||||
"error_revoking": "共有リンクの取り消しに失敗しました: {{error}}"
|
"error_revoking": "共有リンクの取り消しに失敗しました: {{error}}"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"quick_switcher": {
|
||||||
|
"placeholder": "ノートを検索...",
|
||||||
|
"no_results": "結果なし",
|
||||||
|
"recent_hint": "最近のノートを表示中。入力して全て検索。",
|
||||||
|
"navigate": "移動",
|
||||||
|
"open": "開く",
|
||||||
|
"close": "閉じる"
|
||||||
|
},
|
||||||
|
|
||||||
"tags": {
|
"tags": {
|
||||||
"title": "タグ",
|
"title": "タグ",
|
||||||
"no_tags": "タグが見つかりません",
|
"no_tags": "タグが見つかりません",
|
||||||
|
|
@ -263,7 +272,8 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"support": {
|
"support": {
|
||||||
"enjoying_demo": "このプロジェクトを支援"
|
"enjoying_demo": "このプロジェクトを支援",
|
||||||
|
"deploy_own": "自分のインスタンスをデプロイ"
|
||||||
},
|
},
|
||||||
|
|
||||||
"demo": {
|
"demo": {
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,15 @@
|
||||||
"error_revoking": "Не удалось отозвать ссылку: {{error}}"
|
"error_revoking": "Не удалось отозвать ссылку: {{error}}"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"quick_switcher": {
|
||||||
|
"placeholder": "Поиск заметок...",
|
||||||
|
"no_results": "Ничего не найдено",
|
||||||
|
"recent_hint": "Показаны недавние заметки. Введите для поиска.",
|
||||||
|
"navigate": "навигация",
|
||||||
|
"open": "открыть",
|
||||||
|
"close": "закрыть"
|
||||||
|
},
|
||||||
|
|
||||||
"tags": {
|
"tags": {
|
||||||
"title": "Теги",
|
"title": "Теги",
|
||||||
"no_tags": "Теги не найдены",
|
"no_tags": "Теги не найдены",
|
||||||
|
|
@ -263,7 +272,8 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"support": {
|
"support": {
|
||||||
"enjoying_demo": "Поддержать проект"
|
"enjoying_demo": "Поддержать проект",
|
||||||
|
"deploy_own": "Развернуть свой экземпляр"
|
||||||
},
|
},
|
||||||
|
|
||||||
"demo": {
|
"demo": {
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,15 @@
|
||||||
"error_revoking": "Napaka pri preklicu povezave: {{error}}"
|
"error_revoking": "Napaka pri preklicu povezave: {{error}}"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"quick_switcher": {
|
||||||
|
"placeholder": "Išči beležke...",
|
||||||
|
"no_results": "Ni rezultatov",
|
||||||
|
"recent_hint": "Prikaz nedavnih beležk. Vnesite za iskanje.",
|
||||||
|
"navigate": "navigiraj",
|
||||||
|
"open": "odpri",
|
||||||
|
"close": "zapri"
|
||||||
|
},
|
||||||
|
|
||||||
"tags": {
|
"tags": {
|
||||||
"title": "Oznake",
|
"title": "Oznake",
|
||||||
"no_tags": "Ni najdenih oznak",
|
"no_tags": "Ni najdenih oznak",
|
||||||
|
|
@ -263,7 +272,8 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"support": {
|
"support": {
|
||||||
"enjoying_demo": "Podprite ta projekt"
|
"enjoying_demo": "Podprite ta projekt",
|
||||||
|
"deploy_own": "Namestite svojo instanco"
|
||||||
},
|
},
|
||||||
|
|
||||||
"demo": {
|
"demo": {
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,15 @@
|
||||||
"error_revoking": "撤销分享链接失败:{{error}}"
|
"error_revoking": "撤销分享链接失败:{{error}}"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"quick_switcher": {
|
||||||
|
"placeholder": "搜索笔记...",
|
||||||
|
"no_results": "无匹配结果",
|
||||||
|
"recent_hint": "显示最近笔记。输入以搜索全部。",
|
||||||
|
"navigate": "导航",
|
||||||
|
"open": "打开",
|
||||||
|
"close": "关闭"
|
||||||
|
},
|
||||||
|
|
||||||
"tags": {
|
"tags": {
|
||||||
"title": "标签",
|
"title": "标签",
|
||||||
"no_tags": "未找到标签",
|
"no_tags": "未找到标签",
|
||||||
|
|
@ -263,7 +272,8 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"support": {
|
"support": {
|
||||||
"enjoying_demo": "支持此项目"
|
"enjoying_demo": "支持此项目",
|
||||||
|
"deploy_own": "部署您自己的实例"
|
||||||
},
|
},
|
||||||
|
|
||||||
"demo": {
|
"demo": {
|
||||||
|
|
|
||||||
25
run.py
25
run.py
|
|
@ -15,6 +15,27 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
colorama = None
|
colorama = None
|
||||||
|
|
||||||
|
def get_port():
|
||||||
|
"""Get port from: 1) ENV variable, 2) config.yaml, 3) default 8000"""
|
||||||
|
# Priority 1: Environment variable
|
||||||
|
if os.getenv("PORT"):
|
||||||
|
return os.getenv("PORT")
|
||||||
|
|
||||||
|
# Priority 2: config.yaml
|
||||||
|
config_path = Path("config.yaml")
|
||||||
|
if config_path.exists():
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
with open(config_path, 'r', encoding='utf-8') as f:
|
||||||
|
config = yaml.safe_load(f)
|
||||||
|
if config and 'server' in config and 'port' in config['server']:
|
||||||
|
return str(config['server']['port'])
|
||||||
|
except Exception:
|
||||||
|
pass # Fall through to default
|
||||||
|
|
||||||
|
# Priority 3: Default
|
||||||
|
return "8000"
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print("🚀 Starting NoteDiscovery...\n")
|
print("🚀 Starting NoteDiscovery...\n")
|
||||||
|
|
||||||
|
|
@ -30,8 +51,8 @@ def main():
|
||||||
Path("data").mkdir(parents=True, exist_ok=True)
|
Path("data").mkdir(parents=True, exist_ok=True)
|
||||||
Path("plugins").mkdir(parents=True, exist_ok=True)
|
Path("plugins").mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Get port from environment variable or use default
|
# Get port from config or environment
|
||||||
port = os.getenv("PORT", "8000")
|
port = get_port()
|
||||||
|
|
||||||
print("✓ Dependencies installed")
|
print("✓ Dependencies installed")
|
||||||
print("✓ Directories created")
|
print("✓ Directories created")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue