mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-07-24 07:45:57 +02:00
Add custom note sorting
This commit is contained in:
@@ -267,6 +267,8 @@ pub struct VaultState {
|
||||
#[serde(default)]
|
||||
pub notebook_order: std::collections::HashMap<String, i32>,
|
||||
#[serde(default)]
|
||||
pub note_order: std::collections::HashMap<String, i32>,
|
||||
#[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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
editorDirty,
|
||||
quickAccessPaths,
|
||||
appConfig,
|
||||
noteOrder,
|
||||
notebooks,
|
||||
tags,
|
||||
mobileView
|
||||
@@ -205,6 +206,11 @@
|
||||
let qaDragFrom = $state<number | null>(null);
|
||||
let qaDragOver = $state<number | null>(null);
|
||||
let qaDragHalf = $state<'top' | 'bottom'>('bottom');
|
||||
let noteDragFrom = $state<number | null>(null);
|
||||
let noteDragOver = $state<number | null>(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<HTMLDivElement>(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}
|
||||
<div class="note-compact-row" title={getNotebookPath(note) ? `${getNotebookPath(note)}/${note.meta.title}` : note.meta.title}>
|
||||
@@ -1415,6 +1494,18 @@
|
||||
Title
|
||||
{#if $sortMode === 'title'}<span class="sort-check">✓</span>{/if}
|
||||
</button>
|
||||
<button class:active={$sortMode === 'custom'} onclick={() => setSortMode('custom')}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="8" y1="6" x2="21" y2="6" />
|
||||
<line x1="8" y1="12" x2="21" y2="12" />
|
||||
<line x1="8" y1="18" x2="21" y2="18" />
|
||||
<circle cx="4" cy="6" r="1" />
|
||||
<circle cx="4" cy="12" r="1" />
|
||||
<circle cx="4" cy="18" r="1" />
|
||||
</svg>
|
||||
Custom
|
||||
{#if $sortMode === 'custom'}<span class="sort-check">✓</span>{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/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);
|
||||
}
|
||||
|
||||
|
||||
+20
-2
@@ -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<Record<string, number>>({});
|
||||
export const noteOrder = writable<Record<string, number>>({});
|
||||
|
||||
// Data
|
||||
export const notebooks = writable<NotebookEntry[]>([]);
|
||||
@@ -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;
|
||||
},
|
||||
);
|
||||
|
||||
+2
-1
@@ -108,6 +108,7 @@ export interface VaultState {
|
||||
collapsed_notebooks: string[];
|
||||
notebook_sort_mode?: string;
|
||||
notebook_order?: Record<string, number>;
|
||||
note_order?: Record<string, number>;
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user