From efd45cf61ef2d730a17382e33af01f6f44808a09 Mon Sep 17 00:00:00 2001 From: PJ Date: Mon, 10 Aug 2026 18:36:30 +0530 Subject: [PATCH] feat(library): show last edited time on book cards --- src-tauri/src/library.rs | 34 +++++++++++++++++++++++++++++++++- src/components/Library.tsx | 7 +++++++ src/components/Sidebar.tsx | 23 ++--------------------- src/library.ts | 1 + src/styles/app.css | 13 +++++++++++++ src/time.ts | 19 +++++++++++++++++++ 6 files changed, 75 insertions(+), 22 deletions(-) create mode 100644 src/time.ts diff --git a/src-tauri/src/library.rs b/src-tauri/src/library.rs index e44602c..44db948 100644 --- a/src-tauri/src/library.rs +++ b/src-tauri/src/library.rs @@ -1,13 +1,16 @@ use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; +use std::time::UNIX_EPOCH; use tauri::Manager; #[derive(serde::Serialize)] +#[serde(rename_all = "camelCase")] pub struct BookSummary { id: String, title: String, author: String, corrupt: bool, + updated_at: u64, } fn corrupt_summary(stem: &str) -> BookSummary { @@ -16,9 +19,33 @@ fn corrupt_summary(stem: &str) -> BookSummary { title: "Unreadable book".to_string(), author: String::new(), corrupt: true, + updated_at: 0, } } +fn file_modified_ms(path: &Path) -> u64 { + fs::metadata(path) + .and_then(|m| m.modified()) + .ok() + .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) + .map(|d| d.as_millis() as u64) + .unwrap_or(0) +} + +fn latest_chapter_ms(value: &serde_json::Value) -> u64 { + value + .get("chapters") + .and_then(|c| c.as_array()) + .map(|chapters| { + chapters + .iter() + .filter_map(|c| c.get("updatedAt").and_then(|v| v.as_u64())) + .max() + .unwrap_or(0) + }) + .unwrap_or(0) +} + pub(crate) fn app_data_dir(app: &tauri::AppHandle) -> Result { let dir = app.path().app_data_dir().map_err(|e| e.to_string())?; fs::create_dir_all(&dir).map_err(|e| e.to_string())?; @@ -81,11 +108,16 @@ pub fn list_books(app: tauri::AppHandle) -> Result, String> { .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); + let updated_at = match latest_chapter_ms(&value) { + 0 => file_modified_ms(&path), + ms => ms, + }; books.push(BookSummary { id: id.to_string(), title, author, corrupt: false, + updated_at, }); } Ok(books) diff --git a/src/components/Library.tsx b/src/components/Library.tsx index abe8177..dac5c4d 100644 --- a/src/components/Library.tsx +++ b/src/components/Library.tsx @@ -10,10 +10,12 @@ import { RowMenu } from "./RowMenu"; import { BackupButton } from "./BackupButton"; import { useBackup } from "../store/useBackup"; import { useBook } from "../store/useBook"; +import { relativeTime } from "../time"; export function Library({ onOpen }: { onOpen: (book: Book) => void }) { const [books, setBooks] = useState([]); const [loaded, setLoaded] = useState(false); + const [now, setNow] = useState(() => Date.now()); const [busy, setBusy] = useState(false); const [pendingDelete, setPendingDelete] = useState(null); const notice = useBook((s) => s.notice); @@ -30,6 +32,10 @@ export function Library({ onOpen }: { onOpen: (book: Book) => void }) { useEffect(() => { refresh(); }, []); + useEffect(() => { + const timer = setInterval(() => setNow(Date.now()), 60_000); + return () => clearInterval(timer); + }, []); useEffect(() => { if (restoreNonce) refresh(); }, [restoreNonce]); @@ -125,6 +131,7 @@ export function Library({ onOpen }: { onOpen: (book: Book) => void }) { > {b.title || "Untitled"} {b.author && {b.author}} + {b.updatedAt > 0 && Edited {relativeTime(b.updatedAt, now)}} setPendingDelete(b)} /> ), diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 65b4677..dcb174d 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -5,6 +5,7 @@ import { AddPageMenu } from "./AddPageMenu"; import { ConfirmDialog } from "./ConfirmDialog"; import { Icon } from "./Icon"; import { RowMenu } from "./RowMenu"; +import { relativeTime } from "../time"; interface Row { chapter: Chapter; @@ -19,26 +20,6 @@ interface DropTarget { index: number; } -function formatEdited(ts: number, now: number): string { - const diff = Math.max(0, now - ts); - const min = 60_000; - const hour = 60 * min; - const day = 24 * hour; - if (diff < min) return "just now"; - if (diff < hour) { - const m = Math.floor(diff / min); - return `${m} minute${m === 1 ? "" : "s"} ago`; - } - if (diff < day) { - const h = Math.floor(diff / hour); - return `${h} hour${h === 1 ? "" : "s"} ago`; - } - if (diff < 2 * day) return "yesterday"; - const d = Math.floor(diff / day); - if (d < 7) return `${d} days ago`; - return new Date(ts).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }); -} - export function Sidebar({ onNavigate }: { onNavigate?: () => void }) { const chapters = useBook((s) => s.book?.chapters ?? []); const activeChapterId = useBook((s) => s.activeChapterId); @@ -254,7 +235,7 @@ export function Sidebar({ onNavigate }: { onNavigate?: () => void }) { )} {row.chapter.id === activeChapterId && ( - Edited {formatEdited(row.chapter.updatedAt, now)} + Edited {relativeTime(row.chapter.updatedAt, now)} )}