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 { useOrientation } from "./hooks/useOrientation";
import { usePageNavigation } from "./hooks/usePageNavigation"; import { usePageNavigation } from "./hooks/usePageNavigation";
import { usePageCache } from "./hooks/usePageCache"; 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 { useState, useEffect, useCallback, useRef } from "react";
import { NavigationBar } from "./components/NavigationBar"; import { NavigationBar } from "./components/NavigationBar";
import { ControlButtons } from "./components/ControlButtons"; import { ControlButtons } from "./components/ControlButtons";
import { ImageLoader } from "@/components/ui/image-loader"; import { ReaderContent } from "./components/ReaderContent";
import { cn } from "@/lib/utils";
import { useReadingDirection } from "./hooks/useReadingDirection"; import { useReadingDirection } from "./hooks/useReadingDirection";
export function BookReader({ book, pages, onClose }: BookReaderProps) { export function BookReader({ book, pages, onClose }: BookReaderProps) {
const [isDoublePage, setIsDoublePage] = useState(false); const [isDoublePage, setIsDoublePage] = useState(false);
const [showControls, setShowControls] = useState(false); const [showControls, setShowControls] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const readerRef = useRef<HTMLDivElement>(null); const readerRef = useRef<HTMLDivElement>(null);
const isLandscape = useOrientation(); const isLandscape = useOrientation();
const { direction, toggleDirection, isRTL } = useReadingDirection(); const { direction, toggleDirection, isRTL } = useReadingDirection();
const { isFullscreen, toggleFullscreen } = useFullscreen();
const { const {
currentPage, currentPage,
@@ -43,130 +45,29 @@ export function BookReader({ book, pages, onClose }: BookReaderProps) {
pages, pages,
}); });
// État pour stocker les URLs des images const { currentPageUrl, nextPageUrl } = usePageUrls({
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, currentPage,
isDoublePage, isDoublePage,
shouldShowDoublePage, shouldShowDoublePage,
getPageUrl, getPageUrl,
setIsLoading, setIsLoading,
setSecondPageLoading, setSecondPageLoading,
]); });
// Effet pour précharger la page courante et les pages adjacentes usePreloadPages({
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, currentPage,
totalPages: pages.length,
isDoublePage, isDoublePage,
shouldShowDoublePage, shouldShowDoublePage,
preloadPage, preloadPage,
cleanCache, cleanCache,
pages.length, });
isRTL,
]);
// Effet pour gérer le mode double page automatiquement en paysage // Effet pour gérer le mode double page automatiquement en paysage
useEffect(() => { useEffect(() => {
setIsDoublePage(isLandscape); setIsDoublePage(isLandscape);
}, [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( const handleThumbnailLoad = useCallback(
(pageNumber: number) => { (pageNumber: number) => {
if (pageNumber === currentPage) { 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" className="relative h-full flex flex-col items-center justify-center"
onClick={() => setShowControls(!showControls)} onClick={() => setShowControls(!showControls)}
> >
{/* Contenu principal */}
<div className="relative h-full w-full flex items-center justify-center"> <div className="relative h-full w-full flex items-center justify-center">
<ControlButtons <ControlButtons
showControls={showControls} showControls={showControls}
@@ -201,82 +101,23 @@ export function BookReader({ book, pages, onClose }: BookReaderProps) {
isDoublePage={isDoublePage} isDoublePage={isDoublePage}
onToggleDoublePage={() => setIsDoublePage(!isDoublePage)} onToggleDoublePage={() => setIsDoublePage(!isDoublePage)}
isFullscreen={isFullscreen} isFullscreen={isFullscreen}
onToggleFullscreen={async () => { onToggleFullscreen={() => toggleFullscreen(readerRef.current)}
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);
}
}}
direction={direction} direction={direction}
onToggleDirection={toggleDirection} onToggleDirection={toggleDirection}
/> />
{/* Pages */} <ReaderContent
<div className="relative flex-1 flex items-center justify-center overflow-hidden p-1"> currentPage={currentPage}
<div className="relative w-full h-[calc(100vh-2rem)] flex items-center justify-center gap-0"> currentPageUrl={currentPageUrl}
{/* nextPageUrl={nextPageUrl}
Note: Nous utilisons intentionnellement des balises <img> natives au lieu de next/image pour : isLoading={isLoading}
1. Avoir un contrôle précis sur le chargement et le préchargement des pages secondPageLoading={secondPageLoading}
2. Gérer efficacement le mode double page et les transitions isDoublePage={isDoublePage}
3. Les images sont déjà optimisées côté serveur shouldShowDoublePage={shouldShowDoublePage}
4. La performance est critique pour une lecture fluide isRTL={isRTL}
*/} onThumbnailLoad={handleThumbnailLoad}
<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>
{/* 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 <NavigationBar
currentPage={currentPage} currentPage={currentPage}
pages={pages} 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]);
};