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 TOKEN_KEY = "booklib.token";
|
||||||
export const LOGOUT_EVENT = "booklib:logout";
|
export const LOGOUT_EVENT = "booklib:logout";
|
||||||
@@ -177,5 +177,12 @@ export const api = {
|
|||||||
|
|
||||||
putProgress: (id: number, locator: Record<string, unknown>, percent: number, keepalive = false) =>
|
putProgress: (id: number, locator: Record<string, unknown>, percent: number, keepalive = false) =>
|
||||||
apiFetch<void>(`/books/${id}/progress`, { method: "PUT", body: { locator, percent }, keepalive }),
|
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"),
|
listProgress: () => apiFetch<ProgressRow[]>("/progress"),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ export interface ProgressRow {
|
|||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Bookmark {
|
||||||
|
id: number;
|
||||||
|
locator: Record<string, unknown>;
|
||||||
|
percent: number;
|
||||||
|
note: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Me {
|
export interface Me {
|
||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ export type ProgressSend = (
|
|||||||
export interface ProgressSaver {
|
export interface ProgressSaver {
|
||||||
report: (locator: Record<string, unknown>, percent: number) => void;
|
report: (locator: Record<string, unknown>, percent: number) => void;
|
||||||
flush: () => void;
|
flush: () => void;
|
||||||
|
at: () => { loc: Record<string, unknown>; pct: number } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// spec §8:PUT 节流 5s(leading-edge + 尾部合并);关页兜底走 keepalive fetch
|
// spec §8:PUT 节流 5s(leading-edge + 尾部合并);关页兜底走 keepalive fetch
|
||||||
//(spec 原文 sendBeacon,但 beacon 无法带 Authorization,后端 Bearer-only,语义等价替换)
|
//(spec 原文 sendBeacon,但 beacon 无法带 Authorization,后端 Bearer-only,语义等价替换)
|
||||||
export function createProgressSaver(send: ProgressSend, intervalMs = 5000): ProgressSaver {
|
export function createProgressSaver(send: ProgressSend, intervalMs = 5000): ProgressSaver {
|
||||||
let lastKey = "";
|
let lastKey = "";
|
||||||
|
let cur: { loc: Record<string, unknown>; pct: number } | null = null;
|
||||||
let pending: { 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 timer: ReturnType<typeof setTimeout> | null = null;
|
||||||
let lastAt = 0;
|
let lastAt = 0;
|
||||||
@@ -31,6 +33,7 @@ export function createProgressSaver(send: ProgressSend, intervalMs = 5000): Prog
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
report(locator, percent) {
|
report(locator, percent) {
|
||||||
|
cur = { loc: locator, pct: percent };
|
||||||
pending = { loc: locator, pct: percent };
|
pending = { loc: locator, pct: percent };
|
||||||
if (timer) return; // 已有尾部定时器:只更新挂起值
|
if (timer) return; // 已有尾部定时器:只更新挂起值
|
||||||
const t = Date.now();
|
const t = Date.now();
|
||||||
@@ -53,5 +56,6 @@ export function createProgressSaver(send: ProgressSend, intervalMs = 5000): Prog
|
|||||||
lastAt = Date.now();
|
lastAt = Date.now();
|
||||||
doSend(true);
|
doSend(true);
|
||||||
},
|
},
|
||||||
|
at: () => cur,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,14 @@ export interface Chrome {
|
|||||||
export interface ReaderProps {
|
export interface ReaderProps {
|
||||||
book: Book;
|
book: Book;
|
||||||
initialLocator?: Record<string, unknown>;
|
initialLocator?: Record<string, unknown>;
|
||||||
|
initialPercent?: number;
|
||||||
chrome: Chrome;
|
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 qc = useQueryClient();
|
||||||
const saver = useMemo(
|
const saver = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -26,6 +30,10 @@ export function useProgressSaver(bookId: number): ProgressSaver {
|
|||||||
}),
|
}),
|
||||||
[bookId, qc],
|
[bookId, qc],
|
||||||
);
|
);
|
||||||
|
const capture = () => {
|
||||||
|
const cur = saver.at();
|
||||||
|
return cur ? { locator: cur.loc, percent: cur.pct } : { locator: seed?.locator ?? {}, percent: seed?.percent ?? 0 };
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onPageHide = () => saver.flush();
|
const onPageHide = () => saver.flush();
|
||||||
@@ -41,5 +49,5 @@ export function useProgressSaver(bookId: number): ProgressSaver {
|
|||||||
};
|
};
|
||||||
}, [saver]);
|
}, [saver]);
|
||||||
|
|
||||||
return saver;
|
return Object.assign(saver, { capture });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default function Reader() {
|
|||||||
const R = READERS[book.format];
|
const R = READERS[book.format];
|
||||||
if (!R) return <Msg text={`暂不支持的格式:${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);
|
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 (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user