Files
ReadaBook/apps/web/src/reader/CbzReader.tsx
Git Agent 5de46a6f6d 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>
2026-08-23 18:06:22 +02:00

75 lines
2.3 KiB
TypeScript

import { useCallback, useEffect, useState } from "react";
import { api } from "../api/client";
import type { CbzPagesDto, ReaderMode } from "../api/types";
import type { ReaderControls } from "./ReaderShell";
export function CbzReader({
bookId,
page,
mode,
onPageCommit,
onControlsChange
}: {
bookId: number;
page: number;
mode: ReaderMode;
onPageCommit: (page: number, pages: number) => void;
onControlsChange: (controls: ReaderControls) => void;
}) {
const [pages, setPages] = useState<CbzPagesDto | null>(null);
const [error, setError] = useState<string>();
const [imageError, setImageError] = useState(false);
useEffect(() => {
let alive = true;
setError(undefined);
api
.cbzPages(bookId)
.then((nextPages) => {
if (!alive) return;
setPages(nextPages);
if (page > nextPages.pageCount) onPageCommit(nextPages.pageCount, nextPages.pageCount);
})
.catch(() => {
if (alive) setError("Archive CBZ indisponible.");
});
return () => {
alive = false;
};
}, [bookId]);
const pageCount = pages?.pageCount ?? 1;
const currentPage = Math.max(1, Math.min(page, pageCount));
const currentName = pages?.pages.find((item) => item.page === currentPage)?.name;
const go = useCallback((nextPage: number) => {
setImageError(false);
onPageCommit(Math.max(1, Math.min(nextPage, pageCount)), pageCount);
}, [onPageCommit, pageCount]);
useEffect(() => {
onControlsChange({
canPrevious: !error && !imageError && currentPage > 1,
canNext: !error && !imageError && currentPage < pageCount,
positionLabel: pages ? `${currentPage} / ${pageCount}` : "Ouverture archive",
onPrevious: () => go(currentPage - 1),
onNext: () => go(currentPage + 1)
});
}, [currentPage, error, go, imageError, onControlsChange, pageCount, pages]);
return (
<div className={`cbz-reader cbz-reader-${mode}`}>
{error || imageError ? (
<div className="reader-fallback">
<span>{error ?? "Page CBZ indisponible."}</span>
<button className="ghost-button" onClick={() => go(currentPage)}>
Reessayer
</button>
</div>
) : (
<img src={api.cbzPageUrl(bookId, currentPage)} alt={currentName ?? `Page ${currentPage}`} onError={() => setImageError(true)} />
)}
</div>
);
}