feat: enhance image loading in PhotoswipeReader with prefetching capabilities for current and next book pages
This commit is contained in:
@@ -27,7 +27,12 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
|
|||||||
const { direction, toggleDirection, isRTL } = useReadingDirection();
|
const { direction, toggleDirection, isRTL } = useReadingDirection();
|
||||||
const { isFullscreen, toggleFullscreen } = useFullscreen();
|
const { isFullscreen, toggleFullscreen } = useFullscreen();
|
||||||
const { isDoublePage, shouldShowDoublePage, toggleDoublePage } = useDoublePageMode();
|
const { isDoublePage, shouldShowDoublePage, toggleDoublePage } = useDoublePageMode();
|
||||||
const { loadedImages, imageBlobUrls, loadImageDimensions, handleForceReload, getPageUrl } = useImageLoader(book.id, pages);
|
const { loadedImages, imageBlobUrls, prefetchPages, prefetchNextBook, handleForceReload, getPageUrl, prefetchCount } = useImageLoader({
|
||||||
|
bookId: book.id,
|
||||||
|
pages,
|
||||||
|
prefetchCount: 5,
|
||||||
|
nextBook: nextBook ? { id: nextBook.id, pages: [] } : null
|
||||||
|
});
|
||||||
const { currentPage, showEndMessage, navigateToPage, handlePreviousPage, handleNextPage } = usePageNavigation({
|
const { currentPage, showEndMessage, navigateToPage, handlePreviousPage, handleNextPage } = usePageNavigation({
|
||||||
book,
|
book,
|
||||||
pages,
|
pages,
|
||||||
@@ -59,20 +64,22 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
// Preload current and next pages dimensions
|
// Prefetch current and next pages
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadImageDimensions(currentPage);
|
// Prefetch pages starting from current page
|
||||||
if (isDoublePage && shouldShowDoublePage(currentPage, pages.length)) {
|
prefetchPages(currentPage, prefetchCount);
|
||||||
loadImageDimensions(currentPage + 1);
|
|
||||||
|
// If double page mode, also prefetch additional pages for smooth double page navigation
|
||||||
|
if (isDoublePage && shouldShowDoublePage(currentPage, pages.length) && currentPage + prefetchCount < pages.length) {
|
||||||
|
prefetchPages(currentPage + prefetchCount, 1);
|
||||||
}
|
}
|
||||||
// Preload next
|
|
||||||
if (currentPage < pages.length) {
|
// If we're near the end of the book, prefetch the next book
|
||||||
loadImageDimensions(currentPage + 1);
|
const pagesFromEnd = pages.length - currentPage;
|
||||||
if (isDoublePage && currentPage + 1 < pages.length) {
|
if (pagesFromEnd <= prefetchCount && nextBook) {
|
||||||
loadImageDimensions(currentPage + 2);
|
prefetchNextBook(prefetchCount);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [currentPage, isDoublePage, shouldShowDoublePage, loadImageDimensions, pages.length]);
|
}, [currentPage, isDoublePage, shouldShowDoublePage, prefetchPages, prefetchNextBook, prefetchCount, pages.length, nextBook]);
|
||||||
|
|
||||||
// Keyboard events
|
// Keyboard events
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -5,32 +5,148 @@ interface ImageDimensions {
|
|||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useImageLoader(bookId: string, _pages: number[]) {
|
type ImageKey = number | string; // Support both numeric pages and prefixed keys like "next-1"
|
||||||
const [loadedImages, setLoadedImages] = useState<Record<number, ImageDimensions>>({});
|
|
||||||
const [imageBlobUrls, setImageBlobUrls] = useState<Record<number, string>>({});
|
|
||||||
const loadedImagesRef = useRef(loadedImages);
|
|
||||||
|
|
||||||
// Keep ref in sync with state
|
interface UseImageLoaderProps {
|
||||||
|
bookId: string;
|
||||||
|
pages: number[];
|
||||||
|
prefetchCount?: number; // Nombre de pages à précharger (défaut: 2)
|
||||||
|
nextBook?: { id: string; pages: number[] } | null; // Livre suivant pour prefetch
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useImageLoader({ bookId, pages: _pages, prefetchCount = 2, nextBook }: UseImageLoaderProps) {
|
||||||
|
const [loadedImages, setLoadedImages] = useState<Record<ImageKey, ImageDimensions>>({});
|
||||||
|
const [imageBlobUrls, setImageBlobUrls] = useState<Record<ImageKey, string>>({});
|
||||||
|
const loadedImagesRef = useRef(loadedImages);
|
||||||
|
const imageBlobUrlsRef = useRef(imageBlobUrls);
|
||||||
|
|
||||||
|
// Keep refs in sync with state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadedImagesRef.current = loadedImages;
|
loadedImagesRef.current = loadedImages;
|
||||||
}, [loadedImages]);
|
}, [loadedImages]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
imageBlobUrlsRef.current = imageBlobUrls;
|
||||||
|
}, [imageBlobUrls]);
|
||||||
|
|
||||||
const getPageUrl = useCallback((pageNum: number) => `/api/komga/books/${bookId}/pages/${pageNum}`, [bookId]);
|
const getPageUrl = useCallback((pageNum: number) => `/api/komga/books/${bookId}/pages/${pageNum}`, [bookId]);
|
||||||
|
|
||||||
// Load image dimensions
|
// Prefetch image and store dimensions
|
||||||
const loadImageDimensions = useCallback((pageNum: number) => {
|
const prefetchImage = useCallback(async (pageNum: number) => {
|
||||||
if (loadedImagesRef.current[pageNum]) return;
|
// Check if we already have both dimensions and blob URL
|
||||||
|
const hasDimensions = loadedImagesRef.current[pageNum];
|
||||||
|
const hasBlobUrl = imageBlobUrlsRef.current[pageNum];
|
||||||
|
|
||||||
const img = new Image();
|
if (hasDimensions && hasBlobUrl) {
|
||||||
img.onload = () => {
|
return;
|
||||||
setLoadedImages(prev => ({
|
}
|
||||||
...prev,
|
|
||||||
[pageNum]: { width: img.naturalWidth, height: img.naturalHeight }
|
try {
|
||||||
}));
|
const response = await fetch(getPageUrl(pageNum));
|
||||||
};
|
if (!response.ok) {
|
||||||
img.src = getPageUrl(pageNum);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Create image to get dimensions
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
setLoadedImages(prev => ({
|
||||||
|
...prev,
|
||||||
|
[pageNum]: { width: img.naturalWidth, height: img.naturalHeight }
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Store the blob URL for immediate use
|
||||||
|
setImageBlobUrls(prev => ({
|
||||||
|
...prev,
|
||||||
|
[pageNum]: blobUrl
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
img.src = blobUrl;
|
||||||
|
} catch {
|
||||||
|
// Silently fail prefetch
|
||||||
|
}
|
||||||
}, [getPageUrl]);
|
}, [getPageUrl]);
|
||||||
|
|
||||||
|
// Prefetch multiple pages starting from a given page
|
||||||
|
const prefetchPages = useCallback(async (startPage: number, count: number = prefetchCount) => {
|
||||||
|
const pagesToPrefetch = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const pageNum = startPage + i;
|
||||||
|
if (pageNum <= _pages.length) {
|
||||||
|
const hasDimensions = loadedImagesRef.current[pageNum];
|
||||||
|
const hasBlobUrl = imageBlobUrlsRef.current[pageNum];
|
||||||
|
|
||||||
|
// Prefetch if we don't have both dimensions AND blob URL
|
||||||
|
if (!hasDimensions || !hasBlobUrl) {
|
||||||
|
pagesToPrefetch.push(pageNum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefetch all pages in parallel
|
||||||
|
if (pagesToPrefetch.length > 0) {
|
||||||
|
await Promise.all(pagesToPrefetch.map(pageNum => prefetchImage(pageNum)));
|
||||||
|
}
|
||||||
|
}, [prefetchImage, prefetchCount, _pages.length]);
|
||||||
|
|
||||||
|
// Prefetch pages from next book
|
||||||
|
const prefetchNextBook = useCallback(async (count: number = prefetchCount) => {
|
||||||
|
if (!nextBook) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pagesToPrefetch = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const pageNum = i + 1; // Pages du livre suivant commencent à 1
|
||||||
|
// Pour le livre suivant, on utilise une clé différente pour éviter les conflits
|
||||||
|
const nextBookPageKey = `next-${pageNum}`;
|
||||||
|
const hasDimensions = loadedImagesRef.current[nextBookPageKey];
|
||||||
|
const hasBlobUrl = imageBlobUrlsRef.current[nextBookPageKey];
|
||||||
|
|
||||||
|
if (!hasDimensions || !hasBlobUrl) {
|
||||||
|
pagesToPrefetch.push({ pageNum, nextBookPageKey });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefetch all pages in parallel
|
||||||
|
if (pagesToPrefetch.length > 0) {
|
||||||
|
await Promise.all(pagesToPrefetch.map(async ({ pageNum, nextBookPageKey }) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/komga/books/${nextBook.id}/pages/${pageNum}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Create image to get dimensions
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
setLoadedImages(prev => ({
|
||||||
|
...prev,
|
||||||
|
[nextBookPageKey]: { width: img.naturalWidth, height: img.naturalHeight }
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Store the blob URL for immediate use
|
||||||
|
setImageBlobUrls(prev => ({
|
||||||
|
...prev,
|
||||||
|
[nextBookPageKey]: blobUrl
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
img.src = blobUrl;
|
||||||
|
} catch {
|
||||||
|
// Silently fail prefetch
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [nextBook, prefetchCount]);
|
||||||
|
|
||||||
// Force reload handler
|
// Force reload handler
|
||||||
const handleForceReload = useCallback(async (currentPage: number, isDoublePage: boolean, shouldShowDoublePage: (page: number) => boolean) => {
|
const handleForceReload = useCallback(async (currentPage: number, isDoublePage: boolean, shouldShowDoublePage: (page: number) => boolean) => {
|
||||||
// Révoquer les anciennes URLs blob
|
// Révoquer les anciennes URLs blob
|
||||||
@@ -89,20 +205,23 @@ export function useImageLoader(bookId: string, _pages: number[]) {
|
|||||||
}
|
}
|
||||||
}, [imageBlobUrls, getPageUrl]);
|
}, [imageBlobUrls, getPageUrl]);
|
||||||
|
|
||||||
// Cleanup blob URLs on unmount
|
// Cleanup blob URLs on unmount only
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
Object.values(imageBlobUrls).forEach(url => {
|
Object.values(imageBlobUrlsRef.current).forEach(url => {
|
||||||
if (url) URL.revokeObjectURL(url);
|
if (url) URL.revokeObjectURL(url);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}, [imageBlobUrls]);
|
}, []); // Empty dependency array - only cleanup on unmount
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loadedImages,
|
loadedImages,
|
||||||
imageBlobUrls,
|
imageBlobUrls,
|
||||||
loadImageDimensions,
|
prefetchImage,
|
||||||
|
prefetchPages,
|
||||||
|
prefetchNextBook,
|
||||||
handleForceReload,
|
handleForceReload,
|
||||||
getPageUrl,
|
getPageUrl,
|
||||||
|
prefetchCount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user