refacto: Split reader
This commit is contained in:
@@ -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}
|
||||
|
||||
53
src/components/reader/components/ReaderContent.tsx
Normal file
53
src/components/reader/components/ReaderContent.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
49
src/components/reader/components/SinglePage.tsx
Normal file
49
src/components/reader/components/SinglePage.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
37
src/components/reader/hooks/useFullscreen.ts
Normal file
37
src/components/reader/hooks/useFullscreen.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
75
src/components/reader/hooks/usePageUrls.ts
Normal file
75
src/components/reader/hooks/usePageUrls.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
61
src/components/reader/hooks/usePreloadPages.ts
Normal file
61
src/components/reader/hooks/usePreloadPages.ts
Normal 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]);
|
||||
};
|
||||
Reference in New Issue
Block a user