import { useEffect, useRef, useState } from "react"; import { ReaderError, readerErrorMessage } from "./ReaderError"; import type { ReaderControls } from "./ReaderShell"; import type { ReaderMode } from "../api/types"; type FoliateLocation = { cfi?: string; fraction?: number; current?: number; total?: number; }; 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, mode, onLocatorChange, onControlsChange }: { url: string; locator?: string; backHref: string; mode: ReaderMode; onLocatorChange: (locator: string, percent: number) => void; onControlsChange: (controls: ReaderControls) => void; }) { const hostRef = useRef(null); const viewRef = useRef(null); const locatorRef = useRef(locator); const [loading, setLoading] = useState(true); const [error, setError] = useState(); const [attempt, setAttempt] = useState(0); useEffect(() => { onControlsChange({ canPrevious: !loading && !error, canNext: !loading && !error, positionLabel: loading ? "Ouverture EPUB" : "Lecture integree", onPrevious: () => void viewRef.current?.goLeft(), onNext: () => void viewRef.current?.goRight() }); }, [error, loading, onControlsChange]); useEffect(() => { locatorRef.current = locator; }, [locator]); useEffect(() => { let cancelled = false; let view: FoliateView | null = null; async function mount() { try { setLoading(true); setError(undefined); await import("foliate-js/view.js"); if (cancelled || !hostRef.current) return; 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); } } } void mount(); return () => { cancelled = true; view?.close?.(); view?.remove(); if (viewRef.current === view) viewRef.current = null; }; }, [attempt, onLocatorChange, url]); useEffect(() => { viewRef.current?.classList.toggle("epub-view-vertical", mode === "vertical"); viewRef.current?.classList.toggle("epub-view-horizontal", mode === "horizontal"); }, [mode]); if (error) { return (
setAttempt((value) => value + 1)} />
); } return (
{loading && (
Ouverture EPUB
)}
); }