Form improvements & ui cleanup
This commit is contained in:
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,10 +3,11 @@ import type React from 'react';
|
|||||||
type DefaultDivProps = JSX.IntrinsicElements['div'];
|
type DefaultDivProps = JSX.IntrinsicElements['div'];
|
||||||
|
|
||||||
interface CardProps extends DefaultDivProps {
|
interface CardProps extends DefaultDivProps {
|
||||||
title: string;
|
title?: string;
|
||||||
description: string | JSX.Element;
|
description?: string | JSX.Element;
|
||||||
buttons?: JSX.Element;
|
buttons?: JSX.Element;
|
||||||
lgPlaceholder?: JSX.Element;
|
lgPlaceholder?: JSX.Element;
|
||||||
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Card = ({
|
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}`}
|
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}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mx-10 mt-10">
|
{(title || description) && (
|
||||||
<div className="flex flex-col">
|
<div className="flex items-center justify-between mx-10 mt-10">
|
||||||
<div className="mr-4 text-2xl font-semibold leading-7 tracking-tight text-black md:text-3xl dark:text-white">
|
<div className="flex flex-col">
|
||||||
{title}
|
{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>
|
||||||
<div className="font-medium text-gray-400">{description}</div>
|
{buttons}
|
||||||
</div>
|
</div>
|
||||||
{buttons}
|
)}
|
||||||
</div>
|
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className={`${lgPlaceholder ? 'w-full xl:w-2/3' : 'w-full'}`}>
|
<div className={`${lgPlaceholder ? 'w-full xl:w-2/3' : 'w-full'}`}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -4,17 +4,27 @@ type DefaulButtonProps = JSX.IntrinsicElements['button'];
|
|||||||
|
|
||||||
export interface IconButtonProps extends DefaulButtonProps {
|
export interface IconButtonProps extends DefaulButtonProps {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
|
active?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IconButton = ({
|
export const IconButton = ({
|
||||||
icon,
|
icon,
|
||||||
|
active,
|
||||||
|
disabled,
|
||||||
...props
|
...props
|
||||||
}: IconButtonProps): JSX.Element => {
|
}: IconButtonProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className="my-auto text-gray-500 dark:text-gray-400">
|
<div className="my-auto text-gray-500 dark:text-gray-400">
|
||||||
<button
|
<button
|
||||||
type="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}
|
{...props}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ export const SidebarItem = ({
|
|||||||
}: SidebarItemProps): JSX.Element => {
|
}: SidebarItemProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div
|
<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'
|
selected ? 'bg-gray-200 dark:bg-primaryDark' : 'dark:bg-secondaryDark'
|
||||||
}`}
|
}`}
|
||||||
{...props}
|
{...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="ml-3 text-left">
|
||||||
<div className="font-medium text-left">{title}</div>
|
<div className="font-medium text-left">{title}</div>
|
||||||
<div className="mt-0.5 text-gray-400 text-sm">{description}</div>
|
<div className="mt-0.5 text-gray-400 text-sm">{description}</div>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
|||||||
id={id}
|
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 ${
|
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
|
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
|
error
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const InputWrapper = ({
|
|||||||
<div
|
<div
|
||||||
className={`flex w-full border-gray-400 dark:border-gray-200 border-y border rounded-md transition duration-200 ease-in-out ${
|
className={`flex w-full border-gray-400 dark:border-gray-200 border-y border rounded-md transition duration-200 ease-in-out ${
|
||||||
disabled
|
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
|
error
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
|||||||
<select
|
<select
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`w-full rounded-md bg-white dark:bg-transparent focus:outline-none focus:border-primary ${
|
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={
|
disabled={
|
||||||
props.disabled
|
props.disabled
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ export interface PrimaryTemplateProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
title: string;
|
title: string;
|
||||||
tagline: string;
|
tagline: string;
|
||||||
button?: JSX.Element;
|
leftButton?: JSX.Element;
|
||||||
|
rightButton?: JSX.Element;
|
||||||
footer?: JSX.Element;
|
footer?: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,13 +13,13 @@ export const PrimaryTemplate = ({
|
|||||||
children,
|
children,
|
||||||
title,
|
title,
|
||||||
tagline,
|
tagline,
|
||||||
button,
|
leftButton,
|
||||||
|
rightButton,
|
||||||
footer,
|
footer,
|
||||||
}: PrimaryTemplateProps): JSX.Element => {
|
}: PrimaryTemplateProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-auto h-full min-w-0">
|
<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">
|
<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">
|
<div className="flex-1 min-w-0">
|
||||||
<a className="font-medium whitespace-nowrap text-primary">
|
<a className="font-medium whitespace-nowrap text-primary">
|
||||||
{tagline}
|
{tagline}
|
||||||
@@ -27,13 +28,16 @@ export const PrimaryTemplate = ({
|
|||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
{rightButton}
|
||||||
</div>
|
</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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
{footer && (
|
{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">
|
<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">
|
||||||
{button && <div className="pr-2 m-auto md:hidden">{button}</div>}
|
{leftButton && (
|
||||||
|
<div className="pr-2 m-auto md:hidden">{leftButton}</div>
|
||||||
|
)}
|
||||||
<div className="flex-1 min-w-0">{footer}</div>
|
<div className="flex-1 min-w-0">{footer}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { useAppSelector } from '../hooks/redux';
|
|||||||
|
|
||||||
export const Messages = (): JSX.Element => {
|
export const Messages = (): JSX.Element => {
|
||||||
const messages = useAppSelector((state) => state.meshtastic.messages);
|
const messages = useAppSelector((state) => state.meshtastic.messages);
|
||||||
const nodes = useAppSelector((state) => state.meshtastic.nodes);
|
|
||||||
const users = useAppSelector((state) => state.meshtastic.users);
|
const users = useAppSelector((state) => state.meshtastic.users);
|
||||||
const channels = useAppSelector((state) => state.meshtastic.channels);
|
const channels = useAppSelector((state) => state.meshtastic.channels);
|
||||||
|
|
||||||
|
|||||||
+25
-15
@@ -1,11 +1,12 @@
|
|||||||
import 'react-json-pretty/themes/acai.css';
|
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 JSONPretty from 'react-json-pretty';
|
||||||
import TimeAgo from 'react-timeago';
|
import TimeAgo from 'react-timeago';
|
||||||
|
|
||||||
|
import { Cover } from '@app/components/generic/Cover';
|
||||||
import { useAppSelector } from '@app/hooks/redux';
|
import { useAppSelector } from '@app/hooks/redux';
|
||||||
import { Card } from '@components/generic/Card';
|
import { Card } from '@components/generic/Card';
|
||||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||||
@@ -28,11 +29,12 @@ export const Node = ({ navOpen, setNavOpen, node }: NodeProps): JSX.Element => {
|
|||||||
const position = useAppSelector(
|
const position = useAppSelector(
|
||||||
(state) => state.meshtastic.positionPackets,
|
(state) => state.meshtastic.positionPackets,
|
||||||
).find((position) => position.packet.from === node.num)?.data;
|
).find((position) => position.packet.from === node.num)?.data;
|
||||||
|
const [debug, setDebug] = React.useState(false);
|
||||||
return (
|
return (
|
||||||
<PrimaryTemplate
|
<PrimaryTemplate
|
||||||
title={user ? user.longName : node.num.toString()}
|
title={user ? user.longName : node.num.toString()}
|
||||||
tagline="Node"
|
tagline="Node"
|
||||||
button={
|
leftButton={
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<FiMenu className="w-5 h-5" />}
|
icon={<FiMenu className="w-5 h-5" />}
|
||||||
onClick={(): void => {
|
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="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
|
<StatCard
|
||||||
title="Last heard"
|
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()} />
|
<StatCard title="SNR" value={node.snr.toString()} />
|
||||||
</div>
|
</div>
|
||||||
@@ -53,20 +71,12 @@ export const Node = ({ navOpen, setNavOpen, node }: NodeProps): JSX.Element => {
|
|||||||
title="Position"
|
title="Position"
|
||||||
description={new Date(node.lastHeard * 1000).toLocaleString()}
|
description={new Date(node.lastHeard * 1000).toLocaleString()}
|
||||||
>
|
>
|
||||||
|
<Cover enabled={debug} content={<JSONPretty data={node} />} />
|
||||||
<div className="p-10">
|
<div className="p-10">
|
||||||
<JSONPretty data={position} />
|
<JSONPretty data={position} />
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card
|
<Card title="Settings" description="Remote node settings">
|
||||||
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>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="p-10">
|
<div className="p-10">
|
||||||
<form className="space-y-4">
|
<form className="space-y-4">
|
||||||
<Input label={'Device Name'} />
|
<Input label={'Device Name'} />
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const Files = ({ navOpen, setNavOpen }: RangeTestProps): JSX.Element => {
|
|||||||
<PrimaryTemplate
|
<PrimaryTemplate
|
||||||
title="File Browser"
|
title="File Browser"
|
||||||
tagline="Plugin"
|
tagline="Plugin"
|
||||||
button={
|
leftButton={
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<FiMenu className="w-5 h-5" />}
|
icon={<FiMenu className="w-5 h-5" />}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { connection } from '@app/core/connection';
|
||||||
import { useAppSelector } from '@app/hooks/redux';
|
import { useAppSelector } from '@app/hooks/redux';
|
||||||
import { Button } from '@components/generic/Button';
|
|
||||||
import { Card } from '@components/generic/Card';
|
import { Card } from '@components/generic/Card';
|
||||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||||
import { Input } from '@components/generic/form/Input';
|
import { Input } from '@components/generic/form/Input';
|
||||||
@@ -26,7 +26,7 @@ export const RangeTest = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const preferences = useAppSelector((state) => state.meshtastic.preferences);
|
const preferences = useAppSelector((state) => state.meshtastic.preferences);
|
||||||
|
|
||||||
const { register, handleSubmit, formState } =
|
const { register, handleSubmit, formState, reset, control } =
|
||||||
useForm<RadioConfig_UserPreferences>({
|
useForm<RadioConfig_UserPreferences>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
rangeTestPluginEnabled: preferences.rangeTestPluginEnabled,
|
rangeTestPluginEnabled: preferences.rangeTestPluginEnabled,
|
||||||
@@ -36,16 +36,20 @@ export const RangeTest = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
console.log(data);
|
|
||||||
|
|
||||||
void connection.setPreferences(data);
|
void connection.setPreferences(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const watchRangeTestPluginEnabled = useWatch({
|
||||||
|
control,
|
||||||
|
name: 'rangeTestPluginEnabled',
|
||||||
|
defaultValue: false,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PrimaryTemplate
|
<PrimaryTemplate
|
||||||
title="Range Test"
|
title="Range Test"
|
||||||
tagline="Plugin"
|
tagline="Plugin"
|
||||||
button={
|
leftButton={
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<FiMenu className="w-5 h-5" />}
|
icon={<FiMenu className="w-5 h-5" />}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
@@ -54,16 +58,11 @@ export const RangeTest = ({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
footer={
|
footer={
|
||||||
<Button
|
<FormFooter
|
||||||
className="px-10 ml-auto"
|
dirty={formState.isDirty}
|
||||||
icon={<FiSave className="w-5 h-5" />}
|
saveAction={onSubmit}
|
||||||
disabled={!formState.isDirty}
|
clearAction={reset}
|
||||||
onClick={onSubmit}
|
/>
|
||||||
active
|
|
||||||
border
|
|
||||||
>
|
|
||||||
{t('strings.save_changes')}
|
|
||||||
</Button>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="w-full space-y-4">
|
<div className="w-full space-y-4">
|
||||||
@@ -76,11 +75,13 @@ export const RangeTest = ({
|
|||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Range Test Plugin Save?"
|
label="Range Test Plugin Save?"
|
||||||
|
disabled={!watchRangeTestPluginEnabled}
|
||||||
{...register('rangeTestPluginSave')}
|
{...register('rangeTestPluginSave')}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
label="Message Interval"
|
label="Message Interval"
|
||||||
|
disabled={!watchRangeTestPluginEnabled}
|
||||||
{...register('rangeTestPluginSender', {
|
{...register('rangeTestPluginSender', {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const Channels = ({
|
|||||||
<PrimaryTemplate
|
<PrimaryTemplate
|
||||||
title="Channels"
|
title="Channels"
|
||||||
tagline="Settings"
|
tagline="Settings"
|
||||||
button={
|
leftButton={
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<FiMenu className="w-5 h-5" />}
|
icon={<FiMenu className="w-5 h-5" />}
|
||||||
onClick={(): void => {
|
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={
|
footer={
|
||||||
<Button
|
<Button
|
||||||
className="px-10 ml-auto"
|
className="px-10 ml-auto"
|
||||||
@@ -50,22 +59,7 @@ export const Channels = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Card
|
<Card>
|
||||||
title="Manage Channels"
|
|
||||||
description="Edit channel throughput and other settings"
|
|
||||||
buttons={
|
|
||||||
<Button
|
|
||||||
border
|
|
||||||
active={debug}
|
|
||||||
onClick={(): void => {
|
|
||||||
setDebug(!debug);
|
|
||||||
}}
|
|
||||||
icon={<FiCode />}
|
|
||||||
>
|
|
||||||
Debug
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Cover enabled={debug} content={<JSONPretty data={channels} />} />
|
<Cover enabled={debug} content={<JSONPretty data={channels} />} />
|
||||||
<div className="w-full p-4 space-y-2 md:p-10">
|
<div className="w-full p-4 space-y-2 md:p-10">
|
||||||
{channels.map((channel) => (
|
{channels.map((channel) => (
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import React from 'react';
|
|||||||
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { connection, setConnection } from '@app/core/connection';
|
||||||
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
|
||||||
import { Button } from '@components/generic/Button';
|
import { Button } from '@components/generic/Button';
|
||||||
@@ -52,7 +53,7 @@ export const Connection = ({
|
|||||||
);
|
);
|
||||||
const hostOverride = useAppSelector((state) => state.meshtastic.hostOverride);
|
const hostOverride = useAppSelector((state) => state.meshtastic.hostOverride);
|
||||||
|
|
||||||
const { register, handleSubmit, formState } = useForm<{
|
const { register, handleSubmit, formState, reset } = useForm<{
|
||||||
method: connType;
|
method: connType;
|
||||||
}>({
|
}>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -121,7 +122,7 @@ export const Connection = ({
|
|||||||
<PrimaryTemplate
|
<PrimaryTemplate
|
||||||
title="Connection"
|
title="Connection"
|
||||||
tagline="Settings"
|
tagline="Settings"
|
||||||
button={
|
leftButton={
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<FiMenu className="w-5 h-5" />}
|
icon={<FiMenu className="w-5 h-5" />}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
@@ -130,21 +131,14 @@ export const Connection = ({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
footer={
|
footer={
|
||||||
<Button
|
<FormFooter
|
||||||
className="px-10 ml-auto"
|
dirty={formState.isDirty}
|
||||||
icon={<FiSave className="w-5 h-5" />}
|
saveAction={onSubmit}
|
||||||
disabled={!formState.isDirty}
|
clearAction={reset}
|
||||||
active
|
/>
|
||||||
border
|
|
||||||
>
|
|
||||||
{t('strings.save_changes')}
|
|
||||||
</Button>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Card
|
<Card>
|
||||||
title="Connection Settings"
|
|
||||||
description="HTTP, BLE and Serial Options"
|
|
||||||
>
|
|
||||||
<div className="w-full max-w-3xl p-10 md:max-w-xl">
|
<div className="w-full max-w-3xl p-10 md:max-w-xl">
|
||||||
<form className="space-y-2" onSubmit={onSubmit}>
|
<form className="space-y-2" onSubmit={onSubmit}>
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -4,55 +4,80 @@ import {
|
|||||||
FiLayers,
|
FiLayers,
|
||||||
FiLayout,
|
FiLayout,
|
||||||
FiLink2,
|
FiLink2,
|
||||||
FiRss,
|
FiMapPin,
|
||||||
FiSmartphone,
|
FiRadio,
|
||||||
|
FiUser,
|
||||||
|
FiWifi,
|
||||||
|
FiZap,
|
||||||
} from 'react-icons/fi';
|
} from 'react-icons/fi';
|
||||||
|
|
||||||
import { PageLayout } from '@components/templates/PageLayout';
|
import { PageLayout } from '@components/templates/PageLayout';
|
||||||
|
|
||||||
import { Channels } from './Channels';
|
import { Channels } from './Channels';
|
||||||
import { Connection } from './Connection';
|
import { Connection } from './Connection';
|
||||||
import { Device } from './Device';
|
|
||||||
import { Interface } from './Interface';
|
import { Interface } from './Interface';
|
||||||
|
import { Position } from './Position';
|
||||||
|
import { Power } from './Power';
|
||||||
import { Radio } from './Radio';
|
import { Radio } from './Radio';
|
||||||
|
import { User } from './User';
|
||||||
|
import { WiFi } from './WiFi';
|
||||||
|
|
||||||
export const Settings = (): JSX.Element => {
|
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 (
|
return (
|
||||||
<PageLayout
|
<PageLayout
|
||||||
title="Settings"
|
title="Settings"
|
||||||
sidebarItems={[
|
sidebarItems={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" />,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
panels={[
|
panels={[
|
||||||
<Connection key={1} />,
|
<Connection key={1} />,
|
||||||
<Device key={2} />,
|
<WiFi key={2} />,
|
||||||
<Radio key={3} />,
|
<Position key={3} />,
|
||||||
<Interface key={4} />,
|
<User key={4} />,
|
||||||
<Channels key={5} />,
|
<Power key={5} />,
|
||||||
|
<Radio key={6} />,
|
||||||
|
<Interface key={7} />,
|
||||||
|
<Channels key={8} />,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const Interface = ({
|
|||||||
<PrimaryTemplate
|
<PrimaryTemplate
|
||||||
title="Interface"
|
title="Interface"
|
||||||
tagline="Settings"
|
tagline="Settings"
|
||||||
button={
|
leftButton={
|
||||||
<Button
|
<Button
|
||||||
icon={<FiMenu className="w-5 h-5" />}
|
icon={<FiMenu className="w-5 h-5" />}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+19
-100
@@ -2,16 +2,15 @@ import React from 'react';
|
|||||||
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 JSONPretty from 'react-json-pretty';
|
||||||
|
|
||||||
|
import { FormFooter } from '@app/components/FormFooter';
|
||||||
import { connection } from '@app/core/connection';
|
import { connection } from '@app/core/connection';
|
||||||
import { useAppSelector } from '@app/hooks/redux';
|
import { useAppSelector } from '@app/hooks/redux';
|
||||||
import { Button } from '@components/generic/Button';
|
|
||||||
import { Card } from '@components/generic/Card';
|
import { Card } from '@components/generic/Card';
|
||||||
import { Cover } from '@components/generic/Cover';
|
import { Cover } from '@components/generic/Cover';
|
||||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||||
import { Input } from '@components/generic/form/Input';
|
|
||||||
import { Select } from '@components/generic/form/Select';
|
import { Select } from '@components/generic/form/Select';
|
||||||
import { IconButton } from '@components/generic/IconButton';
|
import { IconButton } from '@components/generic/IconButton';
|
||||||
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
|
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
|
||||||
@@ -39,7 +38,7 @@ export const Radio = ({ navOpen, setNavOpen }: RadioProps): JSX.Element => {
|
|||||||
<PrimaryTemplate
|
<PrimaryTemplate
|
||||||
title="Radio"
|
title="Radio"
|
||||||
tagline="Settings"
|
tagline="Settings"
|
||||||
button={
|
leftButton={
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<FiMenu className="w-5 h-5" />}
|
icon={<FiMenu className="w-5 h-5" />}
|
||||||
onClick={(): void => {
|
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={
|
footer={
|
||||||
<div className="flex space-x-2">
|
<FormFooter
|
||||||
<IconButton
|
dirty={formState.isDirty}
|
||||||
icon={<FiXCircle className="w-5 h-5" />}
|
saveAction={onSubmit}
|
||||||
disabled={formState.isDirty}
|
clearAction={reset}
|
||||||
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>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Card
|
<Card>
|
||||||
title="Basic settings"
|
|
||||||
description="Device name and user parameters"
|
|
||||||
buttons={
|
|
||||||
<Button
|
|
||||||
border
|
|
||||||
active={debug}
|
|
||||||
onClick={(): void => {
|
|
||||||
setDebug(!debug);
|
|
||||||
}}
|
|
||||||
icon={<FiCode />}
|
|
||||||
>
|
|
||||||
Debug
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Cover enabled={debug} content={<JSONPretty data={radioConfig} />} />
|
<Cover enabled={debug} content={<JSONPretty data={radioConfig} />} />
|
||||||
<div className="w-full max-w-3xl p-10 md:max-w-xl">
|
<div className="w-full max-w-3xl p-10 md:max-w-xl">
|
||||||
<form className="space-y-2" onSubmit={onSubmit}>
|
<form className="space-y-2" onSubmit={onSubmit}>
|
||||||
<div>Power</div>
|
<Checkbox label="Is Router" {...register('isRouter')} />
|
||||||
<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>
|
|
||||||
<Select
|
<Select
|
||||||
label="Region"
|
label="Region"
|
||||||
optionsEnum={Protobuf.RegionCode}
|
optionsEnum={Protobuf.RegionCode}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import React from 'react';
|
|||||||
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 JSONPretty from 'react-json-pretty';
|
||||||
|
|
||||||
|
import { FormFooter } from '@app/components/FormFooter';
|
||||||
import { connection } from '@app/core/connection';
|
import { connection } from '@app/core/connection';
|
||||||
import { addUser } from '@app/core/slices/meshtasticSlice';
|
import { addUser } from '@app/core/slices/meshtasticSlice';
|
||||||
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
|
||||||
import { Button } from '@components/generic/Button';
|
|
||||||
import { Card } from '@components/generic/Card';
|
import { Card } from '@components/generic/Card';
|
||||||
import { Cover } from '@components/generic/Cover';
|
import { Cover } from '@components/generic/Cover';
|
||||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||||
@@ -18,12 +18,12 @@ import { IconButton } from '@components/generic/IconButton';
|
|||||||
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
|
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
|
||||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
export interface DeviceProps {
|
export interface UserProps {
|
||||||
navOpen?: boolean;
|
navOpen?: boolean;
|
||||||
setNavOpen?: React.Dispatch<React.SetStateAction<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 { t } = useTranslation();
|
||||||
const [debug, setDebug] = React.useState(false);
|
const [debug, setDebug] = React.useState(false);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@@ -31,7 +31,7 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
|
|||||||
const user = useAppSelector((state) => state.meshtastic.users).find(
|
const user = useAppSelector((state) => state.meshtastic.users).find(
|
||||||
(user) => user.packet.from === myNodeInfo.myNodeNum,
|
(user) => user.packet.from === myNodeInfo.myNodeNum,
|
||||||
);
|
);
|
||||||
const { register, handleSubmit, formState } = useForm<{
|
const { register, handleSubmit, formState, reset } = useForm<{
|
||||||
longName: string;
|
longName: string;
|
||||||
shortName: string;
|
shortName: string;
|
||||||
isLicensed: boolean;
|
isLicensed: boolean;
|
||||||
@@ -46,7 +46,7 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
// TODO: can be remove once getUser is implemented
|
// TODO: can be removed once getUser is implemented
|
||||||
if (user) {
|
if (user) {
|
||||||
void connection.setOwner({ ...user.data, ...data });
|
void connection.setOwner({ ...user.data, ...data });
|
||||||
dispatch(addUser({ ...user, ...{ data: { ...user.data, ...data } } }));
|
dispatch(addUser({ ...user, ...{ data: { ...user.data, ...data } } }));
|
||||||
@@ -55,9 +55,9 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PrimaryTemplate
|
<PrimaryTemplate
|
||||||
title="Device"
|
title="User"
|
||||||
tagline="Settings"
|
tagline="Settings"
|
||||||
button={
|
leftButton={
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<FiMenu className="w-5 h-5" />}
|
icon={<FiMenu className="w-5 h-5" />}
|
||||||
onClick={(): void => {
|
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={
|
footer={
|
||||||
<Button
|
<FormFooter
|
||||||
className="px-10 ml-auto"
|
dirty={formState.isDirty}
|
||||||
icon={<FiSave className="w-5 h-5" />}
|
saveAction={onSubmit}
|
||||||
disabled={!formState.isDirty}
|
clearAction={reset}
|
||||||
onClick={onSubmit}
|
/>
|
||||||
active
|
|
||||||
border
|
|
||||||
>
|
|
||||||
{t('strings.save_changes')}
|
|
||||||
</Button>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Card
|
<Card>
|
||||||
title="Basic settings"
|
|
||||||
description="Device name and user parameters"
|
|
||||||
buttons={
|
|
||||||
<Button
|
|
||||||
border
|
|
||||||
active={debug}
|
|
||||||
onClick={(): void => {
|
|
||||||
setDebug(!debug);
|
|
||||||
}}
|
|
||||||
icon={<FiCode />}
|
|
||||||
>
|
|
||||||
Debug
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Cover enabled={debug} content={<JSONPretty data={user} />} />
|
<Cover enabled={debug} content={<JSONPretty data={user} />} />
|
||||||
<div className="w-full max-w-3xl p-10 md:max-w-xl">
|
<div className="w-full max-w-3xl p-10 md:max-w-xl">
|
||||||
<form className="space-y-2" onSubmit={onSubmit}>
|
<form className="space-y-2" onSubmit={onSubmit}>
|
||||||
@@ -117,7 +106,7 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
|
|||||||
<Select
|
<Select
|
||||||
label="Team"
|
label="Team"
|
||||||
optionsEnum={Protobuf.Team}
|
optionsEnum={Protobuf.Team}
|
||||||
{...register('team')}
|
{...register('team', { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user