Files
stripstream/src/components/reader/BookReader.tsx
Julien Froidefond 222eeced12 fix: lints
2025-02-19 21:43:26 +01:00

268 lines
8.3 KiB
TypeScript

/* eslint-disable @next/next/no-img-element */
"use client";
import { BookReaderProps } from "./types";
import { useOrientation } from "./hooks/useOrientation";
import { usePageNavigation } from "./hooks/usePageNavigation";
import { usePageCache } from "./hooks/usePageCache";
import { useState, useEffect, useCallback, useRef } from "react";
import { NavigationBar } from "./components/NavigationBar";
import { ControlButtons } from "./components/ControlButtons";
import { ImageLoader } from "@/components/ui/image-loader";
import { cn } from "@/lib/utils";
export function BookReader({ book, pages, onClose }: BookReaderProps) {
const [isDoublePage, setIsDoublePage] = useState(false);
const [showControls, setShowControls] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const readerRef = useRef<HTMLDivElement>(null);
const isLandscape = useOrientation();
const {
currentPage,
navigateToPage,
isLoading,
setIsLoading,
secondPageLoading,
setSecondPageLoading,
handlePreviousPage,
handleNextPage,
shouldShowDoublePage,
} = usePageNavigation({
book,
pages,
isDoublePage,
onClose,
});
const { preloadPage, getPageUrl, cleanCache } = usePageCache({
book,
pages,
});
// État pour stocker les URLs des images
const [currentPageUrl, setCurrentPageUrl] = useState<string>("");
const [nextPageUrl, setNextPageUrl] = useState<string>("");
// Effet pour charger les URLs des images
useEffect(() => {
let isMounted = true;
const loadPageUrls = async () => {
try {
const url = await getPageUrl(currentPage);
if (isMounted) {
setCurrentPageUrl(url);
setIsLoading(false);
}
if (isDoublePage && shouldShowDoublePage(currentPage)) {
const nextUrl = await getPageUrl(currentPage + 1);
if (isMounted) {
setNextPageUrl(nextUrl);
setSecondPageLoading(false);
}
}
} catch (error) {
if (error instanceof Error) {
console.error(
`Erreur de chargement des URLs pour la page ${currentPage}:`,
error.message
);
}
// On s'assure que le chargement est terminé même en cas d'erreur
if (isMounted) {
setIsLoading(false);
setSecondPageLoading(false);
}
}
};
setIsLoading(true);
setSecondPageLoading(true);
loadPageUrls();
return () => {
isMounted = false;
};
}, [
currentPage,
isDoublePage,
shouldShowDoublePage,
getPageUrl,
setIsLoading,
setSecondPageLoading,
]);
// Effet pour précharger la page courante et les pages adjacentes
useEffect(() => {
let isMounted = true;
const preloadCurrentPages = async () => {
if (!isMounted) return;
await preloadPage(currentPage);
if (!isMounted) return;
if (isDoublePage && shouldShowDoublePage(currentPage)) {
await preloadPage(currentPage + 1);
}
if (!isMounted) return;
const pagesToPreload = [];
for (let i = 1; i <= 4 && currentPage + i <= pages.length; i++) {
pagesToPreload.push(currentPage + i);
}
for (let i = 1; i <= 2 && currentPage - i >= 1; i++) {
pagesToPreload.push(currentPage - i);
}
for (const page of pagesToPreload) {
if (!isMounted) break;
await preloadPage(page);
}
};
preloadCurrentPages();
cleanCache(currentPage);
return () => {
isMounted = false;
};
}, [currentPage, isDoublePage, shouldShowDoublePage, preloadPage, cleanCache, pages.length]);
// Effet pour gérer le mode double page automatiquement en paysage
useEffect(() => {
setIsDoublePage(isLandscape);
}, [isLandscape]);
// Effet pour gérer le fullscreen
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement);
};
document.addEventListener("fullscreenchange", handleFullscreenChange);
return () => {
document.removeEventListener("fullscreenchange", handleFullscreenChange);
if (document.fullscreenElement) {
document.exitFullscreen().catch(console.error);
}
};
}, []);
const handleThumbnailLoad = useCallback(
(pageNumber: number) => {
if (pageNumber === currentPage) {
setIsLoading(false);
} else if (pageNumber === currentPage + 1) {
setSecondPageLoading(false);
}
},
[currentPage, setIsLoading, setSecondPageLoading]
);
return (
<div
ref={readerRef}
className="fixed inset-0 bg-background/95 backdrop-blur-sm z-50 overflow-hidden touch-none"
>
<div
className="relative h-full flex flex-col items-center justify-center"
onClick={() => setShowControls(!showControls)}
>
{/* Contenu principal */}
<div className="relative h-full w-full flex items-center justify-center">
<ControlButtons
showControls={showControls}
onToggleControls={() => setShowControls(!showControls)}
onPreviousPage={handlePreviousPage}
onNextPage={handleNextPage}
onClose={onClose}
currentPage={currentPage}
totalPages={pages.length}
isDoublePage={isDoublePage}
onToggleDoublePage={() => setIsDoublePage(!isDoublePage)}
isFullscreen={isFullscreen}
onToggleFullscreen={async () => {
try {
if (isFullscreen) {
await document.exitFullscreen();
} else if (readerRef.current) {
await readerRef.current.requestFullscreen();
}
} catch (error) {
console.error("Erreur lors du changement de mode plein écran:", error);
}
}}
/>
{/* Pages */}
<div className="relative flex-1 flex items-center justify-center overflow-hidden p-1">
<div className="relative w-full h-[calc(100vh-2rem)] flex items-center justify-center gap-0">
{/*
Note: Nous utilisons intentionnellement des balises <img> natives au lieu de next/image pour :
1. Avoir un contrôle précis sur le chargement et le préchargement des pages
2. Gérer efficacement le mode double page et les transitions
3. Les images sont déjà optimisées côté serveur
4. La performance est critique pour une lecture fluide
*/}
{/* Page courante */}
<div
className={cn(
"relative h-full flex items-center",
isDoublePage ? "w-1/2 justify-end" : "w-full justify-center"
)}
>
<ImageLoader isLoading={isLoading} />
{currentPageUrl && (
<img
src={currentPageUrl}
alt={`Page ${currentPage}`}
className={cn(
"max-h-full w-auto object-contain transition-opacity duration-300",
isLoading ? "opacity-0" : "opacity-100"
)}
onLoad={() => handleThumbnailLoad(currentPage)}
/>
)}
</div>
{/* Deuxième page en mode double page */}
{isDoublePage && shouldShowDoublePage(currentPage) && (
<div className="relative h-full w-1/2 flex items-center justify-start">
<ImageLoader isLoading={secondPageLoading} />
{nextPageUrl && (
<img
src={nextPageUrl}
alt={`Page ${currentPage + 1}`}
className={cn(
"max-h-full w-auto object-contain transition-opacity duration-300",
secondPageLoading ? "opacity-0" : "opacity-100"
)}
onLoad={() => handleThumbnailLoad(currentPage + 1)}
/>
)}
</div>
)}
</div>
</div>
</div>
<NavigationBar
currentPage={currentPage}
pages={pages}
onPageChange={navigateToPage}
showControls={showControls}
book={book}
/>
</div>
</div>
);
}