docs(plan): on-demand site detail page parsing (HHClub details.php)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
1 parent
94fd48bff2
commit
ac478673bc
39 files changed
+2446
-339
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { type LocalMediaHit, type SearchAction, type TmdbCandidate, tmdbImageUrl } from "@umt/contracts";
|
||||
import { ArrowRight, CornerDownLeft, Film, Globe, LoaderCircle, type LucideIcon, Plus, Search } from "lucide-react";
|
||||
import { type SearchAction, type SearchWork, tmdbImageUrl } from "@umt/contracts";
|
||||
import { ArrowRight, CornerDownLeft, Film, Globe, LoaderCircle, type LucideIcon, Search } from "lucide-react";
|
||||
import { type ReactNode, useEffect, useId, useMemo, useRef, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { useSearch } from "~/api/search.ts";
|
||||
@@ -7,8 +7,7 @@ import { settingsPath } from "~/components/console/nav.ts";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogTitle } from "~/components/ui/dialog.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
import { actionIcon, mediaKindLabel, slotSummary } from "./search-labels.ts";
|
||||
import { useSubscribeTmdb } from "./search-sections.tsx";
|
||||
import { actionIcon, mediaKindLabel } from "./search-labels.ts";
|
||||
import {
|
||||
isSearchShortcut,
|
||||
openSearchPalette,
|
||||
@@ -16,9 +15,10 @@ import {
|
||||
setSearchPaletteOpen,
|
||||
useSearchPalette,
|
||||
} from "./search-store.ts";
|
||||
import { inventoryBadge, workHref } from "./work-card.tsx";
|
||||
|
||||
// 命令面板(Ctrl/⌘+K):输入即查本地作品 / TMDB 候选 / 页面与操作,最后一项跳资源结果页。
|
||||
// 资源组最慢要等 15s,面板里不查,回车进结果页。
|
||||
// 命令面板(Ctrl/⌘+K):输入即查作品(本地 + TMDB 合并,带库内状态)/ 页面与操作;选中作品打开作品详情页。
|
||||
// 面板里不订阅、不查站点;最后一项进搜索结果页。
|
||||
|
||||
function useDebounced<T>(v: T, ms: number): T {
|
||||
const [d, setD] = useState(v);
|
||||
@@ -68,7 +68,7 @@ export function CommandPalette() {
|
||||
)}
|
||||
>
|
||||
<DialogTitle className="sr-only">搜索</DialogTitle>
|
||||
<DialogDescription className="sr-only">搜索作品、TMDB 和页面;回车在资源中搜索</DialogDescription>
|
||||
<DialogDescription className="sr-only">搜索作品和页面;选中作品打开详情</DialogDescription>
|
||||
{open ? <PaletteBody initial={initial} /> : null}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -82,7 +82,6 @@ function PaletteBody({ initial }: { initial: string }) {
|
||||
const term = useDebounced(q.trim(), 150);
|
||||
const media = useSearch(term, "media", 6);
|
||||
const actions = useSearch(term, "actions", 6);
|
||||
const tmdbSub = useSubscribeTmdb();
|
||||
const [active, setActive] = useState(0);
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -92,36 +91,32 @@ function PaletteBody({ initial }: { initial: string }) {
|
||||
navigate(to);
|
||||
};
|
||||
|
||||
const subscribe = (c: TmdbCandidate) => tmdbSub.subscribe(c, close);
|
||||
|
||||
const stale = q.trim() !== term;
|
||||
const local = term ? (media.data?.media?.local.items ?? []) : [];
|
||||
const works = term ? (media.data?.media?.works ?? []) : [];
|
||||
const tmdb = term ? media.data?.media?.tmdb : undefined;
|
||||
const acts = term ? (actions.data?.actions?.items ?? []) : [];
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: 只随数据变化重建(go/subscribe 每次渲染新建)
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: 只随数据变化重建(go 每次渲染新建)
|
||||
const items = useMemo<Item[]>(() => {
|
||||
const out: Item[] = [];
|
||||
for (const h of local) out.push(localItem(h, () => go(`/console/library/${h.entity.id}`)));
|
||||
for (const c of tmdb?.items ?? [])
|
||||
out.push(tmdbItem(c, () => go(`/console/library/${c.localEntityId}`), subscribe));
|
||||
for (const w of works) out.push(workItem(w, () => go(workHref(w))));
|
||||
for (const a of acts) out.push(actionItem(a, () => go(a.href)));
|
||||
if (term)
|
||||
out.push({
|
||||
key: "releases",
|
||||
group: "资源",
|
||||
group: "搜索页",
|
||||
icon: Search,
|
||||
title: (
|
||||
<span>
|
||||
在资源中搜索 <span className="font-medium">“{term}”</span>
|
||||
查看「<span className="font-medium">{term}</span>」的全部结果
|
||||
</span>
|
||||
),
|
||||
meta: "查询全部已启用站点,最慢约 15 秒",
|
||||
meta: "作品卡上可以选站点搜资源",
|
||||
hint: "打开",
|
||||
run: () => go(searchPagePath(term)),
|
||||
});
|
||||
return out;
|
||||
}, [local, tmdb, acts, term]);
|
||||
}, [works, acts, term]);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: 结果变化时高亮回到第一项
|
||||
useEffect(() => setActive(0), [items.length, term]);
|
||||
@@ -160,7 +155,7 @@ function PaletteBody({ initial }: { initial: string }) {
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="搜索作品、TMDB、页面"
|
||||
placeholder="搜索作品、页面"
|
||||
role="combobox"
|
||||
aria-expanded={items.length > 0}
|
||||
aria-controls={listId}
|
||||
@@ -190,7 +185,7 @@ function PaletteBody({ initial }: { initial: string }) {
|
||||
>
|
||||
{!term ? (
|
||||
<p className="px-3 py-6 text-center text-caption text-muted-foreground">
|
||||
输入作品名、别名或页面名称。回车在资源中搜索。
|
||||
输入作品名、别名或页面名称。选中作品打开详情页。
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
@@ -261,30 +256,36 @@ function PaletteBody({ initial }: { initial: string }) {
|
||||
<span>↵ 打开</span>
|
||||
<span className="ml-auto inline-flex items-center gap-1">
|
||||
<ArrowRight className="h-3 w-3" />
|
||||
回车在资源中搜索
|
||||
搜资源在作品详情页
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function localItem(h: LocalMediaHit, run: () => void): Item {
|
||||
const e = h.entity;
|
||||
const parts = [mediaKindLabel(e.kind), e.year ? String(e.year) : null, slotSummary(h)].filter(Boolean);
|
||||
function workItem(w: SearchWork, run: () => void): Item {
|
||||
const inv = inventoryBadge(w.inventory);
|
||||
const parts = [
|
||||
mediaKindLabel(w.kind),
|
||||
w.year ? String(w.year) : null,
|
||||
inv.label,
|
||||
w.inventory.subscribed ? "已订阅" : null,
|
||||
];
|
||||
return {
|
||||
key: `local-${e.id}`,
|
||||
group: "本地作品",
|
||||
icon: Film,
|
||||
key: `work-${w.key}`,
|
||||
group: "作品",
|
||||
icon: w.localEntityId ? Film : Globe,
|
||||
posterPath: w.posterPath,
|
||||
title: (
|
||||
<span>
|
||||
{e.title}
|
||||
{h.matchedAlias && h.matchedAlias !== e.title ? (
|
||||
<span className="ml-1.5 text-muted-foreground">· {h.matchedAlias}</span>
|
||||
{w.title}
|
||||
{w.originalTitle && w.originalTitle !== w.title ? (
|
||||
<span className="ml-1.5 text-muted-foreground">· {w.originalTitle}</span>
|
||||
) : null}
|
||||
</span>
|
||||
),
|
||||
meta: parts.join(" · "),
|
||||
hint: "打开",
|
||||
meta: parts.filter(Boolean).join(" · "),
|
||||
hint: "详情",
|
||||
run,
|
||||
};
|
||||
}
|
||||
@@ -306,27 +307,6 @@ function ItemIcon({ it }: { it: Item }) {
|
||||
return <it.icon className="h-4 w-4 shrink-0 text-muted-foreground" />;
|
||||
}
|
||||
|
||||
function tmdbItem(c: TmdbCandidate, view: () => void, subscribe: (c: TmdbCandidate) => void): Item {
|
||||
const local = !!c.localEntityId;
|
||||
return {
|
||||
key: `tmdb-${c.entityType}-${c.externalId}`,
|
||||
group: "TMDB",
|
||||
icon: local ? Globe : Plus,
|
||||
posterPath: c.posterPath,
|
||||
title: (
|
||||
<span>
|
||||
{c.title}
|
||||
{c.year ? <span className="text-muted-foreground"> ({c.year})</span> : null}
|
||||
</span>
|
||||
),
|
||||
meta: [c.entityType === "movie" ? "电影" : "剧集", local ? "本地已有" : c.originalTitle, c.overview]
|
||||
.filter(Boolean)
|
||||
.join(" · "),
|
||||
hint: local ? "查看" : "订阅",
|
||||
run: local ? view : () => subscribe(c),
|
||||
};
|
||||
}
|
||||
|
||||
function actionItem(a: SearchAction, run: () => void): Item {
|
||||
return {
|
||||
key: `action-${a.id}`,
|
||||
|
||||
Reference in new issue
Block a user