import { revalidatePath } from "next/cache"; import Image from "next/image"; import Link from "next/link"; import { listFolders, createLibrary, deleteLibrary, fetchLibraries, getBookCoverUrl, LibraryDto, FolderItem } from "../../lib/api"; import type { TranslationKey } from "../../lib/i18n/fr"; import { getServerTranslations } from "../../lib/i18n/server"; import { LibraryActions } from "../components/LibraryActions"; import { LibraryForm } from "../components/LibraryForm"; import { ProviderIcon } from "../components/ProviderIcon"; import { Card, CardHeader, CardTitle, CardDescription, CardContent, Button, Badge } from "../components/ui"; export const dynamic = "force-dynamic"; function formatNextScan(nextScanAt: string | null, imminentLabel: string): string { if (!nextScanAt) return "-"; const date = new Date(nextScanAt); const now = new Date(); const diff = date.getTime() - now.getTime(); if (diff < 0) return imminentLabel; if (diff < 60000) return "< 1 min"; if (diff < 3600000) return `${Math.floor(diff / 60000)}m`; if (diff < 86400000) return `${Math.floor(diff / 3600000)}h`; return `${Math.floor(diff / 86400000)}d`; } export default async function LibrariesPage() { const { t } = await getServerTranslations(); const [libraries, folders] = await Promise.all([ fetchLibraries().catch(() => [] as LibraryDto[]), listFolders().catch(() => [] as FolderItem[]) ]); const thumbnailMap = new Map( libraries.map(lib => [ lib.id, (lib.thumbnail_book_ids || []).map(bookId => getBookCoverUrl(bookId)), ]) ); async function addLibrary(formData: FormData) { "use server"; const name = formData.get("name") as string; const rootPath = formData.get("root_path") as string; if (name && rootPath) { await createLibrary(name, rootPath); revalidatePath("/libraries"); } } async function removeLibrary(formData: FormData) { "use server"; const id = formData.get("id") as string; await deleteLibrary(id); revalidatePath("/libraries"); } return ( <>

{t("libraries.title")}

{/* Add Library Form */} {t("libraries.addLibrary")} {t("libraries.addLibraryDescription")} {/* Libraries Grid */}
{libraries.map((lib) => { const thumbnails = thumbnailMap.get(lib.id) || []; return ( {/* Thumbnail fan */} {thumbnails.length > 0 ? (
{thumbnails.map((url, i) => { const count = thumbnails.length; const mid = (count - 1) / 2; const angle = (i - mid) * 12; const radius = 220; const rad = ((angle - 90) * Math.PI) / 180; const cx = Math.cos(rad) * radius; const cy = Math.sin(rad) * radius; return ( ); })}
) : (
)}
{lib.name} {!lib.enabled && {t("libraries.disabled")}}
{lib.root_path}
{/* Stats */}
{lib.book_count} {t("libraries.books")} {lib.series_count} {t("libraries.series")}
{/* Configuration tags */}
{lib.monitor_enabled ? '●' : '○'} {t("libraries.scanLabel", { mode: t(`monitoring.${lib.scan_mode}` as TranslationKey) })} {lib.watcher_enabled ? '⚡' : '○'} {t("libraries.watcherLabel")} {lib.metadata_provider && lib.metadata_provider !== "none" && ( {lib.metadata_provider.replace('_', ' ')} )} {lib.metadata_refresh_mode !== "manual" && ( {t("libraries.metaRefreshLabel", { mode: t(`monitoring.${lib.metadata_refresh_mode}` as TranslationKey) })} )} {lib.monitor_enabled && lib.next_scan_at && ( {t("libraries.nextScan", { time: formatNextScan(lib.next_scan_at, t("libraries.imminent")) })} )}
); })}
); }