feat: implement image caching mechanism with configurable cache duration and flush functionality
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user