From b63951f81496554db3c2af8dd18a43086aa98744 Mon Sep 17 00:00:00 2001 From: PJ Date: Mon, 22 Jun 2026 13:31:04 -0400 Subject: [PATCH] fix(library): surface unreadable books list_books silently skipped any .margin file it could not read, parse, or that lacked an id, so a truncated manuscript just vanished. Emit a corrupt placeholder entry instead and show it in the library as a non-openable card with a recovery hint pointing at the .bak backup. Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk --- src-tauri/src/library.rs | 23 +++++++++++++++++++++-- src/components/Library.tsx | 30 +++++++++++++++++++----------- src/library.ts | 1 + src/styles/app.css | 10 ++++++++++ 4 files changed, 51 insertions(+), 13 deletions(-) diff --git a/src-tauri/src/library.rs b/src-tauri/src/library.rs index 98a93e0..236107f 100644 --- a/src-tauri/src/library.rs +++ b/src-tauri/src/library.rs @@ -7,6 +7,16 @@ pub struct BookSummary { id: String, title: String, author: String, + corrupt: bool, +} + +fn corrupt_summary(stem: &str) -> BookSummary { + BookSummary { + id: stem.to_string(), + title: "Unreadable book".to_string(), + author: String::new(), + corrupt: true, + } } fn library_dir(app: &tauri::AppHandle) -> Result { @@ -31,16 +41,24 @@ pub fn list_books(app: tauri::AppHandle) -> Result, String> { if path.extension().and_then(|e| e.to_str()) != Some("margin") { continue; } + let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or(""); let contents = match fs::read_to_string(&path) { Ok(contents) => contents, - Err(_) => continue, + Err(_) => { + books.push(corrupt_summary(stem)); + continue; + } }; let value: serde_json::Value = match serde_json::from_str(&contents) { Ok(value) => value, - Err(_) => continue, + Err(_) => { + books.push(corrupt_summary(stem)); + continue; + } }; let id = value.get("id").and_then(|v| v.as_str()).unwrap_or(""); if id.is_empty() { + books.push(corrupt_summary(stem)); continue; } let metadata = value.get("metadata"); @@ -58,6 +76,7 @@ pub fn list_books(app: tauri::AppHandle) -> Result, String> { id: id.to_string(), title, author, + corrupt: false, }); } Ok(books) diff --git a/src/components/Library.tsx b/src/components/Library.tsx index 038c0dc..40a3bf9 100644 --- a/src/components/Library.tsx +++ b/src/components/Library.tsx @@ -78,17 +78,25 @@ export function Library({ onOpen }: { onOpen: (book: Book) => void }) { Example )} - {books.map((b) => ( -
loadBook(b.id).then(onOpen).catch((e) => setNotice(`Could not open book: ${e}`))} - > - {b.title || "Untitled"} - {b.author && {b.author}} - setPendingDelete(b)} /> -
- ))} + {books.map((b) => + b.corrupt ? ( +
+ {b.title} + Couldn't be read; a .bak backup may sit beside it. + setPendingDelete(b)} /> +
+ ) : ( +
loadBook(b.id).then(onOpen).catch((e) => setNotice(`Could not open book: ${e}`))} + > + {b.title || "Untitled"} + {b.author && {b.author}} + setPendingDelete(b)} /> +
+ ), + )} {notice && (
setNotice(null)}> diff --git a/src/library.ts b/src/library.ts index 7f39c92..cdd84ef 100644 --- a/src/library.ts +++ b/src/library.ts @@ -6,6 +6,7 @@ export interface BookSummary { id: string; title: string; author: string; + corrupt?: boolean; } export async function listBooks(): Promise { diff --git a/src/styles/app.css b/src/styles/app.css index e3cd46e..b4a1ae0 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -1209,6 +1209,16 @@ body.resizing { box-shadow: 0 4px 10px rgba(35, 32, 27, 0.1), 0 20px 38px rgba(35, 32, 27, 0.13); } +.card-corrupt { + cursor: default; + opacity: 0.65; + border-style: dashed; +} + +.card-corrupt:hover { + transform: none; +} + .card-title { font-family: var(--font-book); font-size: 20px;