mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-02 19:17:03 +00:00
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
This commit is contained in:
1 parent
aa75b3ebda
commit
b63951f814
4 files changed
+42
-4
No files matched your search
@@ -7,6 +7,16 @@ pub struct BookSummary {
|
|||||||
id: String,
|
id: String,
|
||||||
title: String,
|
title: String,
|
||||||
author: 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<PathBuf, String> {
|
fn library_dir(app: &tauri::AppHandle) -> Result<PathBuf, String> {
|
||||||
@@ -31,16 +41,24 @@ pub fn list_books(app: tauri::AppHandle) -> Result<Vec<BookSummary>, String> {
|
|||||||
if path.extension().and_then(|e| e.to_str()) != Some("margin") {
|
if path.extension().and_then(|e| e.to_str()) != Some("margin") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
|
||||||
let contents = match fs::read_to_string(&path) {
|
let contents = match fs::read_to_string(&path) {
|
||||||
Ok(contents) => contents,
|
Ok(contents) => contents,
|
||||||
Err(_) => continue,
|
Err(_) => {
|
||||||
|
books.push(corrupt_summary(stem));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let value: serde_json::Value = match serde_json::from_str(&contents) {
|
let value: serde_json::Value = match serde_json::from_str(&contents) {
|
||||||
Ok(value) => value,
|
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("");
|
let id = value.get("id").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
if id.is_empty() {
|
if id.is_empty() {
|
||||||
|
books.push(corrupt_summary(stem));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let metadata = value.get("metadata");
|
let metadata = value.get("metadata");
|
||||||
@@ -58,6 +76,7 @@ pub fn list_books(app: tauri::AppHandle) -> Result<Vec<BookSummary>, String> {
|
|||||||
id: id.to_string(),
|
id: id.to_string(),
|
||||||
title,
|
title,
|
||||||
author,
|
author,
|
||||||
|
corrupt: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(books)
|
Ok(books)
|
||||||
|
|||||||
@@ -78,7 +78,14 @@ export function Library({ onOpen }: { onOpen: (book: Book) => void }) {
|
|||||||
<span className="card-badge">Example</span>
|
<span className="card-badge">Example</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{books.map((b) => (
|
{books.map((b) =>
|
||||||
|
b.corrupt ? (
|
||||||
|
<div key={b.id} className="card card-book card-corrupt">
|
||||||
|
<span className="card-title">{b.title}</span>
|
||||||
|
<span className="card-author">Couldn't be read; a .bak backup may sit beside it.</span>
|
||||||
|
<RowMenu label="Book options" className="card-menu" onDelete={() => setPendingDelete(b)} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
key={b.id}
|
key={b.id}
|
||||||
className="card card-book"
|
className="card card-book"
|
||||||
@@ -88,7 +95,8 @@ export function Library({ onOpen }: { onOpen: (book: Book) => void }) {
|
|||||||
{b.author && <span className="card-author">{b.author}</span>}
|
{b.author && <span className="card-author">{b.author}</span>}
|
||||||
<RowMenu label="Book options" className="card-menu" onDelete={() => setPendingDelete(b)} />
|
<RowMenu label="Book options" className="card-menu" onDelete={() => setPendingDelete(b)} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
),
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{notice && (
|
{notice && (
|
||||||
<div className="toast" onClick={() => setNotice(null)}>
|
<div className="toast" onClick={() => setNotice(null)}>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface BookSummary {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
author: string;
|
author: string;
|
||||||
|
corrupt?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listBooks(): Promise<BookSummary[]> {
|
export async function listBooks(): Promise<BookSummary[]> {
|
||||||
|
|||||||
@@ -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);
|
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 {
|
.card-title {
|
||||||
font-family: var(--font-book);
|
font-family: var(--font-book);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
|||||||
Reference in new issue
Block a user