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>
This commit is contained in:
Git Agent
2026-08-23 16:24:02 +02:00
parent f3c6da2889
commit 4aacac12da
8 changed files with 316 additions and 50 deletions

View File

@ -1,21 +1,36 @@
import { useEffect, useRef, useState } from "react";
import * as pdfjs from "pdfjs-dist";
import workerUrl from "pdfjs-dist/build/pdf.worker.mjs?url";
import { ReaderError, readerErrorMessage } from "./ReaderError";
import { pdfWorkerSrc } from "./pdfWorker";
pdfjs.GlobalWorkerOptions.workerSrc = workerUrl;
pdfjs.GlobalWorkerOptions.workerSrc = pdfWorkerSrc;
export function PdfReader({ url, page, onPageCommit }: { url: string; page: number; onPageCommit: (page: number, pages: number) => void }) {
export function PdfReader({
url,
page,
backHref,
onPageCommit
}: {
url: string;
page: number;
backHref: string;
onPageCommit: (page: number, pages: number) => void;
}) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [pages, setPages] = useState(1);
const [error, setError] = useState<string>();
const [loading, setLoading] = useState(true);
const [attempt, setAttempt] = useState(0);
useEffect(() => {
let cancelled = false;
let loadingTask: pdfjs.PDFDocumentLoadingTask | undefined;
let renderTask: pdfjs.RenderTask | undefined;
async function render() {
try {
setLoading(true);
setError(undefined);
const loadingTask = pdfjs.getDocument({ url, withCredentials: true });
loadingTask = pdfjs.getDocument({ url, withCredentials: true });
const document = await loadingTask.promise;
if (cancelled) return;
setPages(document.numPages);
@ -27,37 +42,53 @@ export function PdfReader({ url, page, onPageCommit }: { url: string; page: numb
canvas.height = viewport.height;
const context = canvas.getContext("2d");
if (!context) return;
await pdfPage.render({ canvas, canvasContext: context, viewport }).promise;
renderTask = pdfPage.render({ canvas, canvasContext: context, viewport });
await renderTask.promise;
if (!cancelled) setLoading(false);
} catch (renderError) {
setError(renderError instanceof Error ? renderError.message : "PDF indisponible");
if (!cancelled) {
setError(readerErrorMessage(renderError, "PDF indisponible"));
setLoading(false);
}
}
}
render();
void render();
return () => {
cancelled = true;
renderTask?.cancel();
void loadingTask?.destroy();
};
}, [url, page, attempt]);
return (
<div className="pdf-reader">
{error ? (
<div className="reader-fallback">
<span>{error}</span>
<button className="ghost-button" onClick={() => setAttempt((value) => value + 1)}>
Reessayer
</button>
</div>
<ReaderError
title="Lecture PDF indisponible"
detail="ReadaBook n'a pas pu ouvrir ce fichier dans le lecteur web."
technicalDetail={error}
downloadUrl={url}
backHref={backHref}
onRetry={() => setAttempt((value) => value + 1)}
/>
) : (
<canvas ref={canvasRef} />
<>
{loading && (
<div className="reader-fallback">
<span>Ouverture PDF</span>
</div>
)}
<canvas ref={canvasRef} />
</>
)}
<div className="reader-stepper">
<button className="ghost-button" onClick={() => onPageCommit(Math.max(1, page - 1), pages)}>
Precedent
<button className="ghost-button" onClick={() => onPageCommit(Math.max(1, page - 1), pages)} disabled={Boolean(error) || loading}>
Précédent
</button>
<span>
{page} / {pages}
</span>
<button className="ghost-button" onClick={() => onPageCommit(Math.min(pages, page + 1), pages)}>
<button className="ghost-button" onClick={() => onPageCommit(Math.min(pages, page + 1), pages)} disabled={Boolean(error) || loading}>
Suivant
</button>
</div>