import { useCallback, useEffect, useRef, useState } from "react"; import { api } from "../api/client"; import type { CbzPagesDto } from "../api/types"; import { clampReaderPage, orientedContainPageSize, readableViewportSize, readerPositionLabel, zoomReaderSize, type ReaderSize } from "./readerLayout"; import type { ReaderControls } from "./ReaderShell"; type PageCommitStrategy = "immediate" | "queued"; export function CbzReader({ bookId, page, zoom, onPageCommit, onControlsChange }: { bookId: number; page: number; zoom: number; onPageCommit: (page: number, pages: number, anchor?: number, strategy?: PageCommitStrategy) => void; onControlsChange: (controls: ReaderControls) => void; }) { const frameRef = useRef(null); const [pages, setPages] = useState(null); const [documentError, setDocumentError] = useState(); const [pageError, setPageError] = useState(); const [documentAttempt, setDocumentAttempt] = useState(0); const [retryAttempt, setRetryAttempt] = useState(0); const [viewportSize, setViewportSize] = useState(null); const [imageSize, setImageSize] = useState(null); useEffect(() => { let alive = true; setDocumentError(undefined); setPageError(undefined); setPages(null); api .cbzPages(bookId) .then((nextPages) => { if (!alive) return; setPages(nextPages); if (page > nextPages.pageCount) onPageCommit(nextPages.pageCount, nextPages.pageCount, 1, "immediate"); }) .catch((error) => { if (!alive) return; setDocumentError(error instanceof Error && error.message ? `Archive indisponible: ${error.message}` : "Archive indisponible."); }); return () => { alive = false; }; }, [bookId, documentAttempt]); const pageCount = pages?.pageCount ?? 1; const currentPage = clampReaderPage(page, pageCount); const currentName = pages?.pages.find((item) => item.page === currentPage)?.name; const fittedSize = viewportSize && imageSize ? zoomReaderSize(orientedContainPageSize(viewportSize, imageSize), zoom) : null; const imageStyle = fittedSize ? { width: `${fittedSize.width}px`, height: `${fittedSize.height}px` } : undefined; const go = useCallback( (nextPage: number) => { if (!pages) return; const target = clampReaderPage(nextPage, pages.pageCount); setPageError(undefined); onPageCommit(target, pages.pageCount, 1, "immediate"); }, [onPageCommit, pages] ); useEffect(() => { setPageError(undefined); setImageSize(null); }, [bookId, currentPage, retryAttempt]); useEffect(() => { const frame = frameRef.current; const stage = frame?.closest(".reader-stage") as HTMLElement | null; const observedElement = stage ?? frame; if (!observedElement) return; const updateSize = () => { const rect = observedElement.getBoundingClientRect(); const nextSize = readableViewportSize({ width: rect.width, height: rect.height }); if (nextSize) setViewportSize(nextSize); }; updateSize(); const frameId = requestAnimationFrame(updateSize); const observer = new ResizeObserver(updateSize); observer.observe(observedElement); return () => { cancelAnimationFrame(frameId); observer.disconnect(); }; }, []); useEffect(() => { onControlsChange({ canPrevious: Boolean(pages) && !documentError && !pageError && currentPage > 1, canNext: Boolean(pages) && !documentError && !pageError && currentPage < pageCount, positionLabel: pages ? readerPositionLabel(currentPage, pageCount) : "Chargement", onPrevious: () => go(currentPage - 1), onNext: () => go(currentPage + 1) }); }, [currentPage, documentError, go, onControlsChange, pageCount, pageError, pages]); if (documentError) { return (
{documentError}
); } if (!pages) { return (
Chargement de l'archive
); } if (pageError) { return (
{pageError}
); } return (
{!imageSize && (
Page {currentPage}
)} { setImageSize({ width: event.currentTarget.naturalWidth, height: event.currentTarget.naturalHeight }); }} onError={() => setPageError(`Page ${currentPage} indisponible.`)} />
); }