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:
@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user