Files
UHD_Media_Tool/apps/web/app/components/search/site-picker-dialog.tsx
T

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>
);
}