Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
175 lines
6.2 KiB
TypeScript
175 lines
6.2 KiB
TypeScript
import type { SearchSiteMode, Site } from "@umt/contracts";
|
|
import { PackageSearch } from "lucide-react";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { Link } from "react-router";
|
|
import { useSiteOptions } from "~/api/instances.ts";
|
|
import { useSettings } from "~/api/settings.ts";
|
|
import { consolePath } from "~/components/console/nav.ts";
|
|
import { ChipToggleGroup } from "~/components/library/parts.tsx";
|
|
import { Button } from "~/components/ui/button.tsx";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "~/components/ui/dialog.tsx";
|
|
import { cn } from "~/lib/utils.ts";
|
|
|
|
// 搜资源前选站点:默认(站点页勾了「默认参与搜索」的)/ 全部启用站点 / 指定站点。
|
|
// 初始值 = 本机上次的选择,没有就用 settings.search.defaultSiteMode。
|
|
|
|
export type SitePick = { mode: SearchSiteMode; siteIds: string[] };
|
|
|
|
const STORE_KEY = "umt.search.sitePick";
|
|
|
|
export function loadSitePick(): SitePick | null {
|
|
try {
|
|
const raw = globalThis.localStorage?.getItem(STORE_KEY);
|
|
if (!raw) return null;
|
|
const v = JSON.parse(raw) as Partial<SitePick>;
|
|
if (v.mode !== "default" && v.mode !== "all" && v.mode !== "custom") return null;
|
|
return { mode: v.mode, siteIds: Array.isArray(v.siteIds) ? v.siteIds.filter((x) => typeof x === "string") : [] };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function saveSitePick(p: SitePick) {
|
|
try {
|
|
globalThis.localStorage?.setItem(STORE_KEY, JSON.stringify(p));
|
|
} catch {
|
|
// 隐私模式等存不了:只是下次不记得
|
|
}
|
|
}
|
|
|
|
/** 按模式解析出站点 ID(只含启用站点) */
|
|
export function resolvePick(pick: SitePick, sites: Pick<Site, "id" | "enabled" | "searchDefault">[]): string[] {
|
|
const on = sites.filter((s) => s.enabled);
|
|
if (pick.mode === "all") return on.map((s) => s.id);
|
|
if (pick.mode === "custom") {
|
|
const set = new Set(pick.siteIds);
|
|
return on.filter((s) => set.has(s.id)).map((s) => s.id);
|
|
}
|
|
return on.filter((s) => s.searchDefault !== false).map((s) => s.id);
|
|
}
|
|
|
|
const MODES: { value: SearchSiteMode; label: string }[] = [
|
|
{ value: "default", label: "默认站点" },
|
|
{ value: "all", label: "全部站点" },
|
|
{ value: "custom", label: "指定站点" },
|
|
];
|
|
|
|
export function SitePickerDialog({
|
|
open,
|
|
onOpenChange,
|
|
title,
|
|
onConfirm,
|
|
}: {
|
|
open: boolean;
|
|
onOpenChange: (o: boolean) => void;
|
|
/** 作品名,显示在说明里 */
|
|
title: string;
|
|
onConfirm: (siteIds: string[]) => void;
|
|
}) {
|
|
const sitesQ = useSiteOptions();
|
|
const settings = useSettings();
|
|
const sites = useMemo(
|
|
() => (sitesQ.data?.items ?? []).filter((s) => s.enabled && s.capabilities.includes("search")),
|
|
[sitesQ.data],
|
|
);
|
|
const [pick, setPick] = useState<SitePick | null>(null);
|
|
// 打开时取初始值:本机上次 > 全局设置 > 默认
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const cfg = settings.data?.search;
|
|
setPick(
|
|
loadSitePick() ?? (cfg ? { mode: cfg.defaultSiteMode, siteIds: cfg.siteIds } : { mode: "default", siteIds: [] }),
|
|
);
|
|
}, [open, settings.data]);
|
|
|
|
const cur = pick ?? { mode: "default" as const, siteIds: [] };
|
|
const ids = resolvePick(cur, sites);
|
|
const count = (m: SearchSiteMode) => resolvePick({ mode: m, siteIds: cur.siteIds }, sites).length;
|
|
|
|
const confirm = () => {
|
|
saveSitePick(cur);
|
|
onConfirm(ids);
|
|
onOpenChange(false);
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-lg">
|
|
<DialogHeader>
|
|
<DialogTitle>搜索资源</DialogTitle>
|
|
<DialogDescription>在所选站点搜「{title}」。每个站点按作品原名和译名各查一次。</DialogDescription>
|
|
</DialogHeader>
|
|
<fieldset aria-label="站点范围" className="m-0 grid grid-cols-3 gap-1 rounded-lg border border-border/70 p-1">
|
|
{MODES.map((m) => (
|
|
<button
|
|
key={m.value}
|
|
type="button"
|
|
aria-pressed={cur.mode === m.value}
|
|
onClick={() =>
|
|
setPick({
|
|
mode: m.value,
|
|
// 切到「指定」时从当前集合起步
|
|
siteIds: m.value === "custom" && cur.mode !== "custom" ? ids : cur.siteIds,
|
|
})
|
|
}
|
|
className={cn(
|
|
"cursor-pointer rounded-md px-2 py-1.5 text-caption transition-colors duration-micro",
|
|
cur.mode === m.value
|
|
? "bg-foreground/[0.08] font-medium text-foreground"
|
|
: "text-muted-foreground hover:text-foreground",
|
|
)}
|
|
>
|
|
{m.label}
|
|
{m.value !== "custom" && sitesQ.data ? (
|
|
<span className="ml-1 text-meta text-muted-foreground tabular-nums">{count(m.value)}</span>
|
|
) : null}
|
|
</button>
|
|
))}
|
|
</fieldset>
|
|
{sitesQ.isPending ? (
|
|
<p className="text-caption text-muted-foreground">加载中…</p>
|
|
) : sites.length === 0 ? (
|
|
<p className="text-caption text-muted-foreground">
|
|
还没有启用的站点。
|
|
<Link to={consolePath("sites")} className="ml-1 underline underline-offset-4">
|
|
去站点页
|
|
</Link>
|
|
</p>
|
|
) : cur.mode === "custom" ? (
|
|
<ChipToggleGroup
|
|
label="站点"
|
|
options={sites.map((s) => ({ value: s.id, label: s.name }))}
|
|
value={cur.siteIds.filter((id) => sites.some((s) => s.id === id))}
|
|
onChange={(v) => setPick({ mode: "custom", siteIds: v })}
|
|
/>
|
|
) : (
|
|
<p className="text-caption break-words text-muted-foreground">
|
|
{ids.length
|
|
? sites
|
|
.filter((s) => ids.includes(s.id))
|
|
.map((s) => s.name)
|
|
.join("、")
|
|
: "没有站点勾选「默认参与搜索」。到站点页勾选,或改选「全部站点」。"}
|
|
</p>
|
|
)}
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
取消
|
|
</Button>
|
|
<Button onClick={confirm} disabled={ids.length === 0}>
|
|
<PackageSearch />
|
|
搜索 {ids.length} 个站点
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|