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 ( -
-