feat(web): mode de lecture vertical type webtoon — pages empilées, défilement souris/flèches, préférence persistée

This commit is contained in:
Git Agent
2026-08-24 15:26:06 +02:00
parent 3d211f7feb
commit f7e88c6bc0
8 changed files with 478 additions and 37 deletions

View File

@ -5,8 +5,10 @@ import { CbzReader } from "../reader/CbzReader";
import { EpubReader } from "../reader/EpubReader";
import { pageLocator, parseCbrPageLocator, parseCbzPageLocator, parsePdfPageLocator, pdfPagePercent } from "../reader/locators";
import { PdfReader } from "../reader/PdfReader";
import { ReaderShell, type ReaderControls, type ReaderZoomControls } from "../reader/ReaderShell";
import { ReaderShell, type ReaderControls, type ReaderModeControls, type ReaderZoomControls } from "../reader/ReaderShell";
import { clampReaderZoom, READER_ZOOM_DEFAULT, READER_ZOOM_STEP } from "../reader/readerLayout";
import { majorityVisiblePage, type ReaderMode } from "../reader/readerScroll";
import { useReaderPreferences } from "../reader/useReaderPreferences";
import { useReaderProgress } from "../reader/useReaderProgress";
const idleControls: ReaderControls = {
@ -23,8 +25,10 @@ export function ReaderPage({ bookId }: { bookId: number }) {
const [error, setError] = useState<string>();
const [page, setPage] = useState(1);
const [zoom, setZoom] = useState(READER_ZOOM_DEFAULT);
const [mode, setMode] = useState<ReaderMode>("horizontal");
const [readerControls, setReaderControls] = useState<ReaderControls>(idleControls);
const { progress, error: progressError, save, queueSave } = useReaderProgress(bookId);
const { preferences, setMode: saveReaderMode, error: preferencesError } = useReaderPreferences(bookId);
async function loadBook() {
setLoading(true);
@ -47,8 +51,14 @@ export function ReaderPage({ bookId }: { bookId: number }) {
useEffect(() => {
setReaderControls(idleControls);
setZoom(READER_ZOOM_DEFAULT);
setMode("horizontal");
}, [bookId]);
useEffect(() => {
if (book?.format === "pdf" || book?.format === "cbz" || book?.format === "cbr") setMode(preferences.mode);
else setMode("horizontal");
}, [book?.format, preferences.mode]);
useEffect(() => {
const nextPage = parsePdfPageLocator(progress?.locator) ?? parseCbzPageLocator(progress?.locator) ?? parseCbrPageLocator(progress?.locator);
if (nextPage) setPage((current) => (current === nextPage ? current : nextPage));
@ -79,8 +89,31 @@ export function ReaderPage({ bookId }: { bookId: number }) {
[book?.format, queueSave, save]
);
const readerError = error ?? progressError;
const readerError = error ?? progressError ?? preferencesError;
const supportsZoom = book?.format === "pdf" || book?.format === "cbz" || book?.format === "cbr";
const supportsMode = supportsZoom;
const currentVisiblePage = useCallback(() => {
const stage = document.querySelector(".reader-stage") as HTMLElement | null;
if (!stage) return null;
const stageRect = stage.getBoundingClientRect();
const pages = Array.from(stage.querySelectorAll<HTMLElement>("[data-reader-page]"))
.map((element) => {
const rect = element.getBoundingClientRect();
const pageNumber = Number(element.dataset.readerPage);
return Number.isFinite(pageNumber) ? { page: pageNumber, top: rect.top, bottom: rect.bottom } : null;
})
.filter((item): item is { page: number; top: number; bottom: number } => Boolean(item));
return majorityVisiblePage(pages, stageRect.top, stageRect.bottom);
}, []);
const changeMode = useCallback(
(nextMode: ReaderMode) => {
const anchorPage = currentVisiblePage() ?? page;
setPage(anchorPage);
setMode(nextMode);
saveReaderMode(nextMode);
},
[currentVisiblePage, page, saveReaderMode]
);
const changeZoom = useCallback((nextZoom: number | ((currentZoom: number) => number)) => {
const stage = document.querySelector(".reader-stage") as HTMLElement | null;
const scrollRatioX = stage && stage.scrollWidth > stage.clientWidth ? (stage.scrollLeft + stage.clientWidth / 2) / stage.scrollWidth : 0.5;
@ -108,6 +141,16 @@ export function ReaderPage({ bookId }: { bookId: number }) {
: undefined,
[changeZoom, supportsZoom, zoom]
);
const modeControls = useMemo<ReaderModeControls | undefined>(
() =>
supportsMode
? {
mode,
onModeChange: changeMode
}
: undefined,
[changeMode, mode, supportsMode]
);
return (
<ReaderShell
@ -117,6 +160,7 @@ export function ReaderPage({ bookId }: { bookId: number }) {
onRetry={error ? () => void loadBook() : undefined}
controls={readerControls}
zoomControls={zoomControls}
modeControls={modeControls}
>
{!book ? (
<div className="reader-fallback">
@ -131,6 +175,7 @@ export function ReaderPage({ bookId }: { bookId: number }) {
page={page}
backHref={backHref}
zoom={zoom}
mode={mode}
onPageCommit={savePdfPage}
onControlsChange={setReaderControls}
/>
@ -139,6 +184,7 @@ export function ReaderPage({ bookId }: { bookId: number }) {
bookId={book.id}
page={page}
zoom={zoom}
mode={mode}
onPageCommit={saveComicPage}
onControlsChange={setReaderControls}
/>