diff --git a/src/app/api/komga/random-book/route.ts b/src/app/api/komga/random-book/route.ts new file mode 100644 index 0000000..61b3a8f --- /dev/null +++ b/src/app/api/komga/random-book/route.ts @@ -0,0 +1,53 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { BookService } from "@/lib/services/book.service"; +import { ERROR_CODES } from "@/constants/errorCodes"; +import { AppError } from "@/utils/errors"; +import { getErrorMessage } from "@/utils/errors"; + +export const dynamic = "force-dynamic"; + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const libraryIds = searchParams.get("libraryIds")?.split(",") || []; + + if (libraryIds.length === 0) { + return NextResponse.json( + { + error: { + code: ERROR_CODES.LIBRARY.FETCH_ERROR, + message: "Au moins une bibliothèque doit être sélectionnée", + }, + }, + { status: 400 } + ); + } + + const bookId = await BookService.getRandomBookFromLibraries(libraryIds); + return NextResponse.json({ bookId }); + } catch (error) { + console.error("Erreur lors de la récupération d'un livre aléatoire:", error); + if (error instanceof AppError) { + return NextResponse.json( + { + error: { + code: error.code, + message: getErrorMessage(error.code), + }, + }, + { status: 500 } + ); + } + return NextResponse.json( + { + error: { + code: ERROR_CODES.SERIES.FETCH_ERROR, + message: getErrorMessage(ERROR_CODES.SERIES.FETCH_ERROR), + }, + }, + { status: 500 } + ); + } +} + diff --git a/src/components/layout/ClientLayout.tsx b/src/components/layout/ClientLayout.tsx index 1211613..ca9bae1 100644 --- a/src/components/layout/ClientLayout.tsx +++ b/src/components/layout/ClientLayout.tsx @@ -1,7 +1,7 @@ "use client"; import { ThemeProvider } from "next-themes"; -import { useState, useEffect, useMemo } from "react"; +import { useState, useEffect, useMemo, useCallback } from "react"; import { Header } from "@/components/layout/Header"; import { Sidebar } from "@/components/layout/Sidebar"; import { InstallPWA } from "../ui/InstallPWA"; @@ -24,9 +24,34 @@ interface ClientLayoutProps { export default function ClientLayout({ children, initialLibraries = [], initialFavorites = [], userIsAdmin = false }: ClientLayoutProps) { const [isSidebarOpen, setIsSidebarOpen] = useState(false); + const [randomBookId, setRandomBookId] = useState(null); const pathname = usePathname(); const { preferences } = usePreferences(); + // Récupérer un book aléatoire pour le background + const fetchRandomBook = useCallback(async () => { + if ( + preferences.background.type === "komga-random" && + preferences.background.komgaLibraries && + preferences.background.komgaLibraries.length > 0 + ) { + try { + const libraryIds = preferences.background.komgaLibraries.join(","); + const response = await fetch(`/api/komga/random-book?libraryIds=${libraryIds}`); + if (response.ok) { + const data = await response.json(); + setRandomBookId(data.bookId); + } + } catch (error) { + console.error("Erreur lors de la récupération d'un book aléatoire:", error); + } + } + }, [preferences.background.type, preferences.background.komgaLibraries]); + + useEffect(() => { + fetchRandomBook(); + }, [fetchRandomBook]); + const backgroundStyle = useMemo(() => { const bg = preferences.background; const blur = bg.blur || 0; @@ -47,9 +72,19 @@ export default function ClientLayout({ children, initialLibraries = [], initialF filter: blur > 0 ? `blur(${blur}px)` : undefined, }; } + + if (bg.type === "komga-random" && randomBookId) { + return { + backgroundImage: `url(/api/komga/images/books/${randomBookId}/thumbnail)`, + backgroundSize: "cover" as const, + backgroundPosition: "center" as const, + backgroundRepeat: "no-repeat" as const, + filter: blur > 0 ? `blur(${blur}px)` : undefined, + }; + } return {}; - }, [preferences.background]); + }, [preferences.background, randomBookId]); const handleCloseSidebar = () => { setIsSidebarOpen(false); @@ -92,7 +127,10 @@ export default function ClientLayout({ children, initialLibraries = [], initialF // Ne pas afficher le header et la sidebar sur les routes publiques et le reader const isPublicRoute = publicRoutes.includes(pathname) || pathname.startsWith('/books/'); - const hasCustomBackground = preferences.background.type === "gradient" || preferences.background.type === "image"; + const hasCustomBackground = + preferences.background.type === "gradient" || + preferences.background.type === "image" || + (preferences.background.type === "komga-random" && randomBookId); const contentOpacity = (preferences.background.opacity || 100) / 100; return ( @@ -108,7 +146,13 @@ export default function ClientLayout({ children, initialLibraries = [], initialF className={`relative min-h-screen ${hasCustomBackground ? "" : "bg-background"}`} style={hasCustomBackground ? { backgroundColor: `rgba(var(--background-rgb, 255, 255, 255), ${contentOpacity})` } : undefined} > - {!isPublicRoute &&
} + {!isPublicRoute && ( +
+ )} {!isPublicRoute && ( void; + onRefreshBackground?: () => Promise; + showRefreshBackground?: boolean; } -export function Header({ onToggleSidebar }: HeaderProps) { +export function Header({ onToggleSidebar, onRefreshBackground, showRefreshBackground = false }: HeaderProps) { const { theme, setTheme } = useTheme(); const { t } = useTranslation(); + const [isRefreshing, setIsRefreshing] = useState(false); const toggleTheme = () => { setTheme(theme === "dark" ? "light" : "dark"); }; + const handleRefreshBackground = async () => { + if (onRefreshBackground && !isRefreshing) { + setIsRefreshing(true); + await onRefreshBackground(); + setIsRefreshing(false); + } + }; + return (
@@ -37,6 +49,17 @@ export function Header({ onToggleSidebar }: HeaderProps) {
+ {komgaConfigValid && ( +
+ + +
+ )}
@@ -232,8 +284,37 @@ export function BackgroundSettings() { )} + {/* Sélection des bibliothèques Komga */} + {preferences.background.type === "komga-random" && ( +
+ +
+ {libraries.map((library) => ( +
+ handleLibraryToggle(library.id)} + /> + +
+ ))} +
+

+ Sélectionnez une ou plusieurs bibliothèques pour choisir une cover aléatoire +

+
+ )} + {/* Contrôles d'opacité et de flou */} - {(preferences.background.type === "gradient" || preferences.background.type === "image") && ( + {(preferences.background.type === "gradient" || + preferences.background.type === "image" || + preferences.background.type === "komga-random") && ( <>
diff --git a/src/lib/services/book.service.ts b/src/lib/services/book.service.ts index 0afee56..b95c9ce 100644 --- a/src/lib/services/book.service.ts +++ b/src/lib/services/book.service.ts @@ -6,6 +6,7 @@ import { PreferencesService } from "./preferences.service"; import { ERROR_CODES } from "../../constants/errorCodes"; import { AppError } from "../../utils/errors"; import { SeriesService } from "./series.service"; +import type { Series } from "@/types/series"; export class BookService extends BaseApiService { static async getBook(bookId: string): Promise { @@ -162,4 +163,71 @@ export class BookService extends BaseApiService { static getCoverUrl(bookId: string): string { return `/api/komga/images/books/${bookId}/thumbnail`; } + + static async getRandomBookFromLibraries(libraryIds: string[]): Promise { + try { + if (libraryIds.length === 0) { + throw new AppError(ERROR_CODES.LIBRARY.NOT_FOUND, { message: "Aucune bibliothèque sélectionnée" }); + } + + const { LibraryService } = await import("./library.service"); + + // Essayer d'abord d'utiliser le cache des bibliothèques + const allSeriesFromCache: Series[] = []; + + for (const libraryId of libraryIds) { + try { + // Essayer de récupérer les séries depuis le cache (rapide si en cache) + const series = await LibraryService.getAllLibrarySeries(libraryId); + allSeriesFromCache.push(...series); + } catch { + // Si erreur, on continue avec les autres bibliothèques + } + } + + if (allSeriesFromCache.length > 0) { + // Choisir une série au hasard parmi toutes celles trouvées + const randomSeriesIndex = Math.floor(Math.random() * allSeriesFromCache.length); + const randomSeries = allSeriesFromCache[randomSeriesIndex]; + + // Récupérer les books de cette série + const books = await SeriesService.getAllSeriesBooks(randomSeries.id); + + if (books.length > 0) { + const randomBookIndex = Math.floor(Math.random() * books.length); + return books[randomBookIndex].id; + } + } + + // Si pas de cache, faire une requête légère : prendre une page de séries d'une bibliothèque au hasard + const randomLibraryIndex = Math.floor(Math.random() * libraryIds.length); + const randomLibraryId = libraryIds[randomLibraryIndex]; + + // Récupérer juste une page de séries (pas toutes) + const seriesResponse = await LibraryService.getLibrarySeries(randomLibraryId, 0, 20); + + if (seriesResponse.content.length === 0) { + throw new AppError(ERROR_CODES.BOOK.NOT_FOUND, { message: "Aucune série trouvée dans les bibliothèques sélectionnées" }); + } + + // Choisir une série au hasard parmi celles récupérées + const randomSeriesIndex = Math.floor(Math.random() * seriesResponse.content.length); + const randomSeries = seriesResponse.content[randomSeriesIndex]; + + // Récupérer les books de cette série + const books = await SeriesService.getAllSeriesBooks(randomSeries.id); + + if (books.length === 0) { + throw new AppError(ERROR_CODES.BOOK.NOT_FOUND, { message: "Aucun livre trouvé dans la série" }); + } + + const randomBookIndex = Math.floor(Math.random() * books.length); + return books[randomBookIndex].id; + } catch (error) { + if (error instanceof AppError) { + throw error; + } + throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error); + } + } } diff --git a/src/types/preferences.ts b/src/types/preferences.ts index 947401d..6438bc9 100644 --- a/src/types/preferences.ts +++ b/src/types/preferences.ts @@ -1,4 +1,4 @@ -export type BackgroundType = "default" | "gradient" | "image"; +export type BackgroundType = "default" | "gradient" | "image" | "komga-random"; export interface BackgroundPreferences { type: BackgroundType; @@ -6,6 +6,7 @@ export interface BackgroundPreferences { imageUrl?: string; opacity?: number; // 0-100 blur?: number; // 0-20 (px) + komgaLibraries?: string[]; // IDs des bibliothèques Komga sélectionnées } export interface UserPreferences {