Browse Source

Form improvements & ui cleanup

pull/2/head
Sacha Weatherstone 5 years ago
parent
commit
6f1f91e70a
  1. 36
      src/components/FormFooter.tsx
  2. 26
      src/components/generic/Card.tsx
  3. 12
      src/components/generic/IconButton.tsx
  4. 4
      src/components/generic/SidebarItem.tsx
  5. 2
      src/components/generic/form/Checkbox.tsx
  6. 2
      src/components/generic/form/InputWrapper.tsx
  7. 2
      src/components/generic/form/Select.tsx
  8. 16
      src/components/templates/PrimaryTemplate.tsx
  9. 1
      src/pages/Messages.tsx
  10. 40
      src/pages/Nodes/Node.tsx
  11. 2
      src/pages/Plugins/Files.tsx
  12. 35
      src/pages/Plugins/RangeTest.tsx
  13. 28
      src/pages/settings/Channels.tsx
  14. 26
      src/pages/settings/Connection.tsx
  15. 93
      src/pages/settings/Index.tsx
  16. 2
      src/pages/settings/Interface.tsx
  17. 127
      src/pages/settings/Position.tsx
  18. 92
      src/pages/settings/Power.tsx
  19. 119
      src/pages/settings/Radio.tsx
  20. 59
      src/pages/settings/User.tsx
  21. 93
      src/pages/settings/WiFi.tsx

36
src/components/FormFooter.tsx

@ -0,0 +1,36 @@
import type React from 'react';
import { FiSave, FiXCircle } from 'react-icons/fi';
import { IconButton } from './generic/IconButton';
export interface FormFooterProps {
dirty: boolean;
clearAction: () => void;
saveAction: () => void;
}
export const FormFooter = ({
dirty,
clearAction,
saveAction,
}: FormFooterProps): JSX.Element => {
return (
<div className="flex float-right gap-2">
<IconButton
icon={<FiXCircle className="w-5 h-5" />}
disabled={!dirty}
onClick={(): void => {
clearAction();
}}
/>
<IconButton
disabled={!dirty}
onClick={(): void => {
saveAction();
}}
icon={<FiSave className="w-5 h-5" />}
/>
</div>
);
};

26
src/components/generic/Card.tsx

