fix(web,api): lecteur — worker pdf.js dédié, shell commun et préférences par livre
Régression worker PDF : le worker pdf.js est désormais instancié une seule fois via un port dédié (?worker&url) et reconfiguré à chaque montage, au lieu d'un workerSrc recalculé qui cassait le rendu. - ReaderShell : chrome commun aux lecteurs (toolbar, zones de tap, statut) et contrat ReaderControls pour EPUB/PDF/CBZ - préférences de lecture par livre (mode horizontal/vertical, fit) : table reader_preferences + migrations idempotentes, module API, DTO partagés, client web avec fallback localStorage hors-ligne Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,26 +1,60 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import * as pdfjs from "pdfjs-dist";
|
||||
import { ReaderError, readerErrorMessage } from "./ReaderError";
|
||||
import { pdfWorkerSrc } from "./pdfWorker";
|
||||
import { configurePdfWorker } from "./pdfWorker";
|
||||
import type { ReaderControls } from "./ReaderShell";
|
||||
import type { ReaderMode } from "../api/types";
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = pdfWorkerSrc;
|
||||
configurePdfWorker(pdfjs);
|
||||
|
||||
export function PdfReader({
|
||||
url,
|
||||
page,
|
||||
backHref,
|
||||
onPageCommit
|
||||
mode,
|
||||
onPageCommit,
|
||||
onControlsChange
|
||||
}: {
|
||||
url: string;
|
||||
page: number;
|
||||
backHref: string;
|
||||
mode: ReaderMode;
|
||||
onPageCommit: (page: number, pages: number) => void;
|
||||
onControlsChange: (controls: ReaderControls) => void;
|
||||
}) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const frameRef = useRef<HTMLDivElement>(null);
|
||||
const [pages, setPages] = useState(1);
|
||||
const [error, setError] = useState<string>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [attempt, setAttempt] = useState(0);
|
||||
const [frameSize, setFrameSize] = useState({ width: 980, height: 900 });
|
||||
|
||||
const currentPage = Math.max(1, Math.min(page, pages));
|
||||
const go = useCallback((nextPage: number) => onPageCommit(Math.max(1, Math.min(nextPage, pages)), pages), [onPageCommit, pages]);
|
||||
|
||||
useEffect(() => {
|
||||
onControlsChange({
|
||||
canPrevious: !loading && !error && currentPage > 1,
|
||||
canNext: !loading && !error && currentPage < pages,
|
||||
positionLabel: loading ? "Ouverture PDF" : `${currentPage} / ${pages}`,
|
||||
onPrevious: () => go(currentPage - 1),
|
||||
onNext: () => go(currentPage + 1)
|
||||
});
|
||||
}, [currentPage, error, go, loading, onControlsChange, pages]);
|
||||
|
||||
useEffect(() => {
|
||||
const frame = frameRef.current;
|
||||
if (!frame) return;
|
||||
const updateSize = () => {
|
||||
const rect = frame.getBoundingClientRect();
|
||||
setFrameSize({ width: Math.max(320, rect.width), height: Math.max(320, rect.height) });
|
||||
};
|
||||
updateSize();
|
||||
const observer = new ResizeObserver(updateSize);
|
||||
observer.observe(frame);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@ -28,6 +62,7 @@ export function PdfReader({
|
||||
let renderTask: pdfjs.RenderTask | undefined;
|
||||
async function render() {
|
||||
try {
|
||||
configurePdfWorker(pdfjs);
|
||||
setLoading(true);
|
||||
setError(undefined);
|
||||
loadingTask = pdfjs.getDocument({ url, withCredentials: true });
|
||||
@ -37,9 +72,17 @@ export function PdfReader({
|
||||
const pdfPage = await document.getPage(Math.max(1, Math.min(page, document.numPages)));
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const viewport = pdfPage.getViewport({ scale: Math.min(1.6, window.devicePixelRatio || 1) });
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const baseViewport = pdfPage.getViewport({ scale: 1 });
|
||||
const fitScale =
|
||||
mode === "vertical"
|
||||
? frameSize.width / baseViewport.width
|
||||
: Math.min(frameSize.width / baseViewport.width, frameSize.height / baseViewport.height);
|
||||
const renderScale = Math.max(0.35, Math.min(3, fitScale)) * Math.min(2, window.devicePixelRatio || 1);
|
||||
const viewport = pdfPage.getViewport({ scale: renderScale });
|
||||
canvas.width = Math.floor(viewport.width);
|
||||
canvas.height = Math.floor(viewport.height);
|
||||
canvas.style.width = `${Math.floor(viewport.width / Math.min(2, window.devicePixelRatio || 1))}px`;
|
||||
canvas.style.height = `${Math.floor(viewport.height / Math.min(2, window.devicePixelRatio || 1))}px`;
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) return;
|
||||
renderTask = pdfPage.render({ canvas, canvasContext: context, viewport });
|
||||
@ -58,10 +101,10 @@ export function PdfReader({
|
||||
renderTask?.cancel();
|
||||
void loadingTask?.destroy();
|
||||
};
|
||||
}, [url, page, attempt]);
|
||||
}, [url, page, attempt, frameSize.height, frameSize.width, mode]);
|
||||
|
||||
return (
|
||||
<div className="pdf-reader">
|
||||
<div className="pdf-reader" ref={frameRef}>
|
||||
{error ? (
|
||||
<ReaderError
|
||||
title="Lecture PDF indisponible"
|
||||
@ -81,17 +124,6 @@ export function PdfReader({
|
||||
<canvas ref={canvasRef} />
|
||||
</>
|
||||
)}
|
||||
<div className="reader-stepper">
|
||||
<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)} disabled={Boolean(error) || loading}>
|
||||
Suivant
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user