feat: implement image caching mechanism with configurable cache duration and flush functionality

This commit is contained in:
Julien Froidefond
2025-10-19 10:36:19 +02:00
parent 7d9bac5c51
commit 0c080bd525
17 changed files with 268 additions and 60 deletions

View File

@@ -10,6 +10,7 @@ import { usePathname } from "next/navigation";
import { registerServiceWorker } from "@/lib/registerSW";
import { NetworkStatus } from "../ui/NetworkStatus";
import { usePreferences } from "@/contexts/PreferencesContext";
import { ImageCacheProvider } from "@/contexts/ImageCacheContext";
import type { KomgaLibrary, KomgaSeries } from "@/types/komga";
// Routes qui ne nécessitent pas d'authentification
@@ -135,38 +136,40 @@ export default function ClientLayout({ children, initialLibraries = [], initialF
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{/* Background fixe pour les images et gradients */}
{hasCustomBackground && (
<ImageCacheProvider>
{/* Background fixe pour les images et gradients */}
{hasCustomBackground && (
<div
className="fixed inset-0 -z-10"
style={backgroundStyle}
/>
)}
<div
className="fixed inset-0 -z-10"
style={backgroundStyle}
/>
)}
<div
className={`relative min-h-screen ${hasCustomBackground ? "" : "bg-background"}`}
style={hasCustomBackground ? { backgroundColor: `rgba(var(--background-rgb, 255, 255, 255), ${contentOpacity})` } : undefined}
>
{!isPublicRoute && (
<Header
onToggleSidebar={handleToggleSidebar}
onRefreshBackground={fetchRandomBook}
showRefreshBackground={preferences.background.type === "komga-random"}
/>
)}
{!isPublicRoute && (
<Sidebar
isOpen={isSidebarOpen}
onClose={handleCloseSidebar}
initialLibraries={initialLibraries}
initialFavorites={initialFavorites}
userIsAdmin={userIsAdmin}
/>
)}
<main className={!isPublicRoute ? "pt-safe" : ""}>{children}</main>
<InstallPWA />
<Toaster />
<NetworkStatus />
</div>
className={`relative min-h-screen ${hasCustomBackground ? "" : "bg-background"}`}
style={hasCustomBackground ? { backgroundColor: `rgba(var(--background-rgb, 255, 255, 255), ${contentOpacity})` } : undefined}
>
{!isPublicRoute && (
<Header
onToggleSidebar={handleToggleSidebar}
onRefreshBackground={fetchRandomBook}
showRefreshBackground={preferences.background.type === "komga-random"}
/>
)}
{!isPublicRoute && (
<Sidebar
isOpen={isSidebarOpen}
onClose={handleCloseSidebar}
initialLibraries={initialLibraries}
initialFavorites={initialFavorites}
userIsAdmin={userIsAdmin}
/>
)}
<main className={!isPublicRoute ? "pt-safe" : ""}>{children}</main>
<InstallPWA />
<Toaster />
<NetworkStatus />
</div>
</ImageCacheProvider>
</ThemeProvider>
);
}

View File

