import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { Library, Trash2 } from "lucide-react"; import { Input } from "@/components/input"; import { FakeSwitch } from "@/components/section-card"; import { PANEL } from "@/components/panel-layout"; import { AddButton, FormFooter, ListBadge, ListRow, ListRowAction, } from "@/components/list-kit"; import { InlineView, useConfirm } from "@/components/surface/surface-scope"; import { DocsLink } from "@/components/docs-link"; import { addRegistry, getRegistries, previewRegistry, removeRegistry, setRegistryEnabled, type RegistriesState, type RegistryListingSource, type RegistryPreview, } from "@/api/plugins-api"; import type { PluginsManager } from "./use-plugins-manager"; function errorOf(error: unknown): { message: string; code?: string; data: Record; } { const data = (error as { response?: { data?: Record } }) ?.response?.data; return { message: typeof data?.error === "string" ? data.error : error instanceof Error ? error.message : String(error), code: typeof data?.code === "string" ? data.code : undefined, data: data ?? {}, }; } export function RegistriesSection({ manager }: { manager: PluginsManager }) { const { t } = useTranslation(); const confirm = useConfirm(); const [state, setState] = useState(null); const [adding, setAdding] = useState(false); const [busy, setBusy] = useState(null); const load = useCallback(async () => { try { setState(await getRegistries()); } catch (error) { toast.error( t("plugins.manager.registries.loadFailed", { error: errorOf(error).message, }), ); } }, [t]); useEffect(() => { void load(); }, [load]); const statusOf = (id: string): RegistryListingSource | undefined => manager.registries.find((entry) => entry.id === id); const toggle = async (id: string, enabled: boolean) => { if (enabled && id === "community") { const ok = await confirm({ title: t("plugins.manager.registries.communityConfirmTitle"), description: t("plugins.manager.registries.communityConfirmBody"), confirmLabel: t("plugins.manager.registries.communityConfirm"), destructive: false, }); if (!ok) return; } setBusy(id); try { await setRegistryEnabled(id, enabled); await load(); await manager.refresh(); } catch (error) { toast.error( t("plugins.manager.registries.changeFailed", { error: errorOf(error).message, }), ); } finally { setBusy(null); } }; const remove = async (id: string, name: string) => { const ok = await confirm({ title: t("plugins.manager.registries.removeTitle", { name }), description: t("plugins.manager.registries.removeBody"), confirmLabel: t("plugins.manager.registries.remove"), }); if (!ok) return; setBusy(id); try { await removeRegistry(id); toast.success(t("plugins.manager.registries.removed", { name })); await load(); await manager.refresh(); } catch (error) { const info = errorOf(error); toast.error( info.code === "REGISTRY_IN_USE" ? t("plugins.manager.registries.inUse", { name, list: ((info.data.plugins as string[]) ?? []).join(", "), }) : t("plugins.manager.registries.changeFailed", { error: info.message, }), ); } finally { setBusy(null); } }; const readOnly = manager.managedByServer || state?.managedByServer; const meta = (id: string, url: string, extra?: string) => { const status = statusOf(id); const parts = [url]; if (status && !status.error) { parts.push( t("plugins.manager.registries.pluginCount", { count: status.plugins }), ); } if (extra) parts.push(extra); return parts.join(" | "); }; const errorLine = (id: string) => { const error = statusOf(id)?.error; return error ? ( {t("plugins.manager.registries.unreachable", { error })} ) : null; }; return (

{t("plugins.manager.registries.intro")}{" "} {t("plugins.manager.registries.hostYourOwn")}

{!readOnly && ( setAdding(true)} /> )}
{state && (
} title={t("plugins.manager.registries.officialName")} badges={ {t("plugins.manager.registries.alwaysOn")} } meta={meta("official", state.official.url)} > {errorLine("official")} } tone={state.community.enabled ? "brand" : "muted"} dimmed={!state.community.enabled} title={t("plugins.manager.registries.communityName")} meta={meta("community", state.community.url)} trailing={ void toggle("community", next)} /> } > {t("plugins.manager.registries.communityHint")}{" "} {t("plugins.manager.communitySubmit")} {state.community.enabled && errorLine("community")} {state.custom.map((entry, index) => ( } tone={entry.enabled ? "warning" : "muted"} dimmed={!entry.enabled} title={entry.name} badges={ {t("plugins.manager.registries.customBadge")} } meta={meta( entry.id, entry.url, t("plugins.manager.registries.keyFingerprint", { keyId: entry.keyId, }), )} trailing={ void toggle(entry.id, next)} /> } actions={ readOnly ? undefined : ( void remove(entry.id, entry.name)} > ) } > {entry.enabled && errorLine(entry.id)} ))}
)} { setAdding(false); await load(); await manager.refresh(); }} />
); } function AddRegistryView({ open, onOpenChange, onAdded, }: { open: boolean; onOpenChange: (open: boolean) => void; onAdded: () => Promise; }) { const { t } = useTranslation(); const [url, setUrl] = useState(""); const [preview, setPreview] = useState(null); const [error, setError] = useState(null); const [working, setWorking] = useState(false); useEffect(() => { if (!open) { setUrl(""); setPreview(null); setError(null); } }, [open]); const check = async () => { setWorking(true); setError(null); setPreview(null); try { setPreview(await previewRegistry(url.trim())); } catch (failure) { setError(errorOf(failure).message); } finally { setWorking(false); } }; const add = async () => { if (!preview) return; setWorking(true); setError(null); try { const added = await addRegistry(preview.url, preview.keyId); toast.success( t("plugins.manager.registries.added", { name: added.name }), ); await onAdded(); } catch (failure) { const info = errorOf(failure); if (info.code === "KEY_MISMATCH") setPreview(null); setError(info.message); } finally { setWorking(false); } }; return ( } footer={ onOpenChange(false)} onSave={() => void (preview ? add() : check())} saveLabel={ preview ? t("plugins.manager.registries.addConfirm") : t("plugins.manager.registries.check") } saving={working} disabled={!url.trim()} /> } >

{t("plugins.manager.registries.addHint")}

{ setUrl(e.target.value); setPreview(null); setError(null); }} onKeyDown={(e) => { if (e.key === "Enter" && url.trim() && !working) { e.preventDefault(); void (preview ? add() : check()); } }} placeholder="https://example.com/termix/index.json" className="h-8 text-xs" aria-label={t("plugins.manager.registries.urlLabel")} autoFocus /> {error && (

{error}

)} {preview && (
{preview.name} {t("plugins.manager.registries.pluginCount", { count: preview.plugins, })} {t("plugins.manager.registries.keyFingerprint", { keyId: preview.keyId, })}

{t("plugins.manager.registries.customWarning")}

)}
); }