Browse Source

Consolidate forms & other minor fixes

pull/21/head
Sacha Weatherstone 4 years ago
parent
commit
ea5ab9376f
  1. 15
      package.json
  2. 474
      pnpm-lock.yaml
  3. 25
      src/App.tsx
  4. 2
      src/components/Tabs.tsx
  5. 57
      src/components/generic/Card.tsx
  6. 6
      src/components/generic/Modal.tsx
  7. 23
      src/components/generic/Sidebar/CollapsibleSection.tsx
  8. 20
      src/components/generic/form/Form.tsx
  9. 18
      src/components/layout/Sidebar/Settings/Channels.tsx
  10. 41
      src/components/layout/Sidebar/Settings/Device.tsx
  11. 56
      src/components/layout/Sidebar/Settings/Display.tsx
  12. 131
      src/components/layout/Sidebar/Settings/GPS.tsx
  13. 156
      src/components/layout/Sidebar/Settings/Index.tsx
  14. 65
      src/components/layout/Sidebar/Settings/LoRa.tsx
  15. 158
      src/components/layout/Sidebar/Settings/Position.tsx
  16. 103
      src/components/layout/Sidebar/Settings/Power.tsx
  17. 112
      src/components/layout/Sidebar/Settings/User.tsx
  18. 72
      src/components/layout/Sidebar/Settings/WiFi.tsx
  19. 108
      src/components/layout/Sidebar/Settings/channels/Channels.tsx
  20. 4
      src/components/layout/Sidebar/Settings/channels/ChannelsGroup.tsx
  21. 99
      src/components/layout/Sidebar/Settings/modules/CannedMessage.tsx
  22. 84
      src/components/layout/Sidebar/Settings/modules/ExternalNotifications.tsx
  23. 68
      src/components/layout/Sidebar/Settings/modules/MQTT.tsx
  24. 59
      src/components/layout/Sidebar/Settings/modules/RangeTest.tsx
  25. 95
      src/components/layout/Sidebar/Settings/modules/Serial.tsx
  26. 82
      src/components/layout/Sidebar/Settings/modules/StoreForward.tsx
  27. 83
      src/components/layout/Sidebar/Settings/modules/Telemetry.tsx
  28. 99
      src/components/layout/Sidebar/Settings/plugins/ExternalNotifications.tsx
  29. 102
      src/components/layout/Sidebar/Settings/plugins/Serial.tsx
  30. 97
      src/components/layout/Sidebar/Settings/plugins/StoreForward.tsx
  31. 11
      src/components/layout/index.tsx
  32. 5
      src/components/menu/BottomNav.tsx
  33. 13
      src/components/modals/VersionInfo.tsx
  34. 2
      src/core/slices/mapSlice.ts
  35. 6
      src/pages/Extensions/Debug.tsx
  36. 4
      src/pages/Extensions/FileBrowser.tsx
  37. 5
      src/pages/Extensions/Index.tsx
  38. 6
      src/pages/Extensions/Info.tsx
  39. 8
      src/pages/Extensions/Logs.tsx
  40. 2
      src/pages/Nodes/NodeCard.tsx
  41. 2
      src/pages/Nodes/index.tsx
  42. 2
      src/pages/NotFound.tsx

15
package.json

@ -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"
} }
} }

474
pnpm-lock.yaml

File diff suppressed because it is too large

25
src/App.tsx

@ -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>
); );
}; };

2
src/components/Tabs.tsx

@ -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'>[];

57
src/components/generic/Card.tsx

@ -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>
); );
}; };

6
src/components/generic/Modal.tsx

@ -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}

23
src/components/generic/Sidebar/CollapsibleSection.tsx

@ -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

20
src/components/generic/form/Form.tsx

@ -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>
); );
}; };

18
src/components/layout/Sidebar/Settings/Channels.tsx

@ -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>
</> </>
)} )}
</> </>

41
src/components/layout/Sidebar/Settings/Radio.tsx → src/components/layout/Sidebar/Settings/Device.tsx

@ -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>
</>
); );
}; };

56
src/components/layout/Sidebar/Settings/Display.tsx

@ -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>
);
};

131
src/components/layout/Sidebar/Settings/GPS.tsx

@ -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>
);
};

156
src/components/layout/Sidebar/Settings/Index.tsx

