mirror of
https://github.com/meshtastic/web.git
synced 2026-02-05 21:32:04 -05:00
Consolidate forms & other minor fixes
This commit is contained in:
15
package.json
15
package.json
@@ -4,7 +4,7 @@
|
||||
"description": "Meshtastic web client",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --host",
|
||||
"build": "tsc && vite build",
|
||||
"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/)",
|
||||
@@ -22,7 +22,7 @@
|
||||
"dependencies": {
|
||||
"@emeraldpay/hashicon-react": "^0.5.2",
|
||||
"@meshtastic/eslint-config": "^1.0.6",
|
||||
"@meshtastic/meshtasticjs": "^0.6.48",
|
||||
"@meshtastic/meshtasticjs": "^0.6.50",
|
||||
"@reduxjs/toolkit": "^1.8.0",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"base64-js": "^1.5.1",
|
||||
@@ -31,9 +31,8 @@
|
||||
"prettier": "^2.5.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-draggable": "^4.4.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-icons": "^4.3.1",
|
||||
"react-json-pretty": "^2.2.0",
|
||||
@@ -49,9 +48,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hookform/devtools": "^4.0.2",
|
||||
"@types/mapbox-gl": "^2.6.2",
|
||||
"@types/mapbox-gl": "^2.6.3",
|
||||
"@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/web-bluetooth": "^0.0.12",
|
||||
"@vitejs/plugin-react": "^1.2.0",
|
||||
@@ -63,9 +62,9 @@
|
||||
"tar": "^6.1.11",
|
||||
"typescript": "^4.6.2",
|
||||
"unimported": "^1.19.1",
|
||||
"vite": "^2.8.5",
|
||||
"vite": "^2.8.6",
|
||||
"vite-plugin-cdn-import": "^0.3.5",
|
||||
"vite-plugin-pwa": "^0.11.13",
|
||||
"workbox-window": "^6.5.0"
|
||||
"workbox-window": "^6.5.1"
|
||||
}
|
||||
}
|
||||
|
||||
474
pnpm-lock.yaml
generated
474
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
25
src/App.tsx
25
src/App.tsx
@@ -1,12 +1,11 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { Map } from '@app/pages/Map';
|
||||
import { Connection } from '@components/Connection';
|
||||
import { ContextMenu } from '@components/generic/ContextMenu';
|
||||
import { BottomNav } from '@components/menu/BottomNav';
|
||||
import { useRoute } from '@core/router';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Extensions } from '@pages/Extensions/Index';
|
||||
import { Map } from '@pages/Map';
|
||||
import { Messages } from '@pages/Messages';
|
||||
import { Nodes } from '@pages/Nodes';
|
||||
import { NotFound } from '@pages/NotFound';
|
||||
@@ -17,19 +16,17 @@ export const App = (): JSX.Element => {
|
||||
|
||||
return (
|
||||
<div className={`h-screen w-screen ${appState.darkMode ? 'dark' : ''}`}>
|
||||
<ContextMenu>
|
||||
<Connection />
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex min-h-0 w-full flex-grow">
|
||||
{route.name === 'messages' && <Messages />}
|
||||
{route.name === 'nodes' && <Nodes />}
|
||||
{route.name === 'map' && <Map />}
|
||||
{route.name === 'extensions' && <Extensions />}
|
||||
{route.name === false && <NotFound />}
|
||||
</div>
|
||||
<BottomNav />
|
||||
<Connection />
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex min-h-0 w-full flex-grow">
|
||||
{route.name === 'messages' && <Messages />}
|
||||
{route.name === 'nodes' && <Nodes />}
|
||||
{route.name === 'map' && <Map />}
|
||||
{route.name === 'extensions' && <Extensions />}
|
||||
{route.name === false && <NotFound />}
|
||||
</div>
|
||||
</ContextMenu>
|
||||
<BottomNav />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { Tab, TabProps } from './Tab';
|
||||
import { Tab, TabProps } from '@components/Tab';
|
||||
|
||||
export interface TabsProps {
|
||||
tabs: Omit<TabProps, 'activeLeft' | 'activeRight'>[];
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { m } from 'framer-motion';
|
||||
import Draggable from 'react-draggable';
|
||||
|
||||
export interface CardProps {
|
||||
className?: string;
|
||||
@@ -9,49 +8,41 @@ export interface CardProps {
|
||||
actions?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
border?: boolean;
|
||||
draggable?: boolean;
|
||||
}
|
||||
|
||||
export const Card = ({
|
||||
className,
|
||||
title,
|
||||
actions,
|
||||
draggable,
|
||||
border,
|
||||
children,
|
||||
}: CardProps): JSX.Element => {
|
||||
return (
|
||||
<Draggable handle=".handle" disabled={!draggable}>
|
||||
<div
|
||||
className={`flex h-full w-full flex-col rounded-md drop-shadow-md ${
|
||||
border ? 'border border-gray-400 dark:border-gray-600' : ''
|
||||
} ${className ?? ''}`}
|
||||
>
|
||||
{(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 ${
|
||||
draggable ? 'cursor-move' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="handle flex h-8 justify-between">
|
||||
<div className="my-auto ml-2 truncate">{title}</div>
|
||||
{actions}
|
||||
</div>
|
||||
<div
|
||||
className={`flex h-full w-full flex-col rounded-md drop-shadow-md ${
|
||||
border ? 'border border-gray-400 dark:border-gray-600' : ''
|
||||
} ${className ?? ''}`}
|
||||
>
|
||||
{(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 className="handle flex h-8 justify-between">
|
||||
<div className="my-auto ml-2 truncate">{title}</div>
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<m.div
|
||||
className={`flex flex-grow select-none flex-col gap-4 bg-white p-4 dark:bg-primaryDark ${
|
||||
title || actions ? 'rounded-b-md' : 'rounded-md'
|
||||
}`}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
>
|
||||
{children}
|
||||
</m.div>
|
||||
</div>
|
||||
</Draggable>
|
||||
<m.div
|
||||
className={`flex flex-grow select-none flex-col gap-4 bg-white p-4 dark:bg-primaryDark ${
|
||||
title || actions ? 'rounded-b-md' : 'rounded-md'
|
||||
}`}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
>
|
||||
{children}
|
||||
</m.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,11 +3,10 @@ import type React from 'react';
|
||||
import { AnimatePresence, m } from 'framer-motion';
|
||||
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 { IconButton } from './button/IconButton';
|
||||
import { Card, CardProps } from './Card';
|
||||
|
||||
export interface ModalProps extends CardProps {
|
||||
open: boolean;
|
||||
bgDismiss?: boolean;
|
||||
@@ -51,7 +50,6 @@ export const Modal = ({
|
||||
<div className="inline-block w-full max-w-3xl align-middle">
|
||||
<Card
|
||||
border
|
||||
draggable
|
||||
actions={
|
||||
<div className="flex gap-2">
|
||||
{actions}
|
||||
|
||||
@@ -7,12 +7,14 @@ import { FiArrowUp } from 'react-icons/fi';
|
||||
export interface CollapsibleSectionProps {
|
||||
title: string;
|
||||
icon?: JSX.Element;
|
||||
status?: boolean;
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
export const CollapsibleSection = ({
|
||||
title,
|
||||
icon,
|
||||
status,
|
||||
children,
|
||||
}: CollapsibleSectionProps): JSX.Element => {
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -21,6 +23,7 @@ export const CollapsibleSection = ({
|
||||
<m.div>
|
||||
<m.div
|
||||
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 ${
|
||||
open
|
||||
? 'border-l-primary dark:border-l-primary'
|
||||
@@ -29,13 +32,25 @@ export const CollapsibleSection = ({
|
||||
>
|
||||
<m.div
|
||||
layout
|
||||
onClick={toggleOpen}
|
||||
whileHover={{ scale: 1.01 }}
|
||||
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="my-auto">{icon}</m.div>
|
||||
<m.div className="flex gap-2">
|
||||
<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}
|
||||
</m.div>
|
||||
<m.div
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { FiSave } from 'react-icons/fi';
|
||||
|
||||
import { IconButton } from '@components/generic/button/IconButton';
|
||||
import { Loading } from '@components/generic/Loading';
|
||||
|
||||
export interface FormProps {
|
||||
loading?: boolean;
|
||||
submit: () => Promise<void>;
|
||||
loading: boolean;
|
||||
dirty: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Form = ({ loading, children }: FormProps): JSX.Element => {
|
||||
export const Form = ({
|
||||
submit,
|
||||
loading,
|
||||
dirty,
|
||||
children,
|
||||
}: FormProps): JSX.Element => {
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e): void => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
className="relative flex-grow gap-3 p-2"
|
||||
>
|
||||
{loading && <Loading />}
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,10 +2,9 @@ import type React from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
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 { 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';
|
||||
@@ -59,7 +58,7 @@ export const Channels = (): JSX.Element => {
|
||||
label="Use Presets"
|
||||
onChange={(e): void => setUsePreset(e.target.checked)}
|
||||
/>
|
||||
<form onSubmit={onSubmit}>
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
{usePreset ? (
|
||||
<Select
|
||||
label="Preset"
|
||||
@@ -103,18 +102,7 @@ export const Channels = (): JSX.Element => {
|
||||
suffix="dBm"
|
||||
{...register('settings.txPower', { 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>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -2,16 +2,15 @@ import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
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 { Form } from '@components/generic/form/Form';
|
||||
import { Select } from '@components/generic/form/Select';
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const Radio = (): JSX.Element => {
|
||||
export const Device = (): JSX.Element => {
|
||||
const preferences = useAppSelector(
|
||||
(state) => state.meshtastic.radio.preferences,
|
||||
);
|
||||
@@ -34,28 +33,18 @@ export const Radio = (): JSX.Element => {
|
||||
});
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<form className="space-y-2" onSubmit={onSubmit}>
|
||||
<Checkbox label="Is Router" {...register('isRouter')} />
|
||||
<Select
|
||||
label="Region"
|
||||
optionsEnum={Protobuf.RegionCode}
|
||||
{...register('region', { valueAsNumber: true })}
|
||||
/>
|
||||
<Checkbox label="Debug Log" {...register('debugLogEnabled')} />
|
||||
<Checkbox label="Serial Disabled" {...register('serialDisabled')} />
|
||||
</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>
|
||||
</>
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
<Checkbox
|
||||
label="Serial Console Disabled"
|
||||
{...register('serialDisabled')}
|
||||
/>
|
||||
<Checkbox label="Factory Reset Device" {...register('factoryReset')} />
|
||||
<Checkbox label="Debug Log Enabled" {...register('debugLogEnabled')} />
|
||||
<Select
|
||||
label="Role"
|
||||
optionsEnum={Protobuf.Role}
|
||||
{...register('role', { valueAsNumber: true })}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
56
src/components/layout/Sidebar/Settings/Display.tsx
Normal file
56
src/components/layout/Sidebar/Settings/Display.tsx
Normal file
@@ -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
Normal file
131
src/components/layout/Sidebar/Settings/GPS.tsx
Normal file
@@ -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 {
|
||||
FiActivity,
|
||||
FiAlignLeft,
|
||||
FiBell,
|
||||
FiFastForward,
|
||||
FiLayers,
|
||||
FiLayout,
|
||||
FiMapPin,
|
||||
FiMessageSquare,
|
||||
FiPackage,
|
||||
FiRadio,
|
||||
FiPower,
|
||||
FiRss,
|
||||
FiSmartphone,
|
||||
FiTv,
|
||||
FiUser,
|
||||
FiWifi,
|
||||
FiZap,
|
||||
} from 'react-icons/fi';
|
||||
|
||||
import { useAppSelector } from '@app/hooks/useAppSelector.js';
|
||||
import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection';
|
||||
import { ExternalSection } from '@components/generic/Sidebar/ExternalSection';
|
||||
import { SidebarOverlay } from '@components/generic/Sidebar/SidebarOverlay';
|
||||
import { Channels } from '@components/layout/Sidebar/Settings/Channels';
|
||||
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 { ExternalNotificationsSettingsPlanel } from '@components/layout/Sidebar/Settings/plugins/ExternalNotifications';
|
||||
import { RangeTestSettingsPanel } from '@components/layout/Sidebar/Settings/plugins/RangeTest';
|
||||
import { SerialSettingsPanel } from '@components/layout/Sidebar/Settings/plugins/Serial';
|
||||
import { StoreForwardSettingsPanel } from '@components/layout/Sidebar/Settings/plugins/StoreForward';
|
||||
import { Position } from '@components/layout/Sidebar/Settings/Position';
|
||||
import { LoRa } from '@components/layout/Sidebar/Settings/LoRa';
|
||||
import { CannedMessage } from '@components/layout/Sidebar/Settings/modules/CannedMessage';
|
||||
import { ExternalNotificationsSettingsPlanel } from '@components/layout/Sidebar/Settings/modules/ExternalNotifications';
|
||||
import { MQTT } from '@components/layout/Sidebar/Settings/modules/MQTT';
|
||||
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 { Radio } from '@components/layout/Sidebar/Settings/Radio';
|
||||
import { User } from '@components/layout/Sidebar/Settings/User';
|
||||
import { WiFi } from '@components/layout/Sidebar/Settings/WiFi';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
|
||||
export interface SettingsProps {
|
||||
open: boolean;
|
||||
@@ -39,13 +46,15 @@ export interface SettingsProps {
|
||||
}
|
||||
|
||||
export const Settings = ({ open, setOpen }: SettingsProps): JSX.Element => {
|
||||
const [pluginsOpen, setPluginsOpen] = useState(false);
|
||||
const [modulesOpen, setModulesOpen] = useState(false);
|
||||
const [channelsOpen, setChannelsOpen] = useState(false);
|
||||
const {
|
||||
rangeTestPluginEnabled,
|
||||
extNotificationPluginEnabled,
|
||||
serialpluginEnabled,
|
||||
storeForwardPluginEnabled,
|
||||
rangeTestModuleEnabled,
|
||||
extNotificationModuleEnabled,
|
||||
serialmoduleEnabled,
|
||||
storeForwardModuleEnabled,
|
||||
mqttDisabled,
|
||||
cannedMessageModuleEnabled,
|
||||
} = useAppSelector((state) => state.meshtastic.radio.preferences);
|
||||
|
||||
const hasGps = true;
|
||||
@@ -61,20 +70,26 @@ export const Settings = ({ open, setOpen }: SettingsProps): JSX.Element => {
|
||||
}}
|
||||
direction="y"
|
||||
>
|
||||
<CollapsibleSection icon={<FiWifi />} title="WiFi & MQTT">
|
||||
<WiFi />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection icon={<FiMapPin />} title="Position">
|
||||
<Position />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection icon={<FiUser />} title="User">
|
||||
<User />
|
||||
</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 />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection icon={<FiRadio />} title="Radio">
|
||||
<Radio />
|
||||
<CollapsibleSection icon={<FiWifi />} title="WiFi">
|
||||
<WiFi />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection icon={<FiTv />} title="Display">
|
||||
<Display />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection icon={<FiRss />} title="LoRa">
|
||||
<LoRa />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection icon={<FiLayers />} title="Primary Channel">
|
||||
<Channels />
|
||||
@@ -88,87 +103,76 @@ export const Settings = ({ open, setOpen }: SettingsProps): JSX.Element => {
|
||||
/>
|
||||
<ExternalSection
|
||||
onClick={(): void => {
|
||||
setPluginsOpen(true);
|
||||
setModulesOpen(true);
|
||||
}}
|
||||
icon={<FiPackage />}
|
||||
title="Plugins"
|
||||
title="Modules"
|
||||
/>
|
||||
<CollapsibleSection icon={<FiLayout />} title="Interface">
|
||||
<Interface />
|
||||
</CollapsibleSection>
|
||||
</SidebarOverlay>
|
||||
|
||||
{/* Plugins */}
|
||||
{/* Modules */}
|
||||
<SidebarOverlay
|
||||
title="Plugins"
|
||||
open={pluginsOpen}
|
||||
title="Modules"
|
||||
open={modulesOpen}
|
||||
close={(): void => {
|
||||
setPluginsOpen(false);
|
||||
setModulesOpen(false);
|
||||
}}
|
||||
direction="x"
|
||||
>
|
||||
<CollapsibleSection
|
||||
title="Range Test"
|
||||
icon={
|
||||
<div className="flex gap-2">
|
||||
<FiRss />
|
||||
<div
|
||||
className={`h-3 w-3 rounded-full ${
|
||||
rangeTestPluginEnabled ? 'bg-green-500' : 'bg-red-500'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
icon={<FiWifi />}
|
||||
title="MQTT"
|
||||
status={!mqttDisabled}
|
||||
>
|
||||
<RangeTestSettingsPanel />
|
||||
</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 />
|
||||
<MQTT />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection
|
||||
icon={<FiAlignLeft />}
|
||||
title="Serial"
|
||||
icon={
|
||||
<div className="flex gap-2">
|
||||
<FiAlignLeft />
|
||||
<div
|
||||
className={`h-3 w-3 rounded-full ${
|
||||
serialpluginEnabled ? 'bg-green-500' : 'bg-red-500'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
status={serialmoduleEnabled}
|
||||
>
|
||||
<SerialSettingsPanel />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection
|
||||
icon={<FiBell />}
|
||||
title="External Notifications"
|
||||
status={extNotificationModuleEnabled}
|
||||
>
|
||||
<ExternalNotificationsSettingsPlanel />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection
|
||||
icon={<FiFastForward />}
|
||||
title="Store & Forward"
|
||||
icon={
|
||||
<div className="flex gap-2">
|
||||
<FiFastForward />
|
||||
<div
|
||||
className={`h-3 w-3 rounded-full ${
|
||||
storeForwardPluginEnabled ? 'bg-green-500' : 'bg-red-500'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
status={storeForwardModuleEnabled}
|
||||
>
|
||||
<StoreForwardSettingsPanel />
|
||||
</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>
|
||||
{/* End Plugins */}
|
||||
{/* End Modules */}
|
||||
|
||||
{/* Channels */}
|
||||
<SidebarOverlay
|
||||
|
||||
65
src/components/layout/Sidebar/Settings/LoRa.tsx
Normal file
65
src/components/layout/Sidebar/Settings/LoRa.tsx
Normal file
@@ -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 { 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 { 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';
|
||||
@@ -37,34 +37,75 @@ export const Power = (): JSX.Element => {
|
||||
});
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<form className="space-y-2" onSubmit={onSubmit}>
|
||||
<Select
|
||||
label="Charge current"
|
||||
optionsEnum={Protobuf.ChargeCurrent}
|
||||
{...register('chargeCurrent', { valueAsNumber: true })}
|
||||
/>
|
||||
<Checkbox label="Always powered" {...register('isAlwaysPowered')} />
|
||||
<Checkbox
|
||||
label="Powered by low power source (solar)"
|
||||
disabled={preferences.isRouter}
|
||||
validationMessage={
|
||||
preferences.isRouter ? 'Enabled by default in router mode' : ''
|
||||
}
|
||||
{...register('isLowPower')}
|
||||
/>
|
||||
</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>
|
||||
</>
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
<Select
|
||||
label="Charge current"
|
||||
optionsEnum={Protobuf.ChargeCurrent}
|
||||
{...register('chargeCurrent', { valueAsNumber: true })}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Powered by low power source (solar)"
|
||||
disabled={preferences.isRouter}
|
||||
validationMessage={
|
||||
preferences.isRouter ? 'Enabled by default in router mode' : ''
|
||||
}
|
||||
{...register('isLowPower')}
|
||||
/>
|
||||
<Checkbox label="Always Powered" {...register('isAlwaysPowered')} />
|
||||
<Input
|
||||
label="Shutdown on battery delay"
|
||||
type="number"
|
||||
suffix="Seconds"
|
||||
{...register('onBatteryShutdownAfterSecs', { valueAsNumber: true })}
|
||||
/>
|
||||
<Checkbox label="Power Saving" {...register('isPowerSaving')} />
|
||||
<Input
|
||||
label="ADC Multiplier Override ratio"
|
||||
type="number"
|
||||
{...register('adcMultiplierOverride', { valueAsNumber: true })}
|
||||
/>
|
||||
<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 { useForm } from 'react-hook-form';
|
||||
import { FiSave } from 'react-icons/fi';
|
||||
import { base16 } from 'rfc4648';
|
||||
|
||||
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 { Select } from '@components/generic/form/Select';
|
||||
import { connection } from '@core/connection';
|
||||
@@ -67,66 +66,53 @@ export const User = (): JSX.Element => {
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<form className="space-y-2" onSubmit={onSubmit}>
|
||||
<Input label="Device ID" value={node?.user?.id} disabled />
|
||||
<Input
|
||||
label="Hardware"
|
||||
value={
|
||||
Protobuf.HardwareModel[
|
||||
node?.user?.hwModel ?? Protobuf.HardwareModel.UNSET
|
||||
]
|
||||
}
|
||||
disabled
|
||||
/>
|
||||
<Input
|
||||
label="Mac Address"
|
||||
defaultValue={
|
||||
base16
|
||||
.stringify(node?.user?.macaddr ?? [])
|
||||
.match(/.{1,2}/g)
|
||||
?.join(':') ?? ''
|
||||
}
|
||||
disabled
|
||||
/>
|
||||
<Input label="Device Name" {...register('longName')} />
|
||||
<Input label="Short Name" maxLength={3} {...register('shortName')} />
|
||||
<Checkbox label="Licenced Operator?" {...register('isLicensed')} />
|
||||
<Select
|
||||
label="Team"
|
||||
optionsEnum={Protobuf.Team}
|
||||
{...register('team', { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
label="Antenna Azimuth"
|
||||
suffix="°"
|
||||
type="number"
|
||||
{...register('antAzimuth', { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
label="Antenna Gain"
|
||||
suffix="dBi"
|
||||
type="number"
|
||||
{...register('antGainDbi', { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
label="Transmit Power"
|
||||
suffix="dBm"
|
||||
type="number"
|
||||
{...register('txPowerDbm', { 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>
|
||||
</>
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
<Input label="Device ID" value={node?.user?.id} disabled />
|
||||
<Input label="Device Name" {...register('longName')} />
|
||||
<Input label="Short Name" maxLength={3} {...register('shortName')} />
|
||||
<Input
|
||||
label="Mac Address"
|
||||
defaultValue={
|
||||
base16
|
||||
.stringify(node?.user?.macaddr ?? [])
|
||||
.match(/.{1,2}/g)
|
||||
?.join(':') ?? ''
|
||||
}
|
||||
disabled
|
||||
/>
|
||||
<Input
|
||||
label="Hardware (DEPRECATED)"
|
||||
value={
|
||||
Protobuf.HardwareModel[
|
||||
node?.user?.hwModel ?? Protobuf.HardwareModel.UNSET
|
||||
]
|
||||
}
|
||||
disabled
|
||||
/>
|
||||
<Checkbox label="Licenced Operator?" {...register('isLicensed')} />
|
||||
<Select
|
||||
label="Team (DEPRECATED)"
|
||||
optionsEnum={Protobuf.Team}
|
||||
{...register('team', { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
label="Transmit Power"
|
||||
suffix="dBm"
|
||||
type="number"
|
||||
{...register('txPowerDbm', { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
label="Antenna Gain"
|
||||
suffix="dBi"
|
||||
type="number"
|
||||
{...register('antGainDbi', { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
label="Antenna Azimuth"
|
||||
suffix="°"
|
||||
type="number"
|
||||
{...register('antAzimuth', { valueAsNumber: true })}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,10 +2,9 @@ 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';
|
||||
@@ -21,18 +20,12 @@ export const WiFi = (): JSX.Element => {
|
||||
defaultValues: preferences,
|
||||
});
|
||||
|
||||
const watchWifiApMode = useWatch({
|
||||
const WifiApMode = useWatch({
|
||||
control,
|
||||
name: 'wifiApMode',
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
const watchMQTTDisabled = useWatch({
|
||||
control,
|
||||
name: 'mqttDisabled',
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset(preferences);
|
||||
}, [reset, preferences]);
|
||||
@@ -46,51 +39,20 @@ export const WiFi = (): JSX.Element => {
|
||||
});
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<form className="space-y-2" onSubmit={onSubmit}>
|
||||
<Checkbox label="Enable WiFi AP" {...register('wifiApMode')} />
|
||||
<Input
|
||||
label="WiFi SSID"
|
||||
disabled={watchWifiApMode}
|
||||
{...register('wifiSsid')}
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
label="WiFi PSK"
|
||||
disabled={watchWifiApMode}
|
||||
{...register('wifiPassword')}
|
||||
/>
|
||||
<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>
|
||||
</>
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
<Checkbox label="Enable WiFi AP" {...register('wifiApMode')} />
|
||||
<Input
|
||||
label="WiFi SSID"
|
||||
disabled={WifiApMode}
|
||||
{...register('wifiSsid')}
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
label="WiFi PSK"
|
||||
disabled={WifiApMode}
|
||||
{...register('wifiPassword')}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useEffect, useState } from 'react';
|
||||
|
||||
import { fromByteArray, toByteArray } from 'base64-js';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { FiSave } from 'react-icons/fi';
|
||||
import { MdRefresh, MdVisibility, MdVisibilityOff } from 'react-icons/md';
|
||||
|
||||
import { IconButton } from '@components/generic/button/IconButton';
|
||||
@@ -18,7 +17,7 @@ export interface SettingsPanelProps {
|
||||
channel: Protobuf.Channel;
|
||||
}
|
||||
|
||||
export const SettingsPanel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
export const Channels = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [keySize, setKeySize] = useState<128 | 256>(256);
|
||||
const [pskHidden, setPskHidden] = useState(true);
|
||||
@@ -75,67 +74,54 @@ export const SettingsPanel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col">
|
||||
<Form loading={loading}>
|
||||
{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={
|
||||
<>
|
||||
<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 />}
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
{channel?.index !== 0 && (
|
||||
<>
|
||||
<Checkbox
|
||||
label="Enabled"
|
||||
{...register('enabled', { valueAsNumber: true })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type React from 'react';
|
||||
|
||||
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 { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
@@ -33,7 +33,7 @@ export const ChannelsGroup = (): JSX.Element => {
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SettingsPanel channel={channel} />
|
||||
<Channels channel={channel} />
|
||||
</CollapsibleSection>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
68
src/components/layout/Sidebar/Settings/modules/MQTT.tsx
Normal file
68
src/components/layout/Sidebar/Settings/modules/MQTT.tsx
Normal file
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -2,9 +2,7 @@ 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';
|
||||
@@ -37,45 +35,32 @@ export const RangeTestSettingsPanel = (): JSX.Element => {
|
||||
});
|
||||
});
|
||||
|
||||
const pluginEnabled = useWatch({
|
||||
const moduleEnabled = useWatch({
|
||||
control,
|
||||
name: 'rangeTestPluginEnabled',
|
||||
name: 'rangeTestModuleEnabled',
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form loading={loading}>
|
||||
<Checkbox
|
||||
label="Range Test Plugin Enabled?"
|
||||
{...register('rangeTestPluginEnabled')}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Range Test Plugin Save?"
|
||||
disabled={!pluginEnabled}
|
||||
{...register('rangeTestPluginSave')}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
label="Message Interval"
|
||||
disabled={!pluginEnabled}
|
||||
suffix="Seconds"
|
||||
{...register('rangeTestPluginSender', {
|
||||
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>
|
||||
</>
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
<Checkbox
|
||||
label="Module Enabled"
|
||||
{...register('rangeTestModuleEnabled')}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
label="Message Interval"
|
||||
disabled={!moduleEnabled}
|
||||
suffix="Seconds"
|
||||
{...register('rangeTestModuleSender', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Save CSV to storage"
|
||||
disabled={!moduleEnabled}
|
||||
{...register('rangeTestModuleSave')}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
95
src/components/layout/Sidebar/Settings/modules/Serial.tsx
Normal file
95
src/components/layout/Sidebar/Settings/modules/Serial.tsx
Normal file
@@ -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>
|
||||
);
|
||||
};
|
||||
83
src/components/layout/Sidebar/Settings/modules/Telemetry.tsx
Normal file
83
src/components/layout/Sidebar/Settings/modules/Telemetry.tsx
Normal file
@@ -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 { 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 { Sidebar } from '@components/layout/Sidebar';
|
||||
|
||||
import { ErrorFallback } from '../ErrorFallback';
|
||||
import type { TabProps } from '../Tab';
|
||||
import { Tabs } from '../Tabs';
|
||||
import type { TabProps } from '@components/Tab';
|
||||
import { Tabs } from '@components/Tabs';
|
||||
import { routes, useRoute } from '@core/router';
|
||||
|
||||
export interface LayoutProps {
|
||||
title: string;
|
||||
@@ -59,7 +58,7 @@ export const Layout = ({
|
||||
];
|
||||
|
||||
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">
|
||||
<Sidebar settingsOpen={settingsOpen} setSettingsOpen={setSettingsOpen}>
|
||||
<div className="bg-white px-1 pt-1 drop-shadow-md dark:bg-primaryDark">
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
RiArrowUpLine,
|
||||
} from 'react-icons/ri';
|
||||
|
||||
import { BottomNavItem } from '@components/menu/BottomNavItem';
|
||||
import { VersionInfo } from '@components/modals/VersionInfo';
|
||||
import {
|
||||
connType,
|
||||
openConnectionModal,
|
||||
@@ -28,9 +30,6 @@ import { useAppDispatch } from '@hooks/useAppDispatch';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Protobuf, Types } from '@meshtastic/meshtasticjs';
|
||||
|
||||
import { VersionInfo } from '../modals/VersionInfo';
|
||||
import { BottomNavItem } from './BottomNavItem';
|
||||
|
||||
export const BottomNav = (): JSX.Element => {
|
||||
const [showVersionInfo, setShowVersionInfo] = useState(false);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -4,14 +4,13 @@ import { useEffect } from 'react';
|
||||
import { MdUpgrade } from 'react-icons/md';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { connectionUrl } from '@app/core/connection.js';
|
||||
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 { IconButton } from '@components/generic/button/IconButton';
|
||||
import { Modal } from '@components/generic/Modal';
|
||||
|
||||
import { IconButton } from '../generic/button/IconButton';
|
||||
import { connectionUrl } from '@core/connection';
|
||||
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 {
|
||||
sha: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { Button } from '@app/components/generic/button/Button';
|
||||
import { Card } from '@app/components/generic/Card';
|
||||
import { connection } from '@app/core/connection.js';
|
||||
import { Button } from '@components/generic/button/Button';
|
||||
import { Card } from '@components/generic/Card';
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
|
||||
export const Debug = (): JSX.Element => {
|
||||
|
||||
@@ -4,8 +4,8 @@ import { AnimatePresence, m } from 'framer-motion';
|
||||
import { FiFilePlus } from 'react-icons/fi';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { Button } from '@app/components/generic/button/Button';
|
||||
import { Card } from '@app/components/generic/Card';
|
||||
import { Button } from '@components/generic/button/Button';
|
||||
import { Card } from '@components/generic/Card';
|
||||
import { fetcher } from '@core/utils/fetcher';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
|
||||
|
||||
@@ -8,12 +8,11 @@ import { VscDebug, VscExtensions } from 'react-icons/vsc';
|
||||
|
||||
import { ExternalSection } from '@components/generic/Sidebar/ExternalSection';
|
||||
import { Layout } from '@components/layout';
|
||||
import { Debug } from '@pages/Extensions/Debug';
|
||||
import { FileBrowser } from '@pages/Extensions/FileBrowser';
|
||||
import { Info } from '@pages/Extensions/Info';
|
||||
import { Logs } from '@pages/Extensions/Logs';
|
||||
|
||||
import { Debug } from './Debug';
|
||||
|
||||
export const Extensions = (): JSX.Element => {
|
||||
const [selectedExtension, setSelectedExtension] = useState<
|
||||
'info' | 'logs' | 'fileBrowser' | 'rangeTest' | 'debug'
|
||||
@@ -24,7 +23,7 @@ export const Extensions = (): JSX.Element => {
|
||||
title="Extensions"
|
||||
icon={<VscExtensions />}
|
||||
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
|
||||
onClick={(): void => {
|
||||
setSelectedExtension('info');
|
||||
|
||||
@@ -3,9 +3,9 @@ import type React from 'react';
|
||||
import { FiRefreshCw } from 'react-icons/fi';
|
||||
import JSONPretty from 'react-json-pretty';
|
||||
|
||||
import { IconButton } from '@app/components/generic/button/IconButton';
|
||||
import { Card } from '@app/components/generic/Card';
|
||||
import { CopyButton } from '@app/components/menu/buttons/CopyButton';
|
||||
import { IconButton } from '@components/generic/button/IconButton';
|
||||
import { Card } from '@components/generic/Card';
|
||||
import { CopyButton } from '@components/menu/buttons/CopyButton';
|
||||
import { Hashicon } from '@emeraldpay/hashicon-react';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ import type React from 'react';
|
||||
import { AnimatePresence, m } from 'framer-motion';
|
||||
import { FiArrowRight, FiPaperclip, FiTrash } from 'react-icons/fi';
|
||||
|
||||
import { IconButton } from '@app/components/generic/button/IconButton';
|
||||
import { Card } from '@app/components/generic/Card';
|
||||
import { clearLogs } from '@app/core/slices/meshtasticSlice';
|
||||
import { useAppDispatch } from '@app/hooks/useAppDispatch';
|
||||
import { IconButton } from '@components/generic/button/IconButton';
|
||||
import { Card } from '@components/generic/Card';
|
||||
import { clearLogs } from '@core/slices/meshtasticSlice';
|
||||
import { useAppDispatch } from '@hooks/useAppDispatch';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
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 JSONPretty from 'react-json-pretty';
|
||||
|
||||
import { Tooltip } from '@app/components/generic/Tooltip';
|
||||
import { IconButton } from '@components/generic/button/IconButton';
|
||||
import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection';
|
||||
import { SidebarOverlay } from '@components/generic/Sidebar/SidebarOverlay';
|
||||
import { Tooltip } from '@components/generic/Tooltip';
|
||||
import { SidebarItem } from '@components/layout/Sidebar/SidebarItem';
|
||||
import { CopyButton } from '@components/menu/buttons/CopyButton';
|
||||
import type { Node } from '@core/slices/meshtasticSlice';
|
||||
|
||||
@@ -8,8 +8,8 @@ import { BiCrown } from 'react-icons/bi';
|
||||
import { FiSettings } from 'react-icons/fi';
|
||||
import { RiMindMap } from 'react-icons/ri';
|
||||
|
||||
import { Tooltip } from '@app/components/generic/Tooltip';
|
||||
import { IconButton } from '@components/generic/button/IconButton';
|
||||
import { Tooltip } from '@components/generic/Tooltip';
|
||||
import { Layout } from '@components/layout';
|
||||
import { SidebarItem } from '@components/layout/Sidebar/SidebarItem';
|
||||
import { Hashicon } from '@emeraldpay/hashicon-react';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { Card } from '@app/components/generic/Card';
|
||||
import { Card } from '@components/generic/Card';
|
||||
|
||||
export const NotFound = (): JSX.Element => {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user