import { useEffect, useMemo, useState } from "react"; import { createPortal } from "react-dom"; import { useTranslation } from "../i18n"; import type { TranslationKey } from "../i18n/zh"; import { providerPresets, type PresetCategory, type ProviderPreset } from "../config/providerPresets"; import { ProviderIcon } from "./ProviderIcon"; interface PresetPickerModalProps { open: boolean; onClose: () => void; onSelect: (preset: ProviderPreset) => void; onCustom: () => void; } const CATEGORY_BADGE: Record, string> = { official: "bg-blue-100 text-blue-700 dark:bg-blue-500/15 dark:text-blue-400", cn_official: "bg-green-100 text-green-700 dark:bg-green-500/15 dark:text-green-400", third_party: "bg-gray-200 text-gray-600 dark:bg-gray-500/15 dark:text-gray-400", aggregator: "bg-purple-100 text-purple-700 dark:bg-purple-500/15 dark:text-purple-400", }; const CATEGORY_LABEL: Record, TranslationKey> = { official: "presetCategoryOfficial", cn_official: "presetCategoryCnOfficial", third_party: "presetCategoryThirdParty", aggregator: "presetCategoryAggregator", }; type SortMode = "original" | "alpha"; export function PresetPickerModal({ open, onClose, onSelect, onCustom }: PresetPickerModalProps) { const { t } = useTranslation(); const [query, setQuery] = useState(""); const [sort, setSort] = useState("original"); // Esc to close useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [open, onClose]); // Scroll lock while open useEffect(() => { if (!open) return; const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, [open]); // Reset transient state each time the modal opens useEffect(() => { if (open) { setQuery(""); setSort("original"); } }, [open]); const presetName = (p: ProviderPreset) => p.nameKey ? t(p.nameKey as TranslationKey) : p.name; const visible = useMemo(() => { const q = query.trim().toLowerCase(); let list = providerPresets; if (q) { list = list.filter( (p) => p.id.toLowerCase().includes(q) || presetName(p).toLowerCase().includes(q) ); } if (sort === "alpha") { list = [...list].sort((a, b) => presetName(a).localeCompare(presetName(b))); } return list; // eslint-disable-next-line react-hooks/exhaustive-deps }, [query, sort, t]); if (!open) return null; return createPortal(
e.stopPropagation()} > {/* Header */}
{t("presetPickerTitle")}
{/* Search + sort */}
setQuery(e.target.value)} placeholder={t("presetSearchPlaceholder")} autoFocus className="h-8 flex-1 rounded border border-border bg-input px-2 text-sm text-content-primary focus:outline-none focus:ring-2 focus:ring-blue-500" />
{/* Preset grid */}
{visible.length === 0 ? (
{t("presetNoResults")}
) : (
{visible.map((p) => ( ))}
)} {/* Custom (empty form) entry */}
{/* Footer hint */}
{t("presetFooterHint")}
, document.body ); }