From 4e5ea6d855355d883015603a3f9157aa12b3f59e Mon Sep 17 00:00:00 2001 From: PJ Date: Mon, 22 Jun 2026 13:29:29 -0400 Subject: [PATCH] fix(ui): handle library-open errors Opening a book card swallowed failures: a corrupt or unreadable file failed silently and normalizeBook could throw on a missing chapters array. Surface load errors as a toast, throw a clear message on unparseable JSON, and make normalizeBook tolerant of missing chapters, metadata and settings. Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk --- src/components/Library.tsx | 6 +++++- src/library.ts | 6 +++++- src/model/book.ts | 14 +++++++++++--- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/components/Library.tsx b/src/components/Library.tsx index ccdee2a..038c0dc 100644 --- a/src/components/Library.tsx +++ b/src/components/Library.tsx @@ -79,7 +79,11 @@ export function Library({ onOpen }: { onOpen: (book: Book) => void }) { )} {books.map((b) => ( -
loadBook(b.id).then(onOpen)}> +
loadBook(b.id).then(onOpen).catch((e) => setNotice(`Could not open book: ${e}`))} + > {b.title || "Untitled"} {b.author && {b.author}} setPendingDelete(b)} /> diff --git a/src/library.ts b/src/library.ts index e329473..7f39c92 100644 --- a/src/library.ts +++ b/src/library.ts @@ -15,7 +15,11 @@ export async function listBooks(): Promise { export async function loadBook(id: string): Promise { const contents = await invoke("load_book", { id }); - return JSON.parse(contents) as Book; + try { + return JSON.parse(contents) as Book; + } catch { + throw new Error("the book file is corrupt or unreadable"); + } } export async function saveBook(book: Book): Promise { diff --git a/src/model/book.ts b/src/model/book.ts index 5db51dc..dc8017e 100644 --- a/src/model/book.ts +++ b/src/model/book.ts @@ -140,11 +140,19 @@ export function createCover(): Cover { return { kind: "default", image: "", bg: COVER_PALETTES[0].bg, ink: COVER_PALETTES[0].ink }; } +const DEFAULT_METADATA: BookMetadata = { title: "Untitled", subtitle: "", author: "", isbn: "", language: "en" }; +const DEFAULT_SETTINGS: BookSettings = { trim: "6x9", bleed: true }; + export function normalizeBook(book: Book): Book { + const chapters = (Array.isArray(book.chapters) ? book.chapters : []).filter(Boolean); return { ...book, + metadata: { ...DEFAULT_METADATA, ...book.metadata }, + settings: { ...DEFAULT_SETTINGS, ...book.settings }, cover: book.cover ? { ...createCover(), ...book.cover } : createCover(), - chapters: book.chapters.map((c) => (c.updatedAt ? c : { ...c, updatedAt: Date.now() })), + chapters: (chapters.length ? chapters : [createChapter()]).map((c) => + c.updatedAt ? c : { ...c, updatedAt: Date.now() }, + ), }; } @@ -152,9 +160,9 @@ export function createBook(): Book { return { schema: "margin/1", id: crypto.randomUUID(), - metadata: { title: "Untitled", subtitle: "", author: "", isbn: "", language: "en" }, + metadata: { ...DEFAULT_METADATA }, theme: "quiet-press", - settings: { trim: "6x9", bleed: true }, + settings: { ...DEFAULT_SETTINGS }, cover: createCover(), chapters: [createChapter("Chapter One")], };