Merge pull request #151 from gamosoft/features/quick-search

Features/quick search
This commit is contained in:
Guillermo Villar 2026-01-16 12:17:20 +01:00 committed by GitHub
commit 1948bfb1b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 412 additions and 41 deletions

View File

@ -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>
&nbsp;&nbsp;
<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>
&nbsp;&nbsp;
<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?

10
docs/demo-button.svg Normal file
View File

@ -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

View File

@ -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>

10
docs/website-button.svg Normal file
View File

@ -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

View File

@ -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:

View File

@ -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,23 +1998,54 @@ 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
onEditorDragEnter(event) { onEditorDragEnter(event) {
if (!this.draggedItem) return; if (!this.draggedItem) return;
@ -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;

View File

@ -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"

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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
View File

@ -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")