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 [loadedImages, setLoadedImages] = useState<Record<number, { width: number; height: number }>>({});
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [secondPageLoading, setSecondPageLoading] = useState(true);
|
const [secondPageLoading, setSecondPageLoading] = useState(true);
|
||||||
|
const [imageBlobUrls, setImageBlobUrls] = useState<Record<number, string>>({});
|
||||||
const isLandscape = useOrientation();
|
const isLandscape = useOrientation();
|
||||||
const { direction, toggleDirection, isRTL } = useReadingDirection();
|
const { direction, toggleDirection, isRTL } = useReadingDirection();
|
||||||
const { isFullscreen, toggleFullscreen } = useFullscreen();
|
const { isFullscreen, toggleFullscreen } = useFullscreen();
|
||||||
@@ -320,8 +321,74 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
|
|||||||
if (pswpRef.current) {
|
if (pswpRef.current) {
|
||||||
pswpRef.current.close();
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -363,6 +430,7 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
|
|||||||
showThumbnails={showThumbnails}
|
showThumbnails={showThumbnails}
|
||||||
onToggleThumbnails={() => setShowThumbnails(!showThumbnails)}
|
onToggleThumbnails={() => setShowThumbnails(!showThumbnails)}
|
||||||
onZoom={handleZoom}
|
onZoom={handleZoom}
|
||||||
|
onForceReload={handleForceReload}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Reader content */}
|
{/* Reader content */}
|
||||||
@@ -390,7 +458,8 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<img
|
<img
|
||||||
src={getPageUrl(currentPage)}
|
key={`page-${currentPage}-${imageBlobUrls[currentPage] || ''}`}
|
||||||
|
src={imageBlobUrls[currentPage] || getPageUrl(currentPage)}
|
||||||
alt={`Page ${currentPage}`}
|
alt={`Page ${currentPage}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"max-h-full max-w-full object-contain transition-opacity",
|
"max-h-full max-w-full object-contain transition-opacity",
|
||||||
@@ -428,7 +497,8 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<img
|
<img
|
||||||
src={getPageUrl(currentPage + 1)}
|
key={`page-${currentPage + 1}-${imageBlobUrls[currentPage + 1] || ''}`}
|
||||||
|
src={imageBlobUrls[currentPage + 1] || getPageUrl(currentPage + 1)}
|
||||||
alt={`Page ${currentPage + 1}`}
|
alt={`Page ${currentPage + 1}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"max-h-full max-w-full object-contain transition-opacity",
|
"max-h-full max-w-full object-contain transition-opacity",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
MoveLeft,
|
MoveLeft,
|
||||||
Images,
|
Images,
|
||||||
ZoomIn,
|
ZoomIn,
|
||||||
|
RotateCw,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PageInput } from "./PageInput";
|
import { PageInput } from "./PageInput";
|
||||||
@@ -35,6 +36,7 @@ export const ControlButtons = ({
|
|||||||
showThumbnails,
|
showThumbnails,
|
||||||
onToggleThumbnails,
|
onToggleThumbnails,
|
||||||
onZoom,
|
onZoom,
|
||||||
|
onForceReload,
|
||||||
}: ControlButtonsProps) => {
|
}: ControlButtonsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -125,6 +127,18 @@ export const ControlButtons = ({
|
|||||||
iconClassName="h-6 w-6"
|
iconClassName="h-6 w-6"
|
||||||
className="rounded-full"
|
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()}>
|
<div className="p-2 rounded-full" onClick={(e) => e.stopPropagation()}>
|
||||||
<PageInput
|
<PageInput
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export interface ControlButtonsProps {
|
|||||||
showThumbnails: boolean;
|
showThumbnails: boolean;
|
||||||
onToggleThumbnails: () => void;
|
onToggleThumbnails: () => void;
|
||||||
onZoom: () => void;
|
onZoom: () => void;
|
||||||
|
onForceReload: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UsePageNavigationProps {
|
export interface UsePageNavigationProps {
|
||||||
|
|||||||
@@ -418,6 +418,7 @@
|
|||||||
"hide": "Hide thumbnails"
|
"hide": "Hide thumbnails"
|
||||||
},
|
},
|
||||||
"zoom": "Zoom",
|
"zoom": "Zoom",
|
||||||
|
"reload": "Reload page",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"previousPage": "Previous page",
|
"previousPage": "Previous page",
|
||||||
"nextPage": "Next page"
|
"nextPage": "Next page"
|
||||||
|
|||||||
@@ -416,6 +416,7 @@
|
|||||||
"hide": "Masquer les vignettes"
|
"hide": "Masquer les vignettes"
|
||||||
},
|
},
|
||||||
"zoom": "Zoom",
|
"zoom": "Zoom",
|
||||||
|
"reload": "Recharger la page",
|
||||||
"close": "Fermer",
|
"close": "Fermer",
|
||||||
"previousPage": "Page précédente",
|
"previousPage": "Page précédente",
|
||||||
"nextPage": "Page suivante"
|
"nextPage": "Page suivante"
|
||||||
|
|||||||
@@ -94,7 +94,14 @@ export class BookService extends BaseApiService {
|
|||||||
const response: ImageResponse = await ImageService.getImage(
|
const response: ImageResponse = await ImageService.getImage(
|
||||||
`books/${bookId}/pages/${adjustedPageNumber}?zero_based=true`
|
`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: {
|
headers: {
|
||||||
"Content-Type": response.contentType || "image/jpeg",
|
"Content-Type": response.contentType || "image/jpeg",
|
||||||
"Cache-Control": "public, max-age=31536000, immutable",
|
"Cache-Control": "public, max-age=31536000, immutable",
|
||||||
|
|||||||
@@ -12,23 +12,16 @@ export class ImageService extends BaseApiService {
|
|||||||
try {
|
try {
|
||||||
const headers = { Accept: "image/jpeg, image/png, image/gif, image/webp, */*" };
|
const headers = { Accept: "image/jpeg, image/png, image/gif, image/webp, */*" };
|
||||||
|
|
||||||
const result = await this.fetchWithCache<ImageResponse>(
|
// NE PAS mettre en cache - les images sont trop grosses et les Buffers ne sérialisent pas bien
|
||||||
`image-${path}`,
|
const response = await this.fetchFromApi<Response>({ path }, headers, { isImage: true });
|
||||||
async () => {
|
const contentType = response.headers.get("content-type");
|
||||||
const response = await this.fetchFromApi<Response>({ path }, headers, { isImage: true });
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
const contentType = response.headers.get("content-type");
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
buffer,
|
buffer,
|
||||||
contentType,
|
contentType,
|
||||||
};
|
};
|
||||||
},
|
|
||||||
"IMAGES"
|
|
||||||
);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la récupération de l'image:", error);
|
console.error("Erreur lors de la récupération de l'image:", error);
|
||||||
throw new AppError(ERROR_CODES.IMAGE.FETCH_ERROR, {}, error);
|
throw new AppError(ERROR_CODES.IMAGE.FETCH_ERROR, {}, error);
|
||||||
|
|||||||
Reference in New Issue
Block a user