added quick search
This commit is contained in:
parent
291bcb5e00
commit
4bbd6f7d9d
|
|
@ -972,6 +972,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 Ctrl+Alt+P
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="benefit-item">
|
<div class="benefit-item">
|
||||||
<span class="emoji">🔐</span>
|
<span class="emoji">🔐</span>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
108
frontend/app.js
108
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();
|
||||||
|
|
@ -5040,6 +5053,101 @@ 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() {
|
||||||
|
const container = document.getElementById('quickSwitcherResults');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// Get all result items (skip the "no results" template)
|
||||||
|
const items = container.querySelectorAll('[data-quick-switcher-item]');
|
||||||
|
const selectedItem = items[this.quickSwitcherIndex];
|
||||||
|
|
||||||
|
if (selectedItem) {
|
||||||
|
// Calculate if we need to scroll
|
||||||
|
const containerRect = container.getBoundingClientRect();
|
||||||
|
const itemRect = selectedItem.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (itemRect.top < containerRect.top) {
|
||||||
|
// Item is above visible area
|
||||||
|
container.scrollTop -= (containerRect.top - itemRect.top);
|
||||||
|
} else if (itemRect.bottom > containerRect.bottom) {
|
||||||
|
// Item is below visible area
|
||||||
|
container.scrollTop += (itemRect.bottom - containerRect.bottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
|
||||||
|
|
@ -2784,6 +2784,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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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": "タグが見つかりません",
|
||||||
|
|
|
||||||
|
|
@ -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": "Теги не найдены",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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": "未找到标签",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue