feat(bookmarks): client plumbing — saver capture + bookmark api

This commit is contained in:
2026-09-08 01:04:44 +08:00
parent 0fe3bc3057
commit b2c5c4a445
5 changed files with 31 additions and 4 deletions
+8 -1
View File
@@ -1,4 +1,4 @@
import type { Book, Library, Me, ProgressRow, User } from "./types";
import type { Book, Bookmark, Library, Me, ProgressRow, User } from "./types";
export const TOKEN_KEY = "booklib.token";
export const LOGOUT_EVENT = "booklib:logout";
@@ -177,5 +177,12 @@ export const api = {
putProgress: (id: number, locator: Record<string, unknown>, percent: number, keepalive = false) =>
apiFetch<void>(`/books/${id}/progress`, { method: "PUT", body: { locator, percent }, keepalive }),
listBookmarks: (id: number) => apiFetch<Bookmark[]>(`/books/${id}/bookmarks`),
createBookmark: (id: number, locator: Record<string, unknown>, percent: number, note: string) =>
apiFetch<Bookmark>(`/books/${id}/bookmarks`, { method: "POST", body: { locator, percent, note } }),
patchBookmark: (id: number, note: string) =>
apiFetch<void>(`/bookmarks/${id}`, { method: "PATCH", body: { note } }),
deleteBookmark: (id: number) => apiFetch<void>(`/bookmarks/${id}`, { method: "DELETE" }),
listProgress: () => apiFetch<ProgressRow[]>("/progress"),
};
+8
View File
@@ -44,6 +44,14 @@ export interface ProgressRow {
updated_at: string;
}
export interface Bookmark {
id: number;
locator: Record<string, unknown>;
percent: number;
note: string;
created_at: string;
}
export interface Me {
id: number;
username: string;
+4
View File
@@ -7,12 +7,14 @@ export type ProgressSend = (
export interface ProgressSaver {
report: (locator: Record<string, unknown>, percent: number) => void;
flush: () => void;
at: () => { loc: Record<string, unknown>; pct: number } | null;
}
// spec §8:PUT 节流 5s(leading-edge + 尾部合并);关页兜底走 keepalive fetch
//(spec 原文 sendBeacon,但 beacon 无法带 Authorization,后端 Bearer-only,语义等价替换)
export function createProgressSaver(send: ProgressSend, intervalMs = 5000): ProgressSaver {
let lastKey = "";
let cur: { loc: Record<string, unknown>; pct: number } | null = null;
let pending: { loc: Record<string, unknown>; pct: number } | null = null;
let timer: ReturnType<typeof setTimeout> | null = null;
let lastAt = 0;
@@ -31,6 +33,7 @@ export function createProgressSaver(send: ProgressSend, intervalMs = 5000): Prog
return {
report(locator, percent) {
cur = { loc: locator, pct: percent };
pending = { loc: locator, pct: percent };
if (timer) return; // 已有尾部定时器:只更新挂起值
const t = Date.now();
@@ -53,5 +56,6 @@ export function createProgressSaver(send: ProgressSend, intervalMs = 5000): Prog
lastAt = Date.now();
doSend(true);
},
at: () => cur,
};
}
+10 -2
View File
@@ -13,10 +13,14 @@ export interface Chrome {
export interface ReaderProps {
book: Book;
initialLocator?: Record<string, unknown>;
initialPercent?: number;
chrome: Chrome;
}
export function useProgressSaver(bookId: number): ProgressSaver {
export function useProgressSaver(
bookId: number,
seed?: { locator?: Record<string, unknown>; percent?: number },
): ProgressSaver & { capture: () => { locator: Record<string, unknown>; percent: number } } {
const qc = useQueryClient();
const saver = useMemo(
() =>
@@ -26,6 +30,10 @@ export function useProgressSaver(bookId: number): ProgressSaver {
}),
[bookId, qc],
);
const capture = () => {
const cur = saver.at();
return cur ? { locator: cur.loc, percent: cur.pct } : { locator: seed?.locator ?? {}, percent: seed?.percent ?? 0 };
};
useEffect(() => {
const onPageHide = () => saver.flush();
@@ -41,5 +49,5 @@ export function useProgressSaver(bookId: number): ProgressSaver {
};
}, [saver]);
return saver;
return Object.assign(saver, { capture });
}
+1 -1
View File
@@ -57,7 +57,7 @@ export default function Reader() {
const R = READERS[book.format];
if (!R) return <Msg text={`暂不支持的格式:${book.format}`} />;
const row = (progQ.data ?? []).find((r) => r.library_id === book.library_id && r.path === book.path);
return <R book={book} initialLocator={row?.locator} chrome={chrome} />;
return <R book={book} initialLocator={row?.locator} initialPercent={row?.percent} chrome={chrome} />;
})();
return (