Consolidate forms & other minor fixes
This commit is contained in:
+7
-8
@@ -4,7 +4,7 @@
|
|||||||
"description": "Meshtastic web client",
|
"description": "Meshtastic web client",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite --host",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"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/)",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emeraldpay/hashicon-react": "^0.5.2",
|
"@emeraldpay/hashicon-react": "^0.5.2",
|
||||||
"@meshtastic/eslint-config": "^1.0.6",
|
"@meshtastic/eslint-config": "^1.0.6",
|
||||||
"@meshtastic/meshtasticjs": "^0.6.48",
|
"@meshtastic/meshtasticjs": "^0.6.50",
|
||||||
"@reduxjs/toolkit": "^1.8.0",
|
"@reduxjs/toolkit": "^1.8.0",
|
||||||
"@tippyjs/react": "^4.2.6",
|
"@tippyjs/react": "^4.2.6",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
@@ -31,9 +31,8 @@
|
|||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-draggable": "^4.4.4",
|
|
||||||
"react-error-boundary": "^3.1.4",
|
"react-error-boundary": "^3.1.4",
|
||||||
"react-flow-renderer": "^10.0.0-next.45",
|
"react-flow-renderer": "^10.0.0-next.48",
|
||||||
"react-hook-form": "^7.27.1",
|
"react-hook-form": "^7.27.1",
|
||||||
"react-icons": "^4.3.1",
|
"react-icons": "^4.3.1",
|
||||||
"react-json-pretty": "^2.2.0",
|
"react-json-pretty": "^2.2.0",
|
||||||
@@ -49,9 +48,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hookform/devtools": "^4.0.2",
|
"@hookform/devtools": "^4.0.2",
|
||||||
"@types/mapbox-gl": "^2.6.2",
|
"@types/mapbox-gl": "^2.6.3",
|
||||||
"@types/react": "^17.0.39",
|
"@types/react": "^17.0.39",
|
||||||
"@types/react-dom": "^17.0.11",
|
"@types/react-dom": "^17.0.13",
|
||||||
"@types/w3c-web-serial": "^1.0.2",
|
"@types/w3c-web-serial": "^1.0.2",
|
||||||
"@types/web-bluetooth": "^0.0.12",
|
"@types/web-bluetooth": "^0.0.12",
|
||||||
"@vitejs/plugin-react": "^1.2.0",
|
"@vitejs/plugin-react": "^1.2.0",
|
||||||
@@ -63,9 +62,9 @@
|
|||||||
"tar": "^6.1.11",
|
"tar": "^6.1.11",
|
||||||
"typescript": "^4.6.2",
|
"typescript": "^4.6.2",
|
||||||
"unimported": "^1.19.1",
|
"unimported": "^1.19.1",
|
||||||
"vite": "^2.8.5",
|
"vite": "^2.8.6",
|
||||||
"vite-plugin-cdn-import": "^0.3.5",
|
"vite-plugin-cdn-import": "^0.3.5",
|
||||||
"vite-plugin-pwa": "^0.11.13",
|
"vite-plugin-pwa": "^0.11.13",
|
||||||
"workbox-window": "^6.5.0"
|
"workbox-window": "^6.5.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+241
-233
File diff suppressed because it is too large
Load Diff
+11
-14
@@ -1,12 +1,11 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
|
||||||
import { Map } from '@app/pages/Map';
|
|
||||||
import { Connection } from '@components/Connection';
|
import { Connection } from '@components/Connection';
|
||||||
import { ContextMenu } from '@components/generic/ContextMenu';
|
|
||||||
import { BottomNav } from '@components/menu/BottomNav';
|
import { BottomNav } from '@components/menu/BottomNav';
|
||||||
import { useRoute } from '@core/router';
|
import { useRoute } from '@core/router';
|
||||||
import { useAppSelector } from '@hooks/useAppSelector';
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
import { Extensions } from '@pages/Extensions/Index';
|
import { Extensions } from '@pages/Extensions/Index';
|
||||||
|
import { Map } from '@pages/Map';
|
||||||
import { Messages } from '@pages/Messages';
|
import { Messages } from '@pages/Messages';
|
||||||
import { Nodes } from '@pages/Nodes';
|
import { Nodes } from '@pages/Nodes';
|
||||||
import { NotFound } from '@pages/NotFound';
|
import { NotFound } from '@pages/NotFound';
|
||||||
@@ -17,19 +16,17 @@ export const App = (): JSX.Element => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`h-screen w-screen ${appState.darkMode ? 'dark' : ''}`}>
|
<div className={`h-screen w-screen ${appState.darkMode ? 'dark' : ''}`}>
|
||||||
<ContextMenu>
|
<Connection />
|
||||||
<Connection />
|
<div className="flex h-full flex-col">
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex min-h-0 w-full flex-grow">
|
||||||
<div className="flex min-h-0 w-full flex-grow">
|
{route.name === 'messages' && <Messages />}
|
||||||
{route.name === 'messages' && <Messages />}
|
{route.name === 'nodes' && <Nodes />}
|
||||||
{route.name === 'nodes' && <Nodes />}
|
{route.name === 'map' && <Map />}
|
||||||
{route.name === 'map' && <Map />}
|
{route.name === 'extensions' && <Extensions />}
|
||||||
{route.name === 'extensions' && <Extensions />}
|
{route.name === false && <NotFound />}
|
||||||
{route.name === false && <NotFound />}
|
|
||||||
</div>
|
|
||||||
<BottomNav />
|
|
||||||
</div>
|
</div>
|
||||||
</ContextMenu>
|
<BottomNav />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
|
||||||
import { Tab, TabProps } from './Tab';
|
import { Tab, TabProps } from '@components/Tab';
|
||||||
|
|
||||||
export interface TabsProps {
|
export interface TabsProps {
|
||||||
tabs: Omit<TabProps, 'activeLeft' | 'activeRight'>[];
|
tabs: Omit<TabProps, 'activeLeft' | 'activeRight'>[];
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
|
||||||
import { m } from 'framer-motion';
|
import { m } from 'framer-motion';
|
||||||
import Draggable from 'react-draggable';
|
|
||||||
|
|
||||||
export interface CardProps {
|
export interface CardProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -9,49 +8,41 @@ export interface CardProps {
|
|||||||
actions?: React.ReactNode;
|
actions?: React.ReactNode;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
border?: boolean;
|
border?: boolean;
|
||||||
draggable?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Card = ({
|
export const Card = ({
|
||||||
className,
|
className,
|
||||||
title,
|
title,
|
||||||
actions,
|
actions,
|
||||||
draggable,
|
|
||||||
border,
|
border,
|
||||||
children,
|
children,
|
||||||
}: CardProps): JSX.Element => {
|
}: CardProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Draggable handle=".handle" disabled={!draggable}>
|
<div
|
||||||
<div
|
className={`flex h-full w-full flex-col rounded-md drop-shadow-md ${
|
||||||
className={`flex h-full w-full flex-col rounded-md drop-shadow-md ${
|
border ? 'border border-gray-400 dark:border-gray-600' : ''
|
||||||
border ? 'border border-gray-400 dark:border-gray-600' : ''
|
} ${className ?? ''}`}
|
||||||
} ${className ?? ''}`}
|
>
|
||||||
>
|
{(title || actions) && (
|
||||||
{(title || actions) && (
|
<div className="w-full select-none justify-between rounded-t-md border-b border-gray-400 bg-gray-200 p-2 px-2 text-lg font-medium dark:border-gray-600 dark:bg-tertiaryDark dark:text-white">
|
||||||
<div
|
<div className="handle flex h-8 justify-between">
|
||||||
className={`w-full select-none justify-between rounded-t-md border-b border-gray-400 bg-gray-200 p-2 px-2 text-lg font-medium dark:border-gray-600 dark:bg-tertiaryDark dark:text-white ${
|
<div className="my-auto ml-2 truncate">{title}</div>
|
||||||
draggable ? 'cursor-move' : ''
|
{actions}
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="handle flex h-8 justify-between">
|
|
||||||
<div className="my-auto ml-2 truncate">{title}</div>
|
|
||||||
{actions}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<m.div
|
<m.div
|
||||||
className={`flex flex-grow select-none flex-col gap-4 bg-white p-4 dark:bg-primaryDark ${
|
className={`flex flex-grow select-none flex-col gap-4 bg-white p-4 dark:bg-primaryDark ${
|
||||||
title || actions ? 'rounded-b-md' : 'rounded-md'
|
title || actions ? 'rounded-b-md' : 'rounded-md'
|
||||||
}`}
|
}`}
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.1 }}
|
transition={{ duration: 0.1 }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</m.div>
|
</m.div>
|
||||||
</div>
|
</div>
|
||||||
</Draggable>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ import type React from 'react';
|
|||||||
import { AnimatePresence, m } from 'framer-motion';
|
import { AnimatePresence, m } from 'framer-motion';
|
||||||
import { FiX } from 'react-icons/fi';
|
import { FiX } from 'react-icons/fi';
|
||||||
|
|
||||||
|
import { IconButton } from '@components/generic/button/IconButton';
|
||||||
|
import { Card, CardProps } from '@components/generic/Card';
|
||||||
import { useAppSelector } from '@hooks/useAppSelector';
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
|
|
||||||
import { IconButton } from './button/IconButton';
|
|
||||||
import { Card, CardProps } from './Card';
|
|
||||||
|
|
||||||
export interface ModalProps extends CardProps {
|
export interface ModalProps extends CardProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
bgDismiss?: boolean;
|
bgDismiss?: boolean;
|
||||||
@@ -51,7 +50,6 @@ export const Modal = ({
|
|||||||
<div className="inline-block w-full max-w-3xl align-middle">
|
<div className="inline-block w-full max-w-3xl align-middle">
|
||||||
<Card
|
<Card
|
||||||
border
|
border
|
||||||
draggable
|
|
||||||
actions={
|
actions={
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{actions}
|
{actions}
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import { FiArrowUp } from 'react-icons/fi';
|
|||||||
export interface CollapsibleSectionProps {
|
export interface CollapsibleSectionProps {
|
||||||
title: string;
|
title: string;
|
||||||
icon?: JSX.Element;
|
icon?: JSX.Element;
|
||||||
|
status?: boolean;
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CollapsibleSection = ({
|
export const CollapsibleSection = ({
|
||||||
title,
|
title,
|
||||||
icon,
|
icon,
|
||||||
|
status,
|
||||||
children,
|
children,
|
||||||
}: CollapsibleSectionProps): JSX.Element => {
|
}: CollapsibleSectionProps): JSX.Element => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -21,6 +23,7 @@ export const CollapsibleSection = ({
|
|||||||
<m.div>
|
<m.div>
|
||||||
<m.div
|
<m.div
|
||||||
layout
|
layout
|
||||||
|
onClick={toggleOpen}
|
||||||
className={`w-full cursor-pointer select-none overflow-hidden border-l-4 border-b bg-gray-200 p-2 text-sm font-medium dark:border-primaryDark dark:bg-tertiaryDark dark:text-gray-400 ${
|
className={`w-full cursor-pointer select-none overflow-hidden border-l-4 border-b bg-gray-200 p-2 text-sm font-medium dark:border-primaryDark dark:bg-tertiaryDark dark:text-gray-400 ${
|
||||||
open
|
open
|
||||||
? 'border-l-primary dark:border-l-primary'
|
? 'border-l-primary dark:border-l-primary'
|
||||||
@@ -29,13 +32,25 @@ export const CollapsibleSection = ({
|
|||||||
>
|
>
|
||||||
<m.div
|
<m.div
|
||||||
layout
|
layout
|
||||||
onClick={toggleOpen}
|
|
||||||
whileHover={{ scale: 1.01 }}
|
whileHover={{ scale: 1.01 }}
|
||||||
whileTap={{ scale: 0.99 }}
|
whileTap={{ scale: 0.99 }}
|
||||||
className="flex justify-between gap-2 "
|
className="my-auto flex justify-between gap-2"
|
||||||
>
|
>
|
||||||
<m.div className="flex gap-2 ">
|
<m.div className="flex gap-2">
|
||||||
<m.div className="my-auto">{icon}</m.div>
|
<m.div className="my-auto flex gap-2">
|
||||||
|
{status !== undefined ? (
|
||||||
|
<>
|
||||||
|
{icon}
|
||||||
|
<div
|
||||||
|
className={`h-3 w-3 rounded-full ${
|
||||||
|
status ? 'bg-green-500' : 'bg-red-500'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>{icon}</>
|
||||||
|
)}
|
||||||
|
</m.div>
|
||||||
{title}
|
{title}
|
||||||
</m.div>
|
</m.div>
|
||||||
<m.div
|
<m.div
|
||||||
|
|||||||
@@ -1,22 +1,36 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
|
||||||
|
import { FiSave } from 'react-icons/fi';
|
||||||
|
|
||||||
|
import { IconButton } from '@components/generic/button/IconButton';
|
||||||
import { Loading } from '@components/generic/Loading';
|
import { Loading } from '@components/generic/Loading';
|
||||||
|
|
||||||
export interface FormProps {
|
export interface FormProps {
|
||||||
loading?: boolean;
|
submit: () => Promise<void>;
|
||||||
|
loading: boolean;
|
||||||
|
dirty: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Form = ({ loading, children }: FormProps): JSX.Element => {
|
export const Form = ({
|
||||||
|
submit,
|
||||||
|
loading,
|
||||||
|
dirty,
|
||||||
|
children,
|
||||||
|
}: FormProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={(e): void => {
|
onSubmit={(e): void => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}
|
}}
|
||||||
className="relative flex-grow gap-3 p-2"
|
|
||||||
>
|
>
|
||||||
{loading && <Loading />}
|
{loading && <Loading />}
|
||||||
{children}
|
{children}
|
||||||
|
<div className="flex w-full bg-white dark:bg-secondaryDark">
|
||||||
|
<div className="ml-auto p-2">
|
||||||
|
<IconButton disabled={dirty} onClick={submit} icon={<FiSave />} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ import type React from 'react';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { FiSave } from 'react-icons/fi';
|
|
||||||
|
|
||||||
import { IconButton } from '@components/generic/button/IconButton';
|
|
||||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||||
|
import { Form } from '@components/generic/form/Form';
|
||||||
import { Input } from '@components/generic/form/Input';
|
import { Input } from '@components/generic/form/Input';
|
||||||
import { Select } from '@components/generic/form/Select';
|
import { Select } from '@components/generic/form/Select';
|
||||||
import { connection } from '@core/connection';
|
import { connection } from '@core/connection';
|
||||||
@@ -59,7 +58,7 @@ export const Channels = (): JSX.Element => {
|
|||||||
label="Use Presets"
|
label="Use Presets"
|
||||||
onChange={(e): void => setUsePreset(e.target.checked)}
|
onChange={(e): void => setUsePreset(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<form onSubmit={onSubmit}>
|
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||||
{usePreset ? (
|
{usePreset ? (
|
||||||
<Select
|
<Select
|
||||||
label="Preset"
|
label="Preset"
|
||||||
@@ -103,18 +102,7 @@ export const Channels = (): JSX.Element => {
|
|||||||
suffix="dBm"
|
suffix="dBm"
|
||||||
{...register('settings.txPower', { valueAsNumber: true })}
|
{...register('settings.txPower', { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
</form>
|
</Form>
|
||||||
<div className="flex w-full bg-white dark:bg-secondaryDark">
|
|
||||||
<div className="ml-auto p-2">
|
|
||||||
<IconButton
|
|
||||||
disabled={!formState.isDirty}
|
|
||||||
onClick={async (): Promise<void> => {
|
|
||||||
await onSubmit();
|
|
||||||
}}
|
|
||||||
icon={<FiSave />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
+15
-26
@@ -2,16 +2,15 @@ import type React from 'react';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { FiSave } from 'react-icons/fi';
|
|
||||||
|
|
||||||
import { IconButton } from '@components/generic/button/IconButton';
|
|
||||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||||
|
import { Form } from '@components/generic/form/Form';
|
||||||
import { Select } from '@components/generic/form/Select';
|
import { Select } from '@components/generic/form/Select';
|
||||||
import { connection } from '@core/connection';
|
import { connection } from '@core/connection';
|
||||||
import { useAppSelector } from '@hooks/useAppSelector';
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
export const Radio = (): JSX.Element => {
|
export const Device = (): JSX.Element => {
|
||||||
const preferences = useAppSelector(
|
const preferences = useAppSelector(
|
||||||
(state) => state.meshtastic.radio.preferences,
|
(state) => state.meshtastic.radio.preferences,
|
||||||
);
|
);
|
||||||
@@ -34,28 +33,18 @@ export const Radio = (): JSX.Element => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||||
<form className="space-y-2" onSubmit={onSubmit}>
|
<Checkbox
|
||||||
<Checkbox label="Is Router" {...register('isRouter')} />
|
label="Serial Console Disabled"
|
||||||
<Select
|
{...register('serialDisabled')}
|
||||||
label="Region"
|
/>
|
||||||
optionsEnum={Protobuf.RegionCode}
|
<Checkbox label="Factory Reset Device" {...register('factoryReset')} />
|
||||||
{...register('region', { valueAsNumber: true })}
|
<Checkbox label="Debug Log Enabled" {...register('debugLogEnabled')} />
|
||||||
/>
|
<Select
|
||||||
<Checkbox label="Debug Log" {...register('debugLogEnabled')} />
|
label="Role"
|
||||||
<Checkbox label="Serial Disabled" {...register('serialDisabled')} />
|
optionsEnum={Protobuf.Role}
|
||||||
</form>
|
{...register('role', { valueAsNumber: true })}
|
||||||
<div className="flex w-full bg-white dark:bg-secondaryDark">
|
/>
|
||||||
<div className="ml-auto p-2">
|
</Form>
|
||||||
<IconButton
|
|
||||||
disabled={!formState.isDirty}
|
|
||||||
onClick={async (): Promise<void> => {
|
|
||||||
await onSubmit();
|
|
||||||
}}
|
|
||||||
icon={<FiSave />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { Form } from '@components/generic/form/Form';
|
||||||
|
import { Input } from '@components/generic/form/Input';
|
||||||
|
import { Select } from '@components/generic/form/Select';
|
||||||
|
import { connection } from '@core/connection';
|
||||||
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
|
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
export const Display = (): JSX.Element => {
|
||||||
|
const preferences = useAppSelector(
|
||||||
|
(state) => state.meshtastic.radio.preferences,
|
||||||
|
);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { register, handleSubmit, formState, reset } =
|
||||||
|
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||||
|
defaultValues: preferences,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset(preferences);
|
||||||
|
}, [reset, preferences]);
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit((data) => {
|
||||||
|
setLoading(true);
|
||||||
|
void connection.setPreferences(data, async () => {
|
||||||
|
reset({ ...data });
|
||||||
|
setLoading(false);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||||
|
<Input
|
||||||
|
label="Screen Timeout"
|
||||||
|
type="number"
|
||||||
|
suffix="Seconds"
|
||||||
|
{...register('screenOnSecs', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Carousel Delay"
|
||||||
|
type="number"
|
||||||
|
suffix="Seconds"
|
||||||
|
{...register('autoScreenCarouselSecs', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="GPS Display Units"
|
||||||
|
optionsEnum={Protobuf.GpsCoordinateFormat}
|
||||||
|
{...register('gpsFormat', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { MultiSelect } from 'react-multi-select-component';
|
||||||
|
|
||||||
|
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||||
|
import { Form } from '@components/generic/form/Form';
|
||||||
|
import { Input } from '@components/generic/form/Input';
|
||||||
|
import { Label } from '@components/generic/form/Label';
|
||||||
|
import { Select } from '@components/generic/form/Select';
|
||||||
|
import { connection } from '@core/connection';
|
||||||
|
import { bitwiseDecode, bitwiseEncode } from '@core/utils/bitwise';
|
||||||
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
|
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
export const GPS = (): JSX.Element => {
|
||||||
|
const preferences = useAppSelector(
|
||||||
|
(state) => state.meshtastic.radio.preferences,
|
||||||
|
);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { register, handleSubmit, formState, reset, control } =
|
||||||
|
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||||
|
defaultValues: {
|
||||||
|
...preferences,
|
||||||
|
positionBroadcastSecs:
|
||||||
|
preferences.positionBroadcastSecs === 0
|
||||||
|
? preferences.isRouter
|
||||||
|
? 43200
|
||||||
|
: 900
|
||||||
|
: preferences.positionBroadcastSecs,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset(preferences);
|
||||||
|
}, [reset, preferences]);
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit((data) => {
|
||||||
|
setLoading(true);
|
||||||
|
void connection.setPreferences(data, async () => {
|
||||||
|
reset({ ...data });
|
||||||
|
setLoading(false);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||||
|
<Input
|
||||||
|
label="Broadcast Interval"
|
||||||
|
type="number"
|
||||||
|
suffix="Seconds"
|
||||||
|
{...register('positionBroadcastSecs', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Use Smart Position"
|
||||||
|
{...register('positionBroadcastSmart')}
|
||||||
|
/>
|
||||||
|
<Checkbox label="Use Fixed Position" {...register('fixedPosition')} />
|
||||||
|
<Select
|
||||||
|
label="Location Sharing"
|
||||||
|
optionsEnum={Protobuf.LocationSharing}
|
||||||
|
{...register('locationShare', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="GPS Mode"
|
||||||
|
optionsEnum={Protobuf.GpsOperation}
|
||||||
|
{...register('gpsOperation', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="GPS Update Interval"
|
||||||
|
type="number"
|
||||||
|
suffix="Seconds"
|
||||||
|
{...register('gpsUpdateInterval', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Last GPS Attempt"
|
||||||
|
disabled
|
||||||
|
{...register('gpsAttemptTime', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Checkbox label="Accept 2D Fix" {...register('gpsAccept2D')} />
|
||||||
|
<Input
|
||||||
|
label="Max DOP"
|
||||||
|
type="number"
|
||||||
|
{...register('gpsMaxDop', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="positionFlags"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }): JSX.Element => {
|
||||||
|
const { value, onChange, ...rest } = field;
|
||||||
|
const { error } = fieldState;
|
||||||
|
const label = 'Position Flags';
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
{label && <Label label={label} error={error?.message} />}
|
||||||
|
<MultiSelect
|
||||||
|
options={Object.entries(Protobuf.PositionFlags)
|
||||||
|
.filter((value) => typeof value[1] !== 'number')
|
||||||
|
.filter(
|
||||||
|
(value) =>
|
||||||
|
parseInt(value[0]) !==
|
||||||
|
Protobuf.PositionFlags.POS_UNDEFINED,
|
||||||
|
)
|
||||||
|
.map((value) => {
|
||||||
|
return {
|
||||||
|
value: parseInt(value[0]),
|
||||||
|
label: value[1].toString().replace('POS_', ''),
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
value={bitwiseDecode(value, Protobuf.PositionFlags).map(
|
||||||
|
(flag) => {
|
||||||
|
return {
|
||||||
|
value: flag,
|
||||||
|
label: Protobuf.PositionFlags[flag].replace('POS_', ''),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
onChange={(e: { value: number; label: string }[]): void =>
|
||||||
|
onChange(bitwiseEncode(e.map((v) => v.value)))
|
||||||
|
}
|
||||||
|
labelledBy="Select"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,36 +2,43 @@ import type React from 'react';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
FiActivity,
|
||||||
FiAlignLeft,
|
FiAlignLeft,
|
||||||
FiBell,
|
FiBell,
|
||||||
FiFastForward,
|
FiFastForward,
|
||||||
FiLayers,
|
FiLayers,
|
||||||
FiLayout,
|
FiLayout,
|
||||||
FiMapPin,
|
FiMapPin,
|
||||||
|
FiMessageSquare,
|
||||||
FiPackage,
|
FiPackage,
|
||||||
FiRadio,
|
FiPower,
|
||||||
FiRss,
|
FiRss,
|
||||||
|
FiSmartphone,
|
||||||
|
FiTv,
|
||||||
FiUser,
|
FiUser,
|
||||||
FiWifi,
|
FiWifi,
|
||||||
FiZap,
|
|
||||||
} from 'react-icons/fi';
|
} from 'react-icons/fi';
|
||||||
|
|
||||||
import { useAppSelector } from '@app/hooks/useAppSelector.js';
|
|
||||||
import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection';
|
import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection';
|
||||||
import { ExternalSection } from '@components/generic/Sidebar/ExternalSection';
|
import { ExternalSection } from '@components/generic/Sidebar/ExternalSection';
|
||||||
import { SidebarOverlay } from '@components/generic/Sidebar/SidebarOverlay';
|
import { SidebarOverlay } from '@components/generic/Sidebar/SidebarOverlay';
|
||||||
import { Channels } from '@components/layout/Sidebar/Settings/Channels';
|
import { Channels } from '@components/layout/Sidebar/Settings/Channels';
|
||||||
import { ChannelsGroup } from '@components/layout/Sidebar/Settings/channels/ChannelsGroup';
|
import { ChannelsGroup } from '@components/layout/Sidebar/Settings/channels/ChannelsGroup';
|
||||||
|
import { Display } from '@components/layout/Sidebar/Settings/Display';
|
||||||
|
import { GPS } from '@components/layout/Sidebar/Settings/GPS';
|
||||||
import { Interface } from '@components/layout/Sidebar/Settings/Interface';
|
import { Interface } from '@components/layout/Sidebar/Settings/Interface';
|
||||||
import { ExternalNotificationsSettingsPlanel } from '@components/layout/Sidebar/Settings/plugins/ExternalNotifications';
|
import { LoRa } from '@components/layout/Sidebar/Settings/LoRa';
|
||||||
import { RangeTestSettingsPanel } from '@components/layout/Sidebar/Settings/plugins/RangeTest';
|
import { CannedMessage } from '@components/layout/Sidebar/Settings/modules/CannedMessage';
|
||||||
import { SerialSettingsPanel } from '@components/layout/Sidebar/Settings/plugins/Serial';
|
import { ExternalNotificationsSettingsPlanel } from '@components/layout/Sidebar/Settings/modules/ExternalNotifications';
|
||||||
import { StoreForwardSettingsPanel } from '@components/layout/Sidebar/Settings/plugins/StoreForward';
|
import { MQTT } from '@components/layout/Sidebar/Settings/modules/MQTT';
|
||||||
import { Position } from '@components/layout/Sidebar/Settings/Position';
|
import { RangeTestSettingsPanel } from '@components/layout/Sidebar/Settings/modules/RangeTest';
|
||||||
|
import { SerialSettingsPanel } from '@components/layout/Sidebar/Settings/modules/Serial';
|
||||||
|
import { StoreForwardSettingsPanel } from '@components/layout/Sidebar/Settings/modules/StoreForward';
|
||||||
|
import { Telemetry } from '@components/layout/Sidebar/Settings/modules/Telemetry';
|
||||||
import { Power } from '@components/layout/Sidebar/Settings/Power';
|
import { Power } from '@components/layout/Sidebar/Settings/Power';
|
||||||
import { Radio } from '@components/layout/Sidebar/Settings/Radio';
|
|
||||||
import { User } from '@components/layout/Sidebar/Settings/User';
|
import { User } from '@components/layout/Sidebar/Settings/User';
|
||||||
import { WiFi } from '@components/layout/Sidebar/Settings/WiFi';
|
import { WiFi } from '@components/layout/Sidebar/Settings/WiFi';
|
||||||
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
|
|
||||||
export interface SettingsProps {
|
export interface SettingsProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -39,13 +46,15 @@ export interface SettingsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Settings = ({ open, setOpen }: SettingsProps): JSX.Element => {
|
export const Settings = ({ open, setOpen }: SettingsProps): JSX.Element => {
|
||||||
const [pluginsOpen, setPluginsOpen] = useState(false);
|
const [modulesOpen, setModulesOpen] = useState(false);
|
||||||
const [channelsOpen, setChannelsOpen] = useState(false);
|
const [channelsOpen, setChannelsOpen] = useState(false);
|
||||||
const {
|
const {
|
||||||
rangeTestPluginEnabled,
|
rangeTestModuleEnabled,
|
||||||
extNotificationPluginEnabled,
|
extNotificationModuleEnabled,
|
||||||
serialpluginEnabled,
|
serialmoduleEnabled,
|
||||||
storeForwardPluginEnabled,
|
storeForwardModuleEnabled,
|
||||||
|
mqttDisabled,
|
||||||
|
cannedMessageModuleEnabled,
|
||||||
} = useAppSelector((state) => state.meshtastic.radio.preferences);
|
} = useAppSelector((state) => state.meshtastic.radio.preferences);
|
||||||
|
|
||||||
const hasGps = true;
|
const hasGps = true;
|
||||||
@@ -61,20 +70,26 @@ export const Settings = ({ open, setOpen }: SettingsProps): JSX.Element => {
|
|||||||
}}
|
}}
|
||||||
direction="y"
|
direction="y"
|
||||||
>
|
>
|
||||||
<CollapsibleSection icon={<FiWifi />} title="WiFi & MQTT">
|
|
||||||
<WiFi />
|
|
||||||
</CollapsibleSection>
|
|
||||||
<CollapsibleSection icon={<FiMapPin />} title="Position">
|
|
||||||
<Position />
|
|
||||||
</CollapsibleSection>
|
|
||||||
<CollapsibleSection icon={<FiUser />} title="User">
|
<CollapsibleSection icon={<FiUser />} title="User">
|
||||||
<User />
|
<User />
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
<CollapsibleSection icon={<FiZap />} title="Power">
|
<CollapsibleSection icon={<FiSmartphone />} title="Device">
|
||||||
|
<WiFi />
|
||||||
|
</CollapsibleSection>
|
||||||
|
<CollapsibleSection icon={<FiMapPin />} title="GPS">
|
||||||
|
<GPS />
|
||||||
|
</CollapsibleSection>
|
||||||
|
<CollapsibleSection icon={<FiPower />} title="Power">
|
||||||
<Power />
|
<Power />
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
<CollapsibleSection icon={<FiRadio />} title="Radio">
|
<CollapsibleSection icon={<FiWifi />} title="WiFi">
|
||||||
<Radio />
|
<WiFi />
|
||||||
|
</CollapsibleSection>
|
||||||
|
<CollapsibleSection icon={<FiTv />} title="Display">
|
||||||
|
<Display />
|
||||||
|
</CollapsibleSection>
|
||||||
|
<CollapsibleSection icon={<FiRss />} title="LoRa">
|
||||||
|
<LoRa />
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
<CollapsibleSection icon={<FiLayers />} title="Primary Channel">
|
<CollapsibleSection icon={<FiLayers />} title="Primary Channel">
|
||||||
<Channels />
|
<Channels />
|
||||||
@@ -88,87 +103,76 @@ export const Settings = ({ open, setOpen }: SettingsProps): JSX.Element => {
|
|||||||
/>
|
/>
|
||||||
<ExternalSection
|
<ExternalSection
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
setPluginsOpen(true);
|
setModulesOpen(true);
|
||||||
}}
|
}}
|
||||||
icon={<FiPackage />}
|
icon={<FiPackage />}
|
||||||
title="Plugins"
|
title="Modules"
|
||||||
/>
|
/>
|
||||||
<CollapsibleSection icon={<FiLayout />} title="Interface">
|
<CollapsibleSection icon={<FiLayout />} title="Interface">
|
||||||
<Interface />
|
<Interface />
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
</SidebarOverlay>
|
</SidebarOverlay>
|
||||||
|
|
||||||
{/* Plugins */}
|
{/* Modules */}
|
||||||
<SidebarOverlay
|
<SidebarOverlay
|
||||||
title="Plugins"
|
title="Modules"
|
||||||
open={pluginsOpen}
|
open={modulesOpen}
|
||||||
close={(): void => {
|
close={(): void => {
|
||||||
setPluginsOpen(false);
|
setModulesOpen(false);
|
||||||
}}
|
}}
|
||||||
direction="x"
|
direction="x"
|
||||||
>
|
>
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
title="Range Test"
|
icon={<FiWifi />}
|
||||||
icon={
|
title="MQTT"
|
||||||
<div className="flex gap-2">
|
status={!mqttDisabled}
|
||||||
<FiRss />
|
|
||||||
<div
|
|
||||||
className={`h-3 w-3 rounded-full ${
|
|
||||||
rangeTestPluginEnabled ? 'bg-green-500' : 'bg-red-500'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<RangeTestSettingsPanel />
|
<MQTT />
|
||||||
</CollapsibleSection>
|
|
||||||
<CollapsibleSection
|
|
||||||
title="External Notifications"
|
|
||||||
icon={
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<FiBell />
|
|
||||||
<div
|
|
||||||
className={`h-3 w-3 rounded-full ${
|
|
||||||
extNotificationPluginEnabled ? 'bg-green-500' : 'bg-red-500'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ExternalNotificationsSettingsPlanel />
|
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
|
icon={<FiAlignLeft />}
|
||||||
title="Serial"
|
title="Serial"
|
||||||
icon={
|
status={serialmoduleEnabled}
|
||||||
<div className="flex gap-2">
|
|
||||||
<FiAlignLeft />
|
|
||||||
<div
|
|
||||||
className={`h-3 w-3 rounded-full ${
|
|
||||||
serialpluginEnabled ? 'bg-green-500' : 'bg-red-500'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<SerialSettingsPanel />
|
<SerialSettingsPanel />
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
|
icon={<FiBell />}
|
||||||
|
title="External Notifications"
|
||||||
|
status={extNotificationModuleEnabled}
|
||||||
|
>
|
||||||
|
<ExternalNotificationsSettingsPlanel />
|
||||||
|
</CollapsibleSection>
|
||||||
|
<CollapsibleSection
|
||||||
|
icon={<FiFastForward />}
|
||||||
title="Store & Forward"
|
title="Store & Forward"
|
||||||
icon={
|
status={storeForwardModuleEnabled}
|
||||||
<div className="flex gap-2">
|
|
||||||
<FiFastForward />
|
|
||||||
<div
|
|
||||||
className={`h-3 w-3 rounded-full ${
|
|
||||||
storeForwardPluginEnabled ? 'bg-green-500' : 'bg-red-500'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<StoreForwardSettingsPanel />
|
<StoreForwardSettingsPanel />
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
<CollapsibleSection
|
||||||
|
icon={<FiRss />}
|
||||||
|
title="Range Test"
|
||||||
|
status={rangeTestModuleEnabled}
|
||||||
|
>
|
||||||
|
<RangeTestSettingsPanel />
|
||||||
|
</CollapsibleSection>
|
||||||
|
<CollapsibleSection
|
||||||
|
icon={<FiActivity />}
|
||||||
|
title="Telemetry"
|
||||||
|
status={true}
|
||||||
|
>
|
||||||
|
<Telemetry />
|
||||||
|
</CollapsibleSection>
|
||||||
|
<CollapsibleSection
|
||||||
|
icon={<FiMessageSquare />}
|
||||||
|
title="Canned Message"
|
||||||
|
status={cannedMessageModuleEnabled}
|
||||||
|
>
|
||||||
|
<CannedMessage />
|
||||||
|
</CollapsibleSection>
|
||||||
</SidebarOverlay>
|
</SidebarOverlay>
|
||||||
{/* End Plugins */}
|
{/* End Modules */}
|
||||||
|
|
||||||
{/* Channels */}
|
{/* Channels */}
|
||||||
<SidebarOverlay
|
<SidebarOverlay
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||||
|
import { Form } from '@components/generic/form/Form';
|
||||||
|
import { Input } from '@components/generic/form/Input';
|
||||||
|
import { Select } from '@components/generic/form/Select';
|
||||||
|
import { connection } from '@core/connection';
|
||||||
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
|
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
export const LoRa = (): JSX.Element => {
|
||||||
|
const preferences = useAppSelector(
|
||||||
|
(state) => state.meshtastic.radio.preferences,
|
||||||
|
);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { register, handleSubmit, formState, reset } =
|
||||||
|
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||||
|
defaultValues: preferences,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset(preferences);
|
||||||
|
}, [reset, preferences]);
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit((data) => {
|
||||||
|
setLoading(true);
|
||||||
|
void connection.setPreferences(data, async () => {
|
||||||
|
reset({ ...data });
|
||||||
|
setLoading(false);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||||
|
<Input
|
||||||
|
label="Hop Count"
|
||||||
|
type="number"
|
||||||
|
suffix="Hops"
|
||||||
|
{...register('hopLimit', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Checkbox label="Transmit Disabled" {...register('isLoraTxDisabled')} />
|
||||||
|
<Checkbox label="Router Mode" {...register('isRouter')} />
|
||||||
|
<Input
|
||||||
|
label="Send Owner Interval"
|
||||||
|
type="number"
|
||||||
|
suffix="Seconds"
|
||||||
|
{...register('sendOwnerInterval', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Frequency Offset"
|
||||||
|
type="number"
|
||||||
|
suffix="Hz"
|
||||||
|
{...register('frequencyOffset', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Region"
|
||||||
|
optionsEnum={Protobuf.RegionCode}
|
||||||
|
{...register('region', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
import type React from 'react';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
|
||||||
import { FiSave } from 'react-icons/fi';
|
|
||||||
import { MultiSelect } from 'react-multi-select-component';
|
|
||||||
|
|
||||||
import { IconButton } from '@components/generic/button/IconButton';
|
|
||||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
|
||||||
import { Input } from '@components/generic/form/Input';
|
|
||||||
import { Label } from '@components/generic/form/Label';
|
|
||||||
import { Select } from '@components/generic/form/Select';
|
|
||||||
import { connection } from '@core/connection';
|
|
||||||
import { bitwiseEncode } from '@core/utils/bitwise';
|
|
||||||
import { useAppSelector } from '@hooks/useAppSelector';
|
|
||||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
|
||||||
|
|
||||||
export const Position = (): JSX.Element => {
|
|
||||||
const preferences = useAppSelector(
|
|
||||||
(state) => state.meshtastic.radio.preferences,
|
|
||||||
);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const { register, handleSubmit, formState, reset, control } =
|
|
||||||
useForm<Protobuf.RadioConfig_UserPreferences>({
|
|
||||||
defaultValues: {
|
|
||||||
...preferences,
|
|
||||||
positionBroadcastSecs:
|
|
||||||
preferences.positionBroadcastSecs === 0
|
|
||||||
? preferences.isRouter
|
|
||||||
? 43200
|
|
||||||
: 900
|
|
||||||
: preferences.positionBroadcastSecs,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reset(preferences);
|
|
||||||
}, [reset, preferences]);
|
|
||||||
|
|
||||||
const onSubmit = handleSubmit((data) => {
|
|
||||||
setLoading(true);
|
|
||||||
void connection.setPreferences(data, async () => {
|
|
||||||
reset({ ...data });
|
|
||||||
setLoading(false);
|
|
||||||
await Promise.resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const encode = (enums: Protobuf.PositionFlags[]): number => {
|
|
||||||
return enums.reduce((acc, curr) => acc | curr, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const decode = (value: number): Protobuf.PositionFlags[] => {
|
|
||||||
const enumValues = Object.keys(Protobuf.PositionFlags)
|
|
||||||
.map(Number)
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
return enumValues.map((b) => value & b).filter(Boolean);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<form className="space-y-2" onSubmit={onSubmit}>
|
|
||||||
<Input
|
|
||||||
label="Broadcast Interval"
|
|
||||||
type="number"
|
|
||||||
suffix="Seconds"
|
|
||||||
{...register('positionBroadcastSecs', { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="positionFlags"
|
|
||||||
control={control}
|
|
||||||
render={({ field, fieldState }): JSX.Element => {
|
|
||||||
const { value, onChange, ...rest } = field;
|
|
||||||
const { error } = fieldState;
|
|
||||||
const label = 'Position Flags';
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
{label && <Label label={label} error={error?.message} />}
|
|
||||||
<MultiSelect
|
|
||||||
options={Object.entries(Protobuf.PositionFlags)
|
|
||||||
.filter((value) => typeof value[1] !== 'number')
|
|
||||||
.filter(
|
|
||||||
(value) =>
|
|
||||||
parseInt(value[0]) !==
|
|
||||||
Protobuf.PositionFlags.POS_UNDEFINED,
|
|
||||||
)
|
|
||||||
.map((value) => {
|
|
||||||
return {
|
|
||||||
value: parseInt(value[0]),
|
|
||||||
label: value[1].toString().replace('POS_', ''),
|
|
||||||
};
|
|
||||||
})}
|
|
||||||
value={decode(value).map((flag) => {
|
|
||||||
return {
|
|
||||||
value: flag,
|
|
||||||
label: Protobuf.PositionFlags[flag].replace('POS_', ''),
|
|
||||||
};
|
|
||||||
})}
|
|
||||||
onChange={(e: { value: number; label: string }[]): void =>
|
|
||||||
onChange(bitwiseEncode(e.map((v) => v.value)))
|
|
||||||
}
|
|
||||||
labelledBy="Select"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
label="Position Type (DEBUG)"
|
|
||||||
type="number"
|
|
||||||
disabled
|
|
||||||
{...register('positionFlags', { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<Checkbox label="Use Fixed Position" {...register('fixedPosition')} />
|
|
||||||
<Select
|
|
||||||
label="Location Sharing"
|
|
||||||
optionsEnum={Protobuf.LocationSharing}
|
|
||||||
{...register('locationShare', { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
label="GPS Mode"
|
|
||||||
optionsEnum={Protobuf.GpsOperation}
|
|
||||||
{...register('gpsOperation', { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
label="Display Format"
|
|
||||||
optionsEnum={Protobuf.GpsCoordinateFormat}
|
|
||||||
{...register('gpsFormat', { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<Checkbox label="Accept 2D Fix" {...register('gpsAccept2D')} />
|
|
||||||
<Input
|
|
||||||
label="Max DOP"
|
|
||||||
type="number"
|
|
||||||
{...register('gpsMaxDop', { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label="Last GPS Attempt"
|
|
||||||
disabled
|
|
||||||
{...register('gpsAttemptTime', { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
<div className="flex w-full bg-white dark:bg-secondaryDark">
|
|
||||||
<div className="ml-auto p-2">
|
|
||||||
<IconButton
|
|
||||||
disabled={!formState.isDirty}
|
|
||||||
onClick={async (): Promise<void> => {
|
|
||||||
await onSubmit();
|
|
||||||
}}
|
|
||||||
icon={<FiSave />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -2,10 +2,10 @@ import type React from 'react';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { FiSave } from 'react-icons/fi';
|
|
||||||
|
|
||||||
import { IconButton } from '@components/generic/button/IconButton';
|
|
||||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||||
|
import { Form } from '@components/generic/form/Form';
|
||||||
|
import { Input } from '@components/generic/form/Input';
|
||||||
import { Select } from '@components/generic/form/Select';
|
import { Select } from '@components/generic/form/Select';
|
||||||
import { connection } from '@core/connection';
|
import { connection } from '@core/connection';
|
||||||
import { useAppSelector } from '@hooks/useAppSelector';
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
@@ -37,34 +37,75 @@ export const Power = (): JSX.Element => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||||
<form className="space-y-2" onSubmit={onSubmit}>
|
<Select
|
||||||
<Select
|
label="Charge current"
|
||||||
label="Charge current"
|
optionsEnum={Protobuf.ChargeCurrent}
|
||||||
optionsEnum={Protobuf.ChargeCurrent}
|
{...register('chargeCurrent', { valueAsNumber: true })}
|
||||||
{...register('chargeCurrent', { valueAsNumber: true })}
|
/>
|
||||||
/>
|
<Checkbox
|
||||||
<Checkbox label="Always powered" {...register('isAlwaysPowered')} />
|
label="Powered by low power source (solar)"
|
||||||
<Checkbox
|
disabled={preferences.isRouter}
|
||||||
label="Powered by low power source (solar)"
|
validationMessage={
|
||||||
disabled={preferences.isRouter}
|
preferences.isRouter ? 'Enabled by default in router mode' : ''
|
||||||
validationMessage={
|
}
|
||||||
preferences.isRouter ? 'Enabled by default in router mode' : ''
|
{...register('isLowPower')}
|
||||||
}
|
/>
|
||||||
{...register('isLowPower')}
|
<Checkbox label="Always Powered" {...register('isAlwaysPowered')} />
|
||||||
/>
|
<Input
|
||||||
</form>
|
label="Shutdown on battery delay"
|
||||||
<div className="flex w-full bg-white dark:bg-secondaryDark">
|
type="number"
|
||||||
<div className="ml-auto p-2">
|
suffix="Seconds"
|
||||||
<IconButton
|
{...register('onBatteryShutdownAfterSecs', { valueAsNumber: true })}
|
||||||
disabled={!formState.isDirty}
|
/>
|
||||||
onClick={async (): Promise<void> => {
|
<Checkbox label="Power Saving" {...register('isPowerSaving')} />
|
||||||
await onSubmit();
|
<Input
|
||||||
}}
|
label="ADC Multiplier Override ratio"
|
||||||
icon={<FiSave />}
|
type="number"
|
||||||
/>
|
{...register('adcMultiplierOverride', { valueAsNumber: true })}
|
||||||
</div>
|
/>
|
||||||
</div>
|
<Input
|
||||||
</>
|
label="Minumum Wake Time"
|
||||||
|
suffix="Seconds"
|
||||||
|
type="number"
|
||||||
|
{...register('minWakeSecs', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Phone Timeout"
|
||||||
|
suffix="Seconds"
|
||||||
|
type="number"
|
||||||
|
{...register('phoneTimeoutSecs', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Phone SDS Timeout"
|
||||||
|
suffix="Seconds"
|
||||||
|
type="number"
|
||||||
|
{...register('phoneSdsTimeoutSec', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Mesh SDS Timeout"
|
||||||
|
suffix="Seconds"
|
||||||
|
type="number"
|
||||||
|
{...register('meshSdsTimeoutSecs', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="SDS"
|
||||||
|
suffix="Seconds"
|
||||||
|
type="number"
|
||||||
|
{...register('sdsSecs', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="LS"
|
||||||
|
suffix="Seconds"
|
||||||
|
type="number"
|
||||||
|
{...register('lsSecs', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Wait Bluetooth"
|
||||||
|
suffix="Seconds"
|
||||||
|
type="number"
|
||||||
|
{...register('waitBluetoothSecs', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import type React from 'react';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { FiSave } from 'react-icons/fi';
|
|
||||||
import { base16 } from 'rfc4648';
|
import { base16 } from 'rfc4648';
|
||||||
|
|
||||||
import { IconButton } from '@components/generic/button/IconButton';
|
|
||||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||||
|
import { Form } from '@components/generic/form/Form';
|
||||||
import { Input } from '@components/generic/form/Input';
|
import { Input } from '@components/generic/form/Input';
|
||||||
import { Select } from '@components/generic/form/Select';
|
import { Select } from '@components/generic/form/Select';
|
||||||
import { connection } from '@core/connection';
|
import { connection } from '@core/connection';
|
||||||
@@ -67,66 +66,53 @@ export const User = (): JSX.Element => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||||
<form className="space-y-2" onSubmit={onSubmit}>
|
<Input label="Device ID" value={node?.user?.id} disabled />
|
||||||
<Input label="Device ID" value={node?.user?.id} disabled />
|
<Input label="Device Name" {...register('longName')} />
|
||||||
<Input
|
<Input label="Short Name" maxLength={3} {...register('shortName')} />
|
||||||
label="Hardware"
|
<Input
|
||||||
value={
|
label="Mac Address"
|
||||||
Protobuf.HardwareModel[
|
defaultValue={
|
||||||
node?.user?.hwModel ?? Protobuf.HardwareModel.UNSET
|
base16
|
||||||
]
|
.stringify(node?.user?.macaddr ?? [])
|
||||||
}
|
.match(/.{1,2}/g)
|
||||||
disabled
|
?.join(':') ?? ''
|
||||||
/>
|
}
|
||||||
<Input
|
disabled
|
||||||
label="Mac Address"
|
/>
|
||||||
defaultValue={
|
<Input
|
||||||
base16
|
label="Hardware (DEPRECATED)"
|
||||||
.stringify(node?.user?.macaddr ?? [])
|
value={
|
||||||
.match(/.{1,2}/g)
|
Protobuf.HardwareModel[
|
||||||
?.join(':') ?? ''
|
node?.user?.hwModel ?? Protobuf.HardwareModel.UNSET
|
||||||
}
|
]
|
||||||
disabled
|
}
|
||||||
/>
|
disabled
|
||||||
<Input label="Device Name" {...register('longName')} />
|
/>
|
||||||
<Input label="Short Name" maxLength={3} {...register('shortName')} />
|
<Checkbox label="Licenced Operator?" {...register('isLicensed')} />
|
||||||
<Checkbox label="Licenced Operator?" {...register('isLicensed')} />
|
<Select
|
||||||
<Select
|
label="Team (DEPRECATED)"
|
||||||
label="Team"
|
optionsEnum={Protobuf.Team}
|
||||||
optionsEnum={Protobuf.Team}
|
{...register('team', { valueAsNumber: true })}
|
||||||
{...register('team', { valueAsNumber: true })}
|
/>
|
||||||
/>
|
<Input
|
||||||
<Input
|
label="Transmit Power"
|
||||||
label="Antenna Azimuth"
|
suffix="dBm"
|
||||||
suffix="°"
|
type="number"
|
||||||
type="number"
|
{...register('txPowerDbm', { valueAsNumber: true })}
|
||||||
{...register('antAzimuth', { valueAsNumber: true })}
|
/>
|
||||||
/>
|
<Input
|
||||||
<Input
|
label="Antenna Gain"
|
||||||
label="Antenna Gain"
|
suffix="dBi"
|
||||||
suffix="dBi"
|
type="number"
|
||||||
type="number"
|
{...register('antGainDbi', { valueAsNumber: true })}
|
||||||
{...register('antGainDbi', { valueAsNumber: true })}
|
/>
|
||||||
/>
|
<Input
|
||||||
<Input
|
label="Antenna Azimuth"
|
||||||
label="Transmit Power"
|
suffix="°"
|
||||||
suffix="dBm"
|
type="number"
|
||||||
type="number"
|
{...register('antAzimuth', { valueAsNumber: true })}
|
||||||
{...register('txPowerDbm', { valueAsNumber: true })}
|
/>
|
||||||
/>
|
</Form>
|
||||||
</form>
|
|
||||||
<div className="flex w-full bg-white dark:bg-secondaryDark">
|
|
||||||
<div className="ml-auto p-2">
|
|
||||||
<IconButton
|
|
||||||
disabled={!formState.isDirty}
|
|
||||||
onClick={async (): Promise<void> => {
|
|
||||||
await onSubmit();
|
|
||||||
}}
|
|
||||||
icon={<FiSave />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ import type React from 'react';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import { FiSave } from 'react-icons/fi';
|
|
||||||
|
|
||||||
import { IconButton } from '@components/generic/button/IconButton';
|
|
||||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||||
|
import { Form } from '@components/generic/form/Form';
|
||||||
import { Input } from '@components/generic/form/Input';
|
import { Input } from '@components/generic/form/Input';
|
||||||
import { connection } from '@core/connection';
|
import { connection } from '@core/connection';
|
||||||
import { useAppSelector } from '@hooks/useAppSelector';
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
@@ -21,18 +20,12 @@ export const WiFi = (): JSX.Element => {
|
|||||||
defaultValues: preferences,
|
defaultValues: preferences,
|
||||||
});
|
});
|
||||||
|
|
||||||
const watchWifiApMode = useWatch({
|
const WifiApMode = useWatch({
|
||||||
control,
|
control,
|
||||||
name: 'wifiApMode',
|
name: 'wifiApMode',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const watchMQTTDisabled = useWatch({
|
|
||||||
control,
|
|
||||||
name: 'mqttDisabled',
|
|
||||||
defaultValue: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset(preferences);
|
reset(preferences);
|
||||||
}, [reset, preferences]);
|
}, [reset, preferences]);
|
||||||
@@ -46,51 +39,20 @@ export const WiFi = (): JSX.Element => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||||
<form className="space-y-2" onSubmit={onSubmit}>
|
<Checkbox label="Enable WiFi AP" {...register('wifiApMode')} />
|
||||||
<Checkbox label="Enable WiFi AP" {...register('wifiApMode')} />
|
<Input
|
||||||
<Input
|
label="WiFi SSID"
|
||||||
label="WiFi SSID"
|
disabled={WifiApMode}
|
||||||
disabled={watchWifiApMode}
|
{...register('wifiSsid')}
|
||||||
{...register('wifiSsid')}
|
/>
|
||||||
/>
|
<Input
|
||||||
<Input
|
type="password"
|
||||||
type="password"
|
autoComplete="off"
|
||||||
autoComplete="off"
|
label="WiFi PSK"
|
||||||
label="WiFi PSK"
|
disabled={WifiApMode}
|
||||||
disabled={watchWifiApMode}
|
{...register('wifiPassword')}
|
||||||
{...register('wifiPassword')}
|
/>
|
||||||
/>
|
</Form>
|
||||||
<Checkbox label="Disable MQTT" {...register('mqttDisabled')} />
|
|
||||||
<Input
|
|
||||||
label="MQTT Server Address"
|
|
||||||
disabled={watchMQTTDisabled}
|
|
||||||
{...register('mqttServer')}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label="MQTT Username"
|
|
||||||
disabled={watchMQTTDisabled}
|
|
||||||
{...register('mqttUsername')}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label="MQTT Password"
|
|
||||||
type="password"
|
|
||||||
autoComplete="off"
|
|
||||||
disabled={watchMQTTDisabled}
|
|
||||||
{...register('mqttPassword')}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
<div className="flex w-full bg-white dark:bg-secondaryDark">
|
|
||||||
<div className="ml-auto p-2">
|
|
||||||
<IconButton
|
|
||||||
disabled={!formState.isDirty}
|
|
||||||
onClick={async (): Promise<void> => {
|
|
||||||
await onSubmit();
|
|
||||||
}}
|
|
||||||
icon={<FiSave />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useEffect, useState } from 'react';
|
|||||||
|
|
||||||
import { fromByteArray, toByteArray } from 'base64-js';
|
import { fromByteArray, toByteArray } from 'base64-js';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { FiSave } from 'react-icons/fi';
|
|
||||||
import { MdRefresh, MdVisibility, MdVisibilityOff } from 'react-icons/md';
|
import { MdRefresh, MdVisibility, MdVisibilityOff } from 'react-icons/md';
|
||||||
|
|
||||||
import { IconButton } from '@components/generic/button/IconButton';
|
import { IconButton } from '@components/generic/button/IconButton';
|
||||||
@@ -18,7 +17,7 @@ export interface SettingsPanelProps {
|
|||||||
channel: Protobuf.Channel;
|
channel: Protobuf.Channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SettingsPanel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
export const Channels = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [keySize, setKeySize] = useState<128 | 256>(256);
|
const [keySize, setKeySize] = useState<128 | 256>(256);
|
||||||
const [pskHidden, setPskHidden] = useState(true);
|
const [pskHidden, setPskHidden] = useState(true);
|
||||||
@@ -75,67 +74,54 @@ export const SettingsPanel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col">
|
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||||
<Form loading={loading}>
|
{channel?.index !== 0 && (
|
||||||
{channel?.index !== 0 && (
|
<>
|
||||||
<>
|
<Checkbox
|
||||||
<Checkbox
|
label="Enabled"
|
||||||
label="Enabled"
|
{...register('enabled', { valueAsNumber: true })}
|
||||||
{...register('enabled', { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<Input label="Name" {...register('name')} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Select
|
|
||||||
label="Key Size"
|
|
||||||
options={[
|
|
||||||
{ name: '128 Bit', value: 128 },
|
|
||||||
{ name: '256 Bit', value: 256 },
|
|
||||||
]}
|
|
||||||
value={keySize}
|
|
||||||
onChange={(e): void => {
|
|
||||||
setKeySize(parseInt(e.target.value) as 128 | 256);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label="Pre-Shared Key"
|
|
||||||
type={pskHidden ? 'password' : 'text'}
|
|
||||||
disabled
|
|
||||||
action={
|
|
||||||
<>
|
|
||||||
<IconButton
|
|
||||||
onClick={(): void => {
|
|
||||||
setPskHidden(!pskHidden);
|
|
||||||
}}
|
|
||||||
icon={pskHidden ? <MdVisibility /> : <MdVisibilityOff />}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
onClick={(): void => {
|
|
||||||
const key = new Uint8Array(keySize);
|
|
||||||
crypto.getRandomValues(key);
|
|
||||||
setValue('psk', fromByteArray(key));
|
|
||||||
}}
|
|
||||||
icon={<MdRefresh />}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
{...register('psk')}
|
|
||||||
/>
|
|
||||||
<Checkbox label="Uplink Enabled" {...register('uplinkEnabled')} />
|
|
||||||
<Checkbox label="Downlink Enabled" {...register('downlinkEnabled')} />
|
|
||||||
</Form>
|
|
||||||
<div className="flex w-full bg-white dark:bg-secondaryDark">
|
|
||||||
<div className="ml-auto p-2">
|
|
||||||
<IconButton
|
|
||||||
disabled={!formState.isDirty}
|
|
||||||
onClick={async (): Promise<void> => {
|
|
||||||
await onSubmit();
|
|
||||||
}}
|
|
||||||
icon={<FiSave />}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<Input label="Name" {...register('name')} />
|
||||||
</div>
|
</>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Key Size"
|
||||||
|
options={[
|
||||||
|
{ name: '128 Bit', value: 128 },
|
||||||
|
{ name: '256 Bit', value: 256 },
|
||||||
|
]}
|
||||||
|
value={keySize}
|
||||||
|
onChange={(e): void => {
|
||||||
|
setKeySize(parseInt(e.target.value) as 128 | 256);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Pre-Shared Key"
|
||||||
|
type={pskHidden ? 'password' : 'text'}
|
||||||
|
disabled
|
||||||
|
action={
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
onClick={(): void => {
|
||||||
|
setPskHidden(!pskHidden);
|
||||||
|
}}
|
||||||
|
icon={pskHidden ? <MdVisibility /> : <MdVisibilityOff />}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
onClick={(): void => {
|
||||||
|
const key = new Uint8Array(keySize);
|
||||||
|
crypto.getRandomValues(key);
|
||||||
|
setValue('psk', fromByteArray(key));
|
||||||
|
}}
|
||||||
|
icon={<MdRefresh />}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
{...register('psk')}
|
||||||
|
/>
|
||||||
|
<Checkbox label="Uplink Enabled" {...register('uplinkEnabled')} />
|
||||||
|
<Checkbox label="Downlink Enabled" {...register('downlinkEnabled')} />
|
||||||
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
|
||||||
import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection';
|
import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection';
|
||||||
import { SettingsPanel } from '@components/layout/Sidebar/Settings/channels/Channels';
|
import { Channels } from '@components/layout/Sidebar/Settings/channels/Channels';
|
||||||
import { useAppSelector } from '@hooks/useAppSelector';
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ export const ChannelsGroup = (): JSX.Element => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SettingsPanel channel={channel} />
|
<Channels channel={channel} />
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||||
|
import { Form } from '@components/generic/form/Form';
|
||||||
|
import { Input } from '@components/generic/form/Input';
|
||||||
|
import { Select } from '@components/generic/form/Select';
|
||||||
|
import { connection } from '@core/connection';
|
||||||
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
|
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
export const CannedMessage = (): JSX.Element => {
|
||||||
|
const preferences = useAppSelector(
|
||||||
|
(state) => state.meshtastic.radio.preferences,
|
||||||
|
);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { register, handleSubmit, formState, reset, control } =
|
||||||
|
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||||
|
defaultValues: preferences,
|
||||||
|
});
|
||||||
|
|
||||||
|
const moduleEnabled = useWatch({
|
||||||
|
control,
|
||||||
|
name: 'rotary1Enabled',
|
||||||
|
defaultValue: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset(preferences);
|
||||||
|
}, [reset, preferences]);
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit((data) => {
|
||||||
|
setLoading(true);
|
||||||
|
void connection.setPreferences(data, async () => {
|
||||||
|
reset({ ...data });
|
||||||
|
setLoading(false);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||||
|
<Checkbox
|
||||||
|
label="Module Enabled"
|
||||||
|
{...register('cannedMessageModuleEnabled')}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Rotary Encoder #1 Enabled"
|
||||||
|
{...register('rotary1Enabled')}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Encoder #1 Pin A"
|
||||||
|
type="number"
|
||||||
|
disabled={moduleEnabled}
|
||||||
|
{...register('rotary1PinA', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Encoder #1 Pin B"
|
||||||
|
type="number"
|
||||||
|
disabled={moduleEnabled}
|
||||||
|
{...register('rotary1PinB', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Endoer #1 Pin Press"
|
||||||
|
type="number"
|
||||||
|
disabled={moduleEnabled}
|
||||||
|
{...register('rotary1PinPress', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Clockwise event"
|
||||||
|
disabled={moduleEnabled}
|
||||||
|
optionsEnum={Protobuf.InputEventChar}
|
||||||
|
{...register('rotary1EventCw', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Counter Clockwise event"
|
||||||
|
disabled={moduleEnabled}
|
||||||
|
optionsEnum={Protobuf.InputEventChar}
|
||||||
|
{...register('rotary1EventCcw', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Press event"
|
||||||
|
disabled={moduleEnabled}
|
||||||
|
optionsEnum={Protobuf.InputEventChar}
|
||||||
|
{...register('rotary1EventPress', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Allow Input Source"
|
||||||
|
disabled={moduleEnabled}
|
||||||
|
{...register('cannedMessageModuleAllowInputSource')}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Send Bell"
|
||||||
|
{...register('cannedMessageModuleSendBell')}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||||
|
import { Form } from '@components/generic/form/Form';
|
||||||
|
import { Input } from '@components/generic/form/Input';
|
||||||
|
import { connection } from '@core/connection';
|
||||||
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
|
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
export const ExternalNotificationsSettingsPlanel = (): JSX.Element => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const preferences = useAppSelector(
|
||||||
|
(state) => state.meshtastic.radio.preferences,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { register, handleSubmit, formState, reset, control } =
|
||||||
|
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||||
|
defaultValues: preferences,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset(preferences);
|
||||||
|
}, [reset, preferences]);
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit(async (data) => {
|
||||||
|
setLoading(true);
|
||||||
|
await connection.setPreferences(data, async (): Promise<void> => {
|
||||||
|
reset({ ...data });
|
||||||
|
setLoading(false);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const moduleEnabled = useWatch({
|
||||||
|
control,
|
||||||
|
name: 'extNotificationModuleEnabled',
|
||||||
|
defaultValue: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||||
|
<Checkbox
|
||||||
|
label="Module Enabled"
|
||||||
|
{...register('extNotificationModuleEnabled')}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
label="Output MS"
|
||||||
|
suffix="ms"
|
||||||
|
disabled={!moduleEnabled}
|
||||||
|
{...register('extNotificationModuleOutputMs', {
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
label="Output"
|
||||||
|
disabled={!moduleEnabled}
|
||||||
|
{...register('extNotificationModuleOutput', {
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Active"
|
||||||
|
disabled={!moduleEnabled}
|
||||||
|
{...register('extNotificationModuleActive')}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Message"
|
||||||
|
disabled={!moduleEnabled}
|
||||||
|
{...register('extNotificationModuleAlertMessage')}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Bell"
|
||||||
|
disabled={!moduleEnabled}
|
||||||
|
{...register('extNotificationModuleAlertBell')}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||||
|
import { Form } from '@components/generic/form/Form';
|
||||||
|
import { Input } from '@components/generic/form/Input';
|
||||||
|
import { connection } from '@core/connection';
|
||||||
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
|
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
export const MQTT = (): JSX.Element => {
|
||||||
|
const preferences = useAppSelector(
|
||||||
|
(state) => state.meshtastic.radio.preferences,
|
||||||
|
);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { register, handleSubmit, formState, reset, control } =
|
||||||
|
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||||
|
defaultValues: preferences,
|
||||||
|
});
|
||||||
|
|
||||||
|
const moduleEnabled = useWatch({
|
||||||
|
control,
|
||||||
|
name: 'mqttDisabled',
|
||||||
|
defaultValue: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset(preferences);
|
||||||
|
}, [reset, preferences]);
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit((data) => {
|
||||||
|
setLoading(true);
|
||||||
|
void connection.setPreferences(data, async () => {
|
||||||
|
reset({ ...data });
|
||||||
|
setLoading(false);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||||
|
<Checkbox label="Module Disabled" {...register('mqttDisabled')} />
|
||||||
|
<Input
|
||||||
|
label="MQTT Server Address"
|
||||||
|
disabled={moduleEnabled}
|
||||||
|
{...register('mqttServer')}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="MQTT Username"
|
||||||
|
disabled={moduleEnabled}
|
||||||
|
{...register('mqttUsername')}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="MQTT Password"
|
||||||
|
type="password"
|
||||||
|
autoComplete="off"
|
||||||
|
disabled={moduleEnabled}
|
||||||
|
{...register('mqttPassword')}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Encryption Enabled"
|
||||||
|
disabled={moduleEnabled}
|
||||||
|
{...register('mqttEncryptionEnabled')}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
+22
-37
@@ -2,9 +2,7 @@ import type React from 'react';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import { FiSave } from 'react-icons/fi';
|
|
||||||
|
|
||||||
import { IconButton } from '@components/generic/button/IconButton';
|
|
||||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||||
import { Form } from '@components/generic/form/Form';
|
import { Form } from '@components/generic/form/Form';
|
||||||
import { Input } from '@components/generic/form/Input';
|
import { Input } from '@components/generic/form/Input';
|
||||||
@@ -37,45 +35,32 @@ export const RangeTestSettingsPanel = (): JSX.Element => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const pluginEnabled = useWatch({
|
const moduleEnabled = useWatch({
|
||||||
control,
|
control,
|
||||||
name: 'rangeTestPluginEnabled',
|
name: 'rangeTestModuleEnabled',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||||
<Form loading={loading}>
|
<Checkbox
|
||||||
<Checkbox
|
label="Module Enabled"
|
||||||
label="Range Test Plugin Enabled?"
|
{...register('rangeTestModuleEnabled')}
|
||||||
{...register('rangeTestPluginEnabled')}
|
/>
|
||||||
/>
|
<Input
|
||||||
<Checkbox
|
type="number"
|
||||||
label="Range Test Plugin Save?"
|
label="Message Interval"
|
||||||
disabled={!pluginEnabled}
|
disabled={!moduleEnabled}
|
||||||
{...register('rangeTestPluginSave')}
|
suffix="Seconds"
|
||||||
/>
|
{...register('rangeTestModuleSender', {
|
||||||
<Input
|
valueAsNumber: true,
|
||||||
type="number"
|
})}
|
||||||
label="Message Interval"
|
/>
|
||||||
disabled={!pluginEnabled}
|
<Checkbox
|
||||||
suffix="Seconds"
|
label="Save CSV to storage"
|
||||||
{...register('rangeTestPluginSender', {
|
disabled={!moduleEnabled}
|
||||||
valueAsNumber: true,
|
{...register('rangeTestModuleSave')}
|
||||||
})}
|
/>
|
||||||
/>
|
</Form>
|
||||||
</Form>
|
|
||||||
<div className="flex w-full bg-white dark:bg-secondaryDark">
|
|
||||||
<div className="ml-auto p-2">
|
|
||||||
<IconButton
|
|
||||||
disabled={!formState.isDirty}
|
|
||||||
onClick={async (): Promise<void> => {
|
|
||||||
await onSubmit();
|
|
||||||
}}
|
|
||||||
icon={<FiSave />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||||
|
import { Form } from '@components/generic/form/Form';
|
||||||
|
import { Input } from '@components/generic/form/Input';
|
||||||
|
import { connection } from '@core/connection';
|
||||||
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
|
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
export const SerialSettingsPanel = (): JSX.Element => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const preferences = useAppSelector(
|
||||||
|
(state) => state.meshtastic.radio.preferences,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { register, handleSubmit, formState, reset, control } =
|
||||||
|
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||||
|
defaultValues: preferences,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset(preferences);
|
||||||
|
}, [reset, preferences]);
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit(async (data) => {
|
||||||
|
setLoading(true);
|
||||||
|
await connection.setPreferences(data, async (): Promise<void> => {
|
||||||
|
reset({ ...data });
|
||||||
|
setLoading(false);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const moduleEnabled = useWatch({
|
||||||
|
control,
|
||||||
|
name: 'serialmoduleEnabled',
|
||||||
|
defaultValue: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||||
|
<Checkbox label="Module Enabled" {...register('serialmoduleEnabled')} />
|
||||||
|
<Checkbox
|
||||||
|
label="Echo"
|
||||||
|
disabled={!moduleEnabled}
|
||||||
|
{...register('serialmoduleEcho')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
label="RX"
|
||||||
|
disabled={!moduleEnabled}
|
||||||
|
{...register('serialmoduleRxd', {
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
label="TX"
|
||||||
|
disabled={!moduleEnabled}
|
||||||
|
{...register('serialmoduleTxd', {
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
label="TX"
|
||||||
|
disabled={!moduleEnabled}
|
||||||
|
{...register('serialmoduleBaud', {
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
label="Timeout"
|
||||||
|
disabled={!moduleEnabled}
|
||||||
|
{...register('serialmoduleTimeout', {
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
label="Mode"
|
||||||
|
disabled={!moduleEnabled}
|
||||||
|
{...register('serialmoduleMode', {
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||||
|
import { Form } from '@components/generic/form/Form';
|
||||||
|
import { Input } from '@components/generic/form/Input';
|
||||||
|
import { connection } from '@core/connection';
|
||||||
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
|
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
export const StoreForwardSettingsPanel = (): JSX.Element => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const preferences = useAppSelector(
|
||||||
|
(state) => state.meshtastic.radio.preferences,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { register, handleSubmit, formState, reset, control } =
|
||||||
|
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||||
|
defaultValues: preferences,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset(preferences);
|
||||||
|
}, [reset, preferences]);
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit(async (data) => {
|
||||||
|
setLoading(true);
|
||||||
|
await connection.setPreferences(data, async (): Promise<void> => {
|
||||||
|
reset({ ...data });
|
||||||
|
setLoading(false);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const moduleEnabled = useWatch({
|
||||||
|
control,
|
||||||
|
name: 'storeForwardModuleEnabled',
|
||||||
|
defaultValue: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||||
|
<Checkbox
|
||||||
|
label="Module Enabled"
|
||||||
|
{...register('storeForwardModuleEnabled')}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Heartbeat Enabled"
|
||||||
|
disabled={!moduleEnabled}
|
||||||
|
{...register('storeForwardModuleHeartbeat')}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
label="Number of records"
|
||||||
|
suffix="Records"
|
||||||
|
disabled={!moduleEnabled}
|
||||||
|
{...register('storeForwardModuleRecords', {
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
label="History return max"
|
||||||
|
disabled={!moduleEnabled}
|
||||||
|
{...register('storeForwardModuleHistoryReturnMax', {
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
label="History return window"
|
||||||
|
disabled={!moduleEnabled}
|
||||||
|
{...register('storeForwardModuleHistoryReturnWindow', {
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||||
|
import { Form } from '@components/generic/form/Form';
|
||||||
|
import { Input } from '@components/generic/form/Input';
|
||||||
|
import { Select } from '@components/generic/form/Select';
|
||||||
|
import { connection } from '@core/connection';
|
||||||
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
|
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
export const Telemetry = (): JSX.Element => {
|
||||||
|
const preferences = useAppSelector(
|
||||||
|
(state) => state.meshtastic.radio.preferences,
|
||||||
|
);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { register, handleSubmit, formState, reset, control } =
|
||||||
|
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||||
|
defaultValues: preferences,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset(preferences);
|
||||||
|
}, [reset, preferences]);
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit((data) => {
|
||||||
|
setLoading(true);
|
||||||
|
void connection.setPreferences(data, async () => {
|
||||||
|
reset({ ...data });
|
||||||
|
setLoading(false);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||||
|
<Checkbox
|
||||||
|
label="Measurement Enabled"
|
||||||
|
{...register('telemetryModuleMeasurementEnabled')}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Displayed on Screen"
|
||||||
|
{...register('telemetryModuleScreenEnabled')}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Read Error Count Threshold"
|
||||||
|
type="number"
|
||||||
|
{...register('telemetryModuleReadErrorCountThreshold', {
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Update Interval"
|
||||||
|
type="number"
|
||||||
|
{...register('telemetryModuleUpdateInterval', {
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Recovery Interval"
|
||||||
|
type="number"
|
||||||
|
{...register('telemetryModuleRecoveryInterval', {
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Display Farenheit"
|
||||||
|
{...register('telemetryModuleDisplayFarenheit')}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Sensor Type"
|
||||||
|
optionsEnum={Protobuf.RadioConfig_UserPreferences_TelemetrySensorType}
|
||||||
|
{...register('telemetryModuleSensorType', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Sensor Pin"
|
||||||
|
type="number"
|
||||||
|
{...register('telemetryModuleSensorPin', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import type React from 'react';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
|
||||||
import { FiSave } from 'react-icons/fi';
|
|
||||||
|
|
||||||
import { IconButton } from '@components/generic/button/IconButton';
|
|
||||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
|
||||||
import { Form } from '@components/generic/form/Form';
|
|
||||||
import { Input } from '@components/generic/form/Input';
|
|
||||||
import { connection } from '@core/connection';
|
|
||||||
import { useAppSelector } from '@hooks/useAppSelector';
|
|
||||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
|
||||||
|
|
||||||
export const ExternalNotificationsSettingsPlanel = (): JSX.Element => {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const preferences = useAppSelector(
|
|
||||||
(state) => state.meshtastic.radio.preferences,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { register, handleSubmit, formState, reset, control } =
|
|
||||||
useForm<Protobuf.RadioConfig_UserPreferences>({
|
|
||||||
defaultValues: preferences,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reset(preferences);
|
|
||||||
}, [reset, preferences]);
|
|
||||||
|
|
||||||
const onSubmit = handleSubmit(async (data) => {
|
|
||||||
setLoading(true);
|
|
||||||
await connection.setPreferences(data, async (): Promise<void> => {
|
|
||||||
reset({ ...data });
|
|
||||||
setLoading(false);
|
|
||||||
await Promise.resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const pluginEnabled = useWatch({
|
|
||||||
control,
|
|
||||||
name: 'extNotificationPluginEnabled',
|
|
||||||
defaultValue: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Form loading={loading}>
|
|
||||||
<Checkbox
|
|
||||||
label="Plugin Enabled"
|
|
||||||
{...register('extNotificationPluginEnabled')}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label="Active"
|
|
||||||
disabled={!pluginEnabled}
|
|
||||||
{...register('extNotificationPluginActive')}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label="Bell"
|
|
||||||
disabled={!pluginEnabled}
|
|
||||||
{...register('extNotificationPluginAlertBell')}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label="Message"
|
|
||||||
disabled={!pluginEnabled}
|
|
||||||
{...register('extNotificationPluginAlertMessage')}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
label="Output"
|
|
||||||
disabled={!pluginEnabled}
|
|
||||||
{...register('extNotificationPluginOutput', {
|
|
||||||
valueAsNumber: true,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
label="Output MS"
|
|
||||||
suffix="ms"
|
|
||||||
disabled={!pluginEnabled}
|
|
||||||
{...register('extNotificationPluginOutputMs', {
|
|
||||||
valueAsNumber: true,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Form>
|
|
||||||
<div className="flex w-full bg-white dark:bg-secondaryDark">
|
|
||||||
<div className="ml-auto p-2">
|
|
||||||
<IconButton
|
|
||||||
disabled={!formState.isDirty}
|
|
||||||
onClick={async (): Promise<void> => {
|
|
||||||
await onSubmit();
|
|
||||||
}}
|
|
||||||
icon={<FiSave />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import type React from 'react';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
|
||||||
import { FiSave } from 'react-icons/fi';
|
|
||||||
|
|
||||||
import { IconButton } from '@components/generic/button/IconButton';
|
|
||||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
|
||||||
import { Form } from '@components/generic/form/Form';
|
|
||||||
import { Input } from '@components/generic/form/Input';
|
|
||||||
import { connection } from '@core/connection';
|
|
||||||
import { useAppSelector } from '@hooks/useAppSelector';
|
|
||||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
|
||||||
|
|
||||||
export const SerialSettingsPanel = (): JSX.Element => {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const preferences = useAppSelector(
|
|
||||||
(state) => state.meshtastic.radio.preferences,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { register, handleSubmit, formState, reset, control } =
|
|
||||||
useForm<Protobuf.RadioConfig_UserPreferences>({
|
|
||||||
defaultValues: preferences,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reset(preferences);
|
|
||||||
}, [reset, preferences]);
|
|
||||||
|
|
||||||
const onSubmit = handleSubmit(async (data) => {
|
|
||||||
setLoading(true);
|
|
||||||
await connection.setPreferences(data, async (): Promise<void> => {
|
|
||||||
reset({ ...data });
|
|
||||||
setLoading(false);
|
|
||||||
await Promise.resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const pluginEnabled = useWatch({
|
|
||||||
control,
|
|
||||||
name: 'serialpluginEnabled',
|
|
||||||
defaultValue: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Form loading={loading}>
|
|
||||||
<Checkbox label="Plugin Enabled" {...register('serialpluginEnabled')} />
|
|
||||||
<Checkbox
|
|
||||||
label="Echo"
|
|
||||||
disabled={!pluginEnabled}
|
|
||||||
{...register('serialpluginEcho')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
label="RX"
|
|
||||||
disabled={!pluginEnabled}
|
|
||||||
{...register('serialpluginRxd', {
|
|
||||||
valueAsNumber: true,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
label="TX"
|
|
||||||
disabled={!pluginEnabled}
|
|
||||||
{...register('serialpluginTxd', {
|
|
||||||
valueAsNumber: true,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
label="Mode"
|
|
||||||
disabled={!pluginEnabled}
|
|
||||||
{...register('serialpluginMode', {
|
|
||||||
valueAsNumber: true,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
label="Timeout"
|
|
||||||
disabled={!pluginEnabled}
|
|
||||||
{...register('serialpluginTimeout', {
|
|
||||||
valueAsNumber: true,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Form>
|
|
||||||
<div className="flex w-full bg-white dark:bg-secondaryDark">
|
|
||||||
<div className="ml-auto p-2">
|
|
||||||
<IconButton
|
|
||||||
disabled={!formState.isDirty}
|
|
||||||
onClick={async (): Promise<void> => {
|
|
||||||
await onSubmit();
|
|
||||||
}}
|
|
||||||
icon={<FiSave />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import type React from 'react';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
|
||||||
import { FiSave } from 'react-icons/fi';
|
|
||||||
|
|
||||||
import { IconButton } from '@components/generic/button/IconButton';
|
|
||||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
|
||||||
import { Form } from '@components/generic/form/Form';
|
|
||||||
import { Input } from '@components/generic/form/Input';
|
|
||||||
import { connection } from '@core/connection';
|
|
||||||
import { useAppSelector } from '@hooks/useAppSelector';
|
|
||||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
|
||||||
|
|
||||||
export const StoreForwardSettingsPanel = (): JSX.Element => {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const preferences = useAppSelector(
|
|
||||||
(state) => state.meshtastic.radio.preferences,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { register, handleSubmit, formState, reset, control } =
|
|
||||||
useForm<Protobuf.RadioConfig_UserPreferences>({
|
|
||||||
defaultValues: preferences,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reset(preferences);
|
|
||||||
}, [reset, preferences]);
|
|
||||||
|
|
||||||
const onSubmit = handleSubmit(async (data) => {
|
|
||||||
setLoading(true);
|
|
||||||
await connection.setPreferences(data, async (): Promise<void> => {
|
|
||||||
reset({ ...data });
|
|
||||||
setLoading(false);
|
|
||||||
await Promise.resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const pluginEnabled = useWatch({
|
|
||||||
control,
|
|
||||||
name: 'storeForwardPluginEnabled',
|
|
||||||
defaultValue: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Form loading={loading}>
|
|
||||||
<Checkbox
|
|
||||||
label="Plugin Enabled"
|
|
||||||
{...register('storeForwardPluginEnabled')}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label="Heartbeat Enabled"
|
|
||||||
disabled={!pluginEnabled}
|
|
||||||
{...register('storeForwardPluginHeartbeat')}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
label="Number of records"
|
|
||||||
suffix="Records"
|
|
||||||
disabled={!pluginEnabled}
|
|
||||||
{...register('storeForwardPluginRecords', {
|
|
||||||
valueAsNumber: true,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
label="History return max"
|
|
||||||
disabled={!pluginEnabled}
|
|
||||||
{...register('storeForwardPluginHistoryReturnMax', {
|
|
||||||
valueAsNumber: true,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
label="History return window"
|
|
||||||
disabled={!pluginEnabled}
|
|
||||||
{...register('storeForwardPluginHistoryReturnWindow', {
|
|
||||||
valueAsNumber: true,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Form>
|
|
||||||
<div className="flex w-full bg-white dark:bg-secondaryDark">
|
|
||||||
<div className="ml-auto p-2">
|
|
||||||
<IconButton
|
|
||||||
disabled={!formState.isDirty}
|
|
||||||
onClick={async (): Promise<void> => {
|
|
||||||
await onSubmit();
|
|
||||||
}}
|
|
||||||
icon={<FiSave />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -6,13 +6,12 @@ import { FiMessageCircle, FiSettings } from 'react-icons/fi';
|
|||||||
import { RiMindMap, RiRoadMapLine } from 'react-icons/ri';
|
import { RiMindMap, RiRoadMapLine } from 'react-icons/ri';
|
||||||
import { VscExtensions } from 'react-icons/vsc';
|
import { VscExtensions } from 'react-icons/vsc';
|
||||||
|
|
||||||
import { routes, useRoute } from '@app/core/router';
|
import { ErrorFallback } from '@components/ErrorFallback';
|
||||||
import { IconButton } from '@components/generic/button/IconButton';
|
import { IconButton } from '@components/generic/button/IconButton';
|
||||||
import { Sidebar } from '@components/layout/Sidebar';
|
import { Sidebar } from '@components/layout/Sidebar';
|
||||||
|
import type { TabProps } from '@components/Tab';
|
||||||
import { ErrorFallback } from '../ErrorFallback';
|
import { Tabs } from '@components/Tabs';
|
||||||
import type { TabProps } from '../Tab';
|
import { routes, useRoute } from '@core/router';
|
||||||
import { Tabs } from '../Tabs';
|
|
||||||
|
|
||||||
export interface LayoutProps {
|
export interface LayoutProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -59,7 +58,7 @@ export const Layout = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex w-full bg-white dark:bg-secondaryDark ">
|
<div className="relative flex w-full overflow-hidden bg-white dark:bg-secondaryDark">
|
||||||
<div className="flex flex-grow">
|
<div className="flex flex-grow">
|
||||||
<Sidebar settingsOpen={settingsOpen} setSettingsOpen={setSettingsOpen}>
|
<Sidebar settingsOpen={settingsOpen} setSettingsOpen={setSettingsOpen}>
|
||||||
<div className="bg-white px-1 pt-1 drop-shadow-md dark:bg-primaryDark">
|
<div className="bg-white px-1 pt-1 drop-shadow-md dark:bg-primaryDark">
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import {
|
|||||||
RiArrowUpLine,
|
RiArrowUpLine,
|
||||||
} from 'react-icons/ri';
|
} from 'react-icons/ri';
|
||||||
|
|
||||||
|
import { BottomNavItem } from '@components/menu/BottomNavItem';
|
||||||
|
import { VersionInfo } from '@components/modals/VersionInfo';
|
||||||
import {
|
import {
|
||||||
connType,
|
connType,
|
||||||
openConnectionModal,
|
openConnectionModal,
|
||||||
@@ -28,9 +30,6 @@ import { useAppDispatch } from '@hooks/useAppDispatch';
|
|||||||
import { useAppSelector } from '@hooks/useAppSelector';
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
import { Protobuf, Types } from '@meshtastic/meshtasticjs';
|
import { Protobuf, Types } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
import { VersionInfo } from '../modals/VersionInfo';
|
|
||||||
import { BottomNavItem } from './BottomNavItem';
|
|
||||||
|
|
||||||
export const BottomNav = (): JSX.Element => {
|
export const BottomNav = (): JSX.Element => {
|
||||||
const [showVersionInfo, setShowVersionInfo] = useState(false);
|
const [showVersionInfo, setShowVersionInfo] = useState(false);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|||||||
@@ -4,14 +4,13 @@ import { useEffect } from 'react';
|
|||||||
import { MdUpgrade } from 'react-icons/md';
|
import { MdUpgrade } from 'react-icons/md';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
import { connectionUrl } from '@app/core/connection.js';
|
import { IconButton } from '@components/generic/button/IconButton';
|
||||||
import { setUpdateAvaliable } from '@app/core/slices/appSlice';
|
|
||||||
import { fetcher } from '@app/core/utils/fetcher';
|
|
||||||
import { useAppDispatch } from '@app/hooks/useAppDispatch';
|
|
||||||
import { useAppSelector } from '@app/hooks/useAppSelector';
|
|
||||||
import { Modal } from '@components/generic/Modal';
|
import { Modal } from '@components/generic/Modal';
|
||||||
|
import { connectionUrl } from '@core/connection';
|
||||||
import { IconButton } from '../generic/button/IconButton';
|
import { setUpdateAvaliable } from '@core/slices/appSlice';
|
||||||
|
import { fetcher } from '@core/utils/fetcher';
|
||||||
|
import { useAppDispatch } from '@hooks/useAppDispatch';
|
||||||
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
|
|
||||||
export interface Commit {
|
export interface Commit {
|
||||||
sha: string;
|
sha: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LngLat } from 'mapbox-gl';
|
import { LngLat } from 'mapbox-gl';
|
||||||
|
|
||||||
import type { MapStyleName } from '@pages/Map/styles';
|
import type { MapStyleName } from '@core/mapStyles';
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
|
||||||
import { Button } from '@app/components/generic/button/Button';
|
import { Button } from '@components/generic/button/Button';
|
||||||
import { Card } from '@app/components/generic/Card';
|
import { Card } from '@components/generic/Card';
|
||||||
import { connection } from '@app/core/connection.js';
|
import { connection } from '@core/connection';
|
||||||
import { useAppSelector } from '@hooks/useAppSelector';
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
|
|
||||||
export const Debug = (): JSX.Element => {
|
export const Debug = (): JSX.Element => {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { AnimatePresence, m } from 'framer-motion';
|
|||||||
import { FiFilePlus } from 'react-icons/fi';
|
import { FiFilePlus } from 'react-icons/fi';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
import { Button } from '@app/components/generic/button/Button';
|
import { Button } from '@components/generic/button/Button';
|
||||||
import { Card } from '@app/components/generic/Card';
|
import { Card } from '@components/generic/Card';
|
||||||
import { fetcher } from '@core/utils/fetcher';
|
import { fetcher } from '@core/utils/fetcher';
|
||||||
import { useAppSelector } from '@hooks/useAppSelector';
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ import { VscDebug, VscExtensions } from 'react-icons/vsc';
|
|||||||
|
|
||||||
import { ExternalSection } from '@components/generic/Sidebar/ExternalSection';
|
import { ExternalSection } from '@components/generic/Sidebar/ExternalSection';
|
||||||
import { Layout } from '@components/layout';
|
import { Layout } from '@components/layout';
|
||||||
|
import { Debug } from '@pages/Extensions/Debug';
|
||||||
import { FileBrowser } from '@pages/Extensions/FileBrowser';
|
import { FileBrowser } from '@pages/Extensions/FileBrowser';
|
||||||
import { Info } from '@pages/Extensions/Info';
|
import { Info } from '@pages/Extensions/Info';
|
||||||
import { Logs } from '@pages/Extensions/Logs';
|
import { Logs } from '@pages/Extensions/Logs';
|
||||||
|
|
||||||
import { Debug } from './Debug';
|
|
||||||
|
|
||||||
export const Extensions = (): JSX.Element => {
|
export const Extensions = (): JSX.Element => {
|
||||||
const [selectedExtension, setSelectedExtension] = useState<
|
const [selectedExtension, setSelectedExtension] = useState<
|
||||||
'info' | 'logs' | 'fileBrowser' | 'rangeTest' | 'debug'
|
'info' | 'logs' | 'fileBrowser' | 'rangeTest' | 'debug'
|
||||||
@@ -24,7 +23,7 @@ export const Extensions = (): JSX.Element => {
|
|||||||
title="Extensions"
|
title="Extensions"
|
||||||
icon={<VscExtensions />}
|
icon={<VscExtensions />}
|
||||||
sidebarContents={
|
sidebarContents={
|
||||||
<div className="absolute flex h-full w-full flex-col dark:bg-primaryDark">
|
<div className="absolute flex w-full flex-col dark:bg-primaryDark">
|
||||||
<ExternalSection
|
<ExternalSection
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
setSelectedExtension('info');
|
setSelectedExtension('info');
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import type React from 'react';
|
|||||||
import { FiRefreshCw } from 'react-icons/fi';
|
import { FiRefreshCw } from 'react-icons/fi';
|
||||||
import JSONPretty from 'react-json-pretty';
|
import JSONPretty from 'react-json-pretty';
|
||||||
|
|
||||||
import { IconButton } from '@app/components/generic/button/IconButton';
|
import { IconButton } from '@components/generic/button/IconButton';
|
||||||
import { Card } from '@app/components/generic/Card';
|
import { Card } from '@components/generic/Card';
|
||||||
import { CopyButton } from '@app/components/menu/buttons/CopyButton';
|
import { CopyButton } from '@components/menu/buttons/CopyButton';
|
||||||
import { Hashicon } from '@emeraldpay/hashicon-react';
|
import { Hashicon } from '@emeraldpay/hashicon-react';
|
||||||
import { useAppSelector } from '@hooks/useAppSelector';
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import type React from 'react';
|
|||||||
import { AnimatePresence, m } from 'framer-motion';
|
import { AnimatePresence, m } from 'framer-motion';
|
||||||
import { FiArrowRight, FiPaperclip, FiTrash } from 'react-icons/fi';
|
import { FiArrowRight, FiPaperclip, FiTrash } from 'react-icons/fi';
|
||||||
|
|
||||||
import { IconButton } from '@app/components/generic/button/IconButton';
|
import { IconButton } from '@components/generic/button/IconButton';
|
||||||
import { Card } from '@app/components/generic/Card';
|
import { Card } from '@components/generic/Card';
|
||||||
import { clearLogs } from '@app/core/slices/meshtasticSlice';
|
import { clearLogs } from '@core/slices/meshtasticSlice';
|
||||||
import { useAppDispatch } from '@app/hooks/useAppDispatch';
|
import { useAppDispatch } from '@hooks/useAppDispatch';
|
||||||
import { useAppSelector } from '@hooks/useAppSelector';
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
import { Protobuf, Types } from '@meshtastic/meshtasticjs';
|
import { Protobuf, Types } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ import { IoTelescope } from 'react-icons/io5';
|
|||||||
import { MdGpsFixed, MdGpsNotFixed, MdGpsOff } from 'react-icons/md';
|
import { MdGpsFixed, MdGpsNotFixed, MdGpsOff } from 'react-icons/md';
|
||||||
import JSONPretty from 'react-json-pretty';
|
import JSONPretty from 'react-json-pretty';
|
||||||
|
|
||||||
import { Tooltip } from '@app/components/generic/Tooltip';
|
|
||||||
import { IconButton } from '@components/generic/button/IconButton';
|
import { IconButton } from '@components/generic/button/IconButton';
|
||||||
import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection';
|
import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection';
|
||||||
import { SidebarOverlay } from '@components/generic/Sidebar/SidebarOverlay';
|
import { SidebarOverlay } from '@components/generic/Sidebar/SidebarOverlay';
|
||||||
|
import { Tooltip } from '@components/generic/Tooltip';
|
||||||
import { SidebarItem } from '@components/layout/Sidebar/SidebarItem';
|
import { SidebarItem } from '@components/layout/Sidebar/SidebarItem';
|
||||||
import { CopyButton } from '@components/menu/buttons/CopyButton';
|
import { CopyButton } from '@components/menu/buttons/CopyButton';
|
||||||
import type { Node } from '@core/slices/meshtasticSlice';
|
import type { Node } from '@core/slices/meshtasticSlice';
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { BiCrown } from 'react-icons/bi';
|
|||||||
import { FiSettings } from 'react-icons/fi';
|
import { FiSettings } from 'react-icons/fi';
|
||||||
import { RiMindMap } from 'react-icons/ri';
|
import { RiMindMap } from 'react-icons/ri';
|
||||||
|
|
||||||
import { Tooltip } from '@app/components/generic/Tooltip';
|
|
||||||
import { IconButton } from '@components/generic/button/IconButton';
|
import { IconButton } from '@components/generic/button/IconButton';
|
||||||
|
import { Tooltip } from '@components/generic/Tooltip';
|
||||||
import { Layout } from '@components/layout';
|
import { Layout } from '@components/layout';
|
||||||
import { SidebarItem } from '@components/layout/Sidebar/SidebarItem';
|
import { SidebarItem } from '@components/layout/Sidebar/SidebarItem';
|
||||||
import { Hashicon } from '@emeraldpay/hashicon-react';
|
import { Hashicon } from '@emeraldpay/hashicon-react';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
|
||||||
import { Card } from '@app/components/generic/Card';
|
import { Card } from '@components/generic/Card';
|
||||||
|
|
||||||
export const NotFound = (): JSX.Element => {
|
export const NotFound = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user