feat: add force reload functionality to PhotoswipeReader for refreshing images and improve memory management by revoking blob URLs
This commit is contained in:
@@ -31,6 +31,7 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
|
||||
const [loadedImages, setLoadedImages] = useState<Record<number, { width: number; height: number }>>({});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [secondPageLoading, setSecondPageLoading] = useState(true);
|
||||
const [imageBlobUrls, setImageBlobUrls] = useState<Record<number, string>>({});
|
||||
const isLandscape = useOrientation();
|
||||
const { direction, toggleDirection, isRTL } = useReadingDirection();
|
||||
const { isFullscreen, toggleFullscreen } = useFullscreen();
|
||||
@@ -320,8 +321,74 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
|
||||
if (pswpRef.current) {
|
||||
pswpRef.current.close();
|
||||
}
|
||||
// Révoquer toutes les blob URLs
|
||||
Object.values(imageBlobUrls).forEach(url => {
|
||||
if (url) URL.revokeObjectURL(url);
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
}, [imageBlobUrls]);
|
||||
|
||||
// Force reload handler
|
||||
const handleForceReload = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setSecondPageLoading(true);
|
||||
|
||||
// Révoquer les anciennes URLs blob
|
||||
if (imageBlobUrls[currentPage]) {
|
||||
URL.revokeObjectURL(imageBlobUrls[currentPage]);
|
||||
}
|
||||
if (imageBlobUrls[currentPage + 1]) {
|
||||
URL.revokeObjectURL(imageBlobUrls[currentPage + 1]);
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch page 1 avec cache: reload
|
||||
const response1 = await fetch(getPageUrl(currentPage), {
|
||||
cache: 'reload',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response1.ok) {
|
||||
throw new Error(`HTTP ${response1.status}`);
|
||||
}
|
||||
|
||||
const blob1 = await response1.blob();
|
||||
const blobUrl1 = URL.createObjectURL(blob1);
|
||||
|
||||
const newUrls: Record<number, string> = {
|
||||
...imageBlobUrls,
|
||||
[currentPage]: blobUrl1
|
||||
};
|
||||
|
||||
// Fetch page 2 si double page
|
||||
if (isDoublePage && shouldShowDoublePage(currentPage)) {
|
||||
const response2 = await fetch(getPageUrl(currentPage + 1), {
|
||||
cache: 'reload',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response2.ok) {
|
||||
throw new Error(`HTTP ${response2.status}`);
|
||||
}
|
||||
|
||||
const blob2 = await response2.blob();
|
||||
const blobUrl2 = URL.createObjectURL(blob2);
|
||||
newUrls[currentPage + 1] = blobUrl2;
|
||||
}
|
||||
|
||||
setImageBlobUrls(newUrls);
|
||||
} catch (error) {
|
||||
console.error('Error reloading images:', error);
|
||||
setIsLoading(false);
|
||||
setSecondPageLoading(false);
|
||||
}
|
||||
}, [currentPage, imageBlobUrls, isDoublePage, shouldShowDoublePage, getPageUrl]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -363,6 +430,7 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
|
||||
showThumbnails={showThumbnails}
|
||||
onToggleThumbnails={() => setShowThumbnails(!showThumbnails)}
|
||||
onZoom={handleZoom}
|
||||
onForceReload={handleForceReload}
|
||||
/>
|
||||
|
||||
{/* Reader content */}
|
||||
@@ -390,7 +458,8 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
src={getPageUrl(currentPage)}
|
||||
key={`page-${currentPage}-${imageBlobUrls[currentPage] || ''}`}
|
||||
src={imageBlobUrls[currentPage] || getPageUrl(currentPage)}
|
||||
alt={`Page ${currentPage}`}
|
||||
className={cn(
|
||||
"max-h-full max-w-full object-contain transition-opacity",
|
||||
@@ -428,7 +497,8 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
src={getPageUrl(currentPage + 1)}
|
||||
key={`page-${currentPage + 1}-${imageBlobUrls[currentPage + 1] || ''}`}
|
||||
src={imageBlobUrls[currentPage + 1] || getPageUrl(currentPage + 1)}
|
||||
alt={`Page ${currentPage + 1}`}
|
||||
className={cn(
|
||||
"max-h-full max-w-full object-contain transition-opacity",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
MoveLeft,
|
||||
Images,
|
||||
ZoomIn,
|
||||
RotateCw,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PageInput } from "./PageInput";
|
||||
@@ -35,6 +36,7 @@ export const ControlButtons = ({
|
||||
showThumbnails,
|
||||
onToggleThumbnails,
|
||||
onZoom,
|
||||
onForceReload,
|
||||
}: ControlButtonsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -125,6 +127,18 @@ export const ControlButtons = ({
|
||||
iconClassName="h-6 w-6"
|
||||
className="rounded-full"
|
||||
/>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
icon={RotateCw}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onForceReload();
|
||||
}}
|
||||
tooltip={t("reader.controls.reload")}
|
||||
iconClassName="h-6 w-6"
|
||||
className="rounded-full"
|
||||
/>
|
||||
<div className="p-2 rounded-full" onClick={(e) => e.stopPropagation()}>
|
||||
<PageInput
|
||||
currentPage={currentPage}
|
||||
|
||||
@@ -53,6 +53,7 @@ export interface ControlButtonsProps {
|
||||
showThumbnails: boolean;
|
||||
onToggleThumbnails: () => void;
|
||||
onZoom: () => void;
|
||||
onForceReload: () => void;
|
||||
}
|
||||
|
||||
export interface UsePageNavigationProps {
|
||||
|
||||
@@ -418,6 +418,7 @@
|
||||
"hide": "Hide thumbnails"
|
||||
},
|
||||
"zoom": "Zoom",
|
||||
"reload": "Reload page",
|
||||
"close": "Close",
|
||||
"previousPage": "Previous page",
|
||||
"nextPage": "Next page"
|
||||
|
||||
@@ -416,6 +416,7 @@
|
||||
"hide": "Masquer les vignettes"
|
||||
},
|
||||
"zoom": "Zoom",
|
||||
"reload": "Recharger la page",
|
||||
"close": "Fermer",
|
||||
"previousPage": "Page précédente",
|
||||
"nextPage": "Page suivante"
|
||||
|
||||
@@ -94,7 +94,14 @@ export class BookService extends BaseApiService {
|
||||
const response: ImageResponse = await ImageService.getImage(
|
||||
`books/${bookId}/pages/${adjustedPageNumber}?zero_based=true`
|
||||
);
|
||||
return new Response(response.buffer.buffer as ArrayBuffer, {
|
||||
|
||||
// Convertir le Buffer Node.js en ArrayBuffer proprement
|
||||
const arrayBuffer = response.buffer.buffer.slice(
|
||||
response.buffer.byteOffset,
|
||||
response.buffer.byteOffset + response.buffer.byteLength
|
||||
);
|
||||
|
||||
return new Response(arrayBuffer, {
|
||||
headers: {
|
||||
"Content-Type": response.contentType || "image/jpeg",
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
|
||||
@@ -12,23 +12,16 @@ export class ImageService extends BaseApiService {
|
||||
try {
|
||||
const headers = { Accept: "image/jpeg, image/png, image/gif, image/webp, */*" };
|
||||
|
||||
const result = await this.fetchWithCache<ImageResponse>(
|
||||
`image-${path}`,
|
||||
async () => {
|
||||
const response = await this.fetchFromApi<Response>({ path }, headers, { isImage: true });
|
||||
const contentType = response.headers.get("content-type");
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
// NE PAS mettre en cache - les images sont trop grosses et les Buffers ne sérialisent pas bien
|
||||
const response = await this.fetchFromApi<Response>({ path }, headers, { isImage: true });
|
||||
const contentType = response.headers.get("content-type");
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
return {
|
||||
buffer,
|
||||
contentType,
|
||||
};
|
||||
},
|
||||
"IMAGES"
|
||||
);
|
||||
|
||||
return result;
|
||||
return {
|
||||
buffer,
|
||||
contentType,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la récupération de l'image:", error);
|
||||
throw new AppError(ERROR_CODES.IMAGE.FETCH_ERROR, {}, error);
|
||||
|
||||
Reference in New Issue
Block a user