@@ -3,12 +3,13 @@
import { useState, useEffect } from "react";
import { useTranslate } from "@/hooks/useTranslate";
import { useToast } from "@/components/ui/use-toast";
import { Trash2, Loader2, HardDrive, List, ChevronDown, ChevronUp } from "lucide-react";
import { Trash2, Loader2, HardDrive, List, ChevronDown, ChevronUp, ImageOff } from "lucide-react";
import { CacheModeSwitch } from "@/components/settings/CacheModeSwitch";
import { Label } from "@/components/ui/label";
import type { TTLConfigData } from "@/types/komga";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { useImageCache } from "@/contexts/ImageCacheContext";
interface CacheSettingsProps {
initialTTLConfig: TTLConfigData | null;
@@ -35,6 +36,7 @@ interface ServiceWorkerCacheEntry {
export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
const { t } = useTranslate();
const { toast } = useToast();
const { flushImageCache } = useImageCache();
const [isCacheClearing, setIsCacheClearing] = useState(false);
const [isServiceWorkerClearing, setIsServiceWorkerClearing] = useState(false);
const [serverCacheSize, setServerCacheSize] = useState<CacheSizeInfo | null>(null);
@@ -56,6 +58,7 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
seriesTTL: 5,
booksTTL: 5,
imagesTTL: 1440,
imageCacheMaxAge: 2592000,
}
);
@@ -389,7 +392,15 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
}
};
const handleTTLChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const handleFlushImageCache = () => {
flushImageCache();
toast({
title: t("settings.cache.title"),
description: t("settings.cache.messages.imageCacheFlushed"),
});
};
const handleTTLChange = (event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = event.target;
setTTLConfig((prev) => ({
...prev,
@@ -788,6 +799,30 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
className="flex h-9 w-full rounded-md border border-input bg-background/70 backdrop-blur-md px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
/>
</div>
<div className="space-y-2">
<div className="space-y-1">
<label htmlFor="imageCacheMaxAge" className="text-sm font-medium">
{t("settings.cache.ttl.imageCacheMaxAge.label")}
</label>
<p className="text-xs text-muted-foreground">
{t("settings.cache.ttl.imageCacheMaxAge.description")}
</p>
</div>
<select
id="imageCacheMaxAge"
name="imageCacheMaxAge"
value={ttlConfig.imageCacheMaxAge}
onChange={handleTTLChange}
className="flex h-9 w-full rounded-md border border-input bg-background/70 backdrop-blur-md px-3 py-1 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="0">{t("settings.cache.ttl.imageCacheMaxAge.options.noCache")}</option>
<option value="3600">{t("settings.cache.ttl.imageCacheMaxAge.options.oneHour")}</option>
<option value="86400">{t("settings.cache.ttl.imageCacheMaxAge.options.oneDay")}</option>
<option value="604800">{t("settings.cache.ttl.imageCacheMaxAge.options.oneWeek")}</option>
<option value="2592000">{t("settings.cache.ttl.imageCacheMaxAge.options.oneMonth")}</option>
<option value="31536000">{t("settings.cache.ttl.imageCacheMaxAge.options.oneYear")}</option>
</select>
</div>
</div>
<div className="flex gap-3">
<button
@@ -829,6 +864,16 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
)}
</button>
</div>
<div className="flex gap-3">
<button
type="button"
onClick={handleFlushImageCache}
className="flex-1 inline-flex items-center justify-center rounded-md bg-orange-500/90 backdrop-blur-md px-3 py-2 text-sm font-medium text-white ring-offset-background transition-colors hover:bg-orange-500/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
>
<ImageOff className="mr-2 h-4 w-4" />
{t("settings.cache.buttons.flushImageCache")}
</button>
</div>
</form>
</CardContent>
</Card>

View File

@@ -4,6 +4,7 @@ import { CoverClient } from "./cover-client";
import { ProgressBar } from "./progress-bar";
import type { BookCoverProps } from "./cover-utils";
import { getImageUrl } from "./cover-utils";
import { useImageUrl } from "@/hooks/useImageUrl";
import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service";
import { MarkAsReadButton } from "./mark-as-read-button";
import { MarkAsUnreadButton } from "./mark-as-unread-button";
@@ -59,7 +60,8 @@ export function BookCover({
}: BookCoverProps) {
const { t } = useTranslate();
const imageUrl = getImageUrl("book", book.id);
const baseUrl = getImageUrl("book", book.id);
const imageUrl = useImageUrl(baseUrl);
const isCompleted = book.readProgress?.completed || false;
const currentPage = ClientOfflineBookService.getCurrentPage(book);

View File

@@ -20,6 +20,10 @@ export interface SeriesCoverProps extends BaseCoverProps {
series: KomgaSeries;
}
/**
* Génère l'URL de base pour une image (sans cache version)
* Utilisez useImageUrl() dans les composants pour obtenir l'URL avec cache busting
*/
export function getImageUrl(type: "series" | "book", id: string) {
if (type === "series") {
return `/api/komga/images/series/${id}/thumbnail`;

View File

@@ -4,6 +4,7 @@ import { CoverClient } from "./cover-client";
import { ProgressBar } from "./progress-bar";
import type { SeriesCoverProps } from "./cover-utils";
import { getImageUrl } from "./cover-utils";
import { useImageUrl } from "@/hooks/useImageUrl";
export function SeriesCover({
series,
@@ -11,7 +12,8 @@ export function SeriesCover({
className,
showProgressUi = true,
}: SeriesCoverProps) {
const imageUrl = getImageUrl("series", series.id);
const baseUrl = getImageUrl("series", series.id);
const imageUrl = useImageUrl(baseUrl);
const isCompleted = series.booksCount === series.booksReadCount;
const readBooks = series.booksReadCount;