Files
stripstream/src/components/reader/components/PageDisplay.tsx
Froidefond Julien d9ffacc124
All checks were successful
Build, Push & Deploy / deploy (push) Successful in 5m15s
fix: prevent second page flicker in double page mode when image is already loaded
Skip resetting loading state to true when the blob URL already exists,
avoiding an unnecessary opacity-0 → opacity-100 CSS transition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 10:33:38 +01:00

208 lines
7.7 KiB
TypeScript

import { useState, useCallback, useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
interface PageDisplayProps {
currentPage: number;
pages: number[];
isDoublePage: boolean;
shouldShowDoublePage: (page: number) => boolean;
imageBlobUrls: Record<number, string>;
isRTL: boolean;
}
export function PageDisplay({
currentPage,
pages: _pages,
isDoublePage,
shouldShowDoublePage,
imageBlobUrls,
isRTL,
}: PageDisplayProps) {
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
const [secondPageLoading, setSecondPageLoading] = useState(true);
const [secondPageHasError, setSecondPageHasError] = useState(false);
const imageBlobUrlsRef = useRef(imageBlobUrls);
imageBlobUrlsRef.current = imageBlobUrls;
const handleImageLoad = useCallback(() => {
setIsLoading(false);
}, []);
const handleImageError = useCallback(() => {
setIsLoading(false);
setHasError(true);
}, []);
const handleSecondImageLoad = useCallback(() => {
setSecondPageLoading(false);
}, []);
const handleSecondImageError = useCallback(() => {
setSecondPageLoading(false);
setSecondPageHasError(true);
}, []);
// Reset loading when page changes, but skip if blob URL is already available
useEffect(() => {
setIsLoading(!imageBlobUrlsRef.current[currentPage]);
setHasError(false);
setSecondPageLoading(!imageBlobUrlsRef.current[currentPage + 1]);
setSecondPageHasError(false);
}, [currentPage, isDoublePage]);
// Reset error state when blob URL becomes available
useEffect(() => {
if (imageBlobUrls[currentPage] && hasError) {
setHasError(false);
setIsLoading(true);
}
}, [imageBlobUrls[currentPage], currentPage, hasError]);
useEffect(() => {
if (imageBlobUrls[currentPage + 1] && secondPageHasError) {
setSecondPageHasError(false);
setSecondPageLoading(true);
}
}, [imageBlobUrls[currentPage + 1], currentPage, secondPageHasError]);
return (
<div className="relative flex w-full flex-1 items-center justify-center overflow-hidden">
<div className="relative flex h-[calc(100vh-2.5rem)] w-full items-center justify-center px-2 sm:px-4">
{/* Page 1 */}
<div
className={cn(
"relative h-full flex items-center",
isDoublePage && shouldShowDoublePage(currentPage) ? "w-1/2" : "w-full justify-center",
isDoublePage &&
shouldShowDoublePage(currentPage) && {
"order-2 justify-start": isRTL,
"order-1 justify-end": !isRTL,
}
)}
>
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center z-10 opacity-0 animate-fade-in">
<div className="relative">
<div className="animate-spin rounded-full h-16 w-16 border-4 border-primary/20"></div>
<div
className="absolute inset-0 animate-spin rounded-full h-16 w-16 border-4 border-transparent border-t-primary"
style={{ animationDuration: "0.8s" }}
></div>
</div>
</div>
)}
{hasError ? (
<div className="flex flex-col items-center justify-center gap-3 text-muted-foreground">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
className="opacity-40"
>
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
<circle cx="9" cy="9" r="2" />
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
</svg>
<span className="text-sm opacity-60">Image non disponible</span>
</div>
) : imageBlobUrls[currentPage] ? (
<>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
key={`page-${currentPage}-${imageBlobUrls[currentPage]}`}
src={imageBlobUrls[currentPage]}
alt={`Page ${currentPage}`}
className={cn(
"max-h-full max-w-full cursor-pointer object-contain transition-opacity",
isLoading ? "opacity-0" : "opacity-100"
)}
loading="eager"
onLoad={handleImageLoad}
onError={handleImageError}
ref={(img) => {
// Si l'image est déjà en cache, onLoad ne sera pas appelé
if (img?.complete && img?.naturalHeight !== 0) {
handleImageLoad();
}
}}
/>
</>
) : null}
</div>
{/* Page 2 (double page) */}
{isDoublePage && shouldShowDoublePage(currentPage) && (
<div
className={cn("relative h-full w-1/2 flex items-center", {
"order-1 justify-end": isRTL,
"order-2 justify-start": !isRTL,
})}
>
{secondPageLoading && (
<div className="absolute inset-0 flex items-center justify-center z-10 opacity-0 animate-fade-in">
<div className="relative">
<div className="animate-spin rounded-full h-16 w-16 border-4 border-primary/20"></div>
<div
className="absolute inset-0 animate-spin rounded-full h-16 w-16 border-4 border-transparent border-t-primary"
style={{ animationDuration: "0.8s" }}
></div>
</div>
</div>
)}
{secondPageHasError ? (
<div className="flex flex-col items-center justify-center gap-3 text-muted-foreground">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
className="opacity-40"
>
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
<circle cx="9" cy="9" r="2" />
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
</svg>
<span className="text-sm opacity-60">Image non disponible</span>
</div>
) : imageBlobUrls[currentPage + 1] ? (
<>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
key={`page-${currentPage + 1}-${imageBlobUrls[currentPage + 1]}`}
src={imageBlobUrls[currentPage + 1]}
alt={`Page ${currentPage + 1}`}
className={cn(
"max-h-full max-w-full cursor-pointer object-contain transition-opacity",
secondPageLoading ? "opacity-0" : "opacity-100"
)}
loading="eager"
onLoad={handleSecondImageLoad}
onError={handleSecondImageError}
ref={(img) => {
// Si l'image est déjà en cache, onLoad ne sera pas appelé
if (img?.complete && img?.naturalHeight !== 0) {
handleSecondImageLoad();
}
}}
/>
</>
) : null}
</div>
)}
</div>
</div>
);
}