From b2c5c4a445e3391a7321967f76e964fa7e0b408f Mon Sep 17 00:00:00 2001 From: Fendy Date: Tue, 8 Sep 2026 01:04:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(bookmarks):=20client=20plumbing=20?= =?UTF-8?q?=E2=80=94=20saver=20capture=20+=20bookmark=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/client.ts | 9 ++++++++- frontend/src/api/types.ts | 8 ++++++++ frontend/src/lib/progress.ts | 4 ++++ frontend/src/lib/useProgress.ts | 12 ++++++++++-- frontend/src/pages/Reader.tsx | 2 +- 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 59d63e5..36fb65f 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -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, percent: number, keepalive = false) => apiFetch(`/books/${id}/progress`, { method: "PUT", body: { locator, percent }, keepalive }), + + listBookmarks: (id: number) => apiFetch(`/books/${id}/bookmarks`), + createBookmark: (id: number, locator: Record, percent: number, note: string) => + apiFetch(`/books/${id}/bookmarks`, { method: "POST", body: { locator, percent, note } }), + patchBookmark: (id: number, note: string) => + apiFetch(`/bookmarks/${id}`, { method: "PATCH", body: { note } }), + deleteBookmark: (id: number) => apiFetch(`/bookmarks/${id}`, { method: "DELETE" }), listProgress: () => apiFetch("/progress"), }; diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index a01845b..713f715 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -44,6 +44,14 @@ export interface ProgressRow { updated_at: string; } +export interface Bookmark { + id: number; + locator: Record; + percent: number; + note: string; + created_at: string; +} + export interface Me { id: number; username: string; diff --git a/frontend/src/lib/progress.ts b/frontend/src/lib/progress.ts index 05f006a..f8ff874 100644 --- a/frontend/src/lib/progress.ts +++ b/frontend/src/lib/progress.ts @@ -7,12 +7,14 @@ export type ProgressSend = ( export interface ProgressSaver { report: (locator: Record, percent: number) => void; flush: () => void; + at: () => { loc: Record; 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; pct: number } | null = null; let pending: { loc: Record; pct: number } | null = null; let timer: ReturnType | 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, }; } diff --git a/frontend/src/lib/useProgress.ts b/frontend/src/lib/useProgress.ts index eb96af4..fb997cc 100644 --- a/frontend/src/lib/useProgress.ts +++ b/frontend/src/lib/useProgress.ts @@ -13,10 +13,14 @@ export interface Chrome { export interface ReaderProps { book: Book; initialLocator?: Record; + initialPercent?: number; chrome: Chrome; } -export function useProgressSaver(bookId: number): ProgressSaver { +export function useProgressSaver( + bookId: number, + seed?: { locator?: Record; percent?: number }, +): ProgressSaver & { capture: () => { locator: Record; 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 }); } diff --git a/frontend/src/pages/Reader.tsx b/frontend/src/pages/Reader.tsx index 8509725..ef2635a 100644 --- a/frontend/src/pages/Reader.tsx +++ b/frontend/src/pages/Reader.tsx @@ -57,7 +57,7 @@ export default function Reader() { const R = READERS[book.format]; if (!R) return ; const row = (progQ.data ?? []).find((r) => r.library_id === book.library_id && r.path === book.path); - return ; + return ; })(); return (