feat(bookmarks): client plumbing — saver capture + bookmark api
This commit is contained in:
@@ -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"),
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user