import type { AssetVersion, ManualAcquisitionRequest, ManualAcquisitionResult } from "@umt/contracts"; import { Bot, Download } from "lucide-react"; import { useEffect, useId, useState } from "react"; import { Link } from "react-router"; import { toast } from "sonner"; import { downloaders } from "~/api/instances.ts"; import { useLibraryVersions } from "~/api/library-versions.ts"; import { FilterSelect } from "~/components/console/filter-bar.tsx"; import { Button } from "~/components/ui/button.tsx"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "~/components/ui/dialog.tsx"; import { Input } from "~/components/ui/input.tsx"; import { errorMessage } from "~/lib/api.ts"; import { formatBytes } from "~/lib/utils.ts"; export type ManualDownloadOptions = Omit; export interface ManualDownloadResource { title: string; sizeBytes: number | null; entityId?: string; } /** 搜索、工作台和原站视图共用的一次性下载选项。 */ export function ManualDownloadDialog({ resource, open, onOpenChange, submit, initialUnrecognized = false, onDelegate, }: { resource: ManualDownloadResource; open: boolean; onOpenChange: (open: boolean) => void; submit: (options: ManualDownloadOptions) => Promise; initialUnrecognized?: boolean; onDelegate?: () => void; }) { const instances = downloaders.useList(); const formId = useId(); const inventory = useLibraryVersions(open ? resource.entityId : undefined); const [instanceId, setInstanceId] = useState(""); const [savePath, setSavePath] = useState(""); const [mode, setMode] = useState<"normal" | "replace">("normal"); const [selected, setSelected] = useState([]); const [unrecognized, setUnrecognized] = useState(initialUnrecognized); const [busy, setBusy] = useState(false); const [result, setResult] = useState(null); const versions = new Map(); for (const v of inventory.data?.movie?.versions ?? []) versions.set(v.assetId, v); for (const season of inventory.data?.seasons ?? []) for (const episode of season.episodes) for (const v of episode.versions) versions.set(v.assetId, v); for (const v of inventory.data?.unplaced ?? []) versions.set(v.assetId, v); const existing = [...versions.values()]; const single = existing.length === 1 ? existing[0]!.assetId : null; useEffect(() => { if (mode === "replace" && single) setSelected([single]); }, [mode, single]); useEffect(() => { if (open) { setResult(null); setUnrecognized(initialUnrecognized); } }, [open, initialUnrecognized]); const go = async () => { setBusy(true); try { const r = await submit({ ...(resource.entityId ? { entityId: resource.entityId } : {}), ...(instanceId ? { downloaderInstanceId: instanceId } : {}), ...(savePath.trim() ? { savePath: savePath.trim() } : {}), ...(unrecognized ? { allowUnrecognized: true } : {}), importMode: mode, replacementAssetIds: mode === "replace" ? selected : [], }); setResult(r); if (r.requiresConfirmation) setUnrecognized(true); if (r.submitted) toast.success("已提交下载"); } catch (e) { toast.error(errorMessage(e)); } finally { setBusy(false); } }; return ( 下载这个资源 {resource.title} {resource.sizeBytes !== null ? ` · ${formatBytes(resource.sizeBytes, 1)}` : ""}
v.enabled) .map((v) => ({ value: v.id, label: v.name }))} /> setMode(v === "replace" ? "replace" : "normal")} allLabel="正常入库,保留已有版本" options={[{ value: "replace", label: "一次性洗版入库" }]} /> {resource.entityId && inventory.isPending ? (

正在读取已有版本…

) : null} {inventory.error ? (

已有版本读取失败,请重试后选择洗版。

) : null} {existing.length ? (
{mode === "replace" ? "选择要替换的已有文件" : "媒体库已有版本"} {existing.map((v) => (
{mode === "replace" ? ( setSelected((ids) => e.target.checked ? [...ids, v.assetId] : ids.filter((id) => id !== v.assetId), ) } /> ) : null} {mode === "replace" ? ( ) : ( )}
))}
) : mode === "replace" ? (

没有可替换的已有文件,请选择正常入库。

) : null} {mode === "replace" ? (

本次新文件成功入库后替换所选旧文件。剧集只替换成功对应的季集,其余保留。

) : null} {unrecognized ? (

暂未识别到作品。可以继续下载;整理时会尝试补全识别和本地元数据。

) : null} {result && !result.submitted && !result.requiresConfirmation ? (

{result.rejected.map((r) => r.message).join(";")}

) : null} {result?.submitted ? ( 查看下载任务 ) : null}
{onDelegate ? ( ) : null}
); } function VersionDescription({ version }: { version: AssetVersion }) { return ( {version.fileName} {[version.resolution, version.source, version.videoCodec, ...version.hdr, version.edition] .filter(Boolean) .join(" · ")} ); }