import { type DiscoverItem, type DiscoverMatchCandidate, tmdbImageUrl } from "@umt/contracts"; import { ExternalLink, Search } from "lucide-react"; import { type ReactNode, useState } from "react"; import { Link, useNavigate } from "react-router"; import { toast } from "sonner"; import { useDiscoverMatch } from "~/api/discover.ts"; import { consolePath } from "~/components/console/nav.ts"; import { useReleaseSearchLauncher } from "~/components/search/search-sections.tsx"; import { SubscriptionFormDialog } from "~/components/subscriptions/subscription-form.tsx"; import { Button } from "~/components/ui/button.tsx"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "~/components/ui/dialog.tsx"; import { errorMessage } from "~/lib/api.ts"; import type { CardAction } from "./discover-card.tsx"; import { itemHref } from "./discover-state.ts"; // 卡片动作:进作品 / 订阅 / 搜索资源。都要先知道对应的作品:本地作品或 TMDB 条目直接用; // 豆瓣 / bgm 条目先按名称 + 年份匹配 TMDB(服务端记住确定的匹配),不确定时列候选让人选,选中后继续原来的动作。 interface Target { title: string; year: number | null; type: "movie" | "tv"; tmdbId: string; entityId: string | null; } const SOURCE_LABEL: Record = { douban: "豆瓣", bangumi: "bgm.tv", tmdb: "TMDB" }; export function useDiscoverActions() { const navigate = useNavigate(); const match = useDiscoverMatch(); const launcher = useReleaseSearchLauncher(); const [busy, setBusy] = useState<{ key: string; action: CardAction } | null>(null); const [picking, setPicking] = useState<{ item: DiscoverItem; action: CardAction; candidates: DiscoverMatchCandidate[]; } | null>(null); const [subscribing, setSubscribing] = useState(null); const run = (action: CardAction, t: Target) => { if (action === "open") { navigate(t.entityId ? consolePath("library", t.entityId) : consolePath("media", "tmdb", t.type, t.tmdbId)); } else if (action === "subscribe") setSubscribing(t); else launcher.open({ title: t.title, ref: t.entityId ? `local:${t.entityId}` : `tmdb:${t.type}:${t.tmdbId}` }); }; const act = async (item: DiscoverItem, action: CardAction) => { const known = item.tmdb; if (action === "open") { const href = itemHref(item); if (href) return navigate(href); } if (known) { return run(action, { title: item.title, year: item.year, type: known.type, tmdbId: known.id, entityId: item.local?.entityId ?? null, }); } setBusy({ key: `${item.source}:${item.id}`, action }); try { const r = await match.mutateAsync(item); if (r.match) run(action, { ...r.match, tmdbId: r.match.id, entityId: null }); else setPicking({ item, action, candidates: r.candidates }); } catch (err) { toast.error(errorMessage(err, "匹配 TMDB 失败")); } finally { setBusy(null); } }; const dialogs: ReactNode = ( <> {picking ? ( { setPicking(null); run(picking.action, { ...c, tmdbId: c.id, entityId: null }); }} onClose={() => setPicking(null)} /> ) : null} {subscribing ? ( setSubscribing(null)} onCreated={() => toast.success(`已订阅「${subscribing.title}」`)} /> ) : null} {launcher.dialog} ); return { act, busyOf: (item: DiscoverItem): CardAction | null => busy && busy.key === `${item.source}:${item.id}` ? busy.action : null, dialogs, }; } function PickDialog({ item, candidates, onPick, onClose, }: { item: DiscoverItem; candidates: DiscoverMatchCandidate[]; onPick: (c: DiscoverMatchCandidate) => void; onClose: () => void; }) { return ( !o && onClose()}> 选择 TMDB 作品 「{item.title}」{item.year ? `(${item.year})` : ""}没能自动确定对应的 TMDB 作品。 {candidates.length ? (
    {candidates.map((c) => (
  • ))}
) : (

TMDB 上没有找到相近的作品。

)}
); }