mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-02 11:07:04 +00:00
feat(library): show last edited time on book cards
This commit is contained in:
1 parent
742e42d65b
commit
efd45cf61e
6 files changed
+75
-22
No files matched your search
@@ -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<PathBuf, String> {
|
||||
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<Vec<BookSummary>, 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)
|
||||
|
||||
@@ -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<BookSummary[]>([]);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [pendingDelete, setPendingDelete] = useState<BookSummary | null>(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 }) {
|
||||
>
|
||||
<span className="card-title">{b.title || "Untitled"}</span>
|
||||
{b.author && <span className="card-author">{b.author}</span>}
|
||||
{b.updatedAt > 0 && <span className="card-meta">Edited {relativeTime(b.updatedAt, now)}</span>}
|
||||
<RowMenu label="Book options" className="card-menu" onDelete={() => setPendingDelete(b)} />
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -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 }) {
|
||||
</span>
|
||||
)}
|
||||
{row.chapter.id === activeChapterId && (
|
||||
<span className="meta">Edited {formatEdited(row.chapter.updatedAt, now)}</span>
|
||||
<span className="meta">Edited {relativeTime(row.chapter.updatedAt, now)}</span>
|
||||
)}
|
||||
</span>
|
||||
<RowMenu
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface BookSummary {
|
||||
id: string;
|
||||
title: string;
|
||||
author: string;
|
||||
updatedAt: number;
|
||||
corrupt?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1566,6 +1566,19 @@ body.resizing .sidebar {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
right: 14px;
|
||||
bottom: 14px;
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--t-1);
|
||||
color: var(--ink-faint);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.card-action {
|
||||
background: transparent;
|
||||
border: 1px dashed var(--line-strong);
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
export function relativeTime(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" });
|
||||
}
|
||||
Reference in new issue
Block a user