feat(web): subscription calendar view, reset action and quality preset fields
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
70d56645fe
commit
05350460f0
7 files changed
+638
-166
No files matched your search
@@ -1,14 +1,19 @@
|
||||
import type {
|
||||
AcquisitionSlot,
|
||||
AutoFillMode,
|
||||
CalendarEntryState,
|
||||
CoverageCell,
|
||||
JsonObject,
|
||||
Subscription,
|
||||
SubscriptionCalendarEntry,
|
||||
SubscriptionCoverage,
|
||||
SubscriptionEffect,
|
||||
SubscriptionGroup,
|
||||
SubscriptionMediaKind,
|
||||
SubscriptionOverviewItem,
|
||||
SubscriptionResolution,
|
||||
SubscriptionSlotSummary,
|
||||
SubscriptionSourcePreset,
|
||||
} from "@umt/contracts";
|
||||
import type { StatusTone } from "~/components/console/status-badge.tsx";
|
||||
|
||||
@@ -182,6 +187,30 @@ export function moveItem<T>(arr: readonly T[], from: number, to: number): T[] {
|
||||
|
||||
// ── 表单 ──
|
||||
|
||||
export const RESOLUTION_LABEL: Record<SubscriptionResolution, string> = {
|
||||
"4k": "4K",
|
||||
"1080p": "1080p",
|
||||
"720p": "720p",
|
||||
};
|
||||
export const SOURCE_PRESET_LABEL: Record<SubscriptionSourcePreset, string> = {
|
||||
bluray_disc: "蓝光原盘",
|
||||
remux: "Remux",
|
||||
bluray: "BluRay",
|
||||
uhd: "UHD",
|
||||
web: "WEB-DL",
|
||||
hdtv: "HDTV",
|
||||
h265: "H.265",
|
||||
h264: "H.264",
|
||||
};
|
||||
export const EFFECT_LABEL: Record<SubscriptionEffect, string> = {
|
||||
dv: "杜比视界",
|
||||
hdr: "HDR",
|
||||
sdr: "SDR",
|
||||
atmos: "杜比全景声",
|
||||
};
|
||||
const oneOf = <K extends string>(v: unknown, labels: Record<K, string>): K | "" =>
|
||||
typeof v === "string" && v in labels ? (v as K) : "";
|
||||
|
||||
export type SubscriptionDraft = {
|
||||
mediaKind: SubscriptionMediaKind;
|
||||
seasonUnitId: string;
|
||||
@@ -202,6 +231,10 @@ export type SubscriptionDraft = {
|
||||
include: string;
|
||||
exclude: string;
|
||||
keywords: string;
|
||||
/** MP 分辨率 / 资源质量 / 特效预设(写到该订阅自己的版本目标) */
|
||||
resolution: SubscriptionResolution | "";
|
||||
source: SubscriptionSourcePreset | "";
|
||||
effect: SubscriptionEffect | "";
|
||||
searchIntervalHours: string;
|
||||
stopAfter: string;
|
||||
autoFill: AutoFillMode;
|
||||
@@ -239,6 +272,9 @@ export function emptyDraft(kind: SubscriptionMediaKind = "series"): Subscription
|
||||
include: "",
|
||||
exclude: "",
|
||||
keywords: "",
|
||||
resolution: "",
|
||||
source: "",
|
||||
effect: "",
|
||||
searchIntervalHours: "",
|
||||
stopAfter: "",
|
||||
autoFill: "inherit",
|
||||
@@ -281,6 +317,9 @@ export function draftFromSubscription(sub: Subscription, upgrade: UpgradeStrateg
|
||||
include: strArr(p.include).join("\n"),
|
||||
exclude: strArr(p.exclude).join("\n"),
|
||||
keywords: strArr(p.keywords).join("\n"),
|
||||
resolution: oneOf(p.resolution, RESOLUTION_LABEL),
|
||||
source: oneOf(p.source, SOURCE_PRESET_LABEL),
|
||||
effect: oneOf(p.effect, EFFECT_LABEL),
|
||||
searchIntervalHours: numStr(p.searchIntervalHours),
|
||||
stopAfter: typeof p.stopAfter === "string" ? toLocalInput(p.stopAfter) : "",
|
||||
autoFill: p.autoFill === "on" || p.autoFill === "off" ? p.autoFill : "inherit",
|
||||
@@ -318,6 +357,9 @@ export function draftToParams(
|
||||
"downloadSavePath",
|
||||
"autoFill",
|
||||
"fillBudget",
|
||||
"resolution",
|
||||
"source",
|
||||
"effect",
|
||||
];
|
||||
for (const k of known) delete out[k];
|
||||
const set = (k: string, v: unknown) => {
|
||||
@@ -334,6 +376,9 @@ export function draftToParams(
|
||||
const kw = lines(d.keywords);
|
||||
if (kw.length > 10) errors.keywords = "最多 10 个";
|
||||
set("keywords", kw);
|
||||
set("resolution", d.resolution);
|
||||
set("source", d.source);
|
||||
set("effect", d.effect);
|
||||
if (d.mediaKind !== "movie") {
|
||||
const s = d.episodeStart.trim();
|
||||
const e = d.episodeEnd.trim();
|
||||
@@ -380,3 +425,53 @@ export function draftToParams(
|
||||
}
|
||||
return { params: out as JsonObject, errors };
|
||||
}
|
||||
|
||||
// ── 日历(设计 §4)──
|
||||
|
||||
export const CALENDAR_STATE_LABEL: Record<CalendarEntryState, string> = {
|
||||
have: "已入库",
|
||||
partial: "部分入库",
|
||||
missing: "未入库",
|
||||
upcoming: "未播出",
|
||||
};
|
||||
|
||||
const ymd = (d: Date) =>
|
||||
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||
|
||||
/** "YYYY-MM" → 该月第一天;无效时取今天所在月 */
|
||||
export function monthStart(month: string | undefined, today = new Date()): Date {
|
||||
const m = /^(\d{4})-(\d{2})$/.exec(month ?? "");
|
||||
if (m) {
|
||||
const d = new Date(Number(m[1]), Number(m[2]) - 1, 1);
|
||||
if (!Number.isNaN(d.getTime())) return d;
|
||||
}
|
||||
return new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
}
|
||||
|
||||
export const monthKey = (d: Date) => ymd(d).slice(0, 7);
|
||||
|
||||
export function shiftMonth(d: Date, n: number): Date {
|
||||
return new Date(d.getFullYear(), d.getMonth() + n, 1);
|
||||
}
|
||||
|
||||
/** 月视图网格:从该月 1 日所在周的周一起,整周铺满(5 或 6 周) */
|
||||
export function monthGrid(first: Date): { date: string; inMonth: boolean }[] {
|
||||
const start = new Date(first);
|
||||
start.setDate(1 - ((first.getDay() + 6) % 7));
|
||||
const last = new Date(first.getFullYear(), first.getMonth() + 1, 0);
|
||||
const days: { date: string; inMonth: boolean }[] = [];
|
||||
for (let d = new Date(start); days.length < 42; d.setDate(d.getDate() + 1)) {
|
||||
if (d > last && days.length % 7 === 0) break;
|
||||
days.push({ date: ymd(d), inMonth: d.getMonth() === first.getMonth() });
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
/** 按日期分桶 */
|
||||
export function entriesByDate(entries: readonly SubscriptionCalendarEntry[]): Map<string, SubscriptionCalendarEntry[]> {
|
||||
const m = new Map<string, SubscriptionCalendarEntry[]>();
|
||||
for (const e of entries) m.set(e.date, [...(m.get(e.date) ?? []), e]);
|
||||
return m;
|
||||
}
|
||||
|
||||
export const todayKey = (now = new Date()) => ymd(now);
|
||||
Reference in new issue
Block a user