From 4aacac12da7ad183d8ff58e1d15996029bca2913 Mon Sep 17 00:00:00 2001 From: Git Agent Date: Sun, 23 Aug 2026 16:24:02 +0200 Subject: [PATCH] =?UTF-8?q?fix(web):=20lecteurs=20EPUB/PDF=20=E2=80=94=20g?= =?UTF-8?q?estion=20d'erreurs,=20worker=20pdf.js=20local?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Composant ReaderError avec diagnostic et actions de repli, worker pdf.js servi localement (pdfWorker) pour éviter les CDN, typage du view foliate, navigation retour vers la fiche livre et styles lecteur associés. Co-Authored-By: Claude Opus 4.8 --- apps/web/src/pages/ReaderPage.tsx | 7 +- apps/web/src/reader/EpubReader.tsx | 144 ++++++++++++++++++---- apps/web/src/reader/PdfReader.tsx | 65 +++++++--- apps/web/src/reader/ReaderError.tsx | 53 ++++++++ apps/web/src/reader/pdfWorker.ts | 1 + apps/web/src/reader/readerRuntime.test.ts | 20 +++ apps/web/src/styles/app.css | 71 +++++++++-- apps/web/src/vite-env.d.ts | 5 + 8 files changed, 316 insertions(+), 50 deletions(-) create mode 100644 apps/web/src/reader/ReaderError.tsx create mode 100644 apps/web/src/reader/pdfWorker.ts create mode 100644 apps/web/src/reader/readerRuntime.test.ts diff --git a/apps/web/src/pages/ReaderPage.tsx b/apps/web/src/pages/ReaderPage.tsx index de88794..aa2adfa 100644 --- a/apps/web/src/pages/ReaderPage.tsx +++ b/apps/web/src/pages/ReaderPage.tsx @@ -41,6 +41,7 @@ export function ReaderPage({ bookId }: { bookId: number }) { }, [progress]); const fileUrl = useMemo(() => api.bookFileUrl(bookId), [bookId]); + const backHref = useMemo(() => (book ? `/book/${book.id}` : "/home"), [book]); const savePdfPage = useCallback( (nextPage: number, pages: number) => { setPage(nextPage); @@ -61,7 +62,7 @@ export function ReaderPage({ bookId }: { bookId: number }) { return (
- @@ -87,11 +88,11 @@ export function ReaderPage({ bookId }: { bookId: number }) {
) : book.format === "pdf" ? ( - + ) : book.format === "cbz" || book.format === "cbr" ? ( ) : ( - + )} ); diff --git a/apps/web/src/reader/EpubReader.tsx b/apps/web/src/reader/EpubReader.tsx index 3b16cb1..2c089f3 100644 --- a/apps/web/src/reader/EpubReader.tsx +++ b/apps/web/src/reader/EpubReader.tsx @@ -1,43 +1,145 @@ import { useEffect, useRef, useState } from "react"; +import { ArrowLeft, ArrowRight } from "lucide-react"; +import { ReaderError, readerErrorMessage } from "./ReaderError"; -type FoliateModule = { - EPUB?: unknown; - default?: unknown; +type FoliateLocation = { + cfi?: string; + fraction?: number; + current?: number; + total?: number; }; -export function EpubReader({ url, locator, onLocatorChange }: { url: string; locator?: string; onLocatorChange: (locator: string, percent: number) => void }) { +type FoliateView = HTMLElement & { + open(input: File | Blob | string): Promise; + close(): void; + goLeft(): Promise; + goRight(): Promise; + goTo(target: string): Promise; + next(): Promise; + lastLocation?: FoliateLocation; +}; + +export function epubFileName(url: string): string { + try { + const base = globalThis.location?.href ?? "http://readabook.local/"; + const pathname = new URL(url, base).pathname; + const name = pathname.split("/").filter(Boolean).at(-1); + return name && name.includes(".") ? name : "book.epub"; + } catch { + return "book.epub"; + } +} + +function locationPercent(location: FoliateLocation): number { + if (typeof location.fraction === "number") return Math.max(0, Math.min(100, location.fraction * 100)); + if (typeof location.current === "number" && typeof location.total === "number" && location.total > 0) { + return Math.max(0, Math.min(100, (location.current / location.total) * 100)); + } + return 1; +} + +export function EpubReader({ + url, + locator, + backHref, + onLocatorChange +}: { + url: string; + locator?: string; + backHref: string; + onLocatorChange: (locator: string, percent: number) => void; +}) { const hostRef = useRef(null); - const [frameKey, setFrameKey] = useState(0); - const [status, setStatus] = useState("Ouverture EPUB"); + const viewRef = useRef(null); + const locatorRef = useRef(locator); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(); + const [attempt, setAttempt] = useState(0); + + useEffect(() => { + locatorRef.current = locator; + }, [locator]); useEffect(() => { let cancelled = false; + let view: FoliateView | null = null; + async function mount() { try { - const module = (await import("foliate-js/epub.js")) as FoliateModule; + setLoading(true); + setError(undefined); + await import("foliate-js/view.js"); if (cancelled || !hostRef.current) return; - hostRef.current.dataset.engine = module.EPUB || module.default ? "foliate-js" : "fallback"; - setStatus("EPUB pret"); - } catch { - setStatus("Apercu EPUB indisponible dans ce navigateur"); + + const response = await fetch(url, { credentials: "include" }); + if (!response.ok) throw new Error(`${response.status} ${response.statusText}`); + const blob = await response.blob(); + if (cancelled || !hostRef.current) return; + + view = document.createElement("foliate-view") as FoliateView; + view.classList.add("epub-view"); + view.addEventListener("relocate", (event) => { + const location = (event as CustomEvent).detail; + if (location?.cfi) onLocatorChange(location.cfi, locationPercent(location)); + }); + hostRef.current.replaceChildren(view); + viewRef.current = view; + + const file = new File([blob], epubFileName(url), { type: blob.type || "application/epub+zip" }); + await view.open(file); + if (cancelled) return; + if (locatorRef.current) await view.goTo(locatorRef.current); + else await view.next(); + setLoading(false); + } catch (mountError) { + if (!cancelled) { + setError(readerErrorMessage(mountError, "EPUB indisponible")); + setLoading(false); + } } } - mount(); + + void mount(); return () => { cancelled = true; + view?.close?.(); + view?.remove(); + if (viewRef.current === view) viewRef.current = null; }; - }, [url]); + }, [attempt, onLocatorChange, url]); + + if (error) { + return ( +
+ setAttempt((value) => value + 1)} + /> +
+ ); + } return ( -
-