From 14eb3c4427752d241474270642d6be86ba7ae78e Mon Sep 17 00:00:00 2001 From: PJ Date: Mon, 22 Jun 2026 13:16:33 -0400 Subject: [PATCH] fix(persist): flush pending save on navigate-away and quit Edits are saved on an 800ms debounce, so leaving a book within that window lost the latest changes. Flush a dirty book before openBook / closeBook swap state (covers All books, open-another, Cmd+N), and add a window close-request handler that saves before destroying on quit. Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk --- src-tauri/capabilities/default.json | 1 + src/App.tsx | 18 +++++++++++++++++- src/store/useBook.ts | 13 +++++++++++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 4ec5d4a..b8e3643 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -5,6 +5,7 @@ "windows": ["main"], "permissions": [ "core:default", + "core:window:allow-destroy", "opener:default", "dialog:default", "updater:default", diff --git a/src/App.tsx b/src/App.tsx index 4e39ac2..acb50f7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,11 @@ import { useEffect } from "react"; import { listen } from "@tauri-apps/api/event"; +import { getCurrentWindow } from "@tauri-apps/api/window"; import { Library } from "./components/Library"; import { EditorView } from "./components/EditorView"; import { useBook } from "./store/useBook"; import { isDesktop } from "./ipc"; -import { newBook } from "./library"; +import { newBook, saveBook } from "./library"; import { runExport } from "./export/run"; import { checkForUpdates } from "./updater"; @@ -27,6 +28,21 @@ function App() { }; }, []); + useEffect(() => { + if (!isDesktop) return; + const win = getCurrentWindow(); + const unlisten = win.onCloseRequested(async (event) => { + const { book, dirty } = useBook.getState(); + if (!book || !dirty) return; + event.preventDefault(); + await saveBook(book).catch(() => {}); + win.destroy(); + }); + return () => { + unlisten.then((stop) => stop()); + }; + }, []); + return book ? : ; } diff --git a/src/store/useBook.ts b/src/store/useBook.ts index 8b7f2b0..79414f5 100644 --- a/src/store/useBook.ts +++ b/src/store/useBook.ts @@ -13,6 +13,7 @@ import { normalizeBook, } from "../model/book"; import { loadActiveChapter, saveActiveChapter } from "../editor/positions"; +import { saveBook } from "../library"; export const COVER_ID = "__cover__"; @@ -41,7 +42,11 @@ interface BookState { markSaved: () => void; } -export const useBook = create((set) => ({ +function flushOutgoing(book: Book | null, dirty: boolean): void { + if (book && dirty) saveBook(book).catch(() => {}); +} + +export const useBook = create((set, get) => ({ book: null, activeChapterId: "", dirty: false, @@ -50,13 +55,17 @@ export const useBook = create((set) => ({ setExporting: (label) => set({ exporting: label }), setNotice: (message) => set({ notice: message }), openBook: (book) => { + flushOutgoing(get().book, get().dirty); const normalized = normalizeBook(book); const saved = loadActiveChapter(normalized.id); const valid = saved === COVER_ID || normalized.chapters.some((c) => c.id === saved); const activeChapterId = valid ? (saved as string) : normalized.chapters[0]?.id ?? ""; set({ book: normalized, activeChapterId, dirty: false }); }, - closeBook: () => set({ book: null, activeChapterId: "", dirty: false }), + closeBook: () => { + flushOutgoing(get().book, get().dirty); + set({ book: null, activeChapterId: "", dirty: false }); + }, setActiveChapter: (id) => set({ activeChapterId: id }), setChapterContent: (id, content) => set((s) =>