refacto: Split reader

This commit is contained in:
Julien Froidefond
2025-02-28 13:48:23 +01:00
parent 00554d73b0
commit 29b9eca599
6 changed files with 297 additions and 181 deletions

View File

@@ -5,20 +5,22 @@ import { BookReaderProps } from "./types";
import { useOrientation } from "./hooks/useOrientation";
import { usePageNavigation } from "./hooks/usePageNavigation";
import { usePageCache } from "./hooks/usePageCache";
import { usePageUrls } from "./hooks/usePageUrls";
import { usePreloadPages } from "./hooks/usePreloadPages";
import { useFullscreen } from "./hooks/useFullscreen";
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";
import { ReaderContent } from "./components/ReaderContent";
import { useReadingDirection } from "./hooks/useReadingDirection";
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 { direction, toggleDirection, isRTL } = useReadingDirection();
const { isFullscreen, toggleFullscreen } = useFullscreen();
const {
currentPage,
@@ -43,130 +45,29 @@ export function BookReader({ book, pages, onClose }: BookReaderProps) {
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;
};
}, [
const { currentPageUrl, nextPageUrl } = usePageUrls({
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;
};
}, [
usePreloadPages({
currentPage,
totalPages: pages.length,
isDoublePage,
shouldShowDoublePage,
preloadPage,
cleanCache,
pages.length,
isRTL,
]);
});
// 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) {
@@ -187,7 +88,6 @@ export function BookReader({ book, pages, onClose }: BookReaderProps) {
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}
@@ -201,82 +101,23 @@ export function BookReader({ book, pages, onClose }: BookReaderProps) {
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);
}
}}
onToggleFullscreen={() => toggleFullscreen(readerRef.current)}
direction={direction}
onToggleDirection={toggleDirection}
/>
{/* 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
*/}
<div
className={cn(
"relative h-full flex items-center",
isDoublePage && {
"w-1/2": true,
"order-2 justify-start": isRTL,
"order-1 justify-end": !isRTL,
},
!isDoublePage && "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>
<ReaderContent
currentPage={currentPage}
currentPageUrl={currentPageUrl}
nextPageUrl={nextPageUrl}
isLoading={isLoading}
secondPageLoading={secondPageLoading}
isDoublePage={isDoublePage}
shouldShowDoublePage={shouldShowDoublePage}
isRTL={isRTL}
onThumbnailLoad={handleThumbnailLoad}
/>
{/* Deuxième page en mode double page */}
{isDoublePage && shouldShowDoublePage(currentPage) && (
<div
className={cn(
"relative h-full w-1/2 flex items-center",
isRTL ? "order-1 justify-end" : "order-2 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>
{/* Barre de navigation */}
<NavigationBar
currentPage={currentPage}
pages={pages}

View File

@@ -0,0 +1,53 @@
import { SinglePage } from "./SinglePage";
interface ReaderContentProps {
currentPage: number;
currentPageUrl: string;
nextPageUrl: string;
isLoading: boolean;
secondPageLoading: boolean;
isDoublePage: boolean;
shouldShowDoublePage: (page: number) => boolean;
isRTL: boolean;
onThumbnailLoad: (pageNumber: number) => void;
}
export const ReaderContent = ({
currentPage,
currentPageUrl,
nextPageUrl,
isLoading,
secondPageLoading,
isDoublePage,
shouldShowDoublePage,
isRTL,
onThumbnailLoad,
}: ReaderContentProps) => {
return (
<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">
<SinglePage
pageUrl={currentPageUrl}
pageNumber={currentPage}
isLoading={isLoading}
onLoad={onThumbnailLoad}
isDoublePage={isDoublePage}
isRTL={isRTL}
order="first"
/>
{isDoublePage && shouldShowDoublePage(currentPage) && (
<SinglePage
pageUrl={nextPageUrl}
pageNumber={currentPage + 1}
isLoading={secondPageLoading}
onLoad={onThumbnailLoad}
isDoublePage={true}
isRTL={isRTL}
order="second"
/>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,49 @@
import { cn } from "@/lib/utils";
import { ImageLoader } from "@/components/ui/image-loader";
interface SinglePageProps {
pageUrl: string;
pageNumber: number;
isLoading: boolean;
onLoad: (pageNumber: number) => void;
isDoublePage?: boolean;
isRTL?: boolean;
order?: "first" | "second";
}
export const SinglePage = ({
pageUrl,
pageNumber,
isLoading,
onLoad,
isDoublePage = false,
isRTL = false,
order = "first",
}: SinglePageProps) => {
return (
<div
className={cn(
"relative h-full flex items-center",
isDoublePage && {
"w-1/2": true,
"order-2 justify-start": order === "first" ? isRTL : !isRTL,
"order-1 justify-end": order === "first" ? !isRTL : isRTL,
},
!isDoublePage && "w-full justify-center"
)}
>
<ImageLoader isLoading={isLoading} />
{pageUrl && (
<img
src={pageUrl}
alt={`Page ${pageNumber}`}
className={cn(
"max-h-full w-auto object-contain transition-opacity duration-300",
isLoading ? "opacity-0" : "opacity-100"
)}
onLoad={() => onLoad(pageNumber)}
/>
)}
</div>
);
};

View File

@@ -0,0 +1,37 @@
import { useState, useEffect } from "react";
export const useFullscreen = () => {
const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement);
};
document.addEventListener("fullscreenchange", handleFullscreenChange);
return () => {
document.removeEventListener("fullscreenchange", handleFullscreenChange);
if (document.fullscreenElement) {
document.exitFullscreen().catch(console.error);
}
};
}, []);
const toggleFullscreen = async (element: HTMLElement | null) => {
try {
if (isFullscreen) {
await document.exitFullscreen();
} else if (element) {
await element.requestFullscreen();
}
} catch (error) {
console.error("Erreur lors du changement de mode plein écran:", error);
}
};
return {
isFullscreen,
toggleFullscreen,
};
};

View File

@@ -0,0 +1,75 @@
import { useState, useEffect } from "react";
interface UsePageUrlsProps {
currentPage: number;
isDoublePage: boolean;
shouldShowDoublePage: (page: number) => boolean;
getPageUrl: (page: number) => Promise<string>;
setIsLoading: (loading: boolean) => void;
setSecondPageLoading: (loading: boolean) => void;
}
export const usePageUrls = ({
currentPage,
isDoublePage,
shouldShowDoublePage,
getPageUrl,
setIsLoading,
setSecondPageLoading,
}: UsePageUrlsProps) => {
const [currentPageUrl, setCurrentPageUrl] = useState<string>("");
const [nextPageUrl, setNextPageUrl] = useState<string>("");
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
);
}
if (isMounted) {
setIsLoading(false);
setSecondPageLoading(false);
}
}
};
setIsLoading(true);
setSecondPageLoading(true);
loadPageUrls();
return () => {
isMounted = false;
};
}, [
currentPage,
isDoublePage,
shouldShowDoublePage,
getPageUrl,
setIsLoading,
setSecondPageLoading,
]);
return {
currentPageUrl,
nextPageUrl,
};
};

View File

@@ -0,0 +1,61 @@
import { useEffect } from "react";
interface UsePreloadPagesProps {
currentPage: number;
totalPages: number;
isDoublePage: boolean;
shouldShowDoublePage: (page: number) => boolean;
preloadPage: (page: number) => Promise<void>;
cleanCache: (currentPage: number) => void;
}
export const usePreloadPages = ({
currentPage,
totalPages,
isDoublePage,
shouldShowDoublePage,
preloadPage,
cleanCache,
}: UsePreloadPagesProps) => {
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 = [];
// Précharger les 4 pages suivantes
for (let i = 1; i <= 4 && currentPage + i <= totalPages; i++) {
pagesToPreload.push(currentPage + i);
}
// Précharger les 2 pages précédentes
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, totalPages]);
};