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

@ -0,0 +1,145 @@
import { ArrowLeft, ArrowRight, Columns2, RotateCcw, Rows3, Save } from "lucide-react";
import type { ReactNode } from "react";
import { ErrorRibbon, Meter } from "../components/ui";
import { navigate } from "../router";
import type { ReaderMode } from "../api/types";
export type ReaderControls = {
canPrevious: boolean;
canNext: boolean;
positionLabel: string;
onPrevious: () => void;
onNext: () => void;
};
type ReaderShellProps = {
title: string;
status: string;
backHref: string;
progress: number;
error?: string;
onRetry?: () => void;
mode: ReaderMode;
onModeChange: (mode: ReaderMode) => void;
controls: ReaderControls;
controlsVisible: boolean;
onToggleControls: () => void;
children: ReactNode;
};
export function ReaderShell({
title,
status,
backHref,
progress,
error,
onRetry,
mode,
onModeChange,
controls,
controlsVisible,
onToggleControls,
children
}: ReaderShellProps) {
return (
<div className={`reader-page reader-mode-${mode} ${controlsVisible ? "reader-controls-visible" : "reader-controls-hidden"}`}>
<header className="reader-topbar" onClick={(event) => event.stopPropagation()}>
<button className="ghost-button" onClick={() => navigate(backHref)}>
<ArrowLeft size={17} />
Fiche
</button>
<div>
<strong>{title}</strong>
<span>{status}</span>
</div>
<div className="reader-toolbar">
<button
className={`ghost-button icon-only ${mode === "horizontal" ? "active" : ""}`}
onClick={() => onModeChange("horizontal")}
aria-label="Lecture horizontale"
title="Lecture horizontale"
>
<Columns2 size={18} />
</button>
<button
className={`ghost-button icon-only ${mode === "vertical" ? "active" : ""}`}
onClick={() => onModeChange("vertical")}
aria-label="Lecture verticale"
title="Lecture verticale"
>
<Rows3 size={18} />
</button>
{error && onRetry ? (
<button className="ghost-button icon-only" onClick={onRetry} aria-label="Reessayer">
<RotateCcw size={18} />
</button>
) : (
<Save size={18} />
)}
</div>
</header>
<div className="reader-status" onClick={(event) => event.stopPropagation()}>
<ErrorRibbon message={error} />
<Meter value={progress} />
</div>
<div className="reader-stage" onClick={onToggleControls}>
{mode === "horizontal" && (
<>
<button
className="reader-side-button reader-side-left"
onClick={(event) => {
event.stopPropagation();
controls.onPrevious();
}}
disabled={!controls.canPrevious}
aria-label="Page precedente"
>
<ArrowLeft size={22} />
</button>
<button
className="reader-side-button reader-side-right"
onClick={(event) => {
event.stopPropagation();
controls.onNext();
}}
disabled={!controls.canNext}
aria-label="Page suivante"
>
<ArrowRight size={22} />
</button>
<button
className="reader-tap-zone reader-tap-left"
onClick={(event) => {
event.stopPropagation();
controls.onPrevious();
}}
disabled={!controls.canPrevious}
aria-label="Page precedente"
/>
<button
className="reader-tap-zone reader-tap-right"
onClick={(event) => {
event.stopPropagation();
controls.onNext();
}}
disabled={!controls.canNext}
aria-label="Page suivante"
/>
</>
)}
<div className="reader-content">{children}</div>
</div>
<footer className="reader-stepper" onClick={(event) => event.stopPropagation()}>
<button className="ghost-button" onClick={controls.onPrevious} disabled={!controls.canPrevious}>
<ArrowLeft size={16} />
Precedent
</button>
<span>{controls.positionLabel}</span>
<button className="ghost-button" onClick={controls.onNext} disabled={!controls.canNext}>
Suivant
<ArrowRight size={16} />
</button>
</footer>
</div>
);
}