@ -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>
<CollapsibleSection <CollapsibleSection
title="External Notifications" icon={<FiAlignLeft />}
icon={ title="Serial"
<div className="flex gap-2"> status={serialmoduleEnabled}
<FiBell />
<div
className={`h-3 w-3 rounded-full ${
extNotificationPluginEnabled ? 'bg-green-500' : 'bg-red-500'
}`}
/>
</div>
}
> >
<ExternalNotificationsSettingsPlanel /> <SerialSettingsPanel />
</CollapsibleSection> </CollapsibleSection>
<CollapsibleSection <CollapsibleSection
title="Serial" icon={<FiBell />}
icon={ title="External Notifications"
<div className="flex gap-2"> status={extNotificationModuleEnabled}
<FiAlignLeft />
<div
className={`h-3 w-3 rounded-full ${
serialpluginEnabled ? 'bg-green-500' : 'bg-red-500'
}`}
/>
</div>
}
> >
<SerialSettingsPanel /> <ExternalNotificationsSettingsPlanel />
</CollapsibleSection> </CollapsibleSection>
<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

65
src/components/layout/Sidebar/Settings/LoRa.tsx

@ -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>
);
};

158
src/components/layout/Sidebar/Settings/Position.tsx

@ -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>
</>
);
};

103
src/components/layout/Sidebar/Settings/Power.tsx

@ -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>
); );
}; };

112
src/components/layout/Sidebar/Settings/User.tsx

@ -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>
</>
); );
}; };

72
src/components/layout/Sidebar/Settings/WiFi.tsx

@ -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>
</>
); );
}; };

108
src/components/layout/Sidebar/Settings/channels/Channels.tsx

@ -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
label="Enabled"
{...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={
<> <>
<Checkbox <IconButton
label="Enabled" onClick={(): void => {
{...register('enabled', { valueAsNumber: true })} setPskHidden(!pskHidden);
}}
icon={pskHidden ? <MdVisibility /> : <MdVisibilityOff />}
/>
<IconButton
onClick={(): void => {
const key = new Uint8Array(keySize);
crypto.getRandomValues(key);
setValue('psk', fromByteArray(key));
}}
icon={<MdRefresh />}
/> />
<Input label="Name" {...register('name')} />
</> </>
)} }
{...register('psk')}
<Select />
label="Key Size" <Checkbox label="Uplink Enabled" {...register('uplinkEnabled')} />
options={[ <Checkbox label="Downlink Enabled" {...register('downlinkEnabled')} />
{ name: '128 Bit', value: 128 }, </Form>
{ 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>
</div>
</div>
); );
}; };

4
src/components/layout/Sidebar/Settings/channels/ChannelsGroup.tsx

@ -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>
); );

99
src/components/layout/Sidebar/Settings/modules/CannedMessage.tsx

@ -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>
);
};

84
src/components/layout/Sidebar/Settings/modules/ExternalNotifications.tsx

@ -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>
);
};

68
src/components/layout/Sidebar/Settings/modules/MQTT.tsx

@ -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>
);
};

59
src/components/layout/Sidebar/Settings/plugins/RangeTest.tsx → src/components/layout/Sidebar/Settings/modules/RangeTest.tsx

@ -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>
</>
); );
}; };

95
src/components/layout/Sidebar/Settings/modules/Serial.tsx

@ -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>
);
};

82
src/components/layout/Sidebar/Settings/modules/StoreForward.tsx

@ -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>
);
};

83
src/components/layout/Sidebar/Settings/modules/Telemetry.tsx

@ -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>
);
};

99
src/components/layout/Sidebar/Settings/plugins/ExternalNotifications.tsx

@ -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>
</>
);
};

102
src/components/layout/Sidebar/Settings/plugins/Serial.tsx

@ -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>
</>
);
};

97
src/components/layout/Sidebar/Settings/plugins/StoreForward.tsx

@ -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>
</>
);
};

11
src/components/layout/index.tsx

@ -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">

5
src/components/menu/BottomNav.tsx

@ -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();

13
src/components/modals/VersionInfo.tsx

@ -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;

2
src/core/slices/mapSlice.ts

@ -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';

6
src/pages/Extensions/Debug.tsx

@ -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
src/pages/Extensions/FileBrowser.tsx

@ -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';

5
src/pages/Extensions/Index.tsx

@ -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');

6
src/pages/Extensions/Info.tsx

@ -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';

8
src/pages/Extensions/Logs.tsx

@ -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';

2
src/pages/Nodes/NodeCard.tsx

@ -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';

2
src/pages/Nodes/index.tsx

@ -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';

2
src/pages/NotFound.tsx

@ -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 (

Loading…
Cancel
Save