import { forwardRef, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import * as Popover from "@radix-ui/react-popover"; import * as ScrollArea from "@radix-ui/react-scroll-area"; import { Command } from "cmdk"; import { Check, ChevronsUpDown, type LucideProps, SquareArrowUpRight } from "lucide-react"; import { cn } from "@/lib/cn"; import { TruncatedText } from "@/components/TruncatedText"; import { useNetworks } from "@/contexts/NetworksContext"; import { useStatus } from "@/contexts/StatusContext"; import { useFocusVisible } from "@/hooks/useFocusVisible"; const NONE_VALUE = "__none__"; export const MainExitNodeSwitcher = () => { const { t } = useTranslation(); const { status } = useStatus(); const { exitNodes, toggleExitNode } = useNetworks(); const active = exitNodes.find((n) => n.selected) ?? null; const isConnected = status?.status === "Connected"; const hasAny = exitNodes.length > 0; const disabled = !isConnected || !hasAny; const [open, setOpen] = useState(false); const listRef = useRef(null); const handleTriggerKeyDown = (e: React.KeyboardEvent) => { if (open || disabled) return; if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); setOpen(true); } }; const handleSelect = (next: string) => { setOpen(false); if (next === NONE_VALUE) { if (active) toggleExitNode(active.id, true).catch((err: unknown) => console.error("toggle exit node failed", err), ); return; } if (active?.id === next) return; toggleExitNode(next, false).catch((err: unknown) => console.error("toggle exit node failed", err), ); }; const title = active ? active.id : t("exitNodes.card.title"); const activeDescription = active ? t("exitNodes.card.statusActive") : t("exitNodes.card.statusInactive"); const description = hasAny ? activeDescription : t("exitNodes.empty.title"); return ( { e.preventDefault(); listRef.current?.focus(); }} style={{ width: "var(--radix-popover-trigger-width)" }} className={cn( "wails-no-draggable z-50 select-none overflow-hidden rounded-lg border border-nb-gray-900 bg-nb-gray-935 p-1 text-nb-gray-200 shadow-lg", "data-[state=open]:animate-in data-[state=closed]:animate-out", "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95", "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2", "data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", )} > e.stopPropagation()} className={"outline-none focus:outline-none focus-visible:outline-none"} > handleSelect(NONE_VALUE)} /> {hasAny &&
} {hasAny && ( {exitNodes.map((n) => ( handleSelect(n.id)} /> ))} )} ); }; type TriggerProps = React.ButtonHTMLAttributes & { title: string; description: string; active?: boolean; }; const ExitNodeTriggerCard = forwardRef( function ExitNodeTriggerCard( { title, description, disabled, active = false, className, ...props }, ref, ) { const isFocusVisible = useFocusVisible(); return ( ); }, ); type NoneRowProps = { isActive: boolean; onSelect: () => void; }; const NoneRow = ({ isActive, onSelect }: NoneRowProps) => { const { t } = useTranslation(); return ( {t("exitNodes.dropdown.noneTitle")} {isActive && ( )} ); }; type ExitNodeRowProps = { id: string; label: string; isActive: boolean; onSelect: () => void; }; const ExitNodeRow = ({ id, label, isActive, onSelect }: ExitNodeRowProps) => ( {label} {isActive && } ); const ExitNodeIcon = ({ size, ...props }: LucideProps) => ( );