import Link from "next/link"; import { Star, Calendar, Clock, Building2, Film, Tag as TagIcon, Captions } from "lucide-react"; import { CoverFlagToggle } from "./CoverFlagToggle"; import { getImageDetail, listAllCollections, listAttachedImages, listAllActresses, listAllGenres } from "@/lib/db/queries"; import { CoverEditor } from "@/components/cover/CoverEditor"; import { CoverPlayButton } from "@/components/video/CoverPlayButton"; import { TagEditor } from "@/components/tags/TagEditor"; import { CollectionPicker } from "@/components/collections/CollectionPicker"; import { AttachedImages } from "@/components/image/AttachedImages"; import { formatBytes } from "@/lib/utils"; import { imageUrl } from "@/lib/assetUrls"; import { formatBitrate, formatDuration, formatResolution, formatBytes as formatVideoBytes, formatVideoSummary, listStoredVideoMetadataForCode } from "@/lib/video/metadata"; import { Panel, PanelStack, PanelSection, PanelHeader, ChipCluster } from "@/components/ui/panel"; import path from "node:path"; export function ImageDetailView({ imageId }: { imageId: number }) { const detail = getImageDetail(imageId); if (!detail) return null; const allCollections = listAllCollections().map((c) => ({ id: c.id, name: c.name, slug: c.slug })); const attached = listAttachedImages(detail.image.id); const actressSuggestions = listAllActresses().map((a) => { const primaryAliases: string[] = []; const tokens = a.name.trim().split(/\s+/).filter(Boolean); if (tokens.length >= 2) primaryAliases.push(tokens.slice().reverse().join(" ")); const aliases: string[] = []; if (a.altNames) { for (const part of a.altNames.split(/[,、,]/)) { const t = part.trim(); if (t) aliases.push(t); } } return { name: a.name, primaryAliases, aliases }; }); const genreSuggestions = listAllGenres().map((g) => g.name); const { image, studio, label, series, actresses, genres, tags, collections } = detail; const videoMetas = image.hasVideo ? listStoredVideoMetadataForCode(image.code) : []; const videoSummary = videoMetas.map((meta) => formatVideoSummary(meta)).find(Boolean); // Pull the first probed-clean meta for the per-stat hero strip. Falls // back to the very first row if none have a usable probe yet. const heroMeta = videoMetas.find((m) => !m.probeError) ?? videoMetas[0] ?? null; const heroStats = heroMeta && !heroMeta.probeError ? { resolution: formatResolution(heroMeta.width, heroMeta.height), bitrate: formatBitrate(heroMeta.videoBitrate), // Sum across parts so the user sees the actual disk footprint // for the whole title, not just the first file. size: formatVideoBytes(videoMetas.reduce((acc, m) => acc + (m.sizeBytes ?? 0), 0)), // Same for length — total runtime across all parts. length: formatDuration(videoMetas.reduce((acc, m) => acc + (m.durationSec ?? 0), 0)), } : null; return (
{image.notes}