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 (
{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 ? (
) : (
)}
);
}