mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-09-19 17:37:29 +02:00
Add open-note trash action (#177)
This commit is contained in:
@@ -61,7 +61,7 @@
|
||||
const appWindow = getCurrentWindow();
|
||||
const isMac = navigator.platform.startsWith('Mac');
|
||||
const isMobile = $derived($platformIsMobile);
|
||||
import { loadVaultState, saveVaultState, readNote, createDailyNote, createBackup, getPendingOpenFile, addQuickAccess, removeQuickAccess, getQuickAccess, setTheme, syncNow, getAppConfig, setTaskDone, setTaskPriority, setTaskDue, findOrphanedAttachments, trashOrphanedAttachments } from '$lib/api';
|
||||
import { loadVaultState, saveVaultState, readNote, deleteNote, createDailyNote, createBackup, getPendingOpenFile, addQuickAccess, removeQuickAccess, getQuickAccess, setTheme, syncNow, getAppConfig, setTaskDone, setTaskPriority, setTaskDue, findOrphanedAttachments, trashOrphanedAttachments } from '$lib/api';
|
||||
import { darkThemes, isAndroid } from '$lib/platform';
|
||||
import { debounce } from '$lib/utils/debounce';
|
||||
import { openNoteWindow } from '$lib/utils/window';
|
||||
@@ -449,6 +449,28 @@
|
||||
else if ($mobileView === 'notelist') $mobileView = 'sidebar';
|
||||
}
|
||||
|
||||
async function trashOpenNote(path: string): Promise<boolean> {
|
||||
if (path !== $activeNotePath || $viewerNote || $viewMode === 'trash') return false;
|
||||
try {
|
||||
await deleteNote(path);
|
||||
if (Object.hasOwn($noteOrder, path)) {
|
||||
const { [path]: _, ...rest } = $noteOrder;
|
||||
$noteOrder = rest;
|
||||
}
|
||||
$notes = $notes.filter((note) => note.path !== path);
|
||||
if ($activeNotePath === path) {
|
||||
$activeNote = null;
|
||||
$activeNotePath = null;
|
||||
}
|
||||
noteList?.refresh(true).catch((error) => console.error('Failed to refresh notes after trashing:', error));
|
||||
if (isMobile) $mobileView = 'notelist';
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to move open note to Trash:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseDown(e: MouseEvent) {
|
||||
if (e.button === 3) { e.preventDefault(); navigateHistory(-1); }
|
||||
if (e.button === 4) { e.preventDefault(); navigateHistory(1); }
|
||||
@@ -467,7 +489,7 @@
|
||||
// Ctrl/Cmd+Shift+Delete: move the open note to the trash.
|
||||
if (mod && e.shiftKey && code === 'Delete' && $activeNotePath) {
|
||||
e.preventDefault();
|
||||
noteList?.trashActiveNote();
|
||||
editor?.moveOpenNoteToTrash();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -951,7 +973,7 @@
|
||||
<NoteList bind:this={noteList} onNoteSelected={handleNoteSelected} onBeforeNoteSwitch={() => editor?.flushSave()} onNoteMoved={() => sidebar?.refresh()} onNoteCreated={() => { editor?.focusTitle(); }} onToggleTask={toggleTask} onSetTaskPriority={changeTaskPriority} onSetTaskDue={changeTaskDue} />
|
||||
</div>
|
||||
<div class="mobile-panel" class:active={$mobileView === 'editor'}>
|
||||
<Editor bind:this={editor} />
|
||||
<Editor bind:this={editor} onMoveToTrash={trashOpenNote} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1028,7 +1050,7 @@
|
||||
{/if}
|
||||
|
||||
<div class="editor-panel">
|
||||
<Editor bind:this={editor} />
|
||||
<Editor bind:this={editor} onMoveToTrash={trashOpenNote} />
|
||||
{#if $viewMode === 'tasks' && !taskNoteOpened}
|
||||
<div class="tasks-editor-placeholder">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
@@ -57,6 +57,10 @@
|
||||
import { isMobile, isAndroid } from '$lib/platform';
|
||||
import ResizeHandle from './ResizeHandle.svelte';
|
||||
|
||||
let { onMoveToTrash = async () => false }: {
|
||||
onMoveToTrash?: (path: string) => Promise<boolean>;
|
||||
} = $props();
|
||||
|
||||
const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl';
|
||||
const sourceHighlighter = hljs.newInstance();
|
||||
sourceHighlighter.registerLanguage('markdown', markdownLanguage);
|
||||
@@ -103,6 +107,7 @@
|
||||
let pendingContent = $state<string | null>(null);
|
||||
let ignoreNextUpdate = false;
|
||||
let isLoadingNote = false;
|
||||
let trashingNote = $state(false);
|
||||
let fixingBlobsPromise: Promise<void> = Promise.resolve();
|
||||
let hasPendingBlobs = false;
|
||||
let lastSourceMode = $sourceMode;
|
||||
@@ -2993,8 +2998,16 @@
|
||||
return src;
|
||||
}
|
||||
|
||||
let saveQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
function queueSave(task: () => Promise<void>): Promise<void> {
|
||||
const queued = saveQueue.then(task);
|
||||
saveQueue = queued.catch(() => {});
|
||||
return queued;
|
||||
}
|
||||
|
||||
const autoSave = debounce(async () => {
|
||||
if (get(viewerNote)) return; // never autosave external viewer files
|
||||
if (get(viewerNote) || trashingNote) return; // never autosave external viewer files or a note being trashed
|
||||
if (!$activeNote || !$activeNotePath || !$editorDirty) return;
|
||||
// Only fix blob images if a paste occurred (avoids full doc scan on every save)
|
||||
if (hasPendingBlobs) {
|
||||
@@ -3002,38 +3015,65 @@
|
||||
fixingBlobsPromise = fixBlobImages();
|
||||
}
|
||||
await fixingBlobsPromise;
|
||||
if (trashingNote || !$activeNote || !$activeNotePath) return;
|
||||
try {
|
||||
const path = $activeNotePath;
|
||||
const note = $activeNote;
|
||||
const body = $sourceMode
|
||||
? restoreTitleH1(sourceContent)
|
||||
: editorToMarkdown();
|
||||
// Safety: never save empty/near-empty body over a note that had real content
|
||||
const trimmed = body.replace(/^#.*\n?/, '').trim();
|
||||
if (!trimmed && $activeNote.content && $activeNote.content.trim().length > 10) {
|
||||
if (!trimmed && note.content && note.content.trim().length > 10) {
|
||||
console.warn('Auto-save blocked: refusing to overwrite note with empty content');
|
||||
return;
|
||||
}
|
||||
await saveNote($activeNotePath, $activeNote.meta, body);
|
||||
$editorDirty = false;
|
||||
await queueSave(() => saveNote(path, note.meta, body));
|
||||
if ($activeNotePath === path) $editorDirty = false;
|
||||
} catch (e) {
|
||||
console.error('Auto-save failed:', e);
|
||||
}
|
||||
}, isMobile ? 1500 : 500);
|
||||
|
||||
export async function forceSave() {
|
||||
if (get(viewerNote)) return; // viewer files are never written back
|
||||
if (!$activeNote || !$activeNotePath) return;
|
||||
export async function forceSave(allowWhileTrashing = false): Promise<boolean> {
|
||||
if (get(viewerNote) || (trashingNote && !allowWhileTrashing)) return false;
|
||||
if (!$activeNote || !$activeNotePath) return false;
|
||||
await fixingBlobsPromise;
|
||||
if (trashingNote && !allowWhileTrashing) return false;
|
||||
if (!$activeNote || !$activeNotePath) return false;
|
||||
try {
|
||||
const path = $activeNotePath;
|
||||
const note = $activeNote;
|
||||
const body = $sourceMode ? restoreTitleH1(sourceContent) : editorToMarkdown();
|
||||
const trimmed = body.replace(/^#.*\n?/, '').trim();
|
||||
if (!trimmed && $activeNote.content && $activeNote.content.trim().length > 10) {
|
||||
if (!trimmed && note.content && note.content.trim().length > 10) {
|
||||
console.warn('Force-save blocked: refusing to overwrite note with empty content');
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
await saveNote($activeNotePath, $activeNote.meta, body);
|
||||
$editorDirty = false;
|
||||
await queueSave(() => saveNote(path, note.meta, body));
|
||||
if ($activeNotePath === path) $editorDirty = false;
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('Save failed:', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function moveOpenNoteToTrash(): Promise<boolean> {
|
||||
if (trashingNote || !$activeNotePath || $viewerNote) return false;
|
||||
const path = $activeNotePath;
|
||||
const wasDirty = $editorDirty;
|
||||
trashingNote = true;
|
||||
try {
|
||||
// Drain any save already in flight, then persist the latest editor state.
|
||||
await saveQueue;
|
||||
if (wasDirty && !(await forceSave(true))) return false;
|
||||
if ($activeNotePath !== path) return false;
|
||||
// A queued debounce observes this flag and cannot recreate the moved note.
|
||||
$editorDirty = false;
|
||||
return await onMoveToTrash(path);
|
||||
} finally {
|
||||
trashingNote = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6050,6 +6090,13 @@
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if isMobile}
|
||||
<button type="button" class="icon-btn editor-trash-btn mobile" onclick={moveOpenNoteToTrash} disabled={trashingNote} title="Move to Trash" aria-label="Move note to Trash">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 6h18"/><path d="M19 6v14H5V6"/><path d="M8 6V4h8v2"/><path d="M10 11v5"/><path d="M14 11v5"/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
{#if !isMobile}
|
||||
<div class="toolbar-actions">
|
||||
{#if $canGoBack || $canGoForward}
|
||||
@@ -6198,6 +6245,11 @@
|
||||
<path d="M5.854 4.854a.5.5 0 10-.708-.708l-3.5 3.5a.5.5 0 000 .708l3.5 3.5a.5.5 0 00.708-.708L2.707 8l3.147-3.146zm4.292 0a.5.5 0 01.708-.708l3.5 3.5a.5.5 0 010 .708l-3.5 3.5a.5.5 0 01-.708-.708L13.293 8l-3.147-3.146z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="icon-btn editor-trash-btn" onclick={moveOpenNoteToTrash} disabled={trashingNote} title="Move to Trash" aria-label="Move note to Trash">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 6h18"/><path d="M19 6v14H5V6"/><path d="M8 6V4h8v2"/><path d="M10 11v5"/><path d="M14 11v5"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -8156,6 +8208,17 @@
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.editor-trash-btn:hover,
|
||||
.editor-trash-btn:focus-visible {
|
||||
color: var(--danger);
|
||||
background: color-mix(in srgb, var(--danger) 12%, transparent);
|
||||
}
|
||||
|
||||
.editor-trash-btn:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.icon-btn.active {
|
||||
color: var(--text-accent);
|
||||
background: var(--accent-light);
|
||||
@@ -11295,9 +11358,9 @@
|
||||
.editor-container.mobile .editor-toolbar {
|
||||
padding: 8px 16px 6px 16px;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 2px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toolbar-actions.mobile {
|
||||
@@ -11322,6 +11385,20 @@
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.editor-container.mobile .editor-trash-btn.mobile {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.editor-container.mobile .editor-trash-btn.mobile:active {
|
||||
color: var(--danger);
|
||||
background: color-mix(in srgb, var(--danger) 12%, transparent);
|
||||
}
|
||||
|
||||
.editor-container.mobile .editor-body-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
@@ -519,12 +519,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Move the currently open note to the trash (keyboard shortcut from the editor).
|
||||
export async function trashActiveNote() {
|
||||
if ($viewMode === 'trash' || !$activeNotePath) return;
|
||||
const note = $notes.find(n => n.path === $activeNotePath);
|
||||
if (note) await handleDelete(note);
|
||||
}
|
||||
|
||||
async function handleRestore(note: NoteEntry) {
|
||||
contextMenu = null;
|
||||
|
||||
Reference in New Issue
Block a user