New channel config (#807)
* Channel config rework Add staged channel config with tabbed UI, import/export workflow, and global form state refactor * Improve import dialog config comparison and UI labels * Review fixes * Improve state handling * Fix default filter behaviour --------- Co-authored-by: philon- <philon-@users.noreply.github.com>
This commit is contained in:
co-authored by
philon-
parent
01fa030ef9
commit
1946000d14
@@ -11,13 +11,20 @@ import {
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { Input } from "@components/UI/Input.tsx";
|
||||
import { Label } from "@components/UI/Label.tsx";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@components/UI/Select.tsx";
|
||||
import { Switch } from "@components/UI/Switch.tsx";
|
||||
import { useDevice } from "@core/stores";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { toByteArray } from "base64-js";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Checkbox } from "../UI/Checkbox/index.tsx";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
export interface ImportDialogProps {
|
||||
open: boolean;
|
||||
@@ -26,12 +33,15 @@ export interface ImportDialogProps {
|
||||
}
|
||||
|
||||
export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
||||
const { config, channels } = useDevice();
|
||||
const { t } = useTranslation("dialog");
|
||||
const [importDialogInput, setImportDialogInput] = useState<string>("");
|
||||
const [channelSet, setChannelSet] = useState<Protobuf.AppOnly.ChannelSet>();
|
||||
const [validUrl, setValidUrl] = useState<boolean>(false);
|
||||
const [updateConfig, setUpdateConfig] = useState<boolean>(true);
|
||||
const [importIndex, setImportIndex] = useState<number[]>([]);
|
||||
|
||||
const { connection } = useDevice();
|
||||
const { setWorkingChannelConfig, setWorkingConfig } = useDevice();
|
||||
|
||||
useEffect(() => {
|
||||
// the channel information is contained in the URL's fragment, which will be present after a
|
||||
@@ -55,12 +65,17 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
||||
)
|
||||
.replace(/-/g, "+")
|
||||
.replace(/_/g, "/");
|
||||
setChannelSet(
|
||||
fromBinary(
|
||||
Protobuf.AppOnly.ChannelSetSchema,
|
||||
toByteArray(paddedString),
|
||||
),
|
||||
|
||||
const newChannelSet = fromBinary(
|
||||
Protobuf.AppOnly.ChannelSetSchema,
|
||||
toByteArray(paddedString),
|
||||
);
|
||||
|
||||
const newImportChannelArray = newChannelSet.settings.map((_, idx) => idx);
|
||||
|
||||
setChannelSet(newChannelSet);
|
||||
setImportIndex(newImportChannelArray);
|
||||
setUpdateConfig(newChannelSet?.loraConfig !== undefined);
|
||||
setValidUrl(true);
|
||||
} catch (_error) {
|
||||
setValidUrl(false);
|
||||
@@ -71,29 +86,62 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
||||
const apply = () => {
|
||||
channelSet?.settings.map(
|
||||
(ch: Protobuf.Channel.ChannelSettings, index: number) => {
|
||||
connection?.setChannel(
|
||||
create(Protobuf.Channel.ChannelSchema, {
|
||||
index,
|
||||
role:
|
||||
index === 0
|
||||
? Protobuf.Channel.Channel_Role.PRIMARY
|
||||
: Protobuf.Channel.Channel_Role.SECONDARY,
|
||||
settings: ch,
|
||||
}),
|
||||
);
|
||||
if (importIndex[index] === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = create(Protobuf.Channel.ChannelSchema, {
|
||||
index: importIndex[index],
|
||||
role:
|
||||
importIndex[index] === 0
|
||||
? Protobuf.Channel.Channel_Role.PRIMARY
|
||||
: Protobuf.Channel.Channel_Role.SECONDARY,
|
||||
settings: ch,
|
||||
});
|
||||
|
||||
if (
|
||||
!deepCompareConfig(
|
||||
channels.get(importIndex[index] ?? 0),
|
||||
payload,
|
||||
true,
|
||||
)
|
||||
) {
|
||||
setWorkingChannelConfig(payload);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (channelSet?.loraConfig) {
|
||||
connection?.setConfig(
|
||||
create(Protobuf.Config.ConfigSchema, {
|
||||
payloadVariant: {
|
||||
case: "lora",
|
||||
value: channelSet.loraConfig,
|
||||
},
|
||||
}),
|
||||
);
|
||||
if (channelSet?.loraConfig && updateConfig) {
|
||||
const payload = {
|
||||
...config.lora,
|
||||
...channelSet.loraConfig,
|
||||
};
|
||||
|
||||
if (!deepCompareConfig(config.lora, payload, true)) {
|
||||
setWorkingConfig(
|
||||
create(Protobuf.Config.ConfigSchema, {
|
||||
payloadVariant: {
|
||||
case: "lora",
|
||||
value: payload,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Reset state after import
|
||||
setImportDialogInput("");
|
||||
setChannelSet(undefined);
|
||||
setValidUrl(false);
|
||||
setImportIndex([]);
|
||||
setUpdateConfig(true);
|
||||
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const onSelectChange = (value: string, index: number) => {
|
||||
const newImportIndex = [...importIndex];
|
||||
newImportIndex[index] = Number.parseInt(value);
|
||||
setImportIndex(newImportIndex);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -102,55 +150,85 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
||||
<DialogClose />
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("import.title")}</DialogTitle>
|
||||
<DialogDescription>{t("import.description")}</DialogDescription>
|
||||
<DialogDescription>
|
||||
<Trans
|
||||
i18nKey={"import.description"}
|
||||
components={{ italic: <i />, br: <br /> }}
|
||||
/>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>{t("import.channelSetUrl")}</Label>
|
||||
<Input
|
||||
value={importDialogInput}
|
||||
suffix={validUrl ? "✅" : "❌"}
|
||||
variant={
|
||||
importDialogInput === ""
|
||||
? "default"
|
||||
: validUrl
|
||||
? "dirty"
|
||||
: "invalid"
|
||||
}
|
||||
onChange={(e) => {
|
||||
setImportDialogInput(e.target.value);
|
||||
}}
|
||||
/>
|
||||
{validUrl && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-6 mt-2">
|
||||
<div className="flex w-full gap-2">
|
||||
<div className="w-36">
|
||||
<Label>{t("import.usePreset")}</Label>
|
||||
<div className=" flex items-center">
|
||||
<Switch
|
||||
disabled
|
||||
checked={channelSet?.loraConfig?.usePreset ?? true}
|
||||
className="ml-3 mr-4"
|
||||
checked={updateConfig}
|
||||
onCheckedChange={(next) => setUpdateConfig(next)}
|
||||
/>
|
||||
<Label className="">
|
||||
{t("import.useLoraConfig")}
|
||||
<span className="block pt-2 font-normal text-s">
|
||||
{t("import.presetDescription")}
|
||||
</span>
|
||||
</Label>
|
||||
</div>
|
||||
{/* <Select
|
||||
label="Modem Preset"
|
||||
disabled
|
||||
value={channelSet?.loraConfig?.modemPreset}
|
||||
>
|
||||
{renderOptions(Protobuf.Config_LoRaConfig_ModemPreset)}
|
||||
</Select> */}
|
||||
</div>
|
||||
{/* <Select
|
||||
label="Region"
|
||||
disabled
|
||||
value={channelSet?.loraConfig?.region}
|
||||
>
|
||||
{renderOptions(Protobuf.Config_LoRaConfig_RegionCode)}
|
||||
</Select> */}
|
||||
|
||||
<span className="text-md block font-medium text-text-primary">
|
||||
{t("import.channels")}
|
||||
</span>
|
||||
<div className="flex w-40 flex-col gap-1">
|
||||
{channelSet?.settings.map((channel) => (
|
||||
<div className="flex justify-between" key={channel.id}>
|
||||
<Label>
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<div className="flex items-center font-semibold text-sm">
|
||||
<span className="flex-1">{t("import.channelName")}</span>
|
||||
<span className="flex-1">{t("import.channelSlot")}</span>
|
||||
</div>
|
||||
{channelSet?.settings.map((channel, index) => (
|
||||
<div
|
||||
className="flex items-center"
|
||||
key={`channel_${channel.id}_${index}`}
|
||||
>
|
||||
<Label className="flex-1">
|
||||
{channel.name.length
|
||||
? channel.name
|
||||
: `${t("import.channelPrefix")}${channel.id}`}
|
||||
</Label>
|
||||
<Checkbox key={channel.id} />
|
||||
<Select
|
||||
onValueChange={(value) => onSelectChange(value, index)}
|
||||
value={importIndex[index]?.toString()}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Array.from({ length: 8 }, (_, i) => i).map((i) => (
|
||||
<SelectItem
|
||||
key={`index_${i}`}
|
||||
disabled={importIndex.includes(i) && index !== i}
|
||||
value={i.toString()}
|
||||
>
|
||||
{i === 0
|
||||
? t("import.primary")
|
||||
: `${t("import.channelPrefix")}${i}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value="-1">
|
||||
{t("import.doNotImport")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -82,7 +82,7 @@ export const QRDialog = ({
|
||||
: `${t("page.channelIndex", {
|
||||
ns: "channels",
|
||||
index: channel.index,
|
||||
})}${channel.index}`}
|
||||
})}`}
|
||||
</Label>
|
||||
<Checkbox
|
||||
key={channel.index}
|
||||
|
||||
Reference in New Issue
Block a user