@ -3,10 +3,11 @@ import type React from 'react';
type DefaultDivProps = JSX.IntrinsicElements['div'];
interface CardProps extends DefaultDivProps {
title: string;
description: string | JSX.Element;
title?: string;
description?: string | JSX.Element;
buttons?: JSX.Element;
lgPlaceholder?: JSX.Element;
loading?: boolean;
}
export const Card = ({
@ -23,15 +24,22 @@ export const Card = ({
className={`flex flex-col flex-auto dark:text-white border-y md:border shadow-md select-none dark:bg-primaryDark border-gray-300 dark:border-transparent md:rounded-3xl ${className}`}
{...props}
>
<div className="flex items-center justify-between mx-10 mt-10">
<div className="flex flex-col">
<div className="mr-4 text-2xl font-semibold leading-7 tracking-tight text-black md:text-3xl dark:text-white">
{title}
{(title || description) && (
<div className="flex items-center justify-between mx-10 mt-10">
<div className="flex flex-col">
{title && (
<div className="mr-4 text-2xl font-semibold leading-7 tracking-tight text-black md:text-3xl dark:text-white">
{title}
</div>
)}
{description && (
<div className="font-medium text-gray-400">{description}</div>
)}
</div>
<div className="font-medium text-gray-400">{description}</div>
{buttons}
</div>
{buttons}
</div>
)}
<div className="flex">
<div className={`${lgPlaceholder ? 'w-full xl:w-2/3' : 'w-full'}`}>
{children}

12
src/components/generic/IconButton.tsx

@ -4,17 +4,27 @@ type DefaulButtonProps = JSX.IntrinsicElements['button'];
export interface IconButtonProps extends DefaulButtonProps {
icon: React.ReactNode;
active?: boolean;
}
export const IconButton = ({
icon,
active,
disabled,
...props
}: IconButtonProps): JSX.Element => {
return (
<div className="my-auto text-gray-500 dark:text-gray-400">
<button
type="button"
className="p-2 transition duration-200 ease-in-out rounded-md active:scale-95 hover:bg-gray-200 dark:hover:bg-gray-600"
disabled={disabled}
className={`p-2 transition duration-200 ease-in-out rounded-md active:scale-95 ${
active
? 'bg-gray-200 dark:bg-gray-600'
: 'hover:bg-gray-200 dark:hover:bg-gray-600'
} ${
disabled ? 'text-gray-400 dark:text-gray-700 cursor-not-allowed' : ''
}`}
{...props}
>
{icon}

4
src/components/generic/SidebarItem.tsx

@ -18,12 +18,12 @@ export const SidebarItem = ({
}: SidebarItemProps): JSX.Element => {
return (
<div
className={`flex p-5 cursor-pointer select-none dark:hover:bg-primaryDark ${
className={`flex p-3 cursor-pointer select-none dark:hover:bg-primaryDark ${
selected ? 'bg-gray-200 dark:bg-primaryDark' : 'dark:bg-secondaryDark'
}`}
{...props}
>
<div className="text-gray-500 dark:text-gray-400">{icon}</div>
<div className="my-auto text-gray-500 dark:text-gray-400">{icon}</div>
<div className="ml-3 text-left">
<div className="font-medium text-left">{title}</div>
<div className="mt-0.5 text-gray-400 text-sm">{description}</div>

2
src/components/generic/form/Checkbox.tsx

@ -27,7 +27,7 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
id={id}
className={`appearance-none w-8 h-8 border rounded-md focus:outline-none checked:bg-primary checked:border-transparent transition duration-200 ease-in-out border-gray-400 dark:border-gray-200 ${
props.disabled
? 'bg-gray-200 text-gray-500 dark:bg-gray-800 dark:text-gray-400 border-gray-400'
? 'bg-gray-200 text-gray-500 dark:bg-gray-800 dark:text-gray-400 border-gray-400 dark:border-gray-700'
: ''
} ${
error

2
src/components/generic/form/InputWrapper.tsx

@ -14,7 +14,7 @@ export const InputWrapper = ({
<div
className={`flex w-full border-gray-400 dark:border-gray-200 border-y border rounded-md transition duration-200 ease-in-out ${
disabled
? 'bg-gray-200 text-gray-500 dark:bg-gray-800 dark:text-gray-400 border-gray-400'
? 'bg-gray-200 text-gray-500 dark:bg-gray-800 dark:text-gray-400 border-gray-400 dark:border-gray-700'
: ''
} ${
error

2
src/components/generic/form/Select.tsx

@ -30,7 +30,7 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
<select
ref={ref}
className={`w-full rounded-md bg-white dark:bg-transparent focus:outline-none focus:border-primary ${
small ? 'p-1' : 'h-10 px-2'
small ? 'm-1' : 'h-10 mx-2'
}`}
disabled={
props.disabled

16
src/components/templates/PrimaryTemplate.tsx

@ -4,7 +4,8 @@ export interface PrimaryTemplateProps {
children: React.ReactNode;
title: string;
tagline: string;
button?: JSX.Element;
leftButton?: JSX.Element;
rightButton?: JSX.Element;
footer?: JSX.Element;
}
@ -12,13 +13,13 @@ export const PrimaryTemplate = ({
children,
title,
tagline,
button,
leftButton,
rightButton,
footer,
}: PrimaryTemplateProps): JSX.Element => {
return (
<div className="flex flex-col flex-auto h-full min-w-0">
<div className="flex p-4 bg-white border-b border-gray-300 md:flex-row flex-0 md:items-center md:justify-between md:px-10 dark:border-gray-600 dark:bg-secondaryDark">
{button && <div className="pr-2 m-auto md:hidden">{button}</div>}
<div className="flex-1 min-w-0">
<a className="font-medium whitespace-nowrap text-primary">
{tagline}
@ -27,13 +28,16 @@ export const PrimaryTemplate = ({
{title}
</h2>
</div>
{rightButton}
</div>
<div className="flex-auto flex-grow py-6 overflow-y-auto bg-white md:p-10 dark:bg-secondaryDark">
<div className="flex-auto flex-grow py-6 overflow-y-auto bg-white md:p-5 dark:bg-secondaryDark">
{children}
</div>
{footer && (
<div className="flex p-4 bg-white border-t border-gray-300 md:flex-row flex-0 md:items-center md:justify-between md:px-10 dark:border-gray-600 dark:bg-secondaryDark">
{button && <div className="pr-2 m-auto md:hidden">{button}</div>}
<div className="flex px-4 py-2 bg-white border-t border-gray-300 md: md:py-4 md:flex-row flex-0 md:items-center md:justify-between md:px-10 dark:border-gray-600 dark:bg-secondaryDark">
{leftButton && (
<div className="pr-2 m-auto md:hidden">{leftButton}</div>
)}
<div className="flex-1 min-w-0">{footer}</div>
</div>
)}

1
src/pages/Messages.tsx

@ -11,7 +11,6 @@ import { useAppSelector } from '../hooks/redux';
export const Messages = (): JSX.Element => {
const messages = useAppSelector((state) => state.meshtastic.messages);
const nodes = useAppSelector((state) => state.meshtastic.nodes);
const users = useAppSelector((state) => state.meshtastic.users);
const channels = useAppSelector((state) => state.meshtastic.channels);

40
src/pages/Nodes/Node.tsx

@ -1,11 +1,12 @@
import 'react-json-pretty/themes/acai.css';
import type React from 'react';
import React from 'react';
import { FiMenu, FiTerminal } from 'react-icons/fi';
import { FiCode, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import TimeAgo from 'react-timeago';
import { Cover } from '@app/components/generic/Cover';
import { useAppSelector } from '@app/hooks/redux';
import { Card } from '@components/generic/Card';
import { Checkbox } from '@components/generic/form/Checkbox';
@ -28,11 +29,12 @@ export const Node = ({ navOpen, setNavOpen, node }: NodeProps): JSX.Element => {
const position = useAppSelector(
(state) => state.meshtastic.positionPackets,
).find((position) => position.packet.from === node.num)?.data;
const [debug, setDebug] = React.useState(false);
return (
<PrimaryTemplate
title={user ? user.longName : node.num.toString()}
tagline="Node"
button={
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
@ -40,12 +42,28 @@ export const Node = ({ navOpen, setNavOpen, node }: NodeProps): JSX.Element => {
}}
/>
}
rightButton={
<IconButton
icon={<FiCode className="w-5 h-5" />}
active={debug}
onClick={(): void => {
setDebug(!debug);
}}
/>
}
footer={<></>}
>
<div className="w-full space-y-4">
<div className="justify-between space-y-2 md:space-y-0 md:space-x-2 md:flex">
<div className="flex flex-col justify-between gap-4 md:flex-row">
<StatCard
title="Last heard"
value={<TimeAgo date={new Date(node.lastHeard * 1000)} />}
value={
node.lastHeard ? (
<TimeAgo date={new Date(node.lastHeard * 1000)} />
) : (
'Never'
)
}
/>
<StatCard title="SNR" value={node.snr.toString()} />
</div>
@ -53,20 +71,12 @@ export const Node = ({ navOpen, setNavOpen, node }: NodeProps): JSX.Element => {
title="Position"
description={new Date(node.lastHeard * 1000).toLocaleString()}
>
<Cover enabled={debug} content={<JSONPretty data={node} />} />
<div className="p-10">
<JSONPretty data={position} />
</div>
</Card>
<Card
title="Settings"
description="Remote node settings"
lgPlaceholder={
<div className="w-full h-full text-black dark:text-white">
<FiTerminal className="w-24 h-24 m-auto" />
<div className="text-center">Placeholder</div>
</div>
}
>
<Card title="Settings" description="Remote node settings">
<div className="p-10">
<form className="space-y-4">
<Input label={'Device Name'} />

2
src/pages/Plugins/Files.tsx

@ -54,7 +54,7 @@ export const Files = ({ navOpen, setNavOpen }: RangeTestProps): JSX.Element => {
<PrimaryTemplate
title="File Browser"
tagline="Plugin"
button={
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {

35
src/pages/Plugins/RangeTest.tsx

@ -1,12 +1,12 @@
import type React from 'react';
import { useForm } from 'react-hook-form';
import { useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FiMenu, FiSave } from 'react-icons/fi';
import { FiMenu } from 'react-icons/fi';
import { FormFooter } from '@app/components/FormFooter';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { Button } from '@components/generic/Button';
import { Card } from '@components/generic/Card';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
@ -26,7 +26,7 @@ export const RangeTest = ({
const { t } = useTranslation();
const preferences = useAppSelector((state) => state.meshtastic.preferences);
const { register, handleSubmit, formState } =
const { register, handleSubmit, formState, reset, control } =
useForm<RadioConfig_UserPreferences>({
defaultValues: {
rangeTestPluginEnabled: preferences.rangeTestPluginEnabled,
@ -36,16 +36,20 @@ export const RangeTest = ({
});
const onSubmit = handleSubmit((data) => {
console.log(data);
void connection.setPreferences(data);
});
const watchRangeTestPluginEnabled = useWatch({
control,
name: 'rangeTestPluginEnabled',
defaultValue: false,
});
return (
<PrimaryTemplate
title="Range Test"
tagline="Plugin"
button={
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
@ -54,16 +58,11 @@ export const RangeTest = ({
/>
}
footer={
<Button
className="px-10 ml-auto"
icon={<FiSave className="w-5 h-5" />}
disabled={!formState.isDirty}
onClick={onSubmit}
active
border
>
{t('strings.save_changes')}
</Button>
<FormFooter
dirty={formState.isDirty}
saveAction={onSubmit}
clearAction={reset}
/>
}
>
<div className="w-full space-y-4">
@ -76,11 +75,13 @@ export const RangeTest = ({
/>
<Checkbox
label="Range Test Plugin Save?"
disabled={!watchRangeTestPluginEnabled}
{...register('rangeTestPluginSave')}
/>
<Input
type="number"
label="Message Interval"
disabled={!watchRangeTestPluginEnabled}
{...register('rangeTestPluginSender', {
valueAsNumber: true,
})}

28
src/pages/settings/Channels.tsx

@ -30,7 +30,7 @@ export const Channels = ({
<PrimaryTemplate
title="Channels"
tagline="Settings"
button={
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
@ -38,6 +38,15 @@ export const Channels = ({
}}
/>
}
rightButton={
<IconButton
icon={<FiCode className="w-5 h-5" />}
active={debug}
onClick={(): void => {
setDebug(!debug);
}}
/>
}
footer={
<Button
className="px-10 ml-auto"
@ -50,22 +59,7 @@ export const Channels = ({
}
>
<div className="space-y-4">
<Card
title="Manage Channels"
description="Edit channel throughput and other settings"
buttons={
<Button
border
active={debug}
onClick={(): void => {
setDebug(!debug);
}}
icon={<FiCode />}
>
Debug
</Button>
}
>
<Card>
<Cover enabled={debug} content={<JSONPretty data={channels} />} />
<div className="w-full p-4 space-y-2 md:p-10">
{channels.map((channel) => (

26
src/pages/settings/Connection.tsx

@ -2,8 +2,9 @@ import React from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FiCheck, FiMenu, FiSave } from 'react-icons/fi';
import { FiCheck, FiMenu } from 'react-icons/fi';
import { FormFooter } from '@app/components/FormFooter';
import { connection, setConnection } from '@app/core/connection';
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
import { Button } from '@components/generic/Button';
@ -52,7 +53,7 @@ export const Connection = ({
);
const hostOverride = useAppSelector((state) => state.meshtastic.hostOverride);
const { register, handleSubmit, formState } = useForm<{
const { register, handleSubmit, formState, reset } = useForm<{
method: connType;
}>({
defaultValues: {
@ -121,7 +122,7 @@ export const Connection = ({
<PrimaryTemplate
title="Connection"
tagline="Settings"
button={
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
@ -130,21 +131,14 @@ export const Connection = ({
/>
}
footer={
<Button
className="px-10 ml-auto"
icon={<FiSave className="w-5 h-5" />}
disabled={!formState.isDirty}
active
border
>
{t('strings.save_changes')}
</Button>
<FormFooter
dirty={formState.isDirty}
saveAction={onSubmit}
clearAction={reset}
/>
}
>
<Card
title="Connection Settings"
description="HTTP, BLE and Serial Options"
>
<Card>
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form className="space-y-2" onSubmit={onSubmit}>
<Select

93
src/pages/settings/Index.tsx

@ -4,55 +4,80 @@ import {
FiLayers,
FiLayout,
FiLink2,
FiRss,
FiSmartphone,
FiMapPin,
FiRadio,
FiUser,
FiWifi,
FiZap,
} from 'react-icons/fi';
import { PageLayout } from '@components/templates/PageLayout';
import { Channels } from './Channels';
import { Connection } from './Connection';
import { Device } from './Device';
import { Interface } from './Interface';
import { Position } from './Position';
import { Power } from './Power';
import { Radio } from './Radio';
import { User } from './User';
import { WiFi } from './WiFi';
export const Settings = (): JSX.Element => {
const sidebarItems = [
{
title: 'Connection',
description: 'Connection method and parameters',
icon: <FiLink2 className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'WiFi',
description: 'WiFi credentials and mode',
icon: <FiWifi className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Position',
description: 'Position settings and flags',
icon: <FiMapPin className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'User',
description: 'Device name and details',
icon: <FiUser className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Power',
description: 'Power and sleep settings',
icon: <FiZap className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Radio',
description: 'LoRa settings',
icon: <FiRadio className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Interface',
description: 'Language and UI settings',
icon: <FiLayout className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Channels',
description: 'Manage channels',
icon: <FiLayers className="flex-shrink-0 w-6 h-6" />,
},
];
return (
<PageLayout
title="Settings"
sidebarItems={[
{
title: 'Connection',
description: 'Method and peramaters for connecting to the device',
icon: <FiLink2 className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Device',
description: 'Device settings, such as device name and wifi settings',
icon: <FiSmartphone className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Radio',
description: 'Adjust radio power and frequency settings',
icon: <FiRss className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Interface',
description: 'Change language and other UI settings',
icon: <FiLayout className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Channels',
description: 'Manage channels',
icon: <FiLayers className="flex-shrink-0 w-6 h-6" />,
},
]}
sidebarItems={sidebarItems}
panels={[
<Connection key={1} />,
<Device key={2} />,
<Radio key={3} />,
<Interface key={4} />,
<Channels key={5} />,
<WiFi key={2} />,
<Position key={3} />,
<User key={4} />,
<Power key={5} />,
<Radio key={6} />,
<Interface key={7} />,
<Channels key={8} />,
]}
/>
);

2
src/pages/settings/Interface.tsx

@ -24,7 +24,7 @@ export const Interface = ({
<PrimaryTemplate
title="Interface"
tagline="Settings"
button={
leftButton={
<Button
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {

127
src/pages/settings/Position.tsx

@ -0,0 +1,127 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FiCode, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { FormFooter } from '@app/components/FormFooter';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { Card } from '@components/generic/Card';
import { Cover } from '@components/generic/Cover';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select';
import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { Protobuf } from '@meshtastic/meshtasticjs';
export interface PositionProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const Position = ({
navOpen,
setNavOpen,
}: PositionProps): JSX.Element => {
const { t } = useTranslation();
const radioConfig = useAppSelector((state) => state.meshtastic.preferences);
const [debug, setDebug] = React.useState(false);
const { register, handleSubmit, formState, reset } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: {
...radioConfig,
positionBroadcastSecs:
radioConfig.positionBroadcastSecs === 0
? radioConfig.isRouter
? 43200
: 900
: radioConfig.positionBroadcastSecs,
},
});
const onSubmit = handleSubmit((data) => {
void connection.setPreferences(data);
});
return (
<PrimaryTemplate
title="Position"
tagline="Settings"
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
setNavOpen && setNavOpen(!navOpen);
}}
/>
}
rightButton={
<IconButton
icon={<FiCode className="w-5 h-5" />}
active={debug}
onClick={(): void => {
setDebug(!debug);
}}
/>
}
footer={
<FormFooter
dirty={formState.isDirty}
saveAction={onSubmit}
clearAction={reset}
/>
}
>
<Card>
<Cover enabled={debug} content={<JSONPretty data={radioConfig} />} />
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form className="space-y-2" onSubmit={onSubmit}>
<Input
label={'Broadcast Interval (seconds)'}
type="number"
{...register('positionBroadcastSecs', { valueAsNumber: true })}
/>
<Select
label="Position Type"
optionsEnum={Protobuf.PositionFlags}
{...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>
</Card>
</PrimaryTemplate>
);
};

92
src/pages/settings/Power.tsx

@ -0,0 +1,92 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FiCode, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { FormFooter } from '@app/components/FormFooter';
import { Select } from '@app/components/generic/form/Select';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { Card } from '@components/generic/Card';
import { Cover } from '@components/generic/Cover';
import { Checkbox } from '@components/generic/form/Checkbox';
import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { Protobuf } from '@meshtastic/meshtasticjs';
export interface PowerProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const Power = ({ navOpen, setNavOpen }: PowerProps): JSX.Element => {
const { t } = useTranslation();
const radioConfig = useAppSelector((state) => state.meshtastic.preferences);
const [debug, setDebug] = React.useState(false);
const { register, handleSubmit, formState, reset } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: {
...radioConfig,
isLowPower: radioConfig.isRouter ? true : radioConfig.isLowPower,
},
});
const onSubmit = handleSubmit((data) => {
void connection.setPreferences(data);
});
return (
<PrimaryTemplate
title="Power"
tagline="Settings"
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
setNavOpen && setNavOpen(!navOpen);
}}
/>
}
rightButton={
<IconButton
icon={<FiCode className="w-5 h-5" />}
active={debug}
onClick={(): void => {
setDebug(!debug);
}}
/>
}
footer={
<FormFooter
dirty={formState.isDirty}
saveAction={onSubmit}
clearAction={reset}
/>
}
>
<Card>
<Cover enabled={debug} content={<JSONPretty data={radioConfig} />} />
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<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={radioConfig.isRouter}
validationMessage={
radioConfig.isRouter ? 'Enabled by default in router mode' : ''
}
{...register('isLowPower')}
/>
</form>
</div>
</Card>
</PrimaryTemplate>
);
};

119
src/pages/settings/Radio.tsx

@ -2,16 +2,15 @@ import React from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FiCode, FiMenu, FiSave, FiXCircle } from 'react-icons/fi';
import { FiCode, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { FormFooter } from '@app/components/FormFooter';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { Button } from '@components/generic/Button';
import { Card } from '@components/generic/Card';
import { Cover } from '@components/generic/Cover';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select';
import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
@ -39,7 +38,7 @@ export const Radio = ({ navOpen, setNavOpen }: RadioProps): JSX.Element => {
<PrimaryTemplate
title="Radio"
tagline="Settings"
button={
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
@ -47,108 +46,28 @@ export const Radio = ({ navOpen, setNavOpen }: RadioProps): JSX.Element => {
}}
/>
}
rightButton={
<IconButton
icon={<FiCode className="w-5 h-5" />}
active={debug}
onClick={(): void => {
setDebug(!debug);
}}
/>
}
footer={
<div className="flex space-x-2">
<IconButton
icon={<FiXCircle className="w-5 h-5" />}
disabled={formState.isDirty}
onClick={(): void => {
reset();
}}
/>
<Button
className="px-10 ml-auto"
icon={<FiSave className="w-5 h-5" />}
disabled={!formState.isDirty}
onClick={onSubmit}
active
border
>
{t('strings.save_changes')}
</Button>
</div>
<FormFooter
dirty={formState.isDirty}
saveAction={onSubmit}
clearAction={reset}
/>
}
>
<Card
title="Basic settings"
description="Device name and user parameters"
buttons={
<Button
border
active={debug}
onClick={(): void => {
setDebug(!debug);
}}
icon={<FiCode />}
>
Debug
</Button>
}
>
<Card>
<Cover enabled={debug} content={<JSONPretty data={radioConfig} />} />
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form className="space-y-2" onSubmit={onSubmit}>
<div>Power</div>
<hr />
<Input
label={'Charge current'}
disabled
{...register('chargeCurrent', { valueAsNumber: true })}
/>
<Checkbox label="Always powered" {...register('isAlwaysPowered')} />
<Checkbox label="Low Power" {...register('isLowPower')} />
<hr />
<div>WiFi</div>
<Input label={t('strings.wifi_ssid')} {...register('wifiSsid')} />
<Input
type="password"
label={t('strings.wifi_psk')}
{...register('wifiPassword')}
/>
<hr />
<div>Position</div>
<Input
label={'Broadcast Interval (seconds)'}
type="number"
{...register('positionBroadcastSecs', { valueAsNumber: true })}
/>
<Select
label="Position Type"
optionsEnum={Protobuf.PositionFlags}
{...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 })}
/>
<hr />
<div>Other</div>
<Checkbox label="Is Router" {...register('isRouter')} />
<Select
label="Region"
optionsEnum={Protobuf.RegionCode}

59
src/pages/settings/Device.tsx → src/pages/settings/User.tsx

@ -2,13 +2,13 @@ import React from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FiCode, FiMenu, FiSave } from 'react-icons/fi';
import { FiCode, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { FormFooter } from '@app/components/FormFooter';
import { connection } from '@app/core/connection';
import { addUser } from '@app/core/slices/meshtasticSlice';
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
import { Button } from '@components/generic/Button';
import { Card } from '@components/generic/Card';
import { Cover } from '@components/generic/Cover';
import { Checkbox } from '@components/generic/form/Checkbox';
@ -18,12 +18,12 @@ import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { Protobuf } from '@meshtastic/meshtasticjs';
export interface DeviceProps {
export interface UserProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
export const User = ({ navOpen, setNavOpen }: UserProps): JSX.Element => {
const { t } = useTranslation();
const [debug, setDebug] = React.useState(false);
const dispatch = useAppDispatch();
@ -31,7 +31,7 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
const user = useAppSelector((state) => state.meshtastic.users).find(
(user) => user.packet.from === myNodeInfo.myNodeNum,
);
const { register, handleSubmit, formState } = useForm<{
const { register, handleSubmit, formState, reset } = useForm<{
longName: string;
shortName: string;
isLicensed: boolean;
@ -46,7 +46,7 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
});
const onSubmit = handleSubmit((data) => {
// TODO: can be remove once getUser is implemented
// TODO: can be removed once getUser is implemented
if (user) {
void connection.setOwner({ ...user.data, ...data });
dispatch(addUser({ ...user, ...{ data: { ...user.data, ...data } } }));
@ -55,9 +55,9 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
return (
<PrimaryTemplate
title="Device"
title="User"
tagline="Settings"
button={
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
@ -65,35 +65,24 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
}}
/>
}
rightButton={
<IconButton
icon={<FiCode className="w-5 h-5" />}
active={debug}
onClick={(): void => {
setDebug(!debug);
}}
/>
}
footer={
<Button
className="px-10 ml-auto"
icon={<FiSave className="w-5 h-5" />}
disabled={!formState.isDirty}
onClick={onSubmit}
active
border
>
{t('strings.save_changes')}
</Button>
<FormFooter
dirty={formState.isDirty}
saveAction={onSubmit}
clearAction={reset}
/>
}
>
<Card
title="Basic settings"
description="Device name and user parameters"
buttons={
<Button
border
active={debug}
onClick={(): void => {
setDebug(!debug);
}}
icon={<FiCode />}
>
Debug
</Button>
}
>
<Card>
<Cover enabled={debug} content={<JSONPretty data={user} />} />
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form className="space-y-2" onSubmit={onSubmit}>
@ -117,7 +106,7 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
<Select
label="Team"
optionsEnum={Protobuf.Team}
{...register('team')}
{...register('team', { valueAsNumber: true })}
/>
</form>
</div>

93
src/pages/settings/WiFi.tsx

@ -0,0 +1,93 @@
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FiCode, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { FormFooter } from '@app/components/FormFooter';
import { Checkbox } from '@app/components/generic/form/Checkbox';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { Card } from '@components/generic/Card';
import { Cover } from '@components/generic/Cover';
import { Input } from '@components/generic/form/Input';
import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export interface WiFiProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const WiFi = ({ navOpen, setNavOpen }: WiFiProps): JSX.Element => {
const { t } = useTranslation();
const radioConfig = useAppSelector((state) => state.meshtastic.preferences);
const [debug, setDebug] = React.useState(false);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: radioConfig,
});
const watchWifiApMode = useWatch({
control,
name: 'wifiApMode',
defaultValue: false,
});
const onSubmit = handleSubmit((data) => {
void connection.setPreferences(data);
});
return (
<PrimaryTemplate
title="WiFi"
tagline="Settings"
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
setNavOpen && setNavOpen(!navOpen);
}}
/>
}
rightButton={
<IconButton
icon={<FiCode className="w-5 h-5" />}
active={debug}
onClick={(): void => {
setDebug(!debug);
}}
/>
}
footer={
<FormFooter
dirty={formState.isDirty}
saveAction={onSubmit}
clearAction={reset}
/>
}
>
<Card>
<Cover enabled={debug} content={<JSONPretty data={radioConfig} />} />
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form className="space-y-2" onSubmit={onSubmit}>
<Checkbox label="Enable WiFi AP" {...register('wifiApMode')} />
<Input
label={t('strings.wifi_ssid')}
disabled={watchWifiApMode}
{...register('wifiSsid')}
/>
<Input
type="password"
label={t('strings.wifi_psk')}
disabled={watchWifiApMode}
{...register('wifiPassword')}
/>
</form>
</div>
</Card>
</PrimaryTemplate>
);
};
Loading…
Cancel
Save