import { Pause, Pencil, Play, RefreshCw, RotateCcw, Search } from "lucide-react"; import { useMemo, useState } from "react"; import { Link } from "react-router"; import { toast } from "sonner"; import { useRefreshCoverage, useSubscription, useSubscriptionCoverage, useSubscriptionOverviewItem, } from "~/api/subscriptions.ts"; import { DetailSheet } from "~/components/console/detail-sheet.tsx"; import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { KvList } from "~/components/console/kv-list.tsx"; import { consolePath } from "~/components/console/nav.ts"; import { useStickyId } from "~/components/console/record-sheet.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { SubscriptionMappingSummary } from "~/components/episode-mapping/subscription-summary.tsx"; import { PRIORITY, SEASON_SCOPE, SUB_MODE, SUB_ORIGIN, SUB_STATUS } from "~/components/library/labels.ts"; import { CoverPlaceholder, Panel, relTime, Tone } from "~/components/library/parts.tsx"; import { SlotsPanel } from "~/components/library/subscriptions-slots.tsx"; import { SubscriptionMenu, useSubscriptionOps } from "~/components/subscriptions/actions.tsx"; import { CellSheet, CoverageLegend, CoverageMatrix, type SelectedCell, } from "~/components/subscriptions/coverage-matrix.tsx"; import { autoFillText, foldNames, GROUP_LABEL, SUB_KIND_LABEL, seasonLabel, seasonSlices, seasonTitle, } from "~/components/subscriptions/model.ts"; import { ProgressBlock, SlotBadges } from "~/components/subscriptions/overview-item.tsx"; import { SlotPanel } from "~/components/subscriptions/slot-panel.tsx"; import { SubscriptionFormDialog } from "~/components/subscriptions/subscription-form.tsx"; import { Button } from "~/components/ui/button.tsx"; import { errorMessage } from "~/lib/api.ts"; import { actorText } from "~/lib/labels.ts"; import { cn, formatDateTime } from "~/lib/utils.ts"; // 订阅对比视图(FE-5,docs/design/subscriptions.md §2):季页签 → 集 × 收录版本矩阵;收录版本排序与自动补全;订阅信息。 // 记录抽屉(console-ia.md §9):订阅页上 ?open=<订阅 id> 打开的宽抽屉,不再有独立页。 const TOTAL_SOURCE: Record = { tmdb: "TMDB", injected: "订阅里填写", manual: "手动", covered_floor: "按已整理", previous: "沿用上次", }; export function SubscriptionSheet({ id, onClose }: { id: string | null; onClose: () => void }) { const shown = useStickyId(id); return ( !o && onClose()} width="xl" title={}> {shown ? : null} ); } function SheetTitleText({ id }: { id: string | null }) { const subQ = useSubscription(id ?? undefined); const sub = subQ.data; return <>{sub?.mediaEntityTitle ?? sub?.titleQuery ?? "订阅"}; } function SubscriptionDetail({ id }: { id: string }) { const subQ = useSubscription(id); const sub = subQ.data; const itemQ = useSubscriptionOverviewItem(sub); const item = itemQ.data; const covQ = useSubscriptionCoverage(id); const cov = covQ.data; const refresh = useRefreshCoverage(); const ops = useSubscriptionOps(); const [season, setSeason] = useState(0); const [sel, setSel] = useState(null); const [editing, setEditing] = useState(false); const seasonTabs = useMemo(() => { if (!cov) return []; const slices = seasonSlices(cov); const any = cov.rows.find((r) => r.slotId === null); return cov.seasons.map((s, i) => { const sl = slices[i]!; const have = any ? any.cells.slice(sl.start, sl.end).filter((c) => c.state === "have").length : 0; return { title: seasonTitle(s, cov.mediaKind), stat: `${have}/${s.totalKnown ? s.total : "?"}`, source: s.source ? (TOTAL_SOURCE[s.source] ?? s.source) : null, locked: s.locked, }; }); }, [cov]); if (subQ.isPending) return ; if (subQ.isError || !sub) return subQ.refetch()} />; const title = sub.mediaEntityTitle ?? item?.mediaEntityTitle ?? sub.titleQuery ?? "订阅"; const tracking = sub.status === "active" || sub.status === "pending"; const ended = sub.status === "completed" || sub.status === "cancelled"; const sLabel = item ? seasonLabel(item) : null; const doRefresh = () => refresh.mutate(sub.id, { onSuccess: () => toast.success("对比已刷新"), onError: (e) => toast.error(errorMessage(e)), }); const seasonIdx = Math.min(season, Math.max(0, (cov?.seasons.length ?? 1) - 1)); return ( <>

{[item?.year, SUB_KIND_LABEL[sub.mediaKind], sLabel, SUB_MODE[sub.mode]].filter(Boolean).join(" · ")}

{item?.group ? {GROUP_LABEL[item.group]} : null} {item?.upgrading ? 洗版中 : null} {item?.awaitingNewSeason ? 等待新季 : null} {!sub.mediaEntityId ? 不在媒体库 : null}
{!sub.mediaEntityId ? (

只按标题「{sub.titleQuery ?? title} 」订阅,还没关联到媒体库作品,不会自动搜索和下载。先把作品加入媒体库,再从作品订阅。

) : null} {item ? (
) : null}
{tracking && sub.mediaEntityId ? ( ) : null} {!ended ? ( ) : null} {tracking ? ( ) : sub.status === "paused" ? ( ) : ( )} setEditing(true)} showDetail={false} />
刷新对比 } > {!sub.mediaEntityId ? (

作品加入媒体库后才能对比,加入后自动关联到这个订阅。

) : covQ.isPending ? ( ) : covQ.isError ? ( covQ.refetch()} /> ) : !cov || cov.seasons.length === 0 ? (

还没有季和集信息。元数据刷新后出现;也可以在编辑里设置总集数。

) : (
{cov.seasons.length > 1 || cov.mediaKind !== "movie" ? (
{seasonTabs.map((t, i) => ( ))}
) : null}
)}
{sub.mediaEntityId ? : null} {sub.mediaEntityId && sub.mediaKind !== "movie" ? ( ) : null}
s.name), 4, ) : null, }, { label: "规则组", value: item ? `${foldNames( item.ruleGroups.map((g) => g.name), 4, "未设置", )}${item.defaultRuleGroups && item.ruleGroups.length ? "(未单独设置)" : ""}` : null, }, { label: "自动补全", value: item ? autoFillText(item.autoFill) : null }, { label: "上次更新", value: item ? formatDateTime(item.lastUpdatedAt) : null }, { label: "主动搜索", value: sub.lastSearchedAt ? `上次 ${relTime(sub.lastSearchedAt)}${sub.nextSearchAt ? ` · 下次 ${relTime(sub.nextSearchAt)}` : item?.searchEnabled === false ? " · 周期搜索已关" : ""}` : "还没搜索过", }, { label: "状态说明", value: sub.statusReason }, { label: "创建", value: `${formatDateTime(sub.createdAt)} · ${actorText(sub.createdBy)}`, wide: true }, ...(sub.mediaEntityId ? [ { label: "作品", value: ( 查看作品 ), }, ] : []), ]} /> {sub.mediaEntityId ? ( ) : null}
setSel(null)} /> {editing ? setEditing(false)} /> : null} ); }