feat(web): plein écran du lecteur (natif + fallback) et navigation flèches clavier durcie

This commit is contained in:
Git Agent
2026-08-24 14:40:23 +02:00
parent f8069bdfd6
commit de099dc9ca
4 changed files with 236 additions and 12 deletions

View File

@ -1,7 +1,8 @@
import { ArrowLeft, ArrowRight } from "lucide-react";
import { useEffect, type ReactNode } from "react";
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
import { ErrorRibbon } from "../components/ui";
import { navigate } from "../router";
import { enterElementFullscreen, exitElementFullscreen, isElementFullscreen, readerArrowAction, shouldHandleReaderArrowKey, supportsElementFullscreen } from "./readerFullscreen";
import { readerZoomLabel, READER_ZOOM_DEFAULT, READER_ZOOM_MAX, READER_ZOOM_MIN } from "./readerLayout";
export type ReaderControls = {
@ -29,31 +30,72 @@ type ReaderShellProps = {
children: ReactNode;
};
function isFormField(target: EventTarget | null) {
const element = target as HTMLElement | null;
const tagName = element?.tagName.toLowerCase();
return Boolean(element?.isContentEditable || tagName === "input" || tagName === "textarea" || tagName === "select");
}
export function ReaderShell({ title, backHref, error, onRetry, controls, zoomControls, children }: ReaderShellProps) {
const readerRef = useRef<HTMLDivElement>(null);
const [nativeFullscreen, setNativeFullscreen] = useState(false);
const [fallbackFullscreen, setFallbackFullscreen] = useState(false);
const fullscreenActive = nativeFullscreen || fallbackFullscreen;
const toggleFullscreen = useCallback(async () => {
const readerElement = readerRef.current;
if (!readerElement) return;
if (isElementFullscreen(readerElement, document)) {
await exitElementFullscreen(document);
return;
}
if (fallbackFullscreen) {
setFallbackFullscreen(false);
return;
}
if (supportsElementFullscreen(readerElement)) {
try {
await enterElementFullscreen(readerElement);
return;
} catch {
setFallbackFullscreen(true);
return;
}
}
setFallbackFullscreen(true);
}, [fallbackFullscreen]);
useEffect(() => {
const syncFullscreen = () => {
const readerElement = readerRef.current;
setNativeFullscreen(isElementFullscreen(readerElement, document));
};
syncFullscreen();
document.addEventListener("fullscreenchange", syncFullscreen);
return () => document.removeEventListener("fullscreenchange", syncFullscreen);
}, []);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (isFormField(event.target)) return;
if (event.key === "ArrowLeft" && controls.canPrevious) {
if (event.key === "Escape" && fallbackFullscreen) {
setFallbackFullscreen(false);
return;
}
if (!shouldHandleReaderArrowKey(event, readerRef.current)) return;
const action = readerArrowAction(event);
if (action === "previous" && controls.canPrevious) {
event.preventDefault();
controls.onPrevious();
}
if (event.key === "ArrowRight" && controls.canNext) {
if (action === "next" && controls.canNext) {
event.preventDefault();
controls.onNext();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [controls]);
}, [controls, fallbackFullscreen]);
return (
<div className="reader-page reader-mode-horizontal">
<div ref={readerRef} className={`reader-page reader-mode-horizontal${fallbackFullscreen ? " reader-fullscreen-fallback" : ""}`}>
<header className="reader-topbar">
<button className="ghost-button" onClick={() => navigate(backHref)}>
<ArrowLeft size={17} />
@ -83,6 +125,9 @@ export function ReaderShell({ title, backHref, error, onRetry, controls, zoomCon
</button>
</div>
)}
<button className="ghost-button reader-fullscreen-toggle" onClick={() => void toggleFullscreen()} aria-pressed={fullscreenActive}>
{fullscreenActive ? "Quitter le plein écran" : "Plein écran"}
</button>
</header>
{error && (
<div className="reader-status">