added quick search

This commit is contained in:
Gamosoft 2026-01-15 16:32:35 +01:00
parent 291bcb5e00
commit 4bbd6f7d9d
14 changed files with 276 additions and 1 deletions

View File

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

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();
@ -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;

View File

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

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

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

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

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

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

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

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": "タグが見つかりません",

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": "Теги не найдены",

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

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": "未找到标签",