162 lines
5.5 KiB
TypeScript
162 lines
5.5 KiB
TypeScript
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<HTMLDivElement>(null);
|
|
const [pages, setPages] = useState<CbzPagesDto | null>(null);
|
|
const [documentError, setDocumentError] = useState<string>();
|
|
const [pageError, setPageError] = useState<string>();
|
|
const [documentAttempt, setDocumentAttempt] = useState(0);
|
|
const [retryAttempt, setRetryAttempt] = useState(0);
|
|
const [viewportSize, setViewportSize] = useState<ReaderSize | null>(null);
|
|
const [imageSize, setImageSize] = useState<ReaderSize | null>(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 (
|
|
<div className="cbz-reader" ref={frameRef}>
|
|
<div className="reader-fallback reader-fallback-error">
|
|
<span>{documentError}</span>
|
|
<button className="ghost-button" onClick={() => setDocumentAttempt((attempt) => attempt + 1)}>
|
|
Reessayer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!pages) {
|
|
return (
|
|
<div className="cbz-reader" ref={frameRef}>
|
|
<div className="reader-fallback">
|
|
<span>Chargement de l'archive</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (pageError) {
|
|
return (
|
|
<div className="cbz-reader" ref={frameRef}>
|
|
<div className="reader-fallback reader-fallback-error">
|
|
<span>{pageError}</span>
|
|
<button className="ghost-button" onClick={() => setRetryAttempt((attempt) => attempt + 1)}>
|
|
Reessayer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="cbz-reader" ref={frameRef}>
|
|
<figure className="comic-page-frame" data-reader-page={currentPage}>
|
|
{!imageSize && (
|
|
<div className="reader-fallback">
|
|
<span>Page {currentPage}</span>
|
|
</div>
|
|
)}
|
|
<img
|
|
key={`${currentPage}-${retryAttempt}`}
|
|
src={api.cbzPageUrl(bookId, currentPage)}
|
|
alt={currentName ?? `Page ${currentPage}`}
|
|
style={imageStyle}
|
|
hidden={!imageSize}
|
|
onLoad={(event) => {
|
|
setImageSize({ width: event.currentTarget.naturalWidth, height: event.currentTarget.naturalHeight });
|
|
}}
|
|
onError={() => setPageError(`Page ${currentPage} indisponible.`)}
|
|
/>
|
|
</figure>
|
|
</div>
|
|
);
|
|
}
|