fix: improve reader image error handling and double page alignment
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m54s

- Show a clean placeholder (icon + label) instead of the browser's broken image icon when a page fails to load
- Track error state per page (page 1 and page 2) and reset on page navigation
- Center each page within its half in double page mode instead of pushing toward the spine

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 16:03:25 +01:00
parent 99d9f41299
commit 3e5687441d

View File

@@ -20,21 +20,35 @@ export function PageDisplay({
getPageUrl, getPageUrl,
}: PageDisplayProps) { }: PageDisplayProps) {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
const [secondPageLoading, setSecondPageLoading] = useState(true); const [secondPageLoading, setSecondPageLoading] = useState(true);
const [secondPageHasError, setSecondPageHasError] = useState(false);
const { isRTL } = useReadingDirection(); const { isRTL } = useReadingDirection();
const handleImageLoad = useCallback(() => { const handleImageLoad = useCallback(() => {
setIsLoading(false); setIsLoading(false);
}, []); }, []);
const handleImageError = useCallback(() => {
setIsLoading(false);
setHasError(true);
}, []);
const handleSecondImageLoad = useCallback(() => { const handleSecondImageLoad = useCallback(() => {
setSecondPageLoading(false); setSecondPageLoading(false);
}, []); }, []);
const handleSecondImageError = useCallback(() => {
setSecondPageLoading(false);
setSecondPageHasError(true);
}, []);
// Reset loading when page changes // Reset loading when page changes
useEffect(() => { useEffect(() => {
setIsLoading(true); setIsLoading(true);
setHasError(false);
setSecondPageLoading(true); setSecondPageLoading(true);
setSecondPageHasError(false);
}, [currentPage, isDoublePage]); }, [currentPage, isDoublePage]);
return ( return (
@@ -47,8 +61,8 @@ export function PageDisplay({
isDoublePage && shouldShowDoublePage(currentPage) ? "w-1/2" : "w-full justify-center", isDoublePage && shouldShowDoublePage(currentPage) ? "w-1/2" : "w-full justify-center",
isDoublePage && isDoublePage &&
shouldShowDoublePage(currentPage) && { shouldShowDoublePage(currentPage) && {
"order-2 justify-start": isRTL, "order-2 justify-center": isRTL,
"order-1 justify-end": !isRTL, "order-1 justify-center": !isRTL,
} }
)} )}
> >
@@ -63,6 +77,17 @@ export function PageDisplay({
</div> </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>
) : (
<>
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
key={`page-${currentPage}-${imageBlobUrls[currentPage] || ""}`} key={`page-${currentPage}-${imageBlobUrls[currentPage] || ""}`}
@@ -74,7 +99,7 @@ export function PageDisplay({
)} )}
loading="eager" loading="eager"
onLoad={handleImageLoad} onLoad={handleImageLoad}
onError={handleImageLoad} onError={handleImageError}
ref={(img) => { ref={(img) => {
// Si l'image est déjà en cache, onLoad ne sera pas appelé // Si l'image est déjà en cache, onLoad ne sera pas appelé
if (img?.complete && img?.naturalHeight !== 0) { if (img?.complete && img?.naturalHeight !== 0) {
@@ -82,14 +107,16 @@ export function PageDisplay({
} }
}} }}
/> />
</>
)}
</div> </div>
{/* Page 2 (double page) */} {/* Page 2 (double page) */}
{isDoublePage && shouldShowDoublePage(currentPage) && ( {isDoublePage && shouldShowDoublePage(currentPage) && (
<div <div
className={cn("relative h-full w-1/2 flex items-center", { className={cn("relative h-full w-1/2 flex items-center justify-center", {
"order-1 justify-end": isRTL, "order-1": isRTL,
"order-2 justify-start": !isRTL, "order-2": !isRTL,
})} })}
> >
{secondPageLoading && ( {secondPageLoading && (
@@ -103,6 +130,17 @@ export function PageDisplay({
</div> </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>
) : (
<>
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
key={`page-${currentPage + 1}-${imageBlobUrls[currentPage + 1] || ""}`} key={`page-${currentPage + 1}-${imageBlobUrls[currentPage + 1] || ""}`}
@@ -114,7 +152,7 @@ export function PageDisplay({
)} )}
loading="eager" loading="eager"
onLoad={handleSecondImageLoad} onLoad={handleSecondImageLoad}
onError={handleSecondImageLoad} onError={handleSecondImageError}
ref={(img) => { ref={(img) => {
// Si l'image est déjà en cache, onLoad ne sera pas appelé // Si l'image est déjà en cache, onLoad ne sera pas appelé
if (img?.complete && img?.naturalHeight !== 0) { if (img?.complete && img?.naturalHeight !== 0) {
@@ -122,6 +160,8 @@ export function PageDisplay({
} }
}} }}
/> />
</>
)}
</div> </div>
)} )}
</div> </div>