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