import { Download, ExternalLink, LoaderCircle, ScanSearch } from "lucide-react"; import type { ReactNode } from "react"; import { Link } from "react-router"; import { toast } from "sonner"; import { type FileEntry, type FileRoot, fileDownloadUrl, filesErrorText, filesMockEnabled, useFileStat, useProbe, useTextPreview, } from "~/api/files.ts"; import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx"; import { LoadingState } from "~/components/console/empty-state.tsx"; import { KvList } from "~/components/console/kv-list.tsx"; import { Button, buttonVariants } from "~/components/ui/button.tsx"; import { downloadLabel } from "~/lib/labels.ts"; import { formatBytes, formatDateTime } from "~/lib/utils.ts"; import { EntryStatus } from "./file-list.tsx"; import { baseName } from "./model.ts"; const PREVIEW_MAX = 20 * 1024 * 1024; /** 选中单项的详情抽屉:文件信息、作品/Slot/下载链接、sidecars、探测摘要、路径绑定与小文件预览 */ export function FileDetail({ root, entry, onClose, actions, }: { root: FileRoot; entry: FileEntry; onClose: () => void; actions?: ReactNode; }) { const stat = useFileStat(root.id, entry.path); const probe = useProbe(); const e = stat.data?.entry ?? entry; const previewable = e.type === "file" && e.size <= PREVIEW_MAX && (e.fileKind === "nfo" || e.fileKind === "subtitle" || e.fileKind === "image"); const text = useTextPreview(root.id, e.path, previewable && e.fileKind !== "image"); const media = probe.data?.items[0]?.summary ?? e.media; return ( !o && onClose()} title={e.name} description={} footer={actions} > {stat.isPending ? : null} {stat.error ? (

{filesErrorText(stat.error, "读取详情失败")}

) : null}
{e.asset ? ( {e.asset.title} {e.asset.year ? ` (${e.asset.year})` : ""} ), }, { label: "内容单元", value: e.asset.contentUnit ?? "整部" }, { label: "Slot", value: e.asset.slotId ? ( {e.asset.slotStatus ?? "查看"} ) : ( "未满足任何 Slot" ), }, { label: "主资产", value: e.asset.isPrimary ? "是" : "否" }, ]} /> 在整理记录中查看 ) : null} {e.download ? ( {downloadLabel(e.download.status).label} ), }, { label: "进度", value: e.download.progress != null ? `${Math.floor(e.download.progress * 100)}%` : null, }, { label: "H&R", value: e.download.hnr === "satisfied" ? "已满足" : e.download.hnr === "unsatisfied" ? "未满足" : "未知", }, ]} /> ) : null} {stat.data?.bindings.length ? (
    {stat.data.bindings.map((b) => (
  • {b.title} {b.kind === "season" && b.seasonNumber != null ? ` · 第 ${b.seasonNumber} 季` : " · 作品目录"}
  • ))}

改名/移动此目录会同步修订绑定;元数据刷新不会改路径。

) : null} {stat.data?.sidecars.length ? (
    {stat.data.sidecars.map((s) => (
  • {baseName(s)}
  • ))}
) : null} {e.type === "file" && e.fileKind === "video" ? ( probe.mutate( { items: [{ root: root.id, path: e.path }] }, { onSuccess: (r) => { const it = r.items[0]; if (it?.status !== "ok") toast.warning(`探测不到媒体信息:${it?.reason ?? "未知原因"}`); }, onError: (err) => toast.error(filesErrorText(err, "探测失败")), }, ) } > {probe.isPending ? : } 探测 } > {media ? ( ) : (

未登记文件不自动探测;点“探测”读取 ffprobe 摘要(缓存 24 小时)。

)}
) : null} {previewable ? ( 下载 ) } > {e.fileKind === "image" ? ( filesMockEnabled() ? (

演示数据没有图片内容。

) : ( {e.name} 新窗口打开 ) ) : text.isPending ? ( ) : text.error ? (

{filesErrorText(text.error, "预览失败")}

) : (
              {text.data}
            
)}
) : null}
); }