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 { RECORD_STATUS_LABEL, RECORD_STATUS_TONE, useRecordsForEntity } from "~/api/records.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 { Timeline } from "~/components/console/timeline.tsx"; import { SLOT_STATUS } from "~/components/library/labels.ts"; import type { WizardEntity } from "~/components/organize-wizard/model.ts"; 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 { IdentifyPanel } from "./identify-panel.tsx"; import { baseName } from "./model.ts"; const PREVIEW_MAX = 20 * 1024 * 1024; const ACTION_LABEL: Record = { copy: "复制", move: "移动", rename: "重命名", relocate: "目录调整", hardlink: "硬链接", trash: "移入回收站", delete: "永久删除", restore: "从回收站还原", }; /** 抽屉"整理记录"节:已登记作品最近 5 条整理记录 */ function RecordsSection({ entityId }: { entityId: string }) { const q = useRecordsForEntity(entityId); const items = q.data?.items.slice(0, 5) ?? []; return ( 查看整理记录 } > {q.isPending ? ( ) : q.error ? (

读取整理记录失败

) : !items.length ? (

这个作品还没有整理记录。

) : ( ({ id: r.id, at: r.finishedAt ?? r.startedAt ?? r.createdAt, tone: RECORD_STATUS_TONE[r.status], title: ( {ACTION_LABEL[r.action] ?? r.action} · {RECORD_STATUS_LABEL[r.status]} ), description: r.target.path ? {r.target.path} : null, meta: `${r.counts.succeeded}/${r.counts.total}`, }))} /> )}
); } /** 选中单项的详情抽屉:文件信息、作品/版本目标/下载任务、识别、整理记录、附属文件、媒体信息、路径绑定与小文件预览 */ export function FileDetail({ root, entry, onClose, actions, identifyAuto, onOrganize, }: { root: FileRoot; entry: FileEntry; onClose: () => void; actions?: ReactNode; /** 从菜单"识别"进入:自动识别一次 */ identifyAuto?: boolean; /** 识别候选"按此整理" */ onOrganize?: (preset: WizardEntity | null) => void; }) { 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: "版本目标", value: e.asset.slotId ? ( {e.asset.slotStatus ? (SLOT_STATUS[e.asset.slotStatus]?.label ?? e.asset.slotStatus) : "查看"} ) : ( "未满足任何版本目标" ), }, { label: "主资产", value: e.asset.isPrimary ? "是" : "否" }, ]} /> ) : null} {e.asset ? : null} {!e.escape && (e.type === "dir" || (e.type === "file" && e.fileKind === "video")) ? ( ) : 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?.text}
              
{text.data?.truncated ?

只显示前 256 KB

: null} )}
) : null}
); }