import type { Subscription } from "@umt/contracts"; import { Eraser, Grid3x3, MoreHorizontal, Pause, Pencil, Play, RotateCcw, Search, Trash2 } from "lucide-react"; import { type ReactNode, useState } from "react"; import { toast } from "sonner"; import { type SubscriptionAction, useResetSubscription, useSearchSubscription, useSubscriptionAction, useSubscriptionBatch, } from "~/api/subscriptions.ts"; import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx"; import { useOpenRecord } from "~/components/console/record-sheet.tsx"; import { Button } from "~/components/ui/button.tsx"; import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover.tsx"; import { errorMessage } from "~/lib/api.ts"; import { cn } from "~/lib/utils.ts"; // 订阅操作:立即搜索 / 暂停·恢复 / 编辑 / 重新订阅 / 删除 / 查看对比(总览卡片菜单与详情页共用)。 type Sub = Pick; const tracking = (s: Sub) => s.status === "active" || s.status === "pending"; export function useSubscriptionOps() { const search = useSearchSubscription(); const action = useSubscriptionAction(); const batch = useSubscriptionBatch(); const reset = useResetSubscription(); const pending = search.isPending || action.isPending || batch.isPending || reset.isPending; return { pending, search: (id: string) => search.mutate(id, { onSuccess: (r) => toast.success(r.deduplicated ? "搜索已在进行" : "已开始搜索"), onError: (e) => toast.error(errorMessage(e, "搜索失败")), }), act: (id: string, a: SubscriptionAction) => action.mutate( { id, action: a }, { onSuccess: () => toast.success({ pause: "已暂停", resume: "已恢复", resubscribe: "已重新订阅" }[a]), onError: (e) => toast.error(errorMessage(e)), }, ), reset: (id: string) => reset.mutateAsync(id).then((r) => { const kept = r.keptPlans.length ? `,${r.keptPlans.length} 个仍在下载的保留` : ""; toast.success(`已重置:取消 ${r.releasedPlans.length} 个下载${kept}`); }), remove: (ids: string[]) => batch.mutateAsync({ ids, action: "delete" }).then((r) => { const bad = r.results.filter((x) => !x.ok); if (bad.length) toast.error(`${bad.length} 个没有删除:${bad[0]?.error ?? ""}`); else toast.success(ids.length > 1 ? `已删除 ${ids.length} 个订阅` : "订阅已删除"); }), batch: (ids: string[], a: "search" | "pause" | "resume") => batch.mutate( { ids, action: a }, { onSuccess: (r) => { const ok = r.results.filter((x) => x.ok).length; const label = { search: "开始搜索", pause: "暂停", resume: "恢复" }[a]; if (ok === ids.length) toast.success(`${ok} 个订阅已${label}`); else toast.warning(`${ok}/${ids.length} 个已${label};${r.results.find((x) => !x.ok)?.error ?? ""}`); }, onError: (e) => toast.error(errorMessage(e)), }, ), }; } function MenuItem({ icon, children, onClick, danger, disabled, }: { icon: ReactNode; children: ReactNode; onClick: () => void; danger?: boolean; disabled?: boolean; }) { return ( ); } /** 卡片 / 行的"更多"菜单 */ export function SubscriptionMenu({ sub, onEdit, showDetail = true, trigger, }: { sub: Sub; onEdit: () => void; showDetail?: boolean; trigger?: ReactNode; }) { const ops = useSubscriptionOps(); const openRecord = useOpenRecord(); const [open, setOpen] = useState(false); const [confirm, setConfirm] = useState(false); const [confirmReset, setConfirmReset] = useState(false); const run = (fn: () => void) => () => { setOpen(false); fn(); }; const ended = sub.status === "completed" || sub.status === "cancelled"; return ( <> {trigger ?? ( )} e.stopPropagation()}> {showDetail ? ( } onClick={run(() => openRecord("subscriptions", sub.id))}> 查看对比 ) : null} {tracking(sub) && sub.mediaEntityId ? ( } disabled={ops.pending} onClick={run(() => ops.search(sub.id))}> 立即搜索 ) : null} {sub.status !== "cancelled" ? ( } onClick={run(onEdit)}> 编辑 ) : null} {tracking(sub) ? ( } disabled={ops.pending} onClick={run(() => ops.act(sub.id, "pause"))}> 暂停 ) : sub.status === "paused" ? ( } disabled={ops.pending} onClick={run(() => ops.act(sub.id, "resume"))}> 恢复 ) : null} {!ended ? ( } disabled={ops.pending} onClick={run(() => setConfirmReset(true))}> 重置 ) : null} {ended ? ( } disabled={ops.pending} onClick={run(() => ops.act(sub.id, "resubscribe"))}> 重新订阅 ) : null} {sub.status !== "cancelled" ? ( } danger onClick={run(() => setConfirm(true))}> 删除 ) : null} ops.reset(sub.id).catch((e) => { toast.error(errorMessage(e)); throw e; }) } /> ); } export function DeleteSubscriptionsDialog({ open, onOpenChange, ids, onDone, }: { open: boolean; onOpenChange: (o: boolean) => void; ids: string[]; onDone?: () => void; }) { const ops = useSubscriptionOps(); return ( 1 ? `删除 ${ids.length} 个订阅?` : "删除这个订阅?"} description="不再搜索和追更。已整理文件、收录版本和下载记录保留,可在订阅历史里重新订阅。" confirmLabel="删除" destructive onConfirm={() => ops .remove(ids) .then(() => onDone?.()) .catch((e) => { toast.error(errorMessage(e)); throw e; }) } /> ); }