Merge pull request #213 from bmv437/feature/enforce-biome
Update biome, fix and enforce recommended rules
This commit is contained in:
Vendored
+6
-3
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
"editor.defaultFormatter": "biomejs.biome",
|
"editor.defaultFormatter": "biomejs.biome",
|
||||||
"editor.formatOnSave": true
|
"editor.codeActionsOnSave": {
|
||||||
}
|
"quickfix.biome": "explicit"
|
||||||
|
},
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/1.6.3/schema.json",
|
"$schema": "https://biomejs.dev/schemas/1.7.3/schema.json",
|
||||||
"organizeImports": {
|
"organizeImports": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
"all": true
|
"recommended": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -6,8 +6,9 @@
|
|||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && pnpm check && vite build ",
|
||||||
"check": "biome check .",
|
"check": "biome check .",
|
||||||
|
"check:fix": "pnpm check --write",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"package": "gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ $(ls ./dist/output/)"
|
"package": "gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ $(ls ./dist/output/)"
|
||||||
},
|
},
|
||||||
@@ -60,7 +61,7 @@
|
|||||||
"zustand": "4.5.2"
|
"zustand": "4.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.6.3",
|
"@biomejs/biome": "^1.8.1",
|
||||||
"@buf/meshtastic_protobufs.bufbuild_es": "1.8.0-20240325205556-b11811405eea.2",
|
"@buf/meshtastic_protobufs.bufbuild_es": "1.8.0-20240325205556-b11811405eea.2",
|
||||||
"@types/chrome": "^0.0.263",
|
"@types/chrome": "^0.0.263",
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.11.30",
|
||||||
|
|||||||
Generated
+1
-1
@@ -122,7 +122,7 @@ dependencies:
|
|||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@biomejs/biome':
|
'@biomejs/biome':
|
||||||
specifier: ^1.6.3
|
specifier: ^1.8.1
|
||||||
version: 1.8.1
|
version: 1.8.1
|
||||||
'@buf/meshtastic_protobufs.bufbuild_es':
|
'@buf/meshtastic_protobufs.bufbuild_es':
|
||||||
specifier: 1.8.0-20240325205556-b11811405eea.2
|
specifier: 1.8.0-20240325205556-b11811405eea.2
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {}
|
autoprefixer: {},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
LayersIcon,
|
LayersIcon,
|
||||||
LayoutIcon,
|
LayoutIcon,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
LucideIcon,
|
type LucideIcon,
|
||||||
MapIcon,
|
MapIcon,
|
||||||
MessageSquareIcon,
|
MessageSquareIcon,
|
||||||
MoonIcon,
|
MoonIcon,
|
||||||
@@ -350,7 +350,7 @@ export const CommandPalette = (): JSX.Element => {
|
|||||||
|
|
||||||
window.addEventListener("keydown", handleKeydown);
|
window.addEventListener("keydown", handleKeydown);
|
||||||
return () => window.removeEventListener("keydown", handleKeydown);
|
return () => window.removeEventListener("keydown", handleKeydown);
|
||||||
}, []);
|
}, [setCommandPaletteOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandDialog
|
<CommandDialog
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import {
|
|||||||
LanguagesIcon,
|
LanguagesIcon,
|
||||||
MoonIcon,
|
MoonIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
SunIcon,
|
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
|
SunIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
export const DeviceSelector = (): JSX.Element => {
|
export const DeviceSelector = (): JSX.Element => {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import { RemoveNodeDialog } from "@app/components/Dialog/RemoveNodeDialog.js";
|
||||||
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.js";
|
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.js";
|
||||||
import { ImportDialog } from "@components/Dialog/ImportDialog.js";
|
import { ImportDialog } from "@components/Dialog/ImportDialog.js";
|
||||||
import { QRDialog } from "@components/Dialog/QRDialog.js";
|
import { QRDialog } from "@components/Dialog/QRDialog.js";
|
||||||
import { RebootDialog } from "@components/Dialog/RebootDialog.js";
|
import { RebootDialog } from "@components/Dialog/RebootDialog.js";
|
||||||
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.js";
|
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.js";
|
||||||
import { RemoveNodeDialog } from "@app/components/Dialog/RemoveNodeDialog.js"
|
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.js";
|
||||||
|
|
||||||
export const DialogManager = (): JSX.Element => {
|
export const DialogManager = (): JSX.Element => {
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import {
|
|||||||
} from "@components/UI/Dialog.js";
|
} from "@components/UI/Dialog.js";
|
||||||
import { Input } from "@components/UI/Input.js";
|
import { Input } from "@components/UI/Input.js";
|
||||||
import { Label } from "@components/UI/Label.js";
|
import { Label } from "@components/UI/Label.js";
|
||||||
import { Protobuf, Types } from "@meshtastic/js";
|
import { Protobuf, type Types } from "@meshtastic/js";
|
||||||
import { fromByteArray } from "base64-js";
|
import { fromByteArray } from "base64-js";
|
||||||
import { ClipboardIcon } from "lucide-react";
|
import { ClipboardIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { QRCode } from "react-qrcode-logo";
|
import { QRCode } from "react-qrcode-logo";
|
||||||
|
|
||||||
export interface QRDialogProps {
|
export interface QRDialogProps {
|
||||||
@@ -32,7 +32,7 @@ export const QRDialog = ({
|
|||||||
const [qrCodeUrl, setQrCodeUrl] = useState<string>("");
|
const [qrCodeUrl, setQrCodeUrl] = useState<string>("");
|
||||||
const [qrCodeAdd, setQrCodeAdd] = useState<boolean>();
|
const [qrCodeAdd, setQrCodeAdd] = useState<boolean>();
|
||||||
|
|
||||||
const allChannels = Array.from(channels.values());
|
const allChannels = useMemo(() => Array.from(channels.values()), [channels]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const channelsToEncode = allChannels
|
const channelsToEncode = allChannels
|
||||||
@@ -50,8 +50,10 @@ export const QRDialog = ({
|
|||||||
.replace(/\+/g, "-")
|
.replace(/\+/g, "-")
|
||||||
.replace(/\//g, "_");
|
.replace(/\//g, "_");
|
||||||
|
|
||||||
setQrCodeUrl(`https://meshtastic.org/e/#${base64}${qrCodeAdd ? "?add=true" : ""}`);
|
setQrCodeUrl(
|
||||||
}, [channels, selectedChannels, qrCodeAdd, loraConfig]);
|
`https://meshtastic.org/e/#${base64}${qrCodeAdd ? "?add=true" : ""}`,
|
||||||
|
);
|
||||||
|
}, [allChannels, selectedChannels, qrCodeAdd, loraConfig]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
@@ -97,18 +99,26 @@ export const QRDialog = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={ "border-black border-t border-l border-b rounded-l h-10 px-7 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 " + (qrCodeAdd ? "focus:ring-green-800 bg-green-800 text-white" : "focus:ring-slate-400 bg-slate-400 hover:bg-green-600") }
|
className={`border-black border-t border-l border-b rounded-l h-10 px-7 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 ${
|
||||||
|
qrCodeAdd
|
||||||
|
? "focus:ring-green-800 bg-green-800 text-white"
|
||||||
|
: "focus:ring-slate-400 bg-slate-400 hover:bg-green-600"
|
||||||
|
}`}
|
||||||
onClick={() => setQrCodeAdd(true)}
|
onClick={() => setQrCodeAdd(true)}
|
||||||
>
|
>
|
||||||
Add Channels
|
Add Channels
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={ "border-black border-t border-r border-b rounded-r h-10 px-4 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 " + (!qrCodeAdd ? "focus:ring-green-800 bg-green-800 text-white" : "focus:ring-slate-400 bg-slate-400 hover:bg-green-600") }
|
className={`border-black border-t border-r border-b rounded-r h-10 px-4 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 ${
|
||||||
|
!qrCodeAdd
|
||||||
|
? "focus:ring-green-800 bg-green-800 text-white"
|
||||||
|
: "focus:ring-slate-400 bg-slate-400 hover:bg-green-600"
|
||||||
|
}`}
|
||||||
onClick={() => setQrCodeAdd(false)}
|
onClick={() => setQrCodeAdd(false)}
|
||||||
>
|
>
|
||||||
Replace Channels
|
Replace Channels
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const RebootDialog = ({
|
|||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={time}
|
value={time}
|
||||||
onChange={(e) => setTime(parseInt(e.target.value))}
|
onChange={(e) => setTime(Number.parseInt(e.target.value))}
|
||||||
action={{
|
action={{
|
||||||
icon: ClockIcon,
|
icon: ClockIcon,
|
||||||
onClick() {
|
onClick() {
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ export const RemoveNodeDialog = ({
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="destructive" onClick={() => onSubmit()}>Remove</Button>
|
<Button variant="destructive" onClick={() => onSubmit()}>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const ShutdownDialog = ({
|
|||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={time}
|
value={time}
|
||||||
onChange={(e) => setTime(parseInt(e.target.value))}
|
onChange={(e) => setTime(Number.parseInt(e.target.value))}
|
||||||
suffix="Minutes"
|
suffix="Minutes"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
DynamicFormField,
|
DynamicFormField,
|
||||||
FieldProps,
|
type FieldProps,
|
||||||
} from "@components/Form/DynamicFormField.js";
|
} from "@components/Form/DynamicFormField.js";
|
||||||
import { FieldWrapper } from "@components/Form/FormWrapper.js";
|
import { FieldWrapper } from "@components/Form/FormWrapper.js";
|
||||||
import { Button } from "@components/UI/Button.js";
|
import { Button } from "@components/UI/Button.js";
|
||||||
import { H4 } from "@components/UI/Typography/H4.js";
|
import { H4 } from "@components/UI/Typography/H4.js";
|
||||||
import { Subtle } from "@components/UI/Typography/Subtle.js";
|
import { Subtle } from "@components/UI/Typography/Subtle.js";
|
||||||
import {
|
import {
|
||||||
Control,
|
type Control,
|
||||||
DefaultValues,
|
type DefaultValues,
|
||||||
FieldValues,
|
type FieldValues,
|
||||||
Path,
|
type Path,
|
||||||
SubmitHandler,
|
type SubmitHandler,
|
||||||
useForm,
|
useForm,
|
||||||
} from "react-hook-form";
|
} from "react-hook-form";
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ export interface BaseFormBuilderProps<T> {
|
|||||||
disabledBy?: DisabledBy<T>[];
|
disabledBy?: DisabledBy<T>[];
|
||||||
label: string;
|
label: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
properties?: {};
|
properties?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GenericFormElementProps<T extends FieldValues, Y> {
|
export interface GenericFormElementProps<T extends FieldValues, Y> {
|
||||||
@@ -94,9 +94,12 @@ export function DynamicForm<T extends FieldValues>({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{fieldGroup.fields.map((field) => (
|
{fieldGroup.fields.map((field) => (
|
||||||
<FieldWrapper label={field.label} description={field.description}>
|
<FieldWrapper
|
||||||
|
key={field.label}
|
||||||
|
label={field.label}
|
||||||
|
description={field.description}
|
||||||
|
>
|
||||||
<DynamicFormField
|
<DynamicFormField
|
||||||
key={field.label}
|
|
||||||
field={field}
|
field={field}
|
||||||
control={control}
|
control={control}
|
||||||
disabled={isDisabled(field.disabledBy)}
|
disabled={isDisabled(field.disabledBy)}
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import { GenericInput, InputFieldProps } from "@components/Form/FormInput.js";
|
import {
|
||||||
import { SelectFieldProps, SelectInput } from "@components/Form/FormSelect.js";
|
GenericInput,
|
||||||
import { ToggleFieldProps, ToggleInput } from "@components/Form/FormToggle.js";
|
type InputFieldProps,
|
||||||
|
} from "@components/Form/FormInput.js";
|
||||||
|
import {
|
||||||
|
type SelectFieldProps,
|
||||||
|
SelectInput,
|
||||||
|
} from "@components/Form/FormSelect.js";
|
||||||
|
import {
|
||||||
|
type ToggleFieldProps,
|
||||||
|
ToggleInput,
|
||||||
|
} from "@components/Form/FormToggle.js";
|
||||||
import type { Control, FieldValues } from "react-hook-form";
|
import type { Control, FieldValues } from "react-hook-form";
|
||||||
|
|
||||||
export type FieldProps<T> =
|
export type FieldProps<T> =
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type {
|
|||||||
} from "@components/Form/DynamicForm.js";
|
} from "@components/Form/DynamicForm.js";
|
||||||
import { Input } from "@components/UI/Input.js";
|
import { Input } from "@components/UI/Input.js";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import { Controller, FieldValues } from "react-hook-form";
|
import { Controller, type FieldValues } from "react-hook-form";
|
||||||
|
|
||||||
export interface InputFieldProps<T> extends BaseFormBuilderProps<T> {
|
export interface InputFieldProps<T> extends BaseFormBuilderProps<T> {
|
||||||
type: "text" | "number" | "password";
|
type: "text" | "number" | "password";
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@components/UI/Select.js";
|
} from "@components/UI/Select.js";
|
||||||
import { Controller, FieldValues } from "react-hook-form";
|
import { Controller, type FieldValues } from "react-hook-form";
|
||||||
|
|
||||||
export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> {
|
export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> {
|
||||||
type: "select" | "multiSelect";
|
type: "select" | "multiSelect";
|
||||||
@@ -40,7 +40,7 @@ export function SelectInput<T extends FieldValues>({
|
|||||||
: [];
|
: [];
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(e) => onChange(parseInt(e))}
|
onValueChange={(e) => onChange(Number.parseInt(e))}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={value?.toString()}
|
value={value?.toString()}
|
||||||
{...remainingProperties}
|
{...remainingProperties}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import type {
|
|||||||
GenericFormElementProps,
|
GenericFormElementProps,
|
||||||
} from "@components/Form/DynamicForm.js";
|
} from "@components/Form/DynamicForm.js";
|
||||||
import { Switch } from "@components/UI/Switch.js";
|
import { Switch } from "@components/UI/Switch.js";
|
||||||
import { ChangeEvent } from "react";
|
import type { ChangeEvent } from "react";
|
||||||
import { Controller, FieldValues } from "react-hook-form";
|
import { Controller, type FieldValues } from "react-hook-form";
|
||||||
|
|
||||||
export interface ToggleFieldProps<T> extends BaseFormBuilderProps<T> {
|
export interface ToggleFieldProps<T> extends BaseFormBuilderProps<T> {
|
||||||
type: "toggle";
|
type: "toggle";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type{ ChannelValidation } from "@app/validation/channel.js";
|
import type { ChannelValidation } from "@app/validation/channel.js";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||||
import { useToast } from "@core/hooks/useToast.js";
|
import { useToast } from "@core/hooks/useToast.js";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.js";
|
||||||
@@ -20,8 +20,12 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
...data.settings,
|
...data.settings,
|
||||||
psk: toByteArray(data.settings.psk ?? ""),
|
psk: toByteArray(data.settings.psk ?? ""),
|
||||||
moduleSettings: {
|
moduleSettings: {
|
||||||
positionPrecision: data.settings.positionEnabled ? data.settings.preciseLocation ? 32 : data.settings.positionPrecision : 0,
|
positionPrecision: data.settings.positionEnabled
|
||||||
}
|
? data.settings.preciseLocation
|
||||||
|
? 32
|
||||||
|
: data.settings.positionPrecision
|
||||||
|
: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
connection?.setChannel(channel).then(() => {
|
connection?.setChannel(channel).then(() => {
|
||||||
@@ -43,9 +47,16 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
settings: {
|
settings: {
|
||||||
...channel?.settings,
|
...channel?.settings,
|
||||||
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
|
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
|
||||||
positionEnabled: channel?.settings?.moduleSettings?.positionPrecision != undefined && channel?.settings?.moduleSettings?.positionPrecision > 0,
|
positionEnabled:
|
||||||
preciseLocation: channel?.settings?.moduleSettings?.positionPrecision == 32,
|
channel?.settings?.moduleSettings?.positionPrecision !==
|
||||||
positionPrecision: channel?.settings?.moduleSettings?.positionPrecision == undefined ? 10 : channel?.settings?.moduleSettings?.positionPrecision
|
undefined &&
|
||||||
|
channel?.settings?.moduleSettings?.positionPrecision > 0,
|
||||||
|
preciseLocation:
|
||||||
|
channel?.settings?.moduleSettings?.positionPrecision === 32,
|
||||||
|
positionPrecision:
|
||||||
|
channel?.settings?.moduleSettings?.positionPrecision === undefined
|
||||||
|
? 10
|
||||||
|
: channel?.settings?.moduleSettings?.positionPrecision,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -111,9 +122,32 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
description:
|
description:
|
||||||
"If not sharing precise location, position shared on channel will be accurate within this distance",
|
"If not sharing precise location, position shared on channel will be accurate within this distance",
|
||||||
properties: {
|
properties: {
|
||||||
enumValue: config.display?.units == 0 ?
|
enumValue:
|
||||||
{ "Within 23 km":10, "Within 12 km":11, "Within 5.8 km":12, "Within 2.9 km":13, "Within 1.5 km":14, "Within 700 m":15, "Within 350 m":16, "Within 200 m":17, "Within 90 m":18, "Within 50 m":19 } :
|
config.display?.units === 0
|
||||||
{ "Within 15 miles":10, "Within 7.3 miles":11, "Within 3.6 miles":12, "Within 1.8 miles":13, "Within 0.9 miles":14, "Within 0.5 miles":15, "Within 0.2 miles":16, "Within 600 feet":17, "Within 300 feet":18, "Within 150 feet":19 }
|
? {
|
||||||
|
"Within 23 km": 10,
|
||||||
|
"Within 12 km": 11,
|
||||||
|
"Within 5.8 km": 12,
|
||||||
|
"Within 2.9 km": 13,
|
||||||
|
"Within 1.5 km": 14,
|
||||||
|
"Within 700 m": 15,
|
||||||
|
"Within 350 m": 16,
|
||||||
|
"Within 200 m": 17,
|
||||||
|
"Within 90 m": 18,
|
||||||
|
"Within 50 m": 19,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
"Within 15 miles": 10,
|
||||||
|
"Within 7.3 miles": 11,
|
||||||
|
"Within 3.6 miles": 12,
|
||||||
|
"Within 1.8 miles": 13,
|
||||||
|
"Within 0.9 miles": 14,
|
||||||
|
"Within 0.5 miles": 15,
|
||||||
|
"Within 0.2 miles": 16,
|
||||||
|
"Within 600 feet": 17,
|
||||||
|
"Within 300 feet": 18,
|
||||||
|
"Within 150 feet": 19,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const LoRa = (): JSX.Element => {
|
|||||||
label: "Hop Limit",
|
label: "Hop Limit",
|
||||||
description: "Maximum number of hops",
|
description: "Maximum number of hops",
|
||||||
properties: {
|
properties: {
|
||||||
enumValue: {1:1, 2:2, 3:3, 4:4, 5:5, 6:6, 7:7}
|
enumValue: { 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
|
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
|
||||||
import { Button } from "@components/UI/Button.js";
|
import { Button } from "@components/UI/Button.js";
|
||||||
import { Mono } from "@components/generic/Mono.js";
|
import { Mono } from "@components/generic/Mono.js";
|
||||||
import { useAppStore } from "@core/stores/appStore.js";
|
import { useAppStore } from "@core/stores/appStore.js";
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
|
||||||
import { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
|
|
||||||
import { Button } from "@components/UI/Button.js";
|
import { Button } from "@components/UI/Button.js";
|
||||||
import { Input } from "@components/UI/Input.js";
|
import { Input } from "@components/UI/Input.js";
|
||||||
import { Label } from "@components/UI/Label.js";
|
import { Label } from "@components/UI/Label.js";
|
||||||
@@ -9,6 +8,7 @@ import { useDeviceStore } from "@core/stores/deviceStore.js";
|
|||||||
import { subscribeAll } from "@core/subscriptions.js";
|
import { subscribeAll } from "@core/subscriptions.js";
|
||||||
import { randId } from "@core/utils/randId.js";
|
import { randId } from "@core/utils/randId.js";
|
||||||
import { HttpConnection } from "@meshtastic/js";
|
import { HttpConnection } from "@meshtastic/js";
|
||||||
|
import { useState } from "react";
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
||||||
@@ -20,7 +20,7 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
|||||||
}>({
|
}>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
ip: ["client.meshtastic.org", "localhost"].includes(
|
ip: ["client.meshtastic.org", "localhost"].includes(
|
||||||
window.location.hostname
|
window.location.hostname,
|
||||||
)
|
)
|
||||||
? "meshtastic.local"
|
? "meshtastic.local"
|
||||||
: window.location.hostname,
|
: window.location.hostname,
|
||||||
@@ -38,7 +38,7 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
|||||||
|
|
||||||
const onSubmit = handleSubmit(async (data) => {
|
const onSubmit = handleSubmit(async (data) => {
|
||||||
setConnectionInProgress(true);
|
setConnectionInProgress(true);
|
||||||
|
|
||||||
const id = randId();
|
const id = randId();
|
||||||
const device = addDevice(id);
|
const device = addDevice(id);
|
||||||
const connection = new HttpConnection(id);
|
const connection = new HttpConnection(id);
|
||||||
@@ -75,7 +75,9 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
|||||||
<Switch
|
<Switch
|
||||||
// label="Use TLS"
|
// label="Use TLS"
|
||||||
// description="Description"
|
// description="Description"
|
||||||
disabled={location.protocol === "https:" || connectionInProgress}
|
disabled={
|
||||||
|
location.protocol === "https:" || connectionInProgress
|
||||||
|
}
|
||||||
checked={value}
|
checked={value}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
@@ -84,7 +86,7 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" disabled={connectionInProgress}>
|
<Button type="submit" disabled={connectionInProgress}>
|
||||||
<span>{connectionInProgress ? 'Connecting...' : 'Connect' }</span>
|
<span>{connectionInProgress ? "Connecting..." : "Connect"}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
|
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
|
||||||
import { Button } from "@components/UI/Button.js";
|
import { Button } from "@components/UI/Button.js";
|
||||||
import { Mono } from "@components/generic/Mono.js";
|
import { Mono } from "@components/generic/Mono.js";
|
||||||
import { useAppStore } from "@core/stores/appStore.js";
|
import { useAppStore } from "@core/stores/appStore.js";
|
||||||
@@ -48,19 +48,22 @@ export const Serial = ({ closeDialog }: TabElementProps): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-2 p-4">
|
<div className="flex w-full flex-col gap-2 p-4">
|
||||||
<div className="flex h-48 flex-col gap-2 overflow-y-auto">
|
<div className="flex h-48 flex-col gap-2 overflow-y-auto">
|
||||||
{serialPorts.map((port, index) => (
|
{serialPorts.map((port, index) => {
|
||||||
<Button
|
const { usbProductId, usbVendorId } = port.getInfo();
|
||||||
key={index}
|
return (
|
||||||
disabled={port.readable !== null}
|
<Button
|
||||||
onClick={async () => {
|
key={`${usbVendorId ?? "UNK"}-${usbProductId ?? "UNK"}-${index}`}
|
||||||
await onConnect(port);
|
disabled={port.readable !== null}
|
||||||
}}
|
onClick={async () => {
|
||||||
>
|
await onConnect(port);
|
||||||
{`# ${index} - ${port.getInfo().usbVendorId ?? "UNK"} - ${
|
}}
|
||||||
port.getInfo().usbProductId ?? "UNK"
|
>
|
||||||
}`}
|
{`# ${index} - ${usbVendorId ?? "UNK"} - ${
|
||||||
</Button>
|
usbProductId ?? "UNK"
|
||||||
))}
|
}`}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
{serialPorts.length === 0 && (
|
{serialPorts.length === 0 && (
|
||||||
<Mono className="m-auto select-none">No devices paired yet.</Mono>
|
<Mono className="m-auto select-none">No devices paired yet.</Mono>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { Subtle } from "@app/components/UI/Typography/Subtle.js";
|
import { Subtle } from "@app/components/UI/Typography/Subtle.js";
|
||||||
import { MessageWithState, useDevice } from "@app/core/stores/deviceStore.js";
|
import {
|
||||||
|
type MessageWithState,
|
||||||
|
useDevice,
|
||||||
|
} from "@app/core/stores/deviceStore.js";
|
||||||
import { Message } from "@components/PageComponents/Messages/Message.js";
|
import { Message } from "@components/PageComponents/Messages/Message.js";
|
||||||
import { MessageInput } from "@components/PageComponents/Messages/MessageInput.js";
|
import { MessageInput } from "@components/PageComponents/Messages/MessageInput.js";
|
||||||
import type { Types } from "@meshtastic/js";
|
import type { Types } from "@meshtastic/js";
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ export const DetectionSensor = (): JSX.Element => {
|
|||||||
label: "Minimum Broadcast Seconds",
|
label: "Minimum Broadcast Seconds",
|
||||||
description:
|
description:
|
||||||
"The interval in seconds of how often we can send a message to the mesh when a state change is detected",
|
"The interval in seconds of how often we can send a message to the mesh when a state change is detected",
|
||||||
properties: {
|
properties: {
|
||||||
suffix: "Seconds",
|
suffix: "Seconds",
|
||||||
},
|
},
|
||||||
disabledBy: [
|
disabledBy: [
|
||||||
{
|
{
|
||||||
fieldName: "enabled",
|
fieldName: "enabled",
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ export const MQTT = (): JSX.Element => {
|
|||||||
type: "toggle",
|
type: "toggle",
|
||||||
name: "encryptionEnabled",
|
name: "encryptionEnabled",
|
||||||
label: "Encryption Enabled",
|
label: "Encryption Enabled",
|
||||||
description: "Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data.",
|
description:
|
||||||
|
"Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data.",
|
||||||
disabledBy: [
|
disabledBy: [
|
||||||
{
|
{
|
||||||
fieldName: "enabled",
|
fieldName: "enabled",
|
||||||
@@ -157,9 +158,32 @@ export const MQTT = (): JSX.Element => {
|
|||||||
description:
|
description:
|
||||||
"Position shared will be accurate within this distance",
|
"Position shared will be accurate within this distance",
|
||||||
properties: {
|
properties: {
|
||||||
enumValue: config.display?.units == 0 ?
|
enumValue:
|
||||||
{ "Within 23 km":10, "Within 12 km":11, "Within 5.8 km":12, "Within 2.9 km":13, "Within 1.5 km":14, "Within 700 m":15, "Within 350 m":16, "Within 200 m":17, "Within 90 m":18, "Within 50 m":19 } :
|
config.display?.units === 0
|
||||||
{ "Within 15 miles":10, "Within 7.3 miles":11, "Within 3.6 miles":12, "Within 1.8 miles":13, "Within 0.9 miles":14, "Within 0.5 miles":15, "Within 0.2 miles":16, "Within 600 feet":17, "Within 300 feet":18, "Within 150 feet":19 }
|
? {
|
||||||
|
"Within 23 km": 10,
|
||||||
|
"Within 12 km": 11,
|
||||||
|
"Within 5.8 km": 12,
|
||||||
|
"Within 2.9 km": 13,
|
||||||
|
"Within 1.5 km": 14,
|
||||||
|
"Within 700 m": 15,
|
||||||
|
"Within 350 m": 16,
|
||||||
|
"Within 200 m": 17,
|
||||||
|
"Within 90 m": 18,
|
||||||
|
"Within 50 m": 19,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
"Within 15 miles": 10,
|
||||||
|
"Within 7.3 miles": 11,
|
||||||
|
"Within 3.6 miles": 12,
|
||||||
|
"Within 1.8 miles": 13,
|
||||||
|
"Within 0.9 miles": 14,
|
||||||
|
"Within 0.5 miles": 15,
|
||||||
|
"Within 0.2 miles": 16,
|
||||||
|
"Within 600 feet": 17,
|
||||||
|
"Within 300 feet": 18,
|
||||||
|
"Within 150 feet": 19,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
disabledBy: [
|
disabledBy: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const NeighborInfo = (): JSX.Element => {
|
|||||||
type: "number",
|
type: "number",
|
||||||
name: "updateInterval",
|
name: "updateInterval",
|
||||||
label: "Update Interval",
|
label: "Update Interval",
|
||||||
description:
|
description:
|
||||||
"Interval in seconds of how often we should try to send our Neighbor Info to the mesh",
|
"Interval in seconds of how often we should try to send our Neighbor Info to the mesh",
|
||||||
properties: {
|
properties: {
|
||||||
suffix: "Seconds",
|
suffix: "Seconds",
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ export const Paxcounter = (): JSX.Element => {
|
|||||||
type: "number",
|
type: "number",
|
||||||
name: "paxcounterUpdateInterval",
|
name: "paxcounterUpdateInterval",
|
||||||
label: "Update Interval (seconds)",
|
label: "Update Interval (seconds)",
|
||||||
description: "How long to wait between sending paxcounter packets",
|
description:
|
||||||
|
"How long to wait between sending paxcounter packets",
|
||||||
properties: {
|
properties: {
|
||||||
suffix: "Seconds",
|
suffix: "Seconds",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { cn } from "@app/core/utils/cn.js";
|
import { cn } from "@app/core/utils/cn.js";
|
||||||
import { AlignLeftIcon, LucideIcon } from "lucide-react";
|
import { AlignLeftIcon, type LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
export interface PageLayoutProps {
|
export interface PageLayoutProps {
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
+11
-10
@@ -4,16 +4,16 @@ import { Subtle } from "@components/UI/Typography/Subtle.js";
|
|||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.js";
|
||||||
import type { Page } from "@core/stores/deviceStore.js";
|
import type { Page } from "@core/stores/deviceStore.js";
|
||||||
import {
|
import {
|
||||||
|
BatteryMediumIcon,
|
||||||
|
CpuIcon,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
LayersIcon,
|
LayersIcon,
|
||||||
LucideIcon,
|
type LucideIcon,
|
||||||
MapIcon,
|
MapIcon,
|
||||||
MessageSquareIcon,
|
MessageSquareIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
ZapIcon,
|
ZapIcon,
|
||||||
BatteryMediumIcon,
|
|
||||||
CpuIcon
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
export interface SidebarProps {
|
export interface SidebarProps {
|
||||||
@@ -79,16 +79,18 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="px-8 pb-6">
|
<div className="px-8 pb-6">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<BatteryMediumIcon size={24} viewBox={'0 0 28 24'}/>
|
<BatteryMediumIcon size={24} viewBox={"0 0 28 24"} />
|
||||||
<Subtle>{myNode?.deviceMetrics?.batteryLevel ?? "UNK"}%</Subtle>
|
<Subtle>{myNode?.deviceMetrics?.batteryLevel ?? "UNK"}%</Subtle>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<ZapIcon size={24} viewBox={'0 0 36 24'}/>
|
<ZapIcon size={24} viewBox={"0 0 36 24"} />
|
||||||
<Subtle>{myNode?.deviceMetrics?.voltage.toPrecision(3) ?? "UNK"} volts</Subtle>
|
<Subtle>
|
||||||
|
{myNode?.deviceMetrics?.voltage.toPrecision(3) ?? "UNK"} volts
|
||||||
|
</Subtle>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<CpuIcon size={24} viewBox={'0 0 36 24'}/>
|
<CpuIcon size={24} viewBox={"0 0 36 24"} />
|
||||||
<Subtle>v{ myMetadata?.firmwareVersion ?? "UNK"}</Subtle>
|
<Subtle>v{myMetadata?.firmwareVersion ?? "UNK"}</Subtle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -109,4 +111,3 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,14 @@ export function Toaster() {
|
|||||||
{toasts.map(({ id, title, description, action, ...props }) => (
|
{toasts.map(({ id, title, description, action, ...props }) => (
|
||||||
<Toast key={id} {...props}>
|
<Toast key={id} {...props}>
|
||||||
<div className="grid gap-1">
|
<div className="grid gap-1">
|
||||||
{title && <ToastTitle className="dark:text-white">{title}</ToastTitle>}
|
{title && (
|
||||||
{description && <ToastDescription className="dark:text-white-400">{description}</ToastDescription>}
|
<ToastTitle className="dark:text-white">{title}</ToastTitle>
|
||||||
|
)}
|
||||||
|
{description && (
|
||||||
|
<ToastDescription className="dark:text-white-400">
|
||||||
|
{description}
|
||||||
|
</ToastDescription>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{action}
|
{action}
|
||||||
<ToastClose />
|
<ToastClose />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { VariantProps, cva } from "class-variance-authority";
|
import { type VariantProps, cva } from "class-variance-authority";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@core/utils/cn.js";
|
import { cn } from "@core/utils/cn.js";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as ToastPrimitives from "@radix-ui/react-toast";
|
import * as ToastPrimitives from "@radix-ui/react-toast";
|
||||||
import { VariantProps, cva } from "class-variance-authority";
|
import { type VariantProps, cva } from "class-variance-authority";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
|
|||||||
@@ -34,8 +34,12 @@ export const Table = ({ headings, rows }: TableProps): JSX.Element => {
|
|||||||
|
|
||||||
// Custom comparison for 'Last Heard' column
|
// Custom comparison for 'Last Heard' column
|
||||||
if (sortColumn === "Last Heard") {
|
if (sortColumn === "Last Heard") {
|
||||||
const aTimestamp = a[columnIndex].props.timestamp ? a[columnIndex].props.timestamp : 0;
|
const aTimestamp = a[columnIndex].props.timestamp
|
||||||
const bTimestamp = b[columnIndex].props.timestamp ? b[columnIndex].props.timestamp : 0;
|
? a[columnIndex].props.timestamp
|
||||||
|
: 0;
|
||||||
|
const bTimestamp = b[columnIndex].props.timestamp
|
||||||
|
? b[columnIndex].props.timestamp
|
||||||
|
: 0;
|
||||||
|
|
||||||
if (aTimestamp < bTimestamp) {
|
if (aTimestamp < bTimestamp) {
|
||||||
return sortOrder === "asc" ? -1 : 1;
|
return sortOrder === "asc" ? -1 : 1;
|
||||||
@@ -70,11 +74,18 @@ export const Table = ({ headings, rows }: TableProps): JSX.Element => {
|
|||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => heading.sortable && headingSort(heading.title)}
|
onClick={() => heading.sortable && headingSort(heading.title)}
|
||||||
|
onKeyUp={() => heading.sortable && headingSort(heading.title)}
|
||||||
>
|
>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{heading.title}
|
{heading.title}
|
||||||
{sortColumn === heading.title && (
|
{sortColumn === heading.title && (
|
||||||
<>{sortOrder === "asc" ? <ChevronUpIcon size={16} /> : <ChevronDownIcon size={16} />}</>
|
<>
|
||||||
|
{sortOrder === "asc" ? (
|
||||||
|
<ChevronUpIcon size={16} />
|
||||||
|
) : (
|
||||||
|
<ChevronDownIcon size={16} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
@@ -83,9 +94,11 @@ export const Table = ({ headings, rows }: TableProps): JSX.Element => {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{sortedRows.map((row, index) => (
|
{sortedRows.map((row, index) => (
|
||||||
|
// biome-ignore lint/suspicious/noArrayIndexKey: TODO: Once this table is sortable, this should get fixed.
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
{row.map((item, index) => (
|
{row.map((item, index) => (
|
||||||
<td
|
<td
|
||||||
|
// biome-ignore lint/suspicious/noArrayIndexKey: OK because column order never changes.
|
||||||
key={index}
|
key={index}
|
||||||
className="whitespace-nowrap py-2 text-sm text-textSecondary first:pl-2"
|
className="whitespace-nowrap py-2 text-sm text-textSecondary first:pl-2"
|
||||||
>
|
>
|
||||||
|
|||||||
+20
-16
@@ -1,4 +1,4 @@
|
|||||||
import { ReactNode, useEffect, useState } from "react";
|
import { type ReactNode, useSyncExternalStore } from "react";
|
||||||
|
|
||||||
import type { ToastActionElement, ToastProps } from "@components/UI/Toast.js";
|
import type { ToastActionElement, ToastProps } from "@components/UI/Toast.js";
|
||||||
|
|
||||||
@@ -92,9 +92,9 @@ export const reducer = (state: State, action: Action): State => {
|
|||||||
if (toastId) {
|
if (toastId) {
|
||||||
addToRemoveQueue(toastId);
|
addToRemoveQueue(toastId);
|
||||||
} else {
|
} else {
|
||||||
state.toasts.forEach((toast) => {
|
for (const toast of state.toasts) {
|
||||||
addToRemoveQueue(toast.id);
|
addToRemoveQueue(toast.id);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -130,9 +130,9 @@ let memoryState: State = { toasts: [] };
|
|||||||
|
|
||||||
function dispatch(action: Action) {
|
function dispatch(action: Action) {
|
||||||
memoryState = reducer(memoryState, action);
|
memoryState = reducer(memoryState, action);
|
||||||
listeners.forEach((listener) => {
|
for (const listener of listeners) {
|
||||||
listener(memoryState);
|
listener(memoryState);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Toast = Omit<ToasterToast, "id">;
|
type Toast = Omit<ToasterToast, "id">;
|
||||||
@@ -166,18 +166,22 @@ function toast({ ...props }: Toast) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function useToast() {
|
const subscribe = (listener: () => void) => {
|
||||||
const [state, setState] = useState<State>(memoryState);
|
listeners.push(listener);
|
||||||
|
return function unsubscribe() {
|
||||||
|
const index = listeners.indexOf(listener);
|
||||||
|
if (index > -1) {
|
||||||
|
listeners.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const getState = () => {
|
||||||
listeners.push(setState);
|
return memoryState;
|
||||||
return () => {
|
};
|
||||||
const index = listeners.indexOf(setState);
|
|
||||||
if (index > -1) {
|
function useToast() {
|
||||||
listeners.splice(index, 1);
|
const state = useSyncExternalStore(subscribe, getState);
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [state]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -50,7 +50,10 @@ export const useAppStore = create<AppState>()((set) => ({
|
|||||||
currentPage: "messages",
|
currentPage: "messages",
|
||||||
rasterSources: [],
|
rasterSources: [],
|
||||||
commandPaletteOpen: false,
|
commandPaletteOpen: false,
|
||||||
darkMode: (localStorage.getItem('theme-dark') !== null ? (localStorage.getItem('theme-dark') === 'true' ? true : false) : window.matchMedia("(prefers-color-scheme: dark)").matches),
|
darkMode:
|
||||||
|
localStorage.getItem("theme-dark") !== null
|
||||||
|
? localStorage.getItem("theme-dark") === "true"
|
||||||
|
: window.matchMedia("(prefers-color-scheme: dark)").matches,
|
||||||
accent: "orange",
|
accent: "orange",
|
||||||
connectDialogOpen: false,
|
connectDialogOpen: false,
|
||||||
nodeNumToBeRemoved: 0,
|
nodeNumToBeRemoved: 0,
|
||||||
@@ -96,7 +99,7 @@ export const useAppStore = create<AppState>()((set) => ({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
setDarkMode: (enabled: boolean) => {
|
setDarkMode: (enabled: boolean) => {
|
||||||
localStorage.setItem('theme-dark', enabled.toString());
|
localStorage.setItem("theme-dark", enabled.toString());
|
||||||
set(
|
set(
|
||||||
produce<AppState>((draft) => {
|
produce<AppState>((draft) => {
|
||||||
draft.darkMode = enabled;
|
draft.darkMode = enabled;
|
||||||
@@ -105,7 +108,7 @@ export const useAppStore = create<AppState>()((set) => ({
|
|||||||
},
|
},
|
||||||
setNodeNumToBeRemoved: (nodeNum) =>
|
setNodeNumToBeRemoved: (nodeNum) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
nodeNumToBeRemoved: nodeNum
|
nodeNumToBeRemoved: nodeNum,
|
||||||
})),
|
})),
|
||||||
setAccent(color) {
|
setAccent(color) {
|
||||||
set(
|
set(
|
||||||
|
|||||||
@@ -506,8 +506,8 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
device.nodes.delete(nodeNum);
|
device.nodes.delete(nodeNum);
|
||||||
})
|
}),
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
setMessageState: (
|
setMessageState: (
|
||||||
type: "direct" | "broadcast",
|
type: "direct" | "broadcast",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Device } from "@core/stores/deviceStore.js";
|
import type { Device } from "@core/stores/deviceStore.js";
|
||||||
import { Protobuf, Types } from "@meshtastic/js";
|
import { Protobuf, type Types } from "@meshtastic/js";
|
||||||
|
|
||||||
export const subscribeAll = (
|
export const subscribeAll = (
|
||||||
device: Device,
|
device: Device,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ClassValue, clsx } from "clsx";
|
import { type ClassValue, clsx } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
|||||||
@@ -53,10 +53,7 @@ export const DeviceConfig = (): JSX.Element => {
|
|||||||
<Tabs defaultValue="Device">
|
<Tabs defaultValue="Device">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<TabsTrigger
|
<TabsTrigger key={tab.label} value={tab.label}>
|
||||||
key={tab.label}
|
|
||||||
value={tab.label}
|
|
||||||
>
|
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { Audio } from "@components/PageComponents/ModuleConfig/Audio.js";
|
|||||||
import { CannedMessage } from "@components/PageComponents/ModuleConfig/CannedMessage.js";
|
import { CannedMessage } from "@components/PageComponents/ModuleConfig/CannedMessage.js";
|
||||||
import { ExternalNotification } from "@components/PageComponents/ModuleConfig/ExternalNotification.js";
|
import { ExternalNotification } from "@components/PageComponents/ModuleConfig/ExternalNotification.js";
|
||||||
import { MQTT } from "@components/PageComponents/ModuleConfig/MQTT.js";
|
import { MQTT } from "@components/PageComponents/ModuleConfig/MQTT.js";
|
||||||
|
import { Paxcounter } from "@components/PageComponents/ModuleConfig/Paxcounter.js";
|
||||||
import { RangeTest } from "@components/PageComponents/ModuleConfig/RangeTest.js";
|
import { RangeTest } from "@components/PageComponents/ModuleConfig/RangeTest.js";
|
||||||
import { Serial } from "@components/PageComponents/ModuleConfig/Serial.js";
|
import { Serial } from "@components/PageComponents/ModuleConfig/Serial.js";
|
||||||
import { StoreForward } from "@components/PageComponents/ModuleConfig/StoreForward.js";
|
import { StoreForward } from "@components/PageComponents/ModuleConfig/StoreForward.js";
|
||||||
import { Telemetry } from "@components/PageComponents/ModuleConfig/Telemetry.js";
|
import { Telemetry } from "@components/PageComponents/ModuleConfig/Telemetry.js";
|
||||||
import { Paxcounter } from "@components/PageComponents/ModuleConfig/Paxcounter.js";
|
|
||||||
import {
|
import {
|
||||||
Tabs,
|
Tabs,
|
||||||
TabsContent,
|
TabsContent,
|
||||||
|
|||||||
+19
-17
@@ -14,7 +14,7 @@ import {
|
|||||||
ZoomInIcon,
|
ZoomInIcon,
|
||||||
ZoomOutIcon,
|
ZoomOutIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { Marker, useMap } from "react-map-gl";
|
import { Marker, useMap } from "react-map-gl";
|
||||||
import MapGl from "react-map-gl/maplibre";
|
import MapGl from "react-map-gl/maplibre";
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ export const MapPage = (): JSX.Element => {
|
|||||||
|
|
||||||
const allNodes = Array.from(nodes.values());
|
const allNodes = Array.from(nodes.values());
|
||||||
|
|
||||||
const getBBox = () => {
|
const getBBox = useCallback(() => {
|
||||||
if (!map) {
|
if (!map) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ export const MapPage = (): JSX.Element => {
|
|||||||
if (center) {
|
if (center) {
|
||||||
map.easeTo(center);
|
map.easeTo(center);
|
||||||
}
|
}
|
||||||
};
|
}, [allNodes, map]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
map?.on("zoom", () => {
|
map?.on("zoom", () => {
|
||||||
@@ -128,7 +128,11 @@ export const MapPage = (): JSX.Element => {
|
|||||||
attributionControl={false}
|
attributionControl={false}
|
||||||
renderWorldCopies={false}
|
renderWorldCopies={false}
|
||||||
maxPitch={0}
|
maxPitch={0}
|
||||||
style = {{filter: darkMode ? 'brightness(0.6) invert(1) contrast(3) hue-rotate(200deg) saturate(0.3) brightness(0.7)' : ''}}
|
style={{
|
||||||
|
filter: darkMode
|
||||||
|
? "brightness(0.6) invert(1) contrast(3) hue-rotate(200deg) saturate(0.3) brightness(0.7)"
|
||||||
|
: "",
|
||||||
|
}}
|
||||||
dragRotate={false}
|
dragRotate={false}
|
||||||
touchZoomRotate={false}
|
touchZoomRotate={false}
|
||||||
initialViewState={{
|
initialViewState={{
|
||||||
@@ -161,21 +165,19 @@ export const MapPage = (): JSX.Element => {
|
|||||||
key={node.num}
|
key={node.num}
|
||||||
longitude={node.position.longitudeI / 1e7}
|
longitude={node.position.longitudeI / 1e7}
|
||||||
latitude={node.position.latitudeI / 1e7}
|
latitude={node.position.latitudeI / 1e7}
|
||||||
style = {{filter: darkMode ? 'invert(1)' : ''}}
|
style={{ filter: darkMode ? "invert(1)" : "" }}
|
||||||
anchor="bottom"
|
anchor="bottom"
|
||||||
|
onClick={() => {
|
||||||
|
map?.easeTo({
|
||||||
|
zoom: 12,
|
||||||
|
center: [
|
||||||
|
(node.position?.longitudeI ?? 0) / 1e7,
|
||||||
|
(node.position?.latitudeI ?? 0) / 1e7,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div className="flex cursor-pointer gap-2 rounded-md border bg-backgroundPrimary p-1.5">
|
||||||
className="flex cursor-pointer gap-2 rounded-md border bg-backgroundPrimary p-1.5"
|
|
||||||
onClick={() => {
|
|
||||||
map?.easeTo({
|
|
||||||
zoom: 12,
|
|
||||||
center: [
|
|
||||||
(node.position?.longitudeI ?? 0) / 1e7,
|
|
||||||
(node.position?.latitudeI ?? 0) / 1e7,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Hashicon value={node.num.toString()} size={22} />
|
<Hashicon value={node.num.toString()} size={22} />
|
||||||
<Subtle className={cn(zoom < 12 && "hidden")}>
|
<Subtle className={cn(zoom < 12 && "hidden")}>
|
||||||
{node.user?.longName}
|
{node.user?.longName}
|
||||||
|
|||||||
+36
-24
@@ -1,15 +1,15 @@
|
|||||||
|
import { useAppStore } from "@app/core/stores/appStore";
|
||||||
import { Sidebar } from "@components/Sidebar.js";
|
import { Sidebar } from "@components/Sidebar.js";
|
||||||
|
import { Button } from "@components/UI/Button.js";
|
||||||
import { Mono } from "@components/generic/Mono.js";
|
import { Mono } from "@components/generic/Mono.js";
|
||||||
import { Table } from "@components/generic/Table/index.js";
|
import { Table } from "@components/generic/Table/index.js";
|
||||||
import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.js";
|
import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.js";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.js";
|
||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
import { base16 } from "rfc4648";
|
|
||||||
import { Button } from "@components/UI/Button.js";
|
|
||||||
import { TrashIcon } from "lucide-react";
|
import { TrashIcon } from "lucide-react";
|
||||||
import { useAppStore } from "@app/core/stores/appStore";
|
import { Fragment } from "react";
|
||||||
|
import { base16 } from "rfc4648";
|
||||||
|
|
||||||
export interface DeleteNoteDialogProps {
|
export interface DeleteNoteDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -40,8 +40,8 @@ export const NodesPage = (): JSX.Element => {
|
|||||||
{ title: "Remove", type: "normal", sortable: false },
|
{ title: "Remove", type: "normal", sortable: false },
|
||||||
]}
|
]}
|
||||||
rows={filteredNodes.map((node) => [
|
rows={filteredNodes.map((node) => [
|
||||||
<Hashicon size={24} value={node.num.toString()} />,
|
<Hashicon key="icon" size={24} value={node.num.toString()} />,
|
||||||
<h1>
|
<h1 key="header">
|
||||||
{node.user?.longName ??
|
{node.user?.longName ??
|
||||||
(node.user?.macaddr
|
(node.user?.macaddr
|
||||||
? `Meshtastic ${base16
|
? `Meshtastic ${base16
|
||||||
@@ -50,34 +50,46 @@ export const NodesPage = (): JSX.Element => {
|
|||||||
: `UNK: ${node.num}`)}
|
: `UNK: ${node.num}`)}
|
||||||
</h1>,
|
</h1>,
|
||||||
|
|
||||||
<Mono>{Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]}</Mono>,
|
<Mono key="model">
|
||||||
<Mono>
|
{Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]}
|
||||||
|
</Mono>,
|
||||||
|
<Mono key="addr">
|
||||||
{base16
|
{base16
|
||||||
.stringify(node.user?.macaddr ?? [])
|
.stringify(node.user?.macaddr ?? [])
|
||||||
.match(/.{1,2}/g)
|
.match(/.{1,2}/g)
|
||||||
?.join(":") ?? "UNK"}
|
?.join(":") ?? "UNK"}
|
||||||
</Mono>,
|
</Mono>,
|
||||||
node.lastHeard === 0 ? (
|
<Fragment key="lastHeard">
|
||||||
<p>Never</p>
|
{node.lastHeard === 0 ? (
|
||||||
) : (
|
<p>Never</p>
|
||||||
<TimeAgo timestamp={node.lastHeard * 1000} />
|
) : (
|
||||||
),
|
<TimeAgo timestamp={node.lastHeard * 1000} />
|
||||||
<Mono>
|
)}
|
||||||
|
</Fragment>,
|
||||||
|
<Mono key="snr">
|
||||||
{node.snr}db/
|
{node.snr}db/
|
||||||
{Math.min(Math.max((node.snr + 10) * 5, 0), 100)}%/
|
{Math.min(Math.max((node.snr + 10) * 5, 0), 100)}%/
|
||||||
{(node.snr + 10) * 5}raw
|
{(node.snr + 10) * 5}raw
|
||||||
</Mono>,
|
</Mono>,
|
||||||
<Mono>
|
<Mono key="hops">
|
||||||
{node.lastHeard != 0 ?
|
{node.lastHeard !== 0
|
||||||
(node.viaMqtt === false && node.hopsAway === 0
|
? node.viaMqtt === false && node.hopsAway === 0
|
||||||
? "Direct": node.hopsAway.toString() + " hops away")
|
? "Direct"
|
||||||
|
: `${node.hopsAway.toString()} hops away`
|
||||||
: "-"}
|
: "-"}
|
||||||
{node.viaMqtt === true? ", via MQTT": ""}
|
{node.viaMqtt === true ? ", via MQTT" : ""}
|
||||||
</Mono>,
|
</Mono>,
|
||||||
<Button variant="destructive" onClick={() => {
|
<Button
|
||||||
setNodeNumToBeRemoved(node.num);
|
key="remove"
|
||||||
setDialogOpen("nodeRemoval", true)
|
variant="destructive"
|
||||||
}}><TrashIcon />Remove</Button>
|
onClick={() => {
|
||||||
|
setNodeNumToBeRemoved(node.num);
|
||||||
|
setDialogOpen("nodeRemoval", true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
Remove
|
||||||
|
</Button>,
|
||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ import type { Message } from "@bufbuild/protobuf";
|
|||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
import { IsArray, IsBoolean, IsEnum, IsInt } from "class-validator";
|
import { IsArray, IsBoolean, IsEnum, IsInt } from "class-validator";
|
||||||
|
|
||||||
const DeprecatedPositionValidationFields = ['gpsEnabled', 'gpsAttemptTime'];
|
const DeprecatedPositionValidationFields = ["gpsEnabled", "gpsAttemptTime"];
|
||||||
|
|
||||||
export class PositionValidation
|
export class PositionValidation
|
||||||
implements Omit<Protobuf.Config.Config_PositionConfig, keyof Message | typeof DeprecatedPositionValidationFields[number]>
|
implements
|
||||||
|
Omit<
|
||||||
|
Protobuf.Config.Config_PositionConfig,
|
||||||
|
keyof Message | (typeof DeprecatedPositionValidationFields)[number]
|
||||||
|
>
|
||||||
{
|
{
|
||||||
@IsInt()
|
@IsInt()
|
||||||
positionBroadcastSecs: number;
|
positionBroadcastSecs: number;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Message } from "@bufbuild/protobuf";
|
import type { Message } from "@bufbuild/protobuf";
|
||||||
import type { Protobuf } from "@meshtastic/js";
|
import type { Protobuf } from "@meshtastic/js";
|
||||||
import { IsBoolean, IsString, Length, IsNumber } from "class-validator";
|
import { IsBoolean, IsNumber, IsString, Length } from "class-validator";
|
||||||
|
|
||||||
export class MqttValidation
|
export class MqttValidation
|
||||||
implements
|
implements
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import { execSync } from "child_process";
|
import { execSync } from "node:child_process";
|
||||||
import { resolve } from "path";
|
import { resolve } from "node:path";
|
||||||
import { visualizer } from "rollup-plugin-visualizer";
|
import { visualizer } from "rollup-plugin-visualizer";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import EnvironmentPlugin from "vite-plugin-environment";
|
import EnvironmentPlugin from "vite-plugin-environment";
|
||||||
|
|||||||
Reference in New Issue
Block a user