fix: improve reader image error handling and double page alignment
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m54s
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:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user