import AddConnectionDialog from "@app/components/Dialog/AddConnectionDialog/AddConnectionDialog"; import { TimeAgo } from "@app/components/generic/TimeAgo"; import LanguageSwitcher from "@app/components/LanguageSwitcher"; import { ConnectionStatusBadge } from "@app/components/PageComponents/Connections/ConnectionStatusBadge"; import type { Connection } from "@app/core/stores/deviceStore/types"; import { useConnections } from "@app/pages/Connections/useConnections"; import { connectionTypeIcon, formatConnectionSubtext, } from "@app/pages/Connections/utils"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@components/UI/AlertDialog.tsx"; import { Badge } from "@components/UI/Badge.tsx"; import { Button } from "@components/UI/Button.tsx"; import { Card, CardContent, CardFooter, CardHeader, CardTitle, } from "@components/UI/Card.tsx"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@components/UI/DropdownMenu.tsx"; import { Separator } from "@components/UI/Separator.tsx"; import { useToast } from "@core/hooks/useToast.ts"; import { useNavigate } from "@tanstack/react-router"; import { ArrowLeft, LinkIcon, MoreHorizontal, RotateCw, RouterIcon, Star, StarOff, Trash2, } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; export const Connections = () => { const { connections, addConnectionAndConnect, connect, disconnect, removeConnection, setDefaultConnection, refreshStatuses, syncConnectionStatuses, } = useConnections(); const { toast } = useToast(); const navigate = useNavigate({ from: "/" }); const [addOpen, setAddOpen] = useState(false); const isURLHTTPS = useMemo(() => location.protocol === "https:", []); const { t } = useTranslation("connections"); // On first mount, sync statuses and refresh // biome-ignore lint/correctness/useExhaustiveDependencies: This can cause the icon to refresh too often useEffect(() => { syncConnectionStatuses(); refreshStatuses(); }, []); const sorted = useMemo(() => { const copy = [...connections]; return copy.sort((a, b) => { if (a.isDefault && !b.isDefault) { return -1; } if (!a.isDefault && b.isDefault) { return 1; } const aConnected = a.status === "connected" || a.status === "configured"; const bConnected = b.status === "connected" || b.status === "configured"; if (aConnected && !bConnected) { return -1; } return a.name.localeCompare(b.name); }); }, [connections]); return (

{t("page.title")}

{t("page.description")}

{sorted.length === 0 ? ( {t("noConnections.title")}{" "} {t("noConnections.description")} ) : (
{sorted.map((c) => ( { const ok = await connect(c.id, { allowPrompt: true }); toast({ title: ok ? t("toasts.connected") : t("toasts.failed"), description: ok ? t("toasts.nowConnected", { name: c.name, interpolation: { escapeValue: false }, }) : t("toasts.checkConnection"), }); if (ok) { navigate({ to: "/" }); } }} onDisconnect={async () => { await disconnect(c.id); toast({ title: t("toasts.disconnected"), description: t("toasts.nowDisconnected", { name: c.name, interpolation: { escapeValue: false }, }), }); }} onSetDefault={() => { setDefaultConnection(c.id); toast({ title: t("toasts.defaultSet"), description: t("toasts.defaultConnection", { name: c.name, interpolation: { escapeValue: false }, }), }); }} onDelete={async () => { await disconnect(c.id); removeConnection(c.id); toast({ title: t("toasts.deleted"), description: t("toasts.deletedByName", { name: c.name, interpolation: { escapeValue: false }, }), }); }} onRetry={async () => { const ok = await connect(c.id, { allowPrompt: true }); toast({ title: ok ? t("toasts.connected") : t("toasts.failed"), description: ok ? t("toasts.nowConnected", { name: c.name, interpolation: { escapeValue: false }, }) : t("toasts.pickConnectionAgain"), }); if (ok) { navigate({ to: "/" }); } }} /> ))}
)} { const created = await addConnectionAndConnect(partial, btDevice); if (created) { setAddOpen(false); toast({ title: t("toasts.added"), description: t("toasts.savedByName", { name: created.name, interpolation: { escapeValue: false }, }), }); if ( created.status === "connected" || created.status === "configured" ) { navigate({ to: "/" }); } } else { toast({ title: "Unable to connect", description: "savedCantConnect", }); } }} />
); }; function TypeBadge({ type }: { type: Connection["type"] }) { const Icon = connectionTypeIcon(type); const label = type === "http" ? "HTTP" : type === "bluetooth" ? "Bluetooth" : "Serial"; return ( {label} ); } function ConnectionCard({ connection, onConnect, onDisconnect, onSetDefault, onDelete, onRetry, }: { connection: Connection; onConnect: () => Promise | Promise; onDisconnect: () => Promise | Promise; onSetDefault: () => void; onDelete: () => void; onRetry: () => Promise | Promise; }) { const { t } = useTranslation("connections"); const Icon = connectionTypeIcon(connection.type); const isBusy = connection.status === "connecting" || connection.status === "configuring"; const isConnected = connection.status === "connected" || connection.status === "configured"; const isError = connection.status === "error"; return (
{connection.name} {connection.isDefault ? ( {t("default")} ) : null}
{formatConnectionSubtext(connection)}
{connection.type === "http" && connection.isDefault && ( onSetDefault()} > {t("button.unsetDefault")} )} {connection.type === "http" && !connection.isDefault && ( onSetDefault()} > {t("button.setDefault")} )} e.preventDefault()} > {t("button.delete")} {t("deleteConnection")} {t("areYouSure", { name: connection.name })} {t("button.cancel")} onDelete()} > {t("button.delete")}
{connection.error ? (

{connection.error}

) : connection.lastConnectedAt ? (

{t("lastConnectedAt", { date: "" })}{" "}

) : (

{t("neverConnected")}

)}
{isConnected ? ( ) : ( )}
); }