diff --git a/src/components/reader/PhotoswipeReader.tsx b/src/components/reader/PhotoswipeReader.tsx index 09bf816..de6ac85 100644 --- a/src/components/reader/PhotoswipeReader.tsx +++ b/src/components/reader/PhotoswipeReader.tsx @@ -31,6 +31,7 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP const [loadedImages, setLoadedImages] = useState>({}); const [isLoading, setIsLoading] = useState(true); const [secondPageLoading, setSecondPageLoading] = useState(true); + const [imageBlobUrls, setImageBlobUrls] = useState>({}); 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 = { + ...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 (
setShowThumbnails(!showThumbnails)} onZoom={handleZoom} + onForceReload={handleForceReload} /> {/* Reader content */} @@ -390,7 +458,8 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
)} {`Page )} {`Page { const { t } = useTranslation(); @@ -125,6 +127,18 @@ export const ControlButtons = ({ iconClassName="h-6 w-6" className="rounded-full" /> + { + e.stopPropagation(); + onForceReload(); + }} + tooltip={t("reader.controls.reload")} + iconClassName="h-6 w-6" + className="rounded-full" + />
e.stopPropagation()}> void; onZoom: () => void; + onForceReload: () => void; } export interface UsePageNavigationProps { diff --git a/src/i18n/messages/en/common.json b/src/i18n/messages/en/common.json index 1e62cc9..3f95242 100644 --- a/src/i18n/messages/en/common.json +++ b/src/i18n/messages/en/common.json @@ -418,6 +418,7 @@ "hide": "Hide thumbnails" }, "zoom": "Zoom", + "reload": "Reload page", "close": "Close", "previousPage": "Previous page", "nextPage": "Next page" diff --git a/src/i18n/messages/fr/common.json b/src/i18n/messages/fr/common.json index 534f476..b25508f 100644 --- a/src/i18n/messages/fr/common.json +++ b/src/i18n/messages/fr/common.json @@ -416,6 +416,7 @@ "hide": "Masquer les vignettes" }, "zoom": "Zoom", + "reload": "Recharger la page", "close": "Fermer", "previousPage": "Page précédente", "nextPage": "Page suivante" diff --git a/src/lib/services/book.service.ts b/src/lib/services/book.service.ts index bc5d81f..8aefc9d 100644 --- a/src/lib/services/book.service.ts +++ b/src/lib/services/book.service.ts @@ -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", diff --git a/src/lib/services/image.service.ts b/src/lib/services/image.service.ts index c86c703..4aed256 100644 --- a/src/lib/services/image.service.ts +++ b/src/lib/services/image.service.ts @@ -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( - `image-${path}`, - async () => { - const response = await this.fetchFromApi({ 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({ 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);