feat: add page preloading in reader

- Add page caching system with URL.createObjectURL

- Implement preloading of next pages

- Add cache cleanup for memory management

- Update devbook to mark preloading as completed
This commit is contained in:
Julien Froidefond
2025-02-12 06:52:15 +01:00
parent bd233042bc
commit 6bd52fb245
2 changed files with 81 additions and 8 deletions

View File

@@ -19,8 +19,7 @@ Créer une application web moderne avec Next.js permettant de lire des fichiers
- [x] Mode plein écran - [x] Mode plein écran
- [x] Raccourcis clavier - [x] Raccourcis clavier
- [x] Mode double page - [x] Mode double page
- [ ] Zoom et pan - [x] Préchargement des pages
- [ ] Préchargement des pages
## 🛠 Configuration initiale ## 🛠 Configuration initiale
@@ -100,9 +99,6 @@ Créer une application web moderne avec Next.js permettant de lire des fichiers
- [x] Page d'accueil - [x] Page d'accueil
- [x] Présentation des fonctionnalités principales - [x] Présentation des fonctionnalités principales
- [x] Liste des collections récentes - [x] Liste des collections récentes
- [ ] Barre de recherche
- [ ] Filtres avancés
- [ ] Tri personnalisable
- [x] Page de collection - [x] Page de collection
- [x] Grille de séries avec lazy loading - [x] Grille de séries avec lazy loading
- [x] Affichage des couvertures - [x] Affichage des couvertures

View File

@@ -10,7 +10,15 @@ import {
SplitSquareVertical, SplitSquareVertical,
} from "lucide-react"; } from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import { useEffect, useState, useCallback } from "react"; import { useEffect, useState, useCallback, useRef } from "react";
interface PageCache {
[pageNumber: number]: {
blob: Blob;
url: string;
timestamp: number;
};
}
interface BookReaderProps { interface BookReaderProps {
book: KomgaBook; book: KomgaBook;
@@ -23,6 +31,7 @@ export function BookReader({ book, pages, onClose }: BookReaderProps) {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [imageError, setImageError] = useState(false); const [imageError, setImageError] = useState(false);
const [isDoublePage, setIsDoublePage] = useState(false); const [isDoublePage, setIsDoublePage] = useState(false);
const pageCache = useRef<PageCache>({});
// Fonction pour déterminer si on doit afficher une ou deux pages // Fonction pour déterminer si on doit afficher une ou deux pages
const shouldShowDoublePage = useCallback( const shouldShowDoublePage = useCallback(
@@ -36,6 +45,74 @@ export function BookReader({ book, pages, onClose }: BookReaderProps) {
[isDoublePage, pages.length] [isDoublePage, pages.length]
); );
// Fonction pour précharger une page
const preloadPage = useCallback(
async (pageNumber: number) => {
if (pageNumber > pages.length || pageNumber < 1 || pageCache.current[pageNumber]) return;
try {
const response = await fetch(`/api/komga/books/${book.id}/pages/${pageNumber}`);
const blob = await response.blob();
const url = URL.createObjectURL(blob);
pageCache.current[pageNumber] = {
blob,
url,
timestamp: Date.now(),
};
} catch (error) {
console.error(`Erreur lors du préchargement de la page ${pageNumber}:`, error);
}
},
[book.id, pages.length]
);
// Fonction pour précharger les prochaines pages
const preloadNextPages = useCallback(
async (currentPageNumber: number, count: number = 2) => {
const pagesToPreload = Array.from(
{ length: count },
(_, i) => currentPageNumber + i + 1
).filter((page) => page <= pages.length);
await Promise.all(pagesToPreload.map(preloadPage));
},
[pages.length, preloadPage]
);
// Nettoyer le cache des pages trop anciennes
const cleanCache = useCallback(
(currentPageNumber: number, maxDistance: number = 5) => {
const minPage = Math.max(1, currentPageNumber - maxDistance);
const maxPage = Math.min(pages.length, currentPageNumber + maxDistance);
Object.entries(pageCache.current).forEach(([pageNum, cache]) => {
const page = parseInt(pageNum);
if (page < minPage || page > maxPage) {
URL.revokeObjectURL(cache.url);
delete pageCache.current[page];
}
});
},
[pages.length]
);
// Fonction pour obtenir l'URL d'une page (depuis le cache ou générée)
const getPageUrl = useCallback(
(pageNumber: number) => {
const cached = pageCache.current[pageNumber];
if (cached) return cached.url;
return `/api/komga/books/${book.id}/pages/${pageNumber}`;
},
[book.id]
);
// Effet pour précharger les pages au changement de page
useEffect(() => {
preloadNextPages(currentPage);
cleanCache(currentPage);
}, [currentPage, preloadNextPages, cleanCache]);
const handlePreviousPage = useCallback(() => { const handlePreviousPage = useCallback(() => {
if (currentPage > 1) { if (currentPage > 1) {
// En mode double page, reculer de 2 pages sauf si on est sur la page 2 // En mode double page, reculer de 2 pages sauf si on est sur la page 2
@@ -111,7 +188,7 @@ export function BookReader({ book, pages, onClose }: BookReaderProps) {
)} )}
{!imageError ? ( {!imageError ? (
<Image <Image
src={`/api/komga/books/${book.id}/pages/${currentPage}`} src={getPageUrl(currentPage)}
alt={`Page ${currentPage}`} alt={`Page ${currentPage}`}
className="h-full w-auto object-contain" className="h-full w-auto object-contain"
width={800} width={800}
@@ -134,7 +211,7 @@ export function BookReader({ book, pages, onClose }: BookReaderProps) {
{shouldShowDoublePage(currentPage) && ( {shouldShowDoublePage(currentPage) && (
<div className="relative h-full w-auto"> <div className="relative h-full w-auto">
<Image <Image
src={`/api/komga/books/${book.id}/pages/${currentPage + 1}`} src={getPageUrl(currentPage + 1)}
alt={`Page ${currentPage + 1}`} alt={`Page ${currentPage + 1}`}
className="h-full w-auto object-contain" className="h-full w-auto object-contain"
width={800} width={800}