Consolidate forms & other minor fixes

This commit is contained in:
Sacha Weatherstone
2022-03-04 19:19:36 +11:00
parent 4dc39623dc
commit ea5ab9376f
42 changed files with 1426 additions and 1160 deletions

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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