mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-07-25 08:15:55 +02:00
v1.1.8 - Fix image paste persistence, save-flush on note switch, WebKitGTK clipboard fallback
This commit is contained in:
@@ -164,6 +164,10 @@ export async function saveVaultState(vaultState: VaultState): Promise<void> {
|
||||
return invoke("save_vault_state", { vaultState });
|
||||
}
|
||||
|
||||
export async function readClipboardImage(): Promise<number[]> {
|
||||
return invoke("read_clipboard_image");
|
||||
}
|
||||
|
||||
export async function saveImage(name: string, data: number[]): Promise<string> {
|
||||
return invoke("save_image", { name, data });
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
noteHistoryIndex = newIndex;
|
||||
navigatingFromHistory = true;
|
||||
readNote(path).then((content) => {
|
||||
editor?.flushSave();
|
||||
$activeNote = content;
|
||||
$activeNotePath = path;
|
||||
$editorDirty = false;
|
||||
@@ -114,6 +115,7 @@
|
||||
if (!vaultRoot || !filePath.startsWith(vaultRoot)) return;
|
||||
try {
|
||||
const content = await readNote(filePath);
|
||||
editor?.flushSave();
|
||||
$activeNote = content;
|
||||
$activeNotePath = filePath;
|
||||
$editorDirty = false;
|
||||
@@ -167,6 +169,7 @@
|
||||
try {
|
||||
const entry = await createDailyNote();
|
||||
const content = await readNote(entry.path);
|
||||
editor?.flushSave();
|
||||
$activeNote = content;
|
||||
$activeNotePath = entry.path;
|
||||
$editorDirty = false;
|
||||
@@ -391,7 +394,7 @@
|
||||
<Sidebar bind:this={sidebar} onViewChanged={handleViewChanged} />
|
||||
</div>
|
||||
<div class="mobile-panel" class:active={$mobileView === 'notelist'}>
|
||||
<NoteList bind:this={noteList} onNoteSelected={handleNoteSelected} onNoteMoved={() => sidebar?.refresh()} />
|
||||
<NoteList bind:this={noteList} onNoteSelected={handleNoteSelected} onBeforeNoteSwitch={() => editor?.flushSave()} onNoteMoved={() => sidebar?.refresh()} />
|
||||
</div>
|
||||
<div class="mobile-panel" class:active={$mobileView === 'editor'}>
|
||||
<Editor bind:this={editor} />
|
||||
@@ -485,7 +488,7 @@
|
||||
{/if}
|
||||
|
||||
<div class="notelist-panel" style="width: {$notelistWidth}px">
|
||||
<NoteList bind:this={noteList} onNoteSelected={handleNoteSelected} onNoteMoved={() => sidebar?.refresh()} />
|
||||
<NoteList bind:this={noteList} onNoteSelected={handleNoteSelected} onBeforeNoteSwitch={() => editor?.flushSave()} onNoteMoved={() => sidebar?.refresh()} />
|
||||
</div>
|
||||
|
||||
<ResizeHandle onResize={handleNotelistResize} />
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
import { openFile, copyFileTo } from '$lib/api';
|
||||
import { save as saveDialog } from '@tauri-apps/plugin-dialog';
|
||||
import { activeNote, activeNotePath, appConfig, editorDirty, sourceMode, focusMode, readOnly, quickAccessPaths, notes } from '$lib/stores/app';
|
||||
import { saveNote, saveImage, saveAttachment, addQuickAccess, removeQuickAccess, getQuickAccess, getNoteVersions, getNoteVersionContent, createVersion, aiAsk, getAllNoteTitles, readNote, renameNote } from '$lib/api';
|
||||
import { saveNote, saveImage, saveAttachment, readClipboardImage, addQuickAccess, removeQuickAccess, getQuickAccess, getNoteVersions, getNoteVersionContent, createVersion, aiAsk, getAllNoteTitles, readNote, renameNote } from '$lib/api';
|
||||
import type { VersionEntry, AiStreamEvent, NoteTitleEntry } from '$lib/types';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { debounce } from '$lib/utils/debounce';
|
||||
@@ -1203,22 +1203,25 @@
|
||||
});
|
||||
}
|
||||
|
||||
export function loadNote(path: string, content: string) {
|
||||
// Flush any unsaved changes for the CURRENT note before loading the new one
|
||||
if ($editorDirty && $activeNote && $activeNotePath && $activeNotePath !== path) {
|
||||
try {
|
||||
const body = $sourceMode
|
||||
? restoreTitleH1(sourceContent)
|
||||
: editorToMarkdown();
|
||||
const trimmed = body.replace(/^#.*\n?/, '').trim();
|
||||
if (trimmed || !$activeNote.content || $activeNote.content.trim().length <= 10) {
|
||||
saveNote($activeNotePath, $activeNote.meta, body);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Pre-switch save failed:', e);
|
||||
/** Flush unsaved editor content to disk (synchronous serialize + fire-and-forget save).
|
||||
* Call BEFORE updating $activeNote/$activeNotePath stores when switching notes. */
|
||||
export function flushSave() {
|
||||
if (!$editorDirty || !$activeNote || !$activeNotePath) return;
|
||||
try {
|
||||
const body = $sourceMode
|
||||
? restoreTitleH1(sourceContent)
|
||||
: editorToMarkdown();
|
||||
const trimmed = body.replace(/^#.*\n?/, '').trim();
|
||||
if (trimmed || !$activeNote.content || $activeNote.content.trim().length <= 10) {
|
||||
saveNote($activeNotePath, $activeNote.meta, body);
|
||||
}
|
||||
$editorDirty = false;
|
||||
} catch (e) {
|
||||
console.error('Pre-switch save failed:', e);
|
||||
}
|
||||
$editorDirty = false;
|
||||
}
|
||||
|
||||
export function loadNote(path: string, content: string) {
|
||||
loadedPath = path;
|
||||
lastSourceMode = $sourceMode;
|
||||
isLoadingNote = true;
|
||||
@@ -2018,21 +2021,7 @@
|
||||
});
|
||||
|
||||
function destroyEditor() {
|
||||
// Flush unsaved changes before destroying
|
||||
if ($editorDirty && $activeNote && $activeNotePath && editor) {
|
||||
try {
|
||||
const body = $sourceMode
|
||||
? restoreTitleH1(sourceContent)
|
||||
: editorToMarkdown();
|
||||
const trimmed = body.replace(/^#.*\n?/, '').trim();
|
||||
if (trimmed || !$activeNote.content || $activeNote.content.trim().length <= 10) {
|
||||
saveNote($activeNotePath, $activeNote.meta, body);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Destroy-save failed:', e);
|
||||
}
|
||||
$editorDirty = false;
|
||||
}
|
||||
flushSave();
|
||||
if (editor) {
|
||||
editor.destroy();
|
||||
editor = null;
|
||||
@@ -2757,9 +2746,34 @@
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// WebKitGTK fallback (bug #218519): older WebKitGTK versions return
|
||||
// empty DataTransferItemList for image pastes. Detect this and read
|
||||
// the image directly from the system clipboard via Rust/arboard.
|
||||
if (items.length === 0) {
|
||||
const hasText = event.clipboardData!.getData('text/plain');
|
||||
const hasHtml = event.clipboardData!.getData('text/html');
|
||||
if (!hasText && !hasHtml) {
|
||||
event.preventDefault();
|
||||
insertClipboardImage();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function insertClipboardImage() {
|
||||
try {
|
||||
const data = await readClipboardImage();
|
||||
const relativePath = await saveImage('pasted-image.png', data);
|
||||
if (editor) {
|
||||
const displaySrc = resolveImageSrc(relativePath);
|
||||
editor.chain().focus().setImage({ src: displaySrc }).run();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Clipboard image fallback failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function insertImage(file: File) {
|
||||
try {
|
||||
const buffer = await file.arrayBuffer();
|
||||
|
||||
@@ -35,9 +35,10 @@
|
||||
import { openNoteWindow } from '$lib/utils/window';
|
||||
import type { NoteEntry, SortMode } from '$lib/types';
|
||||
|
||||
let { onNoteSelected = (_path: string, _content: string) => {}, onNoteMoved = () => {} }: {
|
||||
let { onNoteSelected = (_path: string, _content: string) => {}, onNoteMoved = () => {}, onBeforeNoteSwitch = () => {} }: {
|
||||
onNoteSelected?: (path: string, content: string) => void;
|
||||
onNoteMoved?: () => void;
|
||||
onBeforeNoteSwitch?: () => void;
|
||||
} = $props();
|
||||
|
||||
const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl';
|
||||
@@ -184,6 +185,7 @@
|
||||
if ($activeNotePath === note.path) return;
|
||||
try {
|
||||
const content = await readNote(note.path);
|
||||
onBeforeNoteSwitch();
|
||||
$activeNote = content;
|
||||
$activeNotePath = note.path;
|
||||
$editorDirty = false;
|
||||
|
||||
Reference in New Issue
Block a user