From 4441c595842eda26fe5159a0567fbea72339a00f Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Sun, 1 Mar 2026 21:36:40 +0100 Subject: [PATCH] fix: close reader immediately while cancelling prefetches --- src/components/reader/ClientBookWrapper.tsx | 23 ++++++++++++++++++- src/components/reader/PhotoswipeReader.tsx | 17 ++++++++++---- src/components/reader/hooks/useImageLoader.ts | 18 +++++++++++++++ 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/components/reader/ClientBookWrapper.tsx b/src/components/reader/ClientBookWrapper.tsx index 89c38bc..5fe683d 100644 --- a/src/components/reader/ClientBookWrapper.tsx +++ b/src/components/reader/ClientBookWrapper.tsx @@ -1,5 +1,7 @@ "use client"; +import { useEffect, useState } from "react"; +import { Loader2 } from "lucide-react"; import type { KomgaBook } from "@/types/komga"; import { PhotoswipeReader } from "./PhotoswipeReader"; import { useRouter } from "next/navigation"; @@ -13,12 +15,31 @@ interface ClientBookWrapperProps { export function ClientBookWrapper({ book, pages, nextBook }: ClientBookWrapperProps) { const router = useRouter(); + const [isClosing, setIsClosing] = useState(false); + const [targetPath, setTargetPath] = useState(null); + + useEffect(() => { + if (!isClosing || !targetPath) return; + router.push(targetPath); + }, [isClosing, targetPath, router]); const handleCloseReader = (currentPage: number) => { ClientOfflineBookService.setCurrentPage(book, currentPage); - router.push(`/series/${book.seriesId}`); + setTargetPath(`/series/${book.seriesId}`); + setIsClosing(true); }; + if (isClosing) { + return ( +
+
+ + Fermeture du lecteur... +
+
+ ); + } + return ( ); diff --git a/src/components/reader/PhotoswipeReader.tsx b/src/components/reader/PhotoswipeReader.tsx index 55a72b9..ebd1a3f 100644 --- a/src/components/reader/PhotoswipeReader.tsx +++ b/src/components/reader/PhotoswipeReader.tsx @@ -34,6 +34,7 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP imageBlobUrls, prefetchPages, prefetchNextBook, + cancelAllPrefetches, handleForceReload, getPageUrl, prefetchCount, @@ -105,6 +106,14 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP ]); // Keyboard events + const handleCloseReader = useCallback( + (page: number) => { + cancelAllPrefetches(); + onClose?.(page); + }, + [cancelAllPrefetches, onClose] + ); + useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "ArrowLeft") { @@ -123,7 +132,7 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP } } else if (e.key === "Escape" && onClose) { e.preventDefault(); - onClose(currentPage); + handleCloseReader(currentPage); } }; @@ -132,7 +141,7 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP return () => { window.removeEventListener("keydown", handleKeyDown); }; - }, [handleNextPage, handlePreviousPage, onClose, isRTL, currentPage]); + }, [handleNextPage, handlePreviousPage, onClose, isRTL, currentPage, handleCloseReader]); const handleContainerClick = useCallback( (e: React.MouseEvent) => { @@ -173,7 +182,7 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP undefined)} + onClose={handleCloseReader} currentPage={currentPage} /> @@ -183,7 +192,7 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP onPreviousPage={handlePreviousPage} onNextPage={handleNextPage} onPageChange={navigateToPage} - onClose={onClose} + onClose={handleCloseReader} currentPage={currentPage} totalPages={pages.length} isDoublePage={isDoublePage} diff --git a/src/components/reader/hooks/useImageLoader.ts b/src/components/reader/hooks/useImageLoader.ts index 5f13866..e020286 100644 --- a/src/components/reader/hooks/useImageLoader.ts +++ b/src/components/reader/hooks/useImageLoader.ts @@ -53,9 +53,18 @@ export function useImageLoader({ }; }, []); + const cancelAllPrefetches = useCallback(() => { + abortControllersRef.current.forEach((controller) => controller.abort()); + abortControllersRef.current.clear(); + pendingFetchesRef.current.clear(); + }, []); + const runWithConcurrency = useCallback( async (items: T[], worker: (item: T) => Promise, concurrency = PREFETCH_CONCURRENCY) => { for (let i = 0; i < items.length; i += concurrency) { + if (!isMountedRef.current) { + return; + } const batch = items.slice(i, i + concurrency); await Promise.all(batch.map((item) => worker(item))); } @@ -71,6 +80,10 @@ export function useImageLoader({ // Prefetch image and store dimensions const prefetchImage = useCallback( async (pageNum: number) => { + if (!isMountedRef.current) { + return; + } + // Check if we already have both dimensions and blob URL const hasDimensions = loadedImagesRef.current[pageNum]; const hasBlobUrl = imageBlobUrlsRef.current[pageNum]; @@ -193,6 +206,10 @@ export function useImageLoader({ // Let all prefetch requests run - server queue handles concurrency if (pagesToPrefetch.length > 0) { runWithConcurrency(pagesToPrefetch, async ({ pageNum, nextBookPageKey }) => { + if (!isMountedRef.current) { + return; + } + // Mark as pending pendingFetchesRef.current.add(nextBookPageKey); const controller = new AbortController(); @@ -329,6 +346,7 @@ export function useImageLoader({ prefetchImage, prefetchPages, prefetchNextBook, + cancelAllPrefetches, handleForceReload, getPageUrl, prefetchCount,