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
|
||||
|
||||
🌐 **[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!
|
||||
---
|
||||
|
||||
<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 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?
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
|
|
@ -972,6 +978,14 @@
|
|||
</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">
|
||||
<span class="emoji">🔐</span>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|---------------|-----|--------|
|
||||
| `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:
|
||||
|
|
|
|||
167
frontend/app.js
167
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;
|
||||
|
|
|
|||
|
|
@ -1795,7 +1795,7 @@
|
|||
</a>
|
||||
<!-- Ko-fi support link (demo mode only) -->
|
||||
<template x-if="demoMode">
|
||||
<div class="mt-3">
|
||||
<div class="mt-3 flex flex-col gap-2 items-center">
|
||||
<a
|
||||
href="https://ko-fi.com/gamosoft"
|
||||
target="_blank"
|
||||
|
|
@ -1806,6 +1806,15 @@
|
|||
<span>☕</span>
|
||||
<span x-text="t('support.enjoying_demo')"></span>
|
||||
</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>
|
||||
</template>
|
||||
</div>
|
||||
|
|
@ -2784,6 +2793,72 @@
|
|||
</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 -->
|
||||
<div x-show="showShareModal"
|
||||
@click.self="showShareModal = false"
|
||||
|
|
|
|||
|
|
@ -129,6 +129,15 @@
|
|||
"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": {
|
||||
"title": "Tags",
|
||||
"no_tags": "Keine Tags gefunden",
|
||||
|
|
@ -263,7 +272,8 @@
|
|||
},
|
||||
|
||||
"support": {
|
||||
"enjoying_demo": "Unterstütze dieses Projekt"
|
||||
"enjoying_demo": "Unterstütze dieses Projekt",
|
||||
"deploy_own": "Eigene Instanz bereitstellen"
|
||||
},
|
||||
|
||||
"demo": {
|
||||
|
|
|
|||
|
|
@ -129,6 +129,15 @@
|
|||
"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": {
|
||||
"title": "Tags",
|
||||
"no_tags": "No tags found",
|
||||
|
|
@ -263,7 +272,8 @@
|
|||
},
|
||||
|
||||
"support": {
|
||||
"enjoying_demo": "Support this project"
|
||||
"enjoying_demo": "Support this project",
|
||||
"deploy_own": "Deploy your own instance"
|
||||
},
|
||||
|
||||
"demo": {
|
||||
|
|
|
|||
|
|
@ -129,6 +129,15 @@
|
|||
"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": {
|
||||
"title": "Tags",
|
||||
"no_tags": "No tags found",
|
||||
|
|
@ -263,7 +272,8 @@
|
|||
},
|
||||
|
||||
"support": {
|
||||
"enjoying_demo": "Support this project"
|
||||
"enjoying_demo": "Support this project",
|
||||
"deploy_own": "Deploy your own instance"
|
||||
},
|
||||
|
||||
"demo": {
|
||||
|
|
|
|||
|
|
@ -129,6 +129,15 @@
|
|||
"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": {
|
||||
"title": "Etiquetas",
|
||||
"no_tags": "No se encontraron etiquetas",
|
||||
|
|
@ -263,7 +272,8 @@
|
|||
},
|
||||
|
||||
"support": {
|
||||
"enjoying_demo": "Apoya este proyecto"
|
||||
"enjoying_demo": "Apoya este proyecto",
|
||||
"deploy_own": "Despliega tu propia instancia"
|
||||
},
|
||||
|
||||
"demo": {
|
||||
|
|
|
|||
|
|
@ -129,6 +129,15 @@
|
|||
"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": {
|
||||
"title": "Étiquettes",
|
||||
"no_tags": "Aucune étiquette trouvée",
|
||||
|
|
@ -263,7 +272,8 @@
|
|||
},
|
||||
|
||||
"support": {
|
||||
"enjoying_demo": "Soutenez ce projet"
|
||||
"enjoying_demo": "Soutenez ce projet",
|
||||
"deploy_own": "Déployez votre propre instance"
|
||||
},
|
||||
|
||||
"demo": {
|
||||
|
|
|
|||
|
|
@ -129,6 +129,15 @@
|
|||
"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": {
|
||||
"title": "Tag",
|
||||
"no_tags": "Nessun tag trovato",
|
||||
|
|
@ -263,7 +272,8 @@
|
|||
},
|
||||
|
||||
"support": {
|
||||
"enjoying_demo": "Supporta questo progetto"
|
||||
"enjoying_demo": "Supporta questo progetto",
|
||||
"deploy_own": "Crea la tua istanza"
|
||||
},
|
||||
|
||||
"demo": {
|
||||
|
|
|
|||
|
|
@ -129,6 +129,15 @@
|
|||
"error_revoking": "共有リンクの取り消しに失敗しました: {{error}}"
|
||||
},
|
||||
|
||||
"quick_switcher": {
|
||||
"placeholder": "ノートを検索...",
|
||||
"no_results": "結果なし",
|
||||
"recent_hint": "最近のノートを表示中。入力して全て検索。",
|
||||
"navigate": "移動",
|
||||
"open": "開く",
|
||||
"close": "閉じる"
|
||||
},
|
||||
|
||||
"tags": {
|
||||
"title": "タグ",
|
||||
"no_tags": "タグが見つかりません",
|
||||
|
|
@ -263,7 +272,8 @@
|
|||
},
|
||||
|
||||
"support": {
|
||||
"enjoying_demo": "このプロジェクトを支援"
|
||||
"enjoying_demo": "このプロジェクトを支援",
|
||||
"deploy_own": "自分のインスタンスをデプロイ"
|
||||
},
|
||||
|
||||
"demo": {
|
||||
|
|
|
|||
|
|
@ -129,6 +129,15 @@
|
|||
"error_revoking": "Не удалось отозвать ссылку: {{error}}"
|
||||
},
|
||||
|
||||
"quick_switcher": {
|
||||
"placeholder": "Поиск заметок...",
|
||||
"no_results": "Ничего не найдено",
|
||||
"recent_hint": "Показаны недавние заметки. Введите для поиска.",
|
||||
"navigate": "навигация",
|
||||
"open": "открыть",
|
||||
"close": "закрыть"
|
||||
},
|
||||
|
||||
"tags": {
|
||||
"title": "Теги",
|
||||
"no_tags": "Теги не найдены",
|
||||
|
|
@ -263,7 +272,8 @@
|
|||
},
|
||||
|
||||
"support": {
|
||||
"enjoying_demo": "Поддержать проект"
|
||||
"enjoying_demo": "Поддержать проект",
|
||||
"deploy_own": "Развернуть свой экземпляр"
|
||||
},
|
||||
|
||||
"demo": {
|
||||
|
|
|
|||
|
|
@ -129,6 +129,15 @@
|
|||
"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": {
|
||||
"title": "Oznake",
|
||||
"no_tags": "Ni najdenih oznak",
|
||||
|
|
@ -263,7 +272,8 @@
|
|||
},
|
||||
|
||||
"support": {
|
||||
"enjoying_demo": "Podprite ta projekt"
|
||||
"enjoying_demo": "Podprite ta projekt",
|
||||
"deploy_own": "Namestite svojo instanco"
|
||||
},
|
||||
|
||||
"demo": {
|
||||
|
|
|
|||
|
|
@ -129,6 +129,15 @@
|
|||
"error_revoking": "撤销分享链接失败:{{error}}"
|
||||
},
|
||||
|
||||
"quick_switcher": {
|
||||
"placeholder": "搜索笔记...",
|
||||
"no_results": "无匹配结果",
|
||||
"recent_hint": "显示最近笔记。输入以搜索全部。",
|
||||
"navigate": "导航",
|
||||
"open": "打开",
|
||||
"close": "关闭"
|
||||
},
|
||||
|
||||
"tags": {
|
||||
"title": "标签",
|
||||
"no_tags": "未找到标签",
|
||||
|
|
@ -263,7 +272,8 @@
|
|||
},
|
||||
|
||||
"support": {
|
||||
"enjoying_demo": "支持此项目"
|
||||
"enjoying_demo": "支持此项目",
|
||||
"deploy_own": "部署您自己的实例"
|
||||
},
|
||||
|
||||
"demo": {
|
||||
|
|
|
|||
25
run.py
25
run.py
|
|
@ -15,6 +15,27 @@ try:
|
|||
except ImportError:
|
||||
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():
|
||||
print("🚀 Starting NoteDiscovery...\n")
|
||||
|
||||
|
|
@ -30,8 +51,8 @@ def main():
|
|||
Path("data").mkdir(parents=True, exist_ok=True)
|
||||
Path("plugins").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Get port from environment variable or use default
|
||||
port = os.getenv("PORT", "8000")
|
||||
# Get port from config or environment
|
||||
port = get_port()
|
||||
|
||||
print("✓ Dependencies installed")
|
||||
print("✓ Directories created")
|
||||
|
|
|
|||
Loading…
Reference in New Issue