fix(web): lecteur PDF/CBZ — pages blanches, timeouts et modes fullscreen

- Rendu PDF via module dédié pdfRender (worker), correction des pages
  blanches et gestion des timeouts de rendu
- Lecteur comics : crop plein écran, locators de page unifiés (pdf/cbz/cbr)
  avec ancrage pour le calcul de progression
- Sauvegarde de progression mise en file (queueSave) pour éviter les
  pertes lors des navigations rapides
- nginx/vite : en-têtes et proxy alignés pour le streaming des livres

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Git Agent
2026-08-24 09:11:25 +02:00
parent d79f502dd2
commit 84e789acc4
11 changed files with 494 additions and 104 deletions

View File

@ -1,5 +1,5 @@
import { ArrowLeft, ArrowRight, Columns2, RotateCcw, Rows3, Save } from "lucide-react";
import type { ReactNode } from "react";
import { ArrowLeft, ArrowRight, ArrowUp, Columns2, Maximize2, Minimize2, RotateCcw, Rows3, Save } from "lucide-react";
import { useCallback, useEffect, useState, type ReactNode } from "react";
import { ErrorRibbon, Meter } from "../components/ui";
import { navigate } from "../router";
import type { ReaderMode } from "../api/types";
@ -7,9 +7,11 @@ import type { ReaderMode } from "../api/types";
export type ReaderControls = {
canPrevious: boolean;
canNext: boolean;
canTop?: boolean;
positionLabel: string;
onPrevious: () => void;
onNext: () => void;
onTop?: () => void;
};
type ReaderShellProps = {
@ -41,6 +43,20 @@ export function ReaderShell({
onToggleControls,
children
}: ReaderShellProps) {
const [isFullscreen, setIsFullscreen] = useState(false);
const toggleFullscreen = useCallback(() => {
const root = document.querySelector(".reader-page");
if (!document.fullscreenElement) void root?.requestFullscreen();
else void document.exitFullscreen();
}, []);
useEffect(() => {
const updateFullscreen = () => setIsFullscreen(Boolean(document.fullscreenElement));
updateFullscreen();
document.addEventListener("fullscreenchange", updateFullscreen);
return () => document.removeEventListener("fullscreenchange", updateFullscreen);
}, []);
return (
<div className={`reader-page reader-mode-${mode} ${controlsVisible ? "reader-controls-visible" : "reader-controls-hidden"}`}>
<header className="reader-topbar" onClick={(event) => event.stopPropagation()}>
@ -54,20 +70,25 @@ export function ReaderShell({
</div>
<div className="reader-toolbar">
<button
className={`ghost-button icon-only ${mode === "horizontal" ? "active" : ""}`}
className={`ghost-button reader-mode-button ${mode === "horizontal" ? "active" : ""}`}
onClick={() => onModeChange("horizontal")}
aria-label="Lecture horizontale"
title="Lecture horizontale"
>
<Columns2 size={18} />
Horizontal
</button>
<button
className={`ghost-button icon-only ${mode === "vertical" ? "active" : ""}`}
className={`ghost-button reader-mode-button ${mode === "vertical" ? "active" : ""}`}
onClick={() => onModeChange("vertical")}
aria-label="Lecture verticale"
title="Lecture verticale"
>
<Rows3 size={18} />
Vertical
</button>
<button className="ghost-button icon-only" onClick={toggleFullscreen} aria-label={isFullscreen ? "Quitter le plein ecran" : "Plein ecran"}>
{isFullscreen ? <Minimize2 size={18} /> : <Maximize2 size={18} />}
</button>
{error && onRetry ? (
<button className="ghost-button icon-only" onClick={onRetry} aria-label="Reessayer">
@ -130,15 +151,29 @@ export function ReaderShell({
<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>
{mode === "vertical" ? (
<button className="ghost-button" onClick={controls.onTop} disabled={!controls.canTop}>
<ArrowUp size={16} />
Haut
</button>
) : (
<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>
{mode === "vertical" ? (
<button className="ghost-button" onClick={controls.onPrevious} disabled={!controls.canPrevious}>
<ArrowLeft size={16} />
Page precedente
</button>
) : (
<button className="ghost-button" onClick={controls.onNext} disabled={!controls.canNext}>
Suivant
<ArrowRight size={16} />
</button>
)}
</footer>
</div>
);