feat(web): 刮削设置与对话框加「更新」覆盖方式,内容没变的跳过不逐条列出

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-03 21:04:57 +08:00
1 parent 37cf0a398e
commit 4306bff6d3
4 files changed
+15 -7

No files matched your search

+3 -2
View File
@@ -1093,8 +1093,8 @@ const SKIP_TEXT: Record<string, string> = {
export function ScrapeDialog({ items, root, onClose }: { items: FileEntry[]; root: FileRoot; onClose: () => void }) { export function ScrapeDialog({ items, root, onClose }: { items: FileEntry[]; root: FileRoot; onClose: () => void }) {
const settings = useSettings(); const settings = useSettings();
const scrape = useScrape(); const scrape = useScrape();
const [mode, setMode] = useState<"missing" | "overwrite" | null>(null); const [mode, setMode] = useState<"missing" | "overwrite" | "update" | null>(null);
const curMode = mode ?? settings.data?.organize.scrapeMode ?? "missing"; const curMode = mode ?? settings.data?.organize.scrapeMode ?? "update";
const [recordIds, setRecordIds] = useState<string[]>([]); const [recordIds, setRecordIds] = useState<string[]>([]);
const [choices, setChoices] = useState<FilesScrapeNeedsChoice[]>([]); const [choices, setChoices] = useState<FilesScrapeNeedsChoice[]>([]);
const [picked, setPicked] = useState<Record<string, WizardEntity | null>>({}); const [picked, setPicked] = useState<Record<string, WizardEntity | null>>({});
@@ -1157,6 +1157,7 @@ export function ScrapeDialog({ items, root, onClose }: { items: FileEntry[]; roo
<fieldset aria-label="已有文件" className="m-0 flex flex-wrap gap-1.5 border-0 p-0"> <fieldset aria-label="已有文件" className="m-0 flex flex-wrap gap-1.5 border-0 p-0">
{( {(
[ [
["update", "更新"],
["missing", "只补缺失"], ["missing", "只补缺失"],
["overwrite", "全部覆盖"], ["overwrite", "全部覆盖"],
] as const ] as const
+1 -1
View File
@@ -704,7 +704,7 @@ let seq = 0;
const SCRAPES = new Map<string, ScrapeRecord>(); const SCRAPES = new Map<string, ScrapeRecord>();
function mockScrape(b: { function mockScrape(b: {
items: { root: string; path: string; mediaEntityId?: string; external?: { externalId: string } }[]; items: { root: string; path: string; mediaEntityId?: string; external?: { externalId: string } }[];
mode?: "missing" | "overwrite"; mode?: "missing" | "overwrite" | "update";
}) { }) {
const records: ScrapeRecord[] = []; const records: ScrapeRecord[] = [];
const needsChoice: unknown[] = []; const needsChoice: unknown[] = [];
@@ -70,7 +70,7 @@ function FileRow({ f }: { f: ScrapeFileResult }) {
)} )}
</span> </span>
<span className="text-meta text-muted-foreground">{ROLE[f.role] ?? f.role}</span> <span className="text-meta text-muted-foreground">{ROLE[f.role] ?? f.role}</span>
{f.action !== "written" && f.reason && f.reason !== "scrape_exists" ? ( {f.action !== "written" && f.reason && !QUIET_SKIPS.has(f.reason) ? (
<span className={f.action === "failed" ? "text-meta text-destructive" : "text-meta text-muted-foreground"}> <span className={f.action === "failed" ? "text-meta text-destructive" : "text-meta text-muted-foreground"}>
{reasonLine(f.reason, "写入失败")} {reasonLine(f.reason, "写入失败")}
</span> </span>
@@ -80,11 +80,14 @@ function FileRow({ f }: { f: ScrapeFileResult }) {
); );
} }
/** 不必逐条列出的跳过:已存在(不是本工具写的 / 只补缺失)、内容没变 */
const QUIET_SKIPS = new Set(["scrape_exists", "scrape_unchanged"]);
/** 一条刮削记录:状态、目录与识别结果、计数、逐文件结果(缺省不列"已存在"跳过的) */ /** 一条刮削记录:状态、目录与识别结果、计数、逐文件结果(缺省不列"已存在"跳过的) */
export function ScrapeRecordView({ record: s, showPath = false }: { record: ScrapeRecord; showPath?: boolean }) { export function ScrapeRecordView({ record: s, showPath = false }: { record: ScrapeRecord; showPath?: boolean }) {
const [showAll, setShowAll] = useState(false); const [showAll, setShowAll] = useState(false);
const files = s.files; const files = s.files;
const important = files.filter((f) => f.action !== "skipped" || f.reason !== "scrape_exists"); const important = files.filter((f) => f.action !== "skipped" || !QUIET_SKIPS.has(f.reason ?? ""));
const shown = showAll ? files : important.slice(0, 12); const shown = showAll ? files : important.slice(0, 12);
const hidden = files.length - shown.length; const hidden = files.length - shown.length;
const done = s.status !== "queued" && s.status !== "running"; const done = s.status !== "queued" && s.status !== "running";
+6 -2
View File
@@ -57,10 +57,11 @@ const SCRAPE_CARD: CardSpec = {
label: "已有文件", label: "已有文件",
kind: "select", kind: "select",
options: [ options: [
{ value: "update", label: "更新" },
{ value: "missing", label: "只补缺失" }, { value: "missing", label: "只补缺失" },
{ value: "overwrite", label: "全部覆盖" }, { value: "overwrite", label: "全部覆盖" },
], ],
hint: "全部覆盖会替换目录里已有的 NFO 和图片,包括其他工具写的", hint: "更新只替换本工具写过、之后没被改过的文件;全部覆盖会替换目录里已有的 NFO 和图片,包括其他工具写的",
}, },
{ {
name: "organize.scrapeImages", name: "organize.scrapeImages",
@@ -84,13 +85,16 @@ const SCRAPE_CARD: CardSpec = {
toPatch: (v) => ({ toPatch: (v) => ({
organize: { organize: {
scrapeAfterImport: v["organize.scrapeAfterImport"] === true, scrapeAfterImport: v["organize.scrapeAfterImport"] === true,
scrapeMode: str(v["organize.scrapeMode"]) === "overwrite" ? "overwrite" : "missing", scrapeMode: scrapeModeOf(str(v["organize.scrapeMode"])),
scrapeImages: v["organize.scrapeImages"] === true, scrapeImages: v["organize.scrapeImages"] === true,
scrapeEpisodeThumbs: v["organize.scrapeEpisodeThumbs"] === true, scrapeEpisodeThumbs: v["organize.scrapeEpisodeThumbs"] === true,
}, },
}), }),
}; };
const scrapeModeOf = (v: string | null | undefined): "missing" | "overwrite" | "update" =>
v === "overwrite" || v === "missing" ? v : "update";
export function LibrarySection() { export function LibrarySection() {
return ( return (
<div className="grid grid-cols-1 gap-8"> <div className="grid grid-cols-1 gap-8">