From 4f4eff871c67eff4896f920d5768988c744b675f Mon Sep 17 00:00:00 2001 From: XingfenD Date: Mon, 7 Sep 2026 16:15:10 +0800 Subject: [PATCH] feat(web): epub reader via epubjs arraybuffer bootstrap with cfi progress --- web/src/pages/Reader.tsx | 2 +- web/src/readers/EpubReader.tsx | 102 +++++++++++++++++++++++++++++++++ web/src/types/shims.d.ts | 1 + 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 web/src/readers/EpubReader.tsx create mode 100644 web/src/types/shims.d.ts diff --git a/web/src/pages/Reader.tsx b/web/src/pages/Reader.tsx index a73e3f1..dfd537a 100644 --- a/web/src/pages/Reader.tsx +++ b/web/src/pages/Reader.tsx @@ -8,12 +8,12 @@ import { toast } from "../components/Toaster"; import { btn } from "../components/ui"; import type { ReaderProps } from "../lib/useProgress"; -// reader 任务逐个往里加:Task 7 cbz → Task 8 txt/md → Task 9 pdf → Task 10 epub const READERS: Partial>>> = { cbz: lazy(() => import("../readers/CbzReader")), txt: lazy(() => import("../readers/TextReader")), md: lazy(() => import("../readers/TextReader")), pdf: lazy(() => import("../readers/PdfReader")), + epub: lazy(() => import("../readers/EpubReader")), }; export default function Reader() { diff --git a/web/src/readers/EpubReader.tsx b/web/src/readers/EpubReader.tsx new file mode 100644 index 0000000..c8abc55 --- /dev/null +++ b/web/src/readers/EpubReader.tsx @@ -0,0 +1,102 @@ +import { useEffect, useRef, useState } from "react"; +import { apiRaw } from "../api/client"; +import { btn } from "../components/ui"; +import { useProgressSaver, type ReaderProps } from "../lib/useProgress"; + +// 仅声明用到的最小子集,避免依赖 epubjs 自带类型的完整性 +interface EpubLocation { + start?: { cfi?: string; sectionIndex?: number }; +} +interface EpubRendition { + display(target?: string): unknown; + next(): unknown; + prev(): unknown; + on(ev: "relocated", cb: (loc: EpubLocation) => void): void; + destroy(): void; +} +interface EpubBookLike { + ready: Promise; + spine?: unknown[]; + renderTo(el: HTMLElement, opts: Record): EpubRendition; + destroy(): void; +} + +async function loadEpub(): Promise<(data: Uint8Array) => EpubBookLike> { + const m = (await import("epubjs")) as { default?: unknown }; + return m.default as (data: Uint8Array) => EpubBookLike; +} + +export default function EpubReader({ book, initialLocator }: ReaderProps) { + const hostRef = useRef(null); + const rendRef = useRef(null); + const saver = useProgressSaver(book.id); + const [err, setErr] = useState(""); + const [nonce, setNonce] = useState(0); + const [ready, setReady] = useState(false); + + useEffect(() => { + let dead = false; + let bookObj: EpubBookLike | null = null; + setErr(""); + setReady(false); + (async () => { + const ePub = await loadEpub(); + const buf = await apiRaw(book.file_url ?? "").then((r) => r.arrayBuffer()); + if (dead) return; + bookObj = ePub(new Uint8Array(buf)); // 字节已在内存,内部 iframe 不再回源,绕开鉴权问题 + await bookObj.ready; + if (dead || !hostRef.current) return; + const spineLen = bookObj.spine?.length ?? 1; + const r = bookObj.renderTo(hostRef.current, { width: "100%", height: "100%", flow: "paginated" }); + rendRef.current = r; + r.on("relocated", (loc) => { + const cfi = loc.start?.cfi ?? ""; + const idx = loc.start?.sectionIndex ?? 0; + const pct = spineLen > 1 ? (idx + 1) / spineLen : 0.5; + saver.report(cfi ? { cfi } : {}, Math.min(1, pct)); + }); + const cfi = typeof initialLocator?.cfi === "string" ? (initialLocator.cfi as string) : undefined; + try { + r.display(cfi); // cfi 失效(书被替换等)时 epubjs 会抛 + } catch { + r.display(); + } + setReady(true); + })().catch((e) => !dead && setErr(e instanceof Error ? e.message : "EPUB 加载失败")); + return () => { + dead = true; + rendRef.current?.destroy(); + rendRef.current = null; + bookObj?.destroy(); + }; + }, [book.file_url, nonce]); // eslint-disable-line react-hooks/exhaustive-deps + + if (err) + return ( +
+
+

{err}

+ +
+
+ ); + + return ( +
+
+
+ {!ready && !err &&
EPUB 加载中…
} +
+
+ + +
+
+ ); +} diff --git a/web/src/types/shims.d.ts b/web/src/types/shims.d.ts new file mode 100644 index 0000000..71ef7c4 --- /dev/null +++ b/web/src/types/shims.d.ts @@ -0,0 +1 @@ +declare module "epubjs";