Files
ReadaBook/apps/web/src/pages/ReaderPage.tsx
Git Agent 4aacac12da fix(web): lecteurs EPUB/PDF — gestion d'erreurs, worker pdf.js local
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 <noreply@anthropic.com>
2026-08-23 16:24:02 +02:00

100 lines
3.8 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from "react";
import { ArrowLeft, RotateCcw, Save } from "lucide-react";
import type { BookDto } from "@readabook/shared";
import { api, getApiFallback } from "../api/client";
import { ErrorRibbon, Meter } from "../components/ui";
import { navigate } from "../router";
import { CbzReader } from "../reader/CbzReader";
import { EpubReader } from "../reader/EpubReader";
import { parseCbrPageLocator, parseCbzPageLocator, parsePdfPageLocator, pdfPagePercent } from "../reader/locators";
import { PdfReader } from "../reader/PdfReader";
import { useReaderProgress } from "../reader/useReaderProgress";
export function ReaderPage({ bookId }: { bookId: number }) {
const [book, setBook] = useState<BookDto | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string>();
const [page, setPage] = useState(1);
const { progress, saving, error: progressError, save } = useReaderProgress(bookId);
async function loadBook() {
setLoading(true);
setError(undefined);
try {
setBook(await api.book(bookId));
} catch (loadError) {
const fallback = getApiFallback<BookDto>(loadError);
setBook(fallback ?? null);
setError(fallback ? "Lecteur ouvert en mode secours." : "Ouvrage indisponible.");
} finally {
setLoading(false);
}
}
useEffect(() => {
void loadBook();
}, [bookId]);
useEffect(() => {
const nextPage = parsePdfPageLocator(progress?.locator) ?? parseCbzPageLocator(progress?.locator) ?? parseCbrPageLocator(progress?.locator);
if (nextPage) setPage((current) => (current === nextPage ? current : nextPage));
}, [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);
void save(`pdf:page:${nextPage}`, pdfPagePercent(nextPage, pages));
},
[save]
);
const saveEpubLocator = useCallback((locator: string, percent: number) => void save(locator, percent), [save]);
const saveComicPage = useCallback(
(nextPage: number, pages: number) => {
setPage(nextPage);
const prefix = book?.format === "cbr" ? "cbr" : "cbz";
void save(`${prefix}:page:${nextPage}`, pdfPagePercent(nextPage, pages));
},
[book?.format, save]
);
return (
<div className="reader-page">
<header className="reader-topbar">
<button className="ghost-button" onClick={() => navigate(backHref)}>
<ArrowLeft size={17} />
Fiche
</button>
<div>
<strong>{book?.title ?? "Ouverture du lecteur"}</strong>
<span>{loading ? "Chargement" : saving ? "Sauvegarde" : "Progression synchronisee"}</span>
</div>
{error ? (
<button className="ghost-button icon-only" onClick={() => void loadBook()} aria-label="Reessayer">
<RotateCcw size={18} />
</button>
) : (
<Save size={18} />
)}
</header>
<ErrorRibbon message={error ?? progressError} />
<Meter value={progress?.percent ?? 0} />
{!book ? (
<div className="reader-fallback">
<span>{error ?? "Chargement du livre."}</span>
<button className="ghost-button" onClick={() => void loadBook()}>
Reessayer
</button>
</div>
) : book.format === "pdf" ? (
<PdfReader url={fileUrl} page={page} backHref={backHref} onPageCommit={savePdfPage} />
) : book.format === "cbz" || book.format === "cbr" ? (
<CbzReader bookId={book.id} page={page} onPageCommit={saveComicPage} />
) : (
<EpubReader url={fileUrl} locator={progress?.locator} backHref={backHref} onLocatorChange={saveEpubLocator} />
)}
</div>
);
}