From e46fda91a42a68b849d8b0e256ba14e16772f587 Mon Sep 17 00:00:00 2001 From: mf Date: Sun, 21 Jun 2026 10:46:22 +0330 Subject: [PATCH] Add custom note sorting --- src-tauri/src/types.rs | 3 + src/lib/components/AppLayout.svelte | 10 ++- src/lib/components/NoteList.svelte | 99 ++++++++++++++++++++++++++++- src/lib/stores/app.ts | 22 ++++++- src/lib/types.ts | 3 +- 5 files changed, 130 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index 421d11d..c1a4af5 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -267,6 +267,8 @@ pub struct VaultState { #[serde(default)] pub notebook_order: std::collections::HashMap, #[serde(default)] + pub note_order: std::collections::HashMap, + #[serde(default)] pub sort_mode: String, #[serde(default)] pub last_view_mode: String, @@ -294,6 +296,7 @@ impl Default for VaultState { collapsed_notebooks: Vec::new(), notebook_sort_mode: String::new(), notebook_order: std::collections::HashMap::new(), + note_order: std::collections::HashMap::new(), sort_mode: String::new(), last_view_mode: String::new(), last_notebook: None, diff --git a/src/lib/components/AppLayout.svelte b/src/lib/components/AppLayout.svelte index 608a9cc..c65f870 100644 --- a/src/lib/components/AppLayout.svelte +++ b/src/lib/components/AppLayout.svelte @@ -48,6 +48,7 @@ viewerNote, notebookSortMode, notebookOrder, + noteOrder, syncState, platformIsMobile } from '$lib/stores/app'; @@ -203,6 +204,7 @@ collapsed_notebooks: $collapsedNotebooks, notebook_sort_mode: $notebookSortMode, notebook_order: $notebookOrder, + note_order: $noteOrder, sort_mode: $sortMode, last_view_mode: $viewMode, last_notebook: $activeNotebook?.relative_path ?? null, @@ -518,6 +520,11 @@ persistState(); }); + $effect(() => { + $noteOrder; + persistState(); + }); + $effect(() => { $sortMode; $tasksLayout; @@ -547,7 +554,8 @@ $collapsedNotebooks = state.collapsed_notebooks ?? []; $notebookSortMode = state.notebook_sort_mode === 'manual' ? 'manual' : 'alphabetical'; $notebookOrder = state.notebook_order ?? {}; - if (state.sort_mode === 'created' || state.sort_mode === 'title' || state.sort_mode === 'modified') $sortMode = state.sort_mode; + $noteOrder = state.note_order ?? {}; + if (state.sort_mode === 'created' || state.sort_mode === 'title' || state.sort_mode === 'modified' || state.sort_mode === 'custom') $sortMode = state.sort_mode; if (state.tasks_layout === 'calendar' || state.tasks_layout === 'list') $tasksLayout = state.tasks_layout; if (typeof state.tasks_hide_completed === 'boolean') $tasksHideCompleted = state.tasks_hide_completed; if (typeof state.tasks_only_flagged === 'boolean') $tasksOnlyFlagged = state.tasks_only_flagged; diff --git a/src/lib/components/NoteList.svelte b/src/lib/components/NoteList.svelte index 7ced63c..5e371ae 100644 --- a/src/lib/components/NoteList.svelte +++ b/src/lib/components/NoteList.svelte @@ -12,6 +12,7 @@ editorDirty, quickAccessPaths, appConfig, + noteOrder, notebooks, tags, mobileView @@ -205,6 +206,11 @@ let qaDragFrom = $state(null); let qaDragOver = $state(null); let qaDragHalf = $state<'top' | 'bottom'>('bottom'); + let noteDragFrom = $state(null); + let noteDragOver = $state(null); + let noteDragHalf = $state<'top' | 'bottom'>('bottom'); + const customOrderViews = new Set(['all', 'notebook', 'tag']); + let canCustomReorder = $derived($sortMode === 'custom' && customOrderViews.has($viewMode)); // Virtual scroll let listContainer = $state(null!); @@ -414,6 +420,7 @@ } try { const entry = await createNote(nbRelative, 'Untitled'); + if ($sortMode === 'custom') appendManualNoteOrder(entry.path); noteCache.clear(); await refresh(); await selectNote(entry); @@ -431,6 +438,7 @@ try { const newTitle = editValue.trim(); const newPath = await renameNote(note.path, newTitle); + renameManualNoteOrderPath(note.path, newPath); noteCache.clear(); editingNote = null; // Update local store immediately (avoid full vault re-scan) @@ -452,6 +460,7 @@ contextMenu = null; try { await deleteNote(note.path); + removeManualNoteOrderPath(note.path); noteCache.clear(); // Remove from local store immediately (avoid full vault re-scan) $notes = $notes.filter(n => n.path !== note.path); @@ -479,6 +488,7 @@ contextMenu = null; try { await permanentDelete(note.path); + removeManualNoteOrderPath(note.path); noteCache.clear(); $notes = $notes.filter(n => n.path !== note.path); if ($activeNotePath === note.path) { @@ -672,11 +682,57 @@ } } + function persistManualNoteOrder(orderedNotes: NoteEntry[]) { + $noteOrder = Object.fromEntries(orderedNotes.map((note, index) => [note.path, index])); + } + + function appendManualNoteOrder(path: string) { + const nextIndex = Math.max(-1, ...Object.values($noteOrder)) + 1; + $noteOrder = { ...$noteOrder, [path]: nextIndex }; + } + + function renameManualNoteOrderPath(oldPath: string, newPath: string) { + if (!Object.hasOwn($noteOrder, oldPath)) return; + const { [oldPath]: index, ...rest } = $noteOrder; + $noteOrder = { ...rest, [newPath]: index }; + } + + function removeManualNoteOrderPath(path: string) { + if (!Object.hasOwn($noteOrder, path)) return; + const { [path]: _, ...rest } = $noteOrder; + $noteOrder = rest; + } + + function resetNoteDrag() { + noteDragFrom = null; + noteDragOver = null; + } + + function handleCustomNoteDrop(targetIndex: number) { + if (noteDragFrom === null || !canCustomReorder) { + resetNoteDrag(); + return; + } + let insertAt = noteDragHalf === 'bottom' ? targetIndex + 1 : targetIndex; + if (noteDragFrom === insertAt || noteDragFrom + 1 === insertAt) { + resetNoteDrag(); + return; + } + const arr = [...$sortedNotes]; + const [moved] = arr.splice(noteDragFrom, 1); + if (insertAt > noteDragFrom) insertAt--; + arr.splice(insertAt, 0, moved); + $notes = arr; + persistManualNoteOrder(arr); + resetNoteDrag(); + } + async function handleMoveNote(note: NoteEntry, destPath: string) { contextMenu = null; movePickerNote = null; try { const newPath = await moveNote(note.path, destPath); + renameManualNoteOrderPath(note.path, newPath); noteCache.clear(); $notes = $notes.filter(n => n.path !== note.path); if ($activeNotePath === note.path) { @@ -778,6 +834,7 @@ async function handleBatchDelete() { const toDelete = new Set(selectedPaths); noteCache.clear(); + for (const path of toDelete) removeManualNoteOrderPath(path); $notes = $notes.filter(n => !toDelete.has(n.path)); if ($activeNotePath && toDelete.has($activeNotePath)) { $activeNote = null; @@ -813,6 +870,7 @@ async function handleBatchPermanentDelete() { const toDelete = new Set(selectedPaths); noteCache.clear(); + for (const path of toDelete) removeManualNoteOrderPath(path); $notes = $notes.filter(n => !toDelete.has(n.path)); if ($activeNotePath && toDelete.has($activeNotePath)) { $activeNote = null; @@ -838,6 +896,9 @@ } function setSortMode(mode: SortMode) { + if (mode === 'custom' && Object.keys($noteOrder).length === 0) { + persistManualNoteOrder($sortedNotes); + } $sortMode = mode; sortMenu = null; } @@ -1078,6 +1139,8 @@ class:compact={compact} class:qa-drag-above={$viewMode === 'quickaccess' && qaDragOver === noteIndex && qaDragFrom !== null && qaDragHalf === 'top'} class:qa-drag-below={$viewMode === 'quickaccess' && qaDragOver === noteIndex && qaDragFrom !== null && qaDragHalf === 'bottom'} + class:note-drag-above={canCustomReorder && noteDragOver === noteIndex && noteDragFrom !== null && noteDragHalf === 'top'} + class:note-drag-below={canCustomReorder && noteDragOver === noteIndex && noteDragFrom !== null && noteDragHalf === 'bottom'} onclick={(e) => handleNoteClick(e, note)} onkeydown={(e) => { if (e.key === 'F2') { @@ -1111,6 +1174,12 @@ e.dataTransfer!.effectAllowed = 'move'; return; } + if (canCustomReorder) { + noteDragFrom = noteIndex; + e.dataTransfer!.setData('text/plain', note.path); + e.dataTransfer!.effectAllowed = 'move'; + return; + } if (selectedPaths.size > 1 && selectedPaths.has(note.path)) { e.dataTransfer!.setData('text/plain', [...selectedPaths].join('\n')); } else { @@ -1125,18 +1194,28 @@ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); qaDragHalf = e.clientY < rect.top + rect.height / 2 ? 'top' : 'bottom'; qaDragOver = noteIndex; + } else if (canCustomReorder && noteDragFrom !== null) { + e.preventDefault(); + e.dataTransfer!.dropEffect = 'move'; + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + noteDragHalf = e.clientY < rect.top + rect.height / 2 ? 'top' : 'bottom'; + noteDragOver = noteIndex; } }} ondragleave={() => { if (qaDragOver === noteIndex) qaDragOver = null; + if (noteDragOver === noteIndex) noteDragOver = null; }} ondrop={(e) => { if ($viewMode === 'quickaccess' && qaDragFrom !== null) { e.preventDefault(); handleQaDrop(noteIndex); + } else if (canCustomReorder && noteDragFrom !== null) { + e.preventDefault(); + handleCustomNoteDrop(noteIndex); } }} - ondragend={() => { qaDragFrom = null; qaDragOver = null; }} + ondragend={() => { qaDragFrom = null; qaDragOver = null; resetNoteDrag(); }} > {#if compact}
@@ -1415,6 +1494,18 @@ Title {#if $sortMode === 'title'}{/if} +
{/if} @@ -1937,11 +2028,13 @@ font-size: 14px; } - .note-item.qa-drag-above { + .note-item.qa-drag-above, + .note-item.note-drag-above { border-top: 2px solid var(--accent); } - .note-item.qa-drag-below { + .note-item.qa-drag-below, + .note-item.note-drag-below { border-bottom: 2px solid var(--accent); } diff --git a/src/lib/stores/app.ts b/src/lib/stores/app.ts index 2b0c110..df66080 100644 --- a/src/lib/stores/app.ts +++ b/src/lib/stores/app.ts @@ -39,6 +39,7 @@ export const notebookSortMode = writable<"alphabetical" | "manual">( ); // Map from notebook absolute path → ordinal position (lower = earlier). Only consulted when notebookSortMode === 'manual'. export const notebookOrder = writable>({}); +export const noteOrder = writable>({}); // Data export const notebooks = writable([]); @@ -176,16 +177,30 @@ export const canGoForward = derived(navHistory, $h => $h.index < $h.stack.length // Derived export const sortedNotes = derived( - [notes, sortMode, viewMode], - ([$notes, $sortMode, $viewMode]) => { + [notes, sortMode, viewMode, noteOrder], + ([$notes, $sortMode, $viewMode, $noteOrder]) => { // Quick Access preserves stored order if ($viewMode === "quickaccess") return $notes; + const customSortable = + $viewMode === "all" || $viewMode === "notebook" || $viewMode === "tag"; const pinned = $notes.filter((n) => n.meta.pinned); const unpinned = $notes.filter((n) => !n.meta.pinned); const sortFn = (a: NoteEntry, b: NoteEntry) => { switch ($sortMode) { + case "custom": { + if (!customSortable) { + return ( + new Date(b.meta.modified).getTime() - + new Date(a.meta.modified).getTime() + ); + } + const oa = $noteOrder[a.path] ?? Number.MAX_SAFE_INTEGER; + const ob = $noteOrder[b.path] ?? Number.MAX_SAFE_INTEGER; + if (oa !== ob) return oa - ob; + return a.meta.title.localeCompare(b.meta.title); + } case "title": return a.meta.title.localeCompare(b.meta.title); case "created": @@ -215,6 +230,7 @@ export const vaultState = derived( collapsedNotebooks, notebookSortMode, notebookOrder, + noteOrder, ], ([ $activeNotePath, @@ -224,6 +240,7 @@ export const vaultState = derived( $collapsedNotebooks, $notebookSortMode, $notebookOrder, + $noteOrder, ]) => { return { last_open_note: $activeNotePath, @@ -233,6 +250,7 @@ export const vaultState = derived( collapsed_notebooks: $collapsedNotebooks, notebook_sort_mode: $notebookSortMode, notebook_order: $notebookOrder, + note_order: $noteOrder, } satisfies VaultState; }, ); diff --git a/src/lib/types.ts b/src/lib/types.ts index 55f538a..417d9e8 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -108,6 +108,7 @@ export interface VaultState { collapsed_notebooks: string[]; notebook_sort_mode?: string; notebook_order?: Record; + note_order?: Record; sort_mode?: string; last_view_mode?: string; last_notebook?: string | null; @@ -166,7 +167,7 @@ export interface NoteTitleEntry { path: string; } -export type SortMode = "modified" | "title" | "created"; +export type SortMode = "modified" | "title" | "created" | "custom"; export type ViewMode = | "all" | "notebook"