import { LoaderCircle, ScanSearch } from "lucide-react"; import type { ReactNode } from "react"; import { Link } from "react-router"; import { toast } from "sonner"; import { type FileEntry, type FileRoot, filesErrorText, useFileStat, useProbe } 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 { RecordLink } from "~/components/console/record-sheet.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 } 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 { FilePreviewSection } from "./file-preview.tsx"; import { IdentifyPanel } from "./identify-panel.tsx"; import { baseName, displayPath, previewKindOf } from "./model.ts"; 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 pk = previewKindOf(e); const media = probe.data?.items[0]?.summary ?? e.media; return ( !o && onClose()} title={e.name} description={} footer={actions} width={pk && pk !== "image" ? "lg" : "md"} > {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 ? `:${it.reason}` : ""}`); }, onError: (err) => toast.error(filesErrorText(err, "探测失败")), }, ) } > {probe.isPending ? : } 探测 } > {media ? ( ) : (

未整理的文件不自动探测。点「探测」用 ffprobe 读取,结果缓存 24 小时。

)}
) : null}
); }