feat(web): simplify subscription and manual download flows and sync UHD styles
This commit is contained in:
1 parent
27c0f27434
commit
c3c4456913
33 files changed
+1083
-633
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
AcquisitionPolicyRules,
|
||||
AcquisitionPolicyScope,
|
||||
type JsonObject,
|
||||
type PolicyRevision,
|
||||
type SlotTemplate,
|
||||
@@ -9,6 +10,7 @@ import { ChevronRight, Eye, FilePlus2, History, Pencil, Plus, ScrollText, Trash2
|
||||
import { useId, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { useClassification } from "~/api/organize-sources.ts";
|
||||
import {
|
||||
PolicyPreview,
|
||||
useCreatePolicy,
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
useRollbackPolicy,
|
||||
useUpdatePolicy,
|
||||
} from "~/api/policies.ts";
|
||||
import { useRuleGroups } from "~/api/rules.ts";
|
||||
import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
|
||||
import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx";
|
||||
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||
@@ -54,14 +57,16 @@ const KIND_OPTS: { value: string; label: string }[] = SubscriptionMediaKind.opti
|
||||
}));
|
||||
|
||||
export const PUBLISH_NOTICE =
|
||||
"发布后,作用范围内的下载、追更和整理按这个策略自动执行,不再逐次审批;升级替换、媒体清理和下载缓存清理仍要单独审批。上一个已发布版本的授权随之失效。";
|
||||
"发布后,作用范围内的下载、追更和整理按这个策略自动执行;已开启的洗版在新文件核验后替换对应旧版本。媒体清理和下载缓存清理仍要单独审批。上一个已发布版本的授权随之失效。";
|
||||
|
||||
type TemplateDraft = { key: string; name: string; rules: SlotRulesDraft; open: boolean };
|
||||
type TemplateDraft = { key: string; name: string; ruleGroupIds: string[]; rules: SlotRulesDraft; open: boolean };
|
||||
type PolicyDraft = {
|
||||
policyKey: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
scopeKinds: string[];
|
||||
scopeCategories: string[];
|
||||
originalScope: JsonObject;
|
||||
defaultSlots: TemplateDraft[];
|
||||
newTitleEnabled: boolean;
|
||||
windowDays: string;
|
||||
@@ -75,6 +80,7 @@ let seq = 0;
|
||||
const tpl = (t?: SlotTemplate, open = false): TemplateDraft => ({
|
||||
key: `t${++seq}`,
|
||||
name: t?.name ?? "",
|
||||
ruleGroupIds: t?.ruleGroupIds ?? [],
|
||||
rules: t ? draftFromJson(t) : emptyDraft(),
|
||||
open,
|
||||
});
|
||||
@@ -88,6 +94,10 @@ function draftFromRevision(r: PolicyRevision | null): PolicyDraft {
|
||||
title: r?.title ?? "",
|
||||
summary: r?.summary ?? "",
|
||||
scopeKinds: kinds ?? [],
|
||||
scopeCategories: Object.entries(AcquisitionPolicyScope.safeParse(r?.scope ?? {}).data?.categories ?? {}).flatMap(
|
||||
([type, names]) => (names ?? []).map((name) => `${type}:${name}`),
|
||||
),
|
||||
originalScope: r?.scope ?? {},
|
||||
defaultSlots: (rules?.defaultSlots ?? []).map((t) => tpl(t)),
|
||||
newTitleEnabled: rules?.newTitle?.enabled ?? false,
|
||||
windowDays: rules?.newTitle?.windowDays ? String(rules.newTitle.windowDays) : "",
|
||||
@@ -121,6 +131,14 @@ function buildPolicy(d: PolicyDraft):
|
||||
const errors: Record<string, string> = {};
|
||||
const meta = Meta.safeParse(d);
|
||||
if (!meta.success) for (const i of meta.error.issues) errors[String(i.path[0])] ??= i.message;
|
||||
for (const selected of d.scopeCategories) {
|
||||
const type = selected.split(":")[0];
|
||||
if (
|
||||
d.scopeKinds.length &&
|
||||
(type === "movie" ? !d.scopeKinds.includes("movie") : !d.scopeKinds.some((k) => k !== "movie"))
|
||||
)
|
||||
errors.scopeCategories = "适用分类与作品类型不一致,请调整其中一项";
|
||||
}
|
||||
const templates = (list: TemplateDraft[]) =>
|
||||
list.map((t) => {
|
||||
if (!t.name.trim()) errors[`${t.key}.name`] = "填写模板名称";
|
||||
@@ -131,6 +149,7 @@ function buildPolicy(d: PolicyDraft):
|
||||
}
|
||||
return {
|
||||
name: t.name.trim(),
|
||||
...(t.ruleGroupIds.length ? { ruleGroupIds: t.ruleGroupIds } : {}),
|
||||
hardConditions: stripEmpty(r.value.hardConditions),
|
||||
softPreferences: stripEmpty(r.value.softPreferences),
|
||||
fallback: stripEmpty(r.value.fallback),
|
||||
@@ -174,7 +193,18 @@ function buildPolicy(d: PolicyDraft):
|
||||
policyKey: meta.data.policyKey,
|
||||
title: meta.data.title,
|
||||
summary: meta.data.summary || null,
|
||||
scope: d.scopeKinds.length ? { mediaKinds: d.scopeKinds } : {},
|
||||
scope: {
|
||||
...d.originalScope,
|
||||
mediaKinds: d.scopeKinds.length ? d.scopeKinds : undefined,
|
||||
categories: d.scopeCategories.length
|
||||
? Object.fromEntries(
|
||||
["movie", "tv"].map((type) => [
|
||||
type,
|
||||
d.scopeCategories.filter((v) => v.startsWith(`${type}:`)).map((v) => v.slice(type.length + 1)),
|
||||
]),
|
||||
)
|
||||
: undefined,
|
||||
} as JsonObject,
|
||||
rules: rules as JsonObject,
|
||||
},
|
||||
};
|
||||
@@ -532,8 +562,12 @@ function PolicyDetailSheet({
|
||||
}
|
||||
|
||||
const scopeText = (scope: JsonObject) => {
|
||||
const k = z.array(z.string()).safeParse((scope as { mediaKinds?: unknown }).mediaKinds).data;
|
||||
return k?.length ? k.map((x) => MEDIA_KIND[x as keyof typeof MEDIA_KIND] ?? x).join(" / ") : "全部作品";
|
||||
const parsed = AcquisitionPolicyScope.safeParse(scope).data;
|
||||
const kinds = parsed?.mediaKinds?.map((x) => MEDIA_KIND[x]).join(" / ") || "全部作品";
|
||||
const categories = Object.values(parsed?.categories ?? {})
|
||||
.flat()
|
||||
.join(" / ");
|
||||
return categories ? `${kinds} · ${categories}` : kinds;
|
||||
};
|
||||
|
||||
function PreviewBlock({ value }: { value: JsonObject | null }) {
|
||||
@@ -606,6 +640,7 @@ function PolicyEditorSheet({
|
||||
onSaved: (r: PolicyRevision) => void;
|
||||
}) {
|
||||
const id = useId();
|
||||
const classification = useClassification();
|
||||
const create = useCreatePolicy();
|
||||
const update = useUpdatePolicy();
|
||||
const [d, setD] = useState<PolicyDraft>(() => {
|
||||
@@ -715,10 +750,37 @@ function PolicyEditorSheet({
|
||||
onChange={(v) => set("scopeKinds", v)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="适用分类"
|
||||
error={errors.scopeCategories}
|
||||
hint="沿用现有分类规则;不选时作为类型默认,订阅可以单独选择其他策略"
|
||||
>
|
||||
<ChipToggleGroup
|
||||
label="适用分类"
|
||||
options={[
|
||||
...Object.entries(classification.data?.categories ?? {}).flatMap(([type, names]) =>
|
||||
names.map((name) => ({
|
||||
value: `${type}:${name}`,
|
||||
label: `${type === "movie" ? "电影" : "剧集"} · ${name}`,
|
||||
})),
|
||||
),
|
||||
...d.scopeCategories
|
||||
.filter(
|
||||
(v) =>
|
||||
!Object.entries(classification.data?.categories ?? {}).some(([type, names]) =>
|
||||
names.some((name) => v === `${type}:${name}`),
|
||||
),
|
||||
)
|
||||
.map((v) => ({ value: v, label: `${v}(分类已删除)` })),
|
||||
]}
|
||||
value={d.scopeCategories}
|
||||
onChange={(v) => set("scopeCategories", v)}
|
||||
/>
|
||||
</FormField>
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection
|
||||
title="默认收录版本模板"
|
||||
title="保留版本"
|
||||
actions={
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -731,6 +793,9 @@ function PolicyEditorSheet({
|
||||
}
|
||||
>
|
||||
<p className="mb-2 text-meta text-muted-foreground">订阅的作品没有收录版本时,按这些模板创建。</p>
|
||||
<p className="text-caption text-muted-foreground">
|
||||
一个目标保留单版本,可在允许分辨率内先低后高;多个目标分别保留、升级各自版本。
|
||||
</p>
|
||||
<TemplateList list={d.defaultSlots} onChange={(v) => set("defaultSlots", v)} errors={errors} />
|
||||
</DetailSection>
|
||||
|
||||
@@ -825,6 +890,7 @@ function TemplateList({
|
||||
errors: Record<string, string>;
|
||||
}) {
|
||||
const id = useId();
|
||||
const groups = useRuleGroups({ enabled: "true" });
|
||||
if (list.length === 0) return <p className="text-caption text-muted-foreground">没有模板。</p>;
|
||||
const patch = (key: string, p: Partial<TemplateDraft>) =>
|
||||
onChange(list.map((t) => (t.key === key ? { ...t, ...p } : t)));
|
||||
@@ -876,6 +942,23 @@ function TemplateList({
|
||||
</div>
|
||||
) : (
|
||||
<div className="border-t border-border/60 px-3 py-3">
|
||||
<FormField
|
||||
label="收录组合规则"
|
||||
hint="在规则页维护组合档位;多个规则按选中顺序生效,最后一个带档位的规则决定优先级"
|
||||
>
|
||||
<ChipToggleGroup
|
||||
ordered
|
||||
label="收录组合规则"
|
||||
options={[
|
||||
...(groups.data?.items ?? []).map((g) => ({ value: g.id, label: g.name })),
|
||||
...t.ruleGroupIds
|
||||
.filter((id) => !(groups.data?.items ?? []).some((g) => g.id === id))
|
||||
.map((id) => ({ value: id, label: `${id}(规则不可用)` })),
|
||||
]}
|
||||
value={t.ruleGroupIds}
|
||||
onChange={(v) => patch(t.key, { ruleGroupIds: v })}
|
||||
/>
|
||||
</FormField>
|
||||
<SlotRulesEditor value={t.rules} onChange={(v) => patch(t.key, { rules: v })} errors={errs} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user