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:
Git Agent
2026-08-23 18:06:22 +02:00
parent 8024cab11c
commit 5de46a6f6d
19 changed files with 790 additions and 89 deletions

View File

@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from "react";
import { ArrowLeft, ArrowRight } from "lucide-react";
import { ReaderError, readerErrorMessage } from "./ReaderError";
import type { ReaderControls } from "./ReaderShell";
import type { ReaderMode } from "../api/types";
type FoliateLocation = {
cfi?: string;
@ -42,12 +43,16 @@ export function EpubReader({
url,
locator,
backHref,
onLocatorChange
mode,
onLocatorChange,
onControlsChange
}: {
url: string;
locator?: string;
backHref: string;
mode: ReaderMode;
onLocatorChange: (locator: string, percent: number) => void;
onControlsChange: (controls: ReaderControls) => void;
}) {
const hostRef = useRef<HTMLDivElement>(null);
const viewRef = useRef<FoliateView | null>(null);
@ -56,6 +61,16 @@ export function EpubReader({
const [error, setError] = useState<string>();
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]);
@ -108,6 +123,11 @@ export function EpubReader({
};
}, [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 (
<div className="epub-reader">
@ -124,24 +144,13 @@ export function EpubReader({
}
return (
<div className="epub-reader">
<div className={`epub-reader epub-reader-${mode}`}>
{loading && (
<div className="reader-fallback">
<span>Ouverture EPUB</span>
</div>
)}
<div className="epub-host" ref={hostRef} />
<div className="reader-stepper">
<button className="ghost-button" onClick={() => void viewRef.current?.goLeft()} disabled={loading}>
<ArrowLeft size={16} />
Précédent
</button>
<span>{loading ? "Chargement" : "Lecture intégrée"}</span>
<button className="ghost-button" onClick={() => void viewRef.current?.goRight()} disabled={loading}>
Suivant
<ArrowRight size={16} />
</button>
</div>
</div>
);
}