import type { LibraryRoot, NamingKind, NamingOverride, NamingTemplatesView } from "@umt/contracts"; import { Save, Trash2 } from "lucide-react"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { useLibraryRoots } from "~/api/library.ts"; import { useNamingTemplates, useSaveGlobalNaming, useSaveRootNaming } from "~/api/naming.ts"; import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { NAMING_KINDS, overrideBody, SOURCE_LABEL, splitNamingErrors } from "~/components/naming/model.ts"; import { NAMING_KIND_LABEL, NAMING_NOTE, NamingTemplateEditor } from "~/components/naming/template-editor.tsx"; import { Button } from "~/components/ui/button.tsx"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx"; import { Switch } from "~/components/ui/switch.tsx"; import { errorMessage } from "~/lib/api.ts"; import { SaveBar, SettingsBlock } from "./settings-block.tsx"; // 设置 → 媒体库与目录 → 命名模板:全局模板 + 库根覆盖(作品覆盖在作品详情)。 const KINDS = NAMING_KINDS; export function NamingSection() { return ( <> > ); } function GlobalNaming() { const q = useNamingTemplates(); const save = useSaveGlobalNaming(); const [draft, setDraft] = useState | null>(null); const [lock, setLock] = useState(false); const [errors, setErrors] = useState> | undefined>(); useEffect(() => { if (q.data && !draft) { setDraft({ movie: q.data.global.movie, tv: q.data.global.tv, variety: q.data.global.variety }); setLock(q.data.global.lockFileNameForBound); } }, [q.data, draft]); const saved = q.data?.global; const dirtyKinds = Object.fromEntries(KINDS.map((k) => [k, !!saved && !!draft && draft[k] !== saved[k]])); const dirty = KINDS.some((k) => dirtyKinds[k]) || (!!saved && lock !== saved.lockFileNameForBound); const onSave = async () => { if (!draft) return; setErrors(undefined); try { const r = await save.mutateAsync({ ...draft, lockFileNameForBound: lock }); setDraft({ movie: r.movie, tv: r.tv, variety: r.variety }); setLock(r.lockFileNameForBound); toast.success("已保存「命名模板」"); } catch (err) { const split = splitNamingErrors(errorMessage(err)); if (split) setErrors(split); toast.error(split ? "模板有错误,未保存" : errorMessage(err, "保存失败")); } }; return ( {!q.data || !draft ? ( q.isError ? ( q.refetch()} /> ) : ( ) ) : ( { setDraft((d) => (d ? { ...d, [k]: v } : d)); setErrors((e) => (e?.[k] ? { ...e, [k]: undefined } : e)); }} defaults={q.data.defaults} saveErrors={errors} dirtyKinds={dirtyKinds} /> 已有目录的作品固定文件名 开启后,这些作品的新文件按建立目录时的模板版本命名 {save.isPending ? "保存中…" : "保存"} )} ); } function RootNaming() { const roots = useLibraryRoots(); const [rootId, setRootId] = useState(""); const list = roots.data?.items ?? []; const root = list.find((r) => r.id === rootId) ?? list[0]; return ( {roots.isPending ? ( ) : roots.isError ? ( roots.refetch()} /> ) : !root ? ( 还没有媒体库目录。先在上方「媒体库目录」添加。 ) : ( {list.map((r) => ( {r.name} {r.naming && Object.keys(r.naming).length ? "(已覆盖)" : ""} ))} {root.localPath} )} ); } function RootNamingEditor({ root }: { root: LibraryRoot }) { const q = useNamingTemplates({ libraryRootId: root.id }); const save = useSaveRootNaming(); const [draft, setDraft] = useState> | null>(null); const [errors, setErrors] = useState> | undefined>(); useEffect(() => { if (q.data && !draft) setDraft({ ...(q.data.root ?? {}) }); }, [q.data, draft]); if (q.isError) return q.refetch()} />; if (q.isPending || !draft) return ; const view: NamingTemplatesView = q.data; const saved = view.root ?? {}; const dirtyKinds = Object.fromEntries(KINDS.map((k) => [k, (draft[k] ?? "") !== (saved[k] ?? "")])); const dirty = KINDS.some((k) => dirtyKinds[k]); const hasOverride = !!view.root && Object.keys(view.root).length > 0; const put = async (body: NamingOverride | null) => { setErrors(undefined); try { await save.mutateAsync({ id: root.id, naming: body }); setDraft({ ...(body ?? {}) }); toast.success(body ? "已保存「目录命名覆盖」" : "已清除「目录命名覆盖」"); } catch (err) { const split = splitNamingErrors(errorMessage(err)); if (split) setErrors(split); toast.error(split ? "模板有错误,未保存" : errorMessage(err, "保存失败")); } }; return ( {KINDS.map((k) => ( {NAMING_KIND_LABEL[k]}:{saved[k] ? "媒体库目录覆盖" : "沿用全局"} ))} { setDraft((d) => ({ ...d, [k]: v })); setErrors((e) => (e?.[k] ? { ...e, [k]: undefined } : e)); }} defaults={view.defaults} inherited={Object.fromEntries(KINDS.map((k) => [k, { template: view.global[k], source: SOURCE_LABEL.global }]))} libraryRootId={root.id} saveErrors={errors} dirtyKinds={dirtyKinds} /> put(null)} > 清除覆盖 put(overrideBody(draft))}> {save.isPending ? "保存中…" : "保存覆盖"} ); }
还没有媒体库目录。先在上方「媒体库目录」添加。