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

@ -7,11 +7,12 @@ import {
exitElementFullscreen,
isElementFullscreen,
isReaderHeaderActive,
readerArrowAction,
shouldHandleReaderArrowKey,
readerNavigationAction,
shouldHandleReaderNavigationKey,
supportsElementFullscreen
} from "./readerFullscreen";
import { readerZoomLabel, READER_ZOOM_DEFAULT, READER_ZOOM_MAX, READER_ZOOM_MIN } from "./readerLayout";
import type { ReaderMode } from "./readerScroll";
export type ReaderControls = {
canPrevious: boolean;
@ -28,6 +29,11 @@ export type ReaderZoomControls = {
onZoomReset: () => void;
};
export type ReaderModeControls = {
mode: ReaderMode;
onModeChange: (mode: ReaderMode) => void;
};
type ReaderShellProps = {
title: string;
backHref: string;
@ -35,17 +41,20 @@ type ReaderShellProps = {
onRetry?: () => void;
controls: ReaderControls;
zoomControls?: ReaderZoomControls;
modeControls?: ReaderModeControls;
children: ReactNode;
};
export function ReaderShell({ title, backHref, error, onRetry, controls, zoomControls, children }: ReaderShellProps) {
export function ReaderShell({ title, backHref, error, onRetry, controls, zoomControls, modeControls, children }: ReaderShellProps) {
const readerRef = useRef<HTMLDivElement>(null);
const headerRef = useRef<HTMLElement>(null);
const stageRef = useRef<HTMLElement>(null);
const headerHideTimerRef = useRef<number | undefined>(undefined);
const [nativeFullscreen, setNativeFullscreen] = useState(false);
const [fallbackFullscreen, setFallbackFullscreen] = useState(false);
const [headerVisible, setHeaderVisible] = useState(true);
const fullscreenActive = nativeFullscreen || fallbackFullscreen;
const readerMode = modeControls?.mode ?? "horizontal";
const clearHeaderHideTimer = useCallback(() => {
if (headerHideTimerRef.current === undefined) return;
@ -129,9 +138,15 @@ export function ReaderShell({ title, backHref, error, onRetry, controls, zoomCon
setFallbackFullscreen(false);
return;
}
if (!shouldHandleReaderArrowKey(event, readerRef.current)) return;
const mode = modeControls?.mode ?? "horizontal";
if (!shouldHandleReaderNavigationKey(event, readerRef.current, mode)) return;
revealHeader();
const action = readerArrowAction(event);
const action = readerNavigationAction(event, mode);
if (mode === "vertical") {
event.preventDefault();
stageRef.current?.scrollBy({ top: (action === "next" ? 1 : -1) * Math.max(120, stageRef.current.clientHeight * 0.82), behavior: "smooth" });
return;
}
if (action === "previous" && controls.canPrevious) {
event.preventDefault();
controls.onPrevious();
@ -143,12 +158,12 @@ export function ReaderShell({ title, backHref, error, onRetry, controls, zoomCon
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [controls, fallbackFullscreen, revealHeader]);
}, [controls, fallbackFullscreen, modeControls?.mode, revealHeader]);
return (
<div
ref={readerRef}
className={`reader-page reader-mode-horizontal${fallbackFullscreen ? " reader-fullscreen-fallback" : ""}${fullscreenActive ? " reader-fullscreen-active" : ""}${
className={`reader-page reader-mode-${readerMode}${fallbackFullscreen ? " reader-fullscreen-fallback" : ""}${fullscreenActive ? " reader-fullscreen-active" : ""}${
fullscreenActive && !headerVisible ? " reader-header-hidden" : ""
}`}
onMouseMove={revealHeader}
@ -180,6 +195,15 @@ export function ReaderShell({ title, backHref, error, onRetry, controls, zoomCon
</button>
<strong className="reader-title">{title}</strong>
<span className="reader-position">{controls.positionLabel}</span>
{modeControls && (
<label className="reader-mode-control">
<span>Mode de lecture</span>
<select value={modeControls.mode} onChange={(event) => modeControls.onModeChange(event.currentTarget.value as ReaderMode)}>
<option value="horizontal">Page par page</option>
<option value="vertical">Défilement vertical</option>
</select>
</label>
)}
<button className="ghost-button" onClick={controls.onPrevious} disabled={!controls.canPrevious}>
<ArrowLeft size={16} />
Precedent
@ -216,7 +240,7 @@ export function ReaderShell({ title, backHref, error, onRetry, controls, zoomCon
)}
</div>
)}
<main className="reader-stage">
<main className="reader-stage" ref={stageRef}>
<div className="reader-content">{children}</div>
</main>
</div>