Rebuild
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
|
||||
import { FiMessageCircle, FiSettings } from 'react-icons/fi';
|
||||
import { RiMindMap, RiRoadMapLine } from 'react-icons/ri';
|
||||
import { VscExtensions } from 'react-icons/vsc';
|
||||
|
||||
import { toggleMobileNav } from '@app/core/slices/appSlice.js';
|
||||
import { useAppDispatch } from '@app/hooks/useAppDispatch.js';
|
||||
import { routes, useRoute } from '@core/router';
|
||||
|
||||
import { NavLinkButton } from './NavLinkButton';
|
||||
|
||||
export interface ButtonNavProps {
|
||||
toggleSettingsOpen: () => void;
|
||||
}
|
||||
|
||||
export const ButtonNav = ({
|
||||
toggleSettingsOpen,
|
||||
}: ButtonNavProps): JSX.Element => {
|
||||
const route = useRoute();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
return (
|
||||
<div className="z-10 flex justify-between border-t border-gray-300 px-6 py-2 dark:border-gray-600 dark:bg-primaryDark">
|
||||
<div
|
||||
onClick={(): void => {
|
||||
dispatch(toggleMobileNav());
|
||||
}}
|
||||
>
|
||||
<NavLinkButton
|
||||
active={route.name === 'messages'}
|
||||
link={routes.messages().link}
|
||||
>
|
||||
<FiMessageCircle className="h-5 w-5" />
|
||||
</NavLinkButton>
|
||||
</div>
|
||||
<div
|
||||
onClick={(): void => {
|
||||
dispatch(toggleMobileNav());
|
||||
}}
|
||||
>
|
||||
<NavLinkButton
|
||||
active={route.name === 'nodes'}
|
||||
link={routes.nodes().link}
|
||||
>
|
||||
<RiMindMap className="h-5 w-5" />
|
||||
</NavLinkButton>
|
||||
</div>
|
||||
<div
|
||||
onClick={(): void => {
|
||||
dispatch(toggleMobileNav());
|
||||
}}
|
||||
>
|
||||
<NavLinkButton active={route.name === 'map'} link={routes.map().link}>
|
||||
<RiRoadMapLine className="h-5 w-5" />
|
||||
</NavLinkButton>
|
||||
</div>
|
||||
<div
|
||||
onClick={(): void => {
|
||||
dispatch(toggleMobileNav());
|
||||
}}
|
||||
>
|
||||
<NavLinkButton
|
||||
active={route.name === 'extensions'}
|
||||
link={routes.extensions().link}
|
||||
>
|
||||
<VscExtensions className="h-5 w-5" />
|
||||
</NavLinkButton>
|
||||
</div>
|
||||
<NavLinkButton
|
||||
action={(): void => {
|
||||
toggleSettingsOpen();
|
||||
}}
|
||||
>
|
||||
<FiSettings className="h-5 w-5" />
|
||||
</NavLinkButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { m } from 'framer-motion';
|
||||
import type { Link } from 'type-route';
|
||||
|
||||
export interface NavLinkButtonProps {
|
||||
link?: Link;
|
||||
active?: boolean;
|
||||
action?: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const NavLinkButton = ({
|
||||
link,
|
||||
active,
|
||||
action,
|
||||
children,
|
||||
}: NavLinkButtonProps): JSX.Element => {
|
||||
return (
|
||||
<m.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileTap={{ scale: 0.99 }}
|
||||
animate={active ? 'selected' : 'deselected'}
|
||||
initial={{ borderColor: '#1C1D23' }}
|
||||
variants={{
|
||||
selected: { borderColor: '#67ea94' },
|
||||
deselected: { borderColor: '#1C1D23' },
|
||||
}}
|
||||
className="cursor-pointer rounded-full border-2 p-3 hover:bg-opacity-80 hover:shadow-md dark:bg-secondaryDark dark:text-white"
|
||||
onClick={(): void => {
|
||||
action && action();
|
||||
}}
|
||||
{...(link && link)}
|
||||
>
|
||||
{children}
|
||||
</m.div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,160 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { FiExternalLink, FiX } from 'react-icons/fi';
|
||||
import {
|
||||
RiArrowDownLine,
|
||||
RiArrowUpDownLine,
|
||||
RiArrowUpLine,
|
||||
} from 'react-icons/ri';
|
||||
|
||||
import { ListItem } from '@app/components/generic/ListItem';
|
||||
import type { ChannelData } from '@app/core/slices/meshtasticSlice';
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Checkbox, Input, Select, Tooltip } from '@meshtastic/components';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const Channels = (): JSX.Element => {
|
||||
const channels = useAppSelector((state) => state.meshtastic.radio.channels);
|
||||
const adminChannel =
|
||||
channels.find(
|
||||
(channel) => channel.channel.role === Protobuf.Channel_Role.PRIMARY,
|
||||
) ?? channels[0];
|
||||
const [usePreset, setUsePreset] = React.useState(true);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [selectedChannel, setSelectedChannel] = React.useState<
|
||||
ChannelData | undefined
|
||||
>();
|
||||
|
||||
const { register, handleSubmit, reset, formState } = useForm<
|
||||
DeepOmit<Protobuf.Channel, 'psk'>
|
||||
>({
|
||||
defaultValues: {
|
||||
...adminChannel.channel,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
setLoading(true);
|
||||
|
||||
const channelData = Protobuf.Channel.create({
|
||||
...data,
|
||||
settings: {
|
||||
...data.settings,
|
||||
psk: adminChannel.channel.settings?.psk,
|
||||
},
|
||||
});
|
||||
|
||||
await connection.setChannel(channelData, (): Promise<void> => {
|
||||
reset({ ...data });
|
||||
setLoading(false);
|
||||
return Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{adminChannel && (
|
||||
<>
|
||||
<Checkbox
|
||||
checked={usePreset}
|
||||
label="Use Presets"
|
||||
onChange={(e): void => setUsePreset(e.target.checked)}
|
||||
/>
|
||||
<form onSubmit={onSubmit}>
|
||||
{usePreset ? (
|
||||
<Select
|
||||
label="Preset"
|
||||
optionsEnum={Protobuf.ChannelSettings_ModemConfig}
|
||||
{...register('settings.modemConfig', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Input
|
||||
label="Bandwidth"
|
||||
type="number"
|
||||
suffix="MHz"
|
||||
{...register('settings.bandwidth', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
label="Spread Factor"
|
||||
type="number"
|
||||
suffix="CPS"
|
||||
min={7}
|
||||
max={12}
|
||||
{...register('settings.spreadFactor', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
label="Coding Rate"
|
||||
type="number"
|
||||
{...register('settings.codingRate', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Input
|
||||
label="Transmit Power"
|
||||
type="number"
|
||||
suffix="dBm"
|
||||
{...register('settings.txPower', { valueAsNumber: true })}
|
||||
/>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
{channels.map((channel) => (
|
||||
<ListItem
|
||||
key={channel.channel.index}
|
||||
onClick={(): void => {
|
||||
setSelectedChannel(channel);
|
||||
}}
|
||||
status={
|
||||
<div
|
||||
className={`my-auto h-3 w-3 rounded-full ${
|
||||
[
|
||||
Protobuf.Channel_Role.SECONDARY,
|
||||
Protobuf.Channel_Role.PRIMARY,
|
||||
].find((role) => role === channel.channel.role)
|
||||
? 'bg-green-500'
|
||||
: 'bg-gray-400'
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
selected={selectedChannel?.channel.index === channel.channel.index}
|
||||
selectedIcon={<FiExternalLink />}
|
||||
actions={
|
||||
<Tooltip content={`MQTT Status`}>
|
||||
<div className="rounded-md p-2">
|
||||
{channel.channel.settings?.uplinkEnabled &&
|
||||
channel.channel.settings?.downlinkEnabled ? (
|
||||
<RiArrowUpDownLine className="p-0.5 group-active:scale-90" />
|
||||
) : channel.channel.settings?.uplinkEnabled ? (
|
||||
<RiArrowUpLine className="p-0.5 group-active:scale-90" />
|
||||
) : channel.channel.settings?.downlinkEnabled ? (
|
||||
<RiArrowDownLine className="p-0.5 group-active:scale-90" />
|
||||
) : (
|
||||
<FiX className="p-0.5" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
{channel.channel.settings?.name.length
|
||||
? channel.channel.settings.name
|
||||
: channel.channel.role === Protobuf.Channel_Role.PRIMARY
|
||||
? 'Primary'
|
||||
: `Channel: ${channel.channel.index}`}
|
||||
</div>
|
||||
</ListItem>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
FiAlignLeft,
|
||||
FiBell,
|
||||
FiFastForward,
|
||||
FiLayers,
|
||||
FiLayout,
|
||||
FiMapPin,
|
||||
FiPackage,
|
||||
FiRadio,
|
||||
FiRss,
|
||||
FiUser,
|
||||
FiWifi,
|
||||
FiZap,
|
||||
} from 'react-icons/fi';
|
||||
|
||||
import { CollapsibleSection } from '@app/components/layout/Sidebar/sections/CollapsibleSection';
|
||||
import { ExternalSection } from '@app/components/layout/Sidebar/sections/ExternalSection';
|
||||
import { SidebarOverlay } from '@app/components/layout/Sidebar/sections/SidebarOverlay';
|
||||
import { SidebarPrimary } from '@app/components/layout/Sidebar/sections/SidebarPrimary';
|
||||
import { Channels } from '@app/components/layout/Sidebar/Settings/Channels';
|
||||
import { ExternalNotificationsSettingsPlanel } from '@app/components/layout/Sidebar/Settings/plugins/panels/ExternalNotifications/SettingsPlanel';
|
||||
import { RangeTestSettingsPanel } from '@app/components/layout/Sidebar/Settings/plugins/panels/RangeTest/SettingsPanel';
|
||||
import { SerialSettingsPanel } from '@app/components/layout/Sidebar/Settings/plugins/panels/Serial/SettingsPanel';
|
||||
import { StoreForwardSettingsPanel } from '@app/components/layout/Sidebar/Settings/plugins/panels/StoreForward/SettingsPanel';
|
||||
import { Position } from '@app/components/layout/Sidebar/Settings/Position';
|
||||
import { Power } from '@app/components/layout/Sidebar/Settings/Power';
|
||||
import { Radio } from '@app/components/layout/Sidebar/Settings/Radio';
|
||||
import { User } from '@app/components/layout/Sidebar/Settings/User';
|
||||
import { WiFi } from '@app/components/layout/Sidebar/Settings/WiFi';
|
||||
|
||||
import { Interface } from './Interface';
|
||||
import { ChannelsGroup } from './radio/channels/panels/ChannelsGroup';
|
||||
|
||||
export interface SettingsProps {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const Settings = ({ open, setOpen }: SettingsProps): JSX.Element => {
|
||||
const [pluginsOpen, setPluginsOpen] = React.useState(false);
|
||||
const [channelsOpen, setChannelsOpen] = React.useState(false);
|
||||
// const { hasGps, hasWifi } = useAppSelector((state) => state.meshtastic.radio.hardware);
|
||||
|
||||
const hasGps = true;
|
||||
const hasWifi = true;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarPrimary
|
||||
title="Settings"
|
||||
open={open}
|
||||
close={(): void => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<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">
|
||||
<Power />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection icon={<FiRadio />} title="Radio">
|
||||
<Radio />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection icon={<FiLayers />} title="Primary Channel">
|
||||
<Channels />
|
||||
</CollapsibleSection>
|
||||
<ExternalSection
|
||||
onClick={(): void => {
|
||||
setChannelsOpen(true);
|
||||
}}
|
||||
icon={<FiLayers />}
|
||||
title="Channels"
|
||||
/>
|
||||
<ExternalSection
|
||||
onClick={(): void => {
|
||||
setPluginsOpen(true);
|
||||
}}
|
||||
icon={<FiPackage />}
|
||||
title="Plugins"
|
||||
/>
|
||||
<CollapsibleSection icon={<FiLayout />} title="Interface">
|
||||
<Interface />
|
||||
</CollapsibleSection>
|
||||
</SidebarPrimary>
|
||||
|
||||
{/* Plugins */}
|
||||
<SidebarOverlay
|
||||
title="Plugins"
|
||||
open={pluginsOpen}
|
||||
close={(): void => {
|
||||
setPluginsOpen(false);
|
||||
}}
|
||||
>
|
||||
<CollapsibleSection title="Range Test" icon={<FiRss />}>
|
||||
<RangeTestSettingsPanel />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection title="External Notifications" icon={<FiBell />}>
|
||||
<ExternalNotificationsSettingsPlanel />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection title="Serial" icon={<FiAlignLeft />}>
|
||||
<SerialSettingsPanel />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection title="Store & Forward" icon={<FiFastForward />}>
|
||||
<StoreForwardSettingsPanel />
|
||||
</CollapsibleSection>
|
||||
</SidebarOverlay>
|
||||
{/* End Plugins */}
|
||||
|
||||
{/* Channels */}
|
||||
<SidebarOverlay
|
||||
title="Channels"
|
||||
open={channelsOpen}
|
||||
close={(): void => {
|
||||
setChannelsOpen(false);
|
||||
}}
|
||||
>
|
||||
<ChannelsGroup />
|
||||
</SidebarOverlay>
|
||||
{/* End Channels */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { Select } from '@meshtastic/components';
|
||||
|
||||
export const Interface = (): JSX.Element => {
|
||||
return (
|
||||
<Select
|
||||
label="Language"
|
||||
options={[
|
||||
{
|
||||
name: 'English',
|
||||
value: 'en',
|
||||
},
|
||||
{
|
||||
name: '日本',
|
||||
value: 'jp',
|
||||
},
|
||||
{
|
||||
name: 'Português',
|
||||
value: 'pt',
|
||||
},
|
||||
]}
|
||||
onChange={(e): void => {
|
||||
console.log('changed language');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { MultiSelect } from 'react-multi-select-component';
|
||||
|
||||
import { bitwiseEncode } from '@app/core/utils/bitwise';
|
||||
import { Label } from '@components/generic/form/Label';
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Checkbox, Input, Select } from '@meshtastic/components';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const Position = (): JSX.Element => {
|
||||
const preferences = useAppSelector(
|
||||
(state) => state.meshtastic.radio.preferences,
|
||||
);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const { register, handleSubmit, formState, reset, control } =
|
||||
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||
defaultValues: {
|
||||
...preferences,
|
||||
positionBroadcastSecs:
|
||||
preferences.positionBroadcastSecs === 0
|
||||
? preferences.isRouter
|
||||
? 43200
|
||||
: 900
|
||||
: preferences.positionBroadcastSecs,
|
||||
},
|
||||
});
|
||||
|
||||
React.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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Checkbox, Select } from '@meshtastic/components';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const Power = (): JSX.Element => {
|
||||
const preferences = useAppSelector(
|
||||
(state) => state.meshtastic.radio.preferences,
|
||||
);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const { register, handleSubmit, formState, reset } =
|
||||
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||
defaultValues: {
|
||||
...preferences,
|
||||
isLowPower: preferences.isRouter ? true : preferences.isLowPower,
|
||||
},
|
||||
});
|
||||
|
||||
React.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 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Checkbox, Select } from '@meshtastic/components';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const Radio = (): JSX.Element => {
|
||||
const preferences = useAppSelector(
|
||||
(state) => state.meshtastic.radio.preferences,
|
||||
);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const { register, handleSubmit, formState, reset } =
|
||||
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||
defaultValues: preferences,
|
||||
});
|
||||
|
||||
React.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 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { base16 } from 'rfc4648';
|
||||
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Checkbox, Input, Select } from '@meshtastic/components';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const User = (): JSX.Element => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const myNodeNum = useAppSelector(
|
||||
(state) => state.meshtastic.radio.hardware,
|
||||
).myNodeNum;
|
||||
const node = useAppSelector((state) => state.meshtastic.nodes).find(
|
||||
(node) => node.number === myNodeNum,
|
||||
);
|
||||
const { register, handleSubmit, formState, reset } = useForm<{
|
||||
longName: string;
|
||||
shortName: string;
|
||||
isLicensed: boolean;
|
||||
team: Protobuf.Team;
|
||||
antAzimuth: number;
|
||||
antGainDbi: number;
|
||||
txPowerDbm: number;
|
||||
}>({
|
||||
defaultValues: {
|
||||
longName: node?.user?.longName,
|
||||
shortName: node?.user?.shortName,
|
||||
isLicensed: node?.user?.isLicensed,
|
||||
team: node?.user?.team,
|
||||
antAzimuth: node?.user?.antAzimuth,
|
||||
antGainDbi: node?.user?.antGainDbi,
|
||||
txPowerDbm: node?.user?.txPowerDbm,
|
||||
},
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
reset({
|
||||
longName: node?.user?.longName,
|
||||
shortName: node?.user?.shortName,
|
||||
isLicensed: node?.user?.isLicensed,
|
||||
team: node?.user?.team,
|
||||
});
|
||||
}, [reset, node]);
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
setLoading(true);
|
||||
|
||||
if (node?.user) {
|
||||
void connection.setOwner({ ...node.user, ...data }, async () => {
|
||||
reset({ ...data });
|
||||
setLoading(false);
|
||||
await Promise.resolve();
|
||||
});
|
||||
// TODO: can be removed once getUser is implemented
|
||||
// dispatch(
|
||||
// addUser({ ...node.user, ...{ data: { ...node.user.data, ...data } } }),
|
||||
// );
|
||||
}
|
||||
});
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Checkbox, Input } from '@meshtastic/components';
|
||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const WiFi = (): JSX.Element => {
|
||||
const preferences = useAppSelector(
|
||||
(state) => state.meshtastic.radio.preferences,
|
||||
);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const { register, handleSubmit, formState, reset, control } =
|
||||
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||
defaultValues: preferences,
|
||||
});
|
||||
|
||||
const watchWifiApMode = useWatch({
|
||||
control,
|
||||
name: 'wifiApMode',
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
const watchMQTTDisabled = useWatch({
|
||||
control,
|
||||
name: 'mqttDisabled',
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
React.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 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>
|
||||
);
|
||||
};
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import type React from 'react';
|
||||
|
||||
import JSONPretty from 'react-json-pretty';
|
||||
|
||||
import { useAppSelector } from '@app/hooks/useAppSelector';
|
||||
import { CopyButton } from '@components/menu/buttons/CopyButton';
|
||||
|
||||
export const ExternalNotificationsDebugPanel = (): JSX.Element => {
|
||||
const preferences = useAppSelector(
|
||||
(state) => state.meshtastic.radio.preferences,
|
||||
);
|
||||
|
||||
const debugData = {
|
||||
extNotificationPluginActive: preferences.extNotificationPluginActive,
|
||||
extNotificationPluginAlertBell: preferences.extNotificationPluginAlertBell,
|
||||
extNotificationPluginAlertMessage:
|
||||
preferences.extNotificationPluginAlertMessage,
|
||||
extNotificationPluginEnabled: preferences.extNotificationPluginEnabled,
|
||||
extNotificationPluginOutput: preferences.extNotificationPluginOutput,
|
||||
extNotificationPluginOutputMs: preferences.extNotificationPluginOutputMs,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed right-0 m-2">
|
||||
<CopyButton data={JSON.stringify(debugData)} />
|
||||
</div>
|
||||
<JSONPretty className="max-w-sm" data={debugData} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { FiSave } from 'react-icons/fi';
|
||||
|
||||
import { Form } from '@app/components/generic/form/Form';
|
||||
import { connection } from '@app/core/connection';
|
||||
import { useAppSelector } from '@app/hooks/useAppSelector';
|
||||
import { Checkbox, IconButton, Input } from '@meshtastic/components';
|
||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const ExternalNotificationsSettingsPlanel = (): JSX.Element => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const preferences = useAppSelector(
|
||||
(state) => state.meshtastic.radio.preferences,
|
||||
);
|
||||
|
||||
const { register, handleSubmit, formState, reset, control } =
|
||||
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||
defaultValues: preferences,
|
||||
});
|
||||
|
||||
React.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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import type React from 'react';
|
||||
|
||||
import JSONPretty from 'react-json-pretty';
|
||||
|
||||
import { useAppSelector } from '@app/hooks/useAppSelector';
|
||||
import { CopyButton } from '@components/menu/buttons/CopyButton';
|
||||
|
||||
export const RangeTestDebugPanel = (): JSX.Element => {
|
||||
const preferences = useAppSelector(
|
||||
(state) => state.meshtastic.radio.preferences,
|
||||
);
|
||||
|
||||
const debugData = {
|
||||
rangeTestPluginEnabled: preferences.rangeTestPluginEnabled,
|
||||
rangeTestPluginSave: preferences.rangeTestPluginSave,
|
||||
rangeTestPluginSender: preferences.rangeTestPluginSender,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed right-0 m-2">
|
||||
<CopyButton data={JSON.stringify(debugData)} />
|
||||
</div>
|
||||
<JSONPretty className="max-w-sm" data={debugData} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { FiSave } from 'react-icons/fi';
|
||||
|
||||
import { Form } from '@app/components/generic/form/Form';
|
||||
import { connection } from '@app/core/connection';
|
||||
import { useAppSelector } from '@app/hooks/useAppSelector';
|
||||
import { Checkbox, IconButton, Input } from '@meshtastic/components';
|
||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const RangeTestSettingsPanel = (): JSX.Element => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const preferences = useAppSelector(
|
||||
(state) => state.meshtastic.radio.preferences,
|
||||
);
|
||||
|
||||
const { register, handleSubmit, formState, reset, control } =
|
||||
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||
defaultValues: preferences,
|
||||
});
|
||||
|
||||
React.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: 'rangeTestPluginEnabled',
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import type React from 'react';
|
||||
|
||||
import JSONPretty from 'react-json-pretty';
|
||||
|
||||
import { useAppSelector } from '@app/hooks/useAppSelector';
|
||||
import { CopyButton } from '@components/menu/buttons/CopyButton';
|
||||
|
||||
export const SerialDebugPanel = (): JSX.Element => {
|
||||
const preferences = useAppSelector(
|
||||
(state) => state.meshtastic.radio.preferences,
|
||||
);
|
||||
|
||||
const debugData = {
|
||||
serialpluginEnabled: preferences.serialpluginEnabled,
|
||||
serialpluginEcho: preferences.serialpluginEcho,
|
||||
serialpluginMode: preferences.serialpluginMode,
|
||||
serialpluginRxd: preferences.serialpluginRxd,
|
||||
serialpluginTxd: preferences.serialpluginTxd,
|
||||
serialpluginTimeout: preferences.serialpluginTimeout,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed right-0 m-2">
|
||||
<CopyButton data={JSON.stringify(debugData)} />
|
||||
</div>
|
||||
<JSONPretty className="max-w-sm" data={debugData} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { FiSave } from 'react-icons/fi';
|
||||
|
||||
import { Form } from '@app/components/generic/form/Form';
|
||||
import { connection } from '@app/core/connection';
|
||||
import { useAppSelector } from '@app/hooks/useAppSelector';
|
||||
import { Checkbox, IconButton, Input } from '@meshtastic/components';
|
||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const SerialSettingsPanel = (): JSX.Element => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const preferences = useAppSelector(
|
||||
(state) => state.meshtastic.radio.preferences,
|
||||
);
|
||||
|
||||
const { register, handleSubmit, formState, reset, control } =
|
||||
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||
defaultValues: preferences,
|
||||
});
|
||||
|
||||
React.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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import type React from 'react';
|
||||
|
||||
import JSONPretty from 'react-json-pretty';
|
||||
|
||||
import { useAppSelector } from '@app/hooks/useAppSelector';
|
||||
import { CopyButton } from '@components/menu/buttons/CopyButton';
|
||||
|
||||
export const StoreForwardDebugPanel = (): JSX.Element => {
|
||||
const preferences = useAppSelector(
|
||||
(state) => state.meshtastic.radio.preferences,
|
||||
);
|
||||
|
||||
const debugData = {
|
||||
storeForwardPluginEnabled: preferences.storeForwardPluginEnabled,
|
||||
storeForwardPluginHeartbeat: preferences.storeForwardPluginHeartbeat,
|
||||
storeForwardPluginRecords: preferences.storeForwardPluginRecords,
|
||||
storeForwardPluginHistoryReturnMax:
|
||||
preferences.storeForwardPluginHistoryReturnMax,
|
||||
storeForwardPluginHistoryReturnWindow:
|
||||
preferences.storeForwardPluginHistoryReturnWindow,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed right-0 m-2">
|
||||
<CopyButton data={JSON.stringify(debugData)} />
|
||||
</div>
|
||||
<JSONPretty className="max-w-sm" data={debugData} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { FiSave } from 'react-icons/fi';
|
||||
|
||||
import { Form } from '@app/components/generic/form/Form';
|
||||
import { connection } from '@app/core/connection';
|
||||
import { useAppSelector } from '@app/hooks/useAppSelector';
|
||||
import { Checkbox, IconButton, Input } from '@meshtastic/components';
|
||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const StoreForwardSettingsPanel = (): JSX.Element => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const preferences = useAppSelector(
|
||||
(state) => state.meshtastic.radio.preferences,
|
||||
);
|
||||
|
||||
const { register, handleSubmit, formState, reset, control } =
|
||||
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||
defaultValues: preferences,
|
||||
});
|
||||
|
||||
React.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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { FaQrcode } from 'react-icons/fa';
|
||||
import { FiCode, FiSave } from 'react-icons/fi';
|
||||
|
||||
import { CollapsibleSection } from '@app/components/layout/Sidebar/sections/CollapsibleSection';
|
||||
import { useAppSelector } from '@app/hooks/useAppSelector';
|
||||
import { IconButton } from '@meshtastic/components';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
import { SettingsPanel } from './SettingsPanel';
|
||||
|
||||
export const ChannelsGroup = (): JSX.Element => {
|
||||
const channels = useAppSelector((state) => state.meshtastic.radio.channels);
|
||||
|
||||
return (
|
||||
<>
|
||||
{channels.map((channel) => {
|
||||
return (
|
||||
<div key={channel.channel.index}>
|
||||
<CollapsibleSection
|
||||
title={
|
||||
channel.channel.settings?.name.length
|
||||
? channel.channel.settings.name
|
||||
: channel.channel.role === Protobuf.Channel_Role.PRIMARY
|
||||
? 'Primary'
|
||||
: `Channel: ${channel.channel.index}`
|
||||
}
|
||||
icon={
|
||||
<div
|
||||
className={`h-3 w-3 rounded-full ${
|
||||
channel.channel.role === Protobuf.Channel_Role.PRIMARY
|
||||
? 'bg-orange-500'
|
||||
: channel.channel.role === Protobuf.Channel_Role.SECONDARY
|
||||
? 'bg-green-500'
|
||||
: 'bg-gray-500'
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<IconButton icon={<FiCode />} />
|
||||
<IconButton icon={<FaQrcode />} />
|
||||
<IconButton icon={<FiSave />} />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<>
|
||||
{/* <DebugPanel channel={channel.channel} /> */}
|
||||
{/* <QRCodePanel channel={channel.channel} /> */}
|
||||
<SettingsPanel channel={channel.channel} />
|
||||
</>
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import type React from 'react';
|
||||
|
||||
import JSONPretty from 'react-json-pretty';
|
||||
|
||||
import { CopyButton } from '@components/menu/buttons/CopyButton';
|
||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export interface DebugPanelProps {
|
||||
channel: Protobuf.Channel;
|
||||
}
|
||||
|
||||
export const DebugPanel = ({ channel }: DebugPanelProps): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<div className="fixed right-0 m-2">
|
||||
<CopyButton data={JSON.stringify(channel)} />
|
||||
</div>
|
||||
<JSONPretty className="max-w-sm" data={channel} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import type React from 'react';
|
||||
|
||||
import QRCode from 'react-qr-code';
|
||||
|
||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export interface QRCodePanelProps {
|
||||
channel: Protobuf.Channel;
|
||||
}
|
||||
|
||||
export const QRCodePanel = ({ channel }: QRCodePanelProps): JSX.Element => {
|
||||
return (
|
||||
<div className="m-auto">
|
||||
<QRCode
|
||||
className="rounded-md"
|
||||
value={`https://www.meshtastic.org/d/#${channel.index}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
import React 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 { Form } from '@app/components/generic/form/Form';
|
||||
import { connection } from '@app/core/connection';
|
||||
import { Checkbox, IconButton, Input, Select } from '@meshtastic/components';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export interface SettingsPanelProps {
|
||||
channel: Protobuf.Channel;
|
||||
}
|
||||
|
||||
export const SettingsPanel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [keySize, setKeySize] = React.useState<128 | 256>(256);
|
||||
const [pskHidden, setPskHidden] = React.useState(true);
|
||||
|
||||
const { register, handleSubmit, setValue, formState, reset } = useForm<
|
||||
Omit<Protobuf.ChannelSettings, 'psk'> & { psk: string; enabled: boolean }
|
||||
>({
|
||||
defaultValues: {
|
||||
enabled: [
|
||||
Protobuf.Channel_Role.SECONDARY,
|
||||
Protobuf.Channel_Role.PRIMARY,
|
||||
].find((role) => role === channel?.role)
|
||||
? true
|
||||
: false,
|
||||
...channel?.settings,
|
||||
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
|
||||
},
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
reset({
|
||||
enabled: [
|
||||
Protobuf.Channel_Role.SECONDARY,
|
||||
Protobuf.Channel_Role.PRIMARY,
|
||||
].find((role) => role === channel?.role)
|
||||
? true
|
||||
: false,
|
||||
...channel?.settings,
|
||||
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
|
||||
});
|
||||
}, [channel, reset]);
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
setLoading(true);
|
||||
const channelData = Protobuf.Channel.create({
|
||||
role:
|
||||
channel?.role === Protobuf.Channel_Role.PRIMARY
|
||||
? Protobuf.Channel_Role.PRIMARY
|
||||
: data.enabled
|
||||
? Protobuf.Channel_Role.SECONDARY
|
||||
: Protobuf.Channel_Role.DISABLED,
|
||||
index: channel?.index,
|
||||
settings: {
|
||||
...data,
|
||||
psk: toByteArray(data.psk ?? ''),
|
||||
},
|
||||
});
|
||||
|
||||
await connection.setChannel(channelData, (): Promise<void> => {
|
||||
reset({ ...data });
|
||||
setLoading(false);
|
||||
return Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
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 />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { m } from 'framer-motion';
|
||||
|
||||
export interface SidebarItemProps {
|
||||
selected: boolean;
|
||||
setSelected: () => void;
|
||||
actions?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SidebarItem = ({
|
||||
selected,
|
||||
setSelected,
|
||||
actions,
|
||||
children,
|
||||
}: SidebarItemProps): JSX.Element => {
|
||||
return (
|
||||
<m.div
|
||||
onClick={(): void => {
|
||||
setSelected();
|
||||
}}
|
||||
animate={selected ? 'selected' : 'deselected'}
|
||||
initial={{ borderColor: '#1C1D23' }}
|
||||
variants={{
|
||||
selected: { borderColor: '#67ea94' },
|
||||
deselected: { borderColor: '#1C1D23' },
|
||||
}}
|
||||
className="mx-2 flex cursor-pointer select-none rounded-md border-2 p-2 first:mt-2 last:mb-2 dark:bg-secondaryDark"
|
||||
>
|
||||
<m.div
|
||||
className="flex w-full justify-between"
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileTap={{ scale: 0.99 }}
|
||||
>
|
||||
<div className="flex gap-2">{children}</div>
|
||||
|
||||
<div className="flex gap-1">{actions}</div>
|
||||
</m.div>
|
||||
</m.div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useAppSelector } from '@app/hooks/useAppSelector.js';
|
||||
|
||||
import { ButtonNav } from './ButtonNav';
|
||||
import { Settings } from './Settings/Index';
|
||||
|
||||
export interface SidebarProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
|
||||
const [settingsOpen, setSettingsOpen] = React.useState(false);
|
||||
|
||||
const appState = useAppSelector((state) => state.app);
|
||||
|
||||
return (
|
||||
<div className="flex flex-grow">
|
||||
<div
|
||||
className={`absolute h-full w-full flex-grow flex-col pb-6 md:relative md:flex md:w-96 md:pb-0 ${
|
||||
appState.mobileNavOpen ? 'flex' : 'hidden'
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col shadow-xl dark:bg-primaryDark">
|
||||
<div className="relative flex-grow gap-1">
|
||||
<div className="absolute h-full w-full">{children}</div>
|
||||
<Settings open={settingsOpen} setOpen={setSettingsOpen} />
|
||||
</div>
|
||||
<ButtonNav
|
||||
toggleSettingsOpen={(): void => {
|
||||
setSettingsOpen(!settingsOpen);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
|
||||
import { AnimatePresence, m } from 'framer-motion';
|
||||
import { FiArrowUp } from 'react-icons/fi';
|
||||
|
||||
export interface CollapsibleSectionProps {
|
||||
title: string;
|
||||
icon?: JSX.Element;
|
||||
actions?: JSX.Element;
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
export const CollapsibleSection = ({
|
||||
title,
|
||||
icon,
|
||||
actions,
|
||||
children,
|
||||
}: CollapsibleSectionProps): JSX.Element => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const toggleOpen = (): void => setOpen(!open);
|
||||
return (
|
||||
<m.div>
|
||||
<m.div
|
||||
layout
|
||||
className="w-full cursor-pointer select-none overflow-hidden shadow-md dark:bg-secondaryDark dark:text-gray-400"
|
||||
>
|
||||
<m.div
|
||||
layout
|
||||
onClick={toggleOpen}
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileTap={{ scale: 0.99 }}
|
||||
className="flex justify-between gap-2 border-b border-primaryDark p-2 text-sm font-medium"
|
||||
>
|
||||
<m.div className="flex gap-2 ">
|
||||
<m.div className="my-auto">{icon}</m.div>
|
||||
{title}
|
||||
</m.div>
|
||||
<m.div
|
||||
animate={open ? 'open' : 'closed'}
|
||||
initial={{ rotate: 180 }}
|
||||
variants={{
|
||||
open: { rotate: 0 },
|
||||
closed: { rotate: 180 },
|
||||
}}
|
||||
className="my-auto"
|
||||
>
|
||||
<FiArrowUp />
|
||||
</m.div>
|
||||
</m.div>
|
||||
</m.div>
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<>
|
||||
{actions && (
|
||||
<m.div className="flex justify-end gap-1 rounded-b-md border-x border-b p-1 shadow-inner dark:border-gray-600">
|
||||
{actions}
|
||||
</m.div>
|
||||
)}
|
||||
<m.div
|
||||
className="p-2"
|
||||
layout
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
{children}
|
||||
</m.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</m.div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
|
||||
import { m } from 'framer-motion';
|
||||
import { FiExternalLink } from 'react-icons/fi';
|
||||
|
||||
export interface ExternalSectionProps {
|
||||
title: string;
|
||||
icon?: JSX.Element;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const ExternalSection = ({
|
||||
title,
|
||||
icon,
|
||||
onClick,
|
||||
}: ExternalSectionProps): JSX.Element => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const toggleOpen = (): void => setOpen(!open);
|
||||
return (
|
||||
<m.div
|
||||
onClick={(): void => {
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
<m.div
|
||||
layout
|
||||
className="w-full cursor-pointer select-none overflow-hidden shadow-md dark:bg-secondaryDark dark:text-gray-400"
|
||||
>
|
||||
<m.div
|
||||
layout
|
||||
onClick={toggleOpen}
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileTap={{ scale: 0.99 }}
|
||||
className="flex justify-between gap-2 border-b border-primaryDark p-2 text-sm font-medium"
|
||||
>
|
||||
<m.div className="flex gap-2 ">
|
||||
<m.div className="my-auto">{icon}</m.div>
|
||||
{title}
|
||||
</m.div>
|
||||
<m.div className="my-auto">
|
||||
<FiExternalLink />
|
||||
</m.div>
|
||||
</m.div>
|
||||
</m.div>
|
||||
</m.div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { AnimatePresence, AnimateSharedLayout, m } from 'framer-motion';
|
||||
import { FiArrowLeft } from 'react-icons/fi';
|
||||
|
||||
import { IconButton } from '@meshtastic/components';
|
||||
|
||||
export interface SidebarOverlayProps {
|
||||
title: string;
|
||||
open: boolean;
|
||||
close: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SidebarOverlay = ({
|
||||
title,
|
||||
open,
|
||||
close,
|
||||
children,
|
||||
}: SidebarOverlayProps): JSX.Element => {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<m.div
|
||||
className="absolute z-20 flex h-full w-full flex-col bg-primaryDark"
|
||||
animate={{ translateX: 0 }}
|
||||
initial={{ translateX: '-100%' }}
|
||||
exit={{ translateX: '-100%' }}
|
||||
transition={{ type: 'just' }}
|
||||
>
|
||||
<AnimateSharedLayout>
|
||||
<div className="flex gap-2 p-2">
|
||||
<IconButton
|
||||
onClick={(): void => {
|
||||
close();
|
||||
}}
|
||||
icon={<FiArrowLeft />}
|
||||
/>
|
||||
<div className="my-auto text-lg font-medium dark:text-white">
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow overflow-y-auto">{children}</div>
|
||||
</AnimateSharedLayout>
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { AnimatePresence, AnimateSharedLayout, m } from 'framer-motion';
|
||||
import { FiArrowDown } from 'react-icons/fi';
|
||||
|
||||
import { IconButton } from '@meshtastic/components';
|
||||
|
||||
export interface SidebarPrimaryProps {
|
||||
title: string;
|
||||
open: boolean;
|
||||
close: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SidebarPrimary = ({
|
||||
title,
|
||||
open,
|
||||
close,
|
||||
children,
|
||||
}: SidebarPrimaryProps): JSX.Element => {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<m.div
|
||||
className="absolute flex h-full w-full flex-col bg-gray-100 dark:bg-primaryDark"
|
||||
animate={{ translateY: 0 }}
|
||||
initial={{ translateY: '100%' }}
|
||||
exit={{ translateY: '100%' }}
|
||||
transition={{ type: 'just' }}
|
||||
>
|
||||
<AnimateSharedLayout>
|
||||
<div className="flex gap-2 p-2">
|
||||
<IconButton
|
||||
onClick={(): void => {
|
||||
close();
|
||||
}}
|
||||
icon={<FiArrowDown />}
|
||||
/>
|
||||
<div className="my-auto text-lg font-medium dark:text-white">
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow overflow-y-auto">{children}</div>
|
||||
</AnimateSharedLayout>
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user