You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

180 lines
5.4 KiB

import { BLE } from "@components/PageComponents/Connect/BLE.tsx";
import { HTTP } from "@components/PageComponents/Connect/HTTP.tsx";
import { Serial } from "@components/PageComponents/Connect/Serial.tsx";
import {
Dialog,
DialogClose,
DialogContent,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.tsx";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@components/UI/Tabs.tsx";
import {
type BrowserFeature,
useBrowserFeatureDetection,
} from "@core/hooks/useBrowserFeatureDetection.ts";
import { AlertCircle } from "lucide-react";
import { Trans, useTranslation } from "react-i18next";
import { Link } from "../UI/Typography/Link.tsx";
export interface TabElementProps {
closeDialog: () => void;
}
export interface TabManifest {
id: "HTTP" | "BLE" | "Serial";
label: string;
element: React.FC<TabElementProps>;
isDisabled: boolean;
}
export interface NewDeviceProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
interface FeatureErrorProps {
missingFeatures: BrowserFeature[];
tabId: "HTTP" | "BLE" | "Serial";
}
const errors: Record<BrowserFeature, { href: string; i18nKey: string }> = {
"Web Bluetooth": {
href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility",
i18nKey: "newDeviceDialog.validation.requiresWebBluetooth",
},
"Web Serial": {
href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility",
i18nKey: "newDeviceDialog.validation.requiresWebSerial",
},
"Secure Context": {
href: "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts",
i18nKey: "newDeviceDialog.validation.requiresSecureContext",
},
};
const ErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => {
if (missingFeatures.length === 0) {
return null;
}
const browserFeatures = missingFeatures.filter(
(feature) => feature !== "Secure Context",
);
const needsSecureContext = missingFeatures.includes("Secure Context");
const needsFeature =
tabId === "BLE" && browserFeatures.includes("Web Bluetooth")
? "Web Bluetooth"
: tabId === "Serial" && browserFeatures.includes("Web Serial")
? "Web Serial"
: undefined;
return (
<div className="flex flex-col items-start gap-2 bg-red-500 p-4 rounded-md text-sm text-slate-500 dark:text-slate-400">
<div className="flex items-center gap-2 w-full">
<AlertCircle size={40} className="mr-2 shrink-0 text-white" />
<div className="flex flex-col gap-3">
<div className="text-sm text-white">
{needsFeature && (
<Trans
i18nKey={errors[needsFeature].i18nKey}
components={[
<Link
key="0"
href={errors[needsFeature].href}
className="underline hover:text-slate-200 text-white dark:text-white dark:hover:text-slate-300"
/>,
]}
/>
)}
{needsFeature && needsSecureContext && " "}
{needsSecureContext && (
<Trans
i18nKey={
browserFeatures.length > 0
? "newDeviceDialog.validation.additionallyRequiresSecureContext"
: "newDeviceDialog.validation.requiresSecureContext"
}
components={{
"0": (
<Link
href={errors["Secure Context"].href}
className="underline hover:text-slate-200"
/>
),
}}
/>
)}
</div>
</div>
</div>
</div>
);
};
export const NewDeviceDialog = ({ open, onOpenChange }: NewDeviceProps) => {
const { t } = useTranslation("dialog");
const { unsupported } = useBrowserFeatureDetection();
const tabs: TabManifest[] = [
{
id: "HTTP",
label: t("newDeviceDialog.tabHttp"),
element: HTTP,
isDisabled: false,
},
{
id: "BLE",
label: t("newDeviceDialog.tabBluetooth"),
element: BLE,
isDisabled:
unsupported.includes("Web Bluetooth") ||
unsupported.includes("Secure Context"),
},
{
id: "Serial",
label: t("newDeviceDialog.tabSerial"),
element: Serial,
isDisabled:
unsupported.includes("Web Serial") ||
unsupported.includes("Secure Context"),
},
];
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent aria-describedby={undefined}>
<DialogClose />
<DialogHeader>
<DialogTitle>{t("newDeviceDialog.title")}</DialogTitle>
</DialogHeader>
<Tabs defaultValue="HTTP">
<TabsList>
{tabs.map((tab) => (
<TabsTrigger key={tab.id} value={tab.id}>
{tab.label}
</TabsTrigger>
))}
</TabsList>
{tabs.map((tab) => (
<TabsContent key={tab.id} value={tab.id}>
<fieldset disabled={tab.isDisabled}>
{tab.id !== "HTTP" && tab.isDisabled ? (
<ErrorMessage missingFeatures={unsupported} tabId={tab.id} />
) : (
<tab.element closeDialog={() => onOpenChange(false)} />
)}
</fieldset>
</TabsContent>
))}
</Tabs>
</DialogContent>
</Dialog>
);
};