diff --git a/src/App.tsx b/src/App.tsx index 7db9e6f1..5424a962 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,7 +11,7 @@ import { requestNotificationPermission } from '@core/utils/notifications'; import { useAppDispatch } from '@hooks/useAppDispatch'; import { useAppSelector } from '@hooks/useAppSelector'; import { Messages } from '@pages/Messages'; -import { Nodes } from '@pages/Nodes/Index'; +import { Nodes } from '@pages/Nodes'; import { NotFound } from '@pages/NotFound'; import { Plugins } from '@pages/Plugins/Index'; import { Settings } from '@pages/settings/Index'; @@ -46,7 +46,6 @@ export const App = (): JSX.Element => { ); } - // Notification.permission === '' requestNotificationPermission().catch((e) => { console.log(e); }); diff --git a/src/components/Channel.tsx b/src/components/Channel.tsx deleted file mode 100644 index c5ae6415..00000000 --- a/src/components/Channel.tsx +++ /dev/null @@ -1,252 +0,0 @@ -import React from 'react'; - -import { fromByteArray, toByteArray } from 'base64-js'; -import { useForm, useWatch } from 'react-hook-form'; -import { FaQrcode } from 'react-icons/fa'; -import { - FiChevronDown, - FiChevronUp, - FiCode, - FiRotateCcw, - FiSave, -} from 'react-icons/fi'; -import { MdRefresh, MdVisibility, MdVisibilityOff } from 'react-icons/md'; -import JSONPretty from 'react-json-pretty'; -import QRCode from 'react-qr-code'; - -import { Loading } from '@components/generic/Loading'; -import { Modal } from '@components/generic/Modal'; -import { connection } from '@core/connection'; -import { Disclosure } from '@headlessui/react'; -import { - Card, - Checkbox, - IconButton, - Input, - Select, -} from '@meshtastic/components'; -import { Protobuf } from '@meshtastic/meshtasticjs'; - -export interface ChannelProps { - channel: Protobuf.Channel; -} - -export const Channel = ({ channel }: ChannelProps): JSX.Element => { - const [loading, setLoading] = React.useState(false); - const [showQr, setShowQr] = React.useState(false); - const [keySize, setKeySize] = React.useState<128 | 256>(256); - const [pskHidden, setPskHidden] = React.useState(true); - const [showDebug, setShowDebug] = React.useState(false); - - const { register, handleSubmit, setValue, control, formState, reset } = - useForm< - Omit & { 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)), - }, - }); - - const watchPsk = useWatch({ - control, - name: 'psk', - defaultValue: '', - }); - - 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 => { - reset({ ...data }); - setLoading(false); - return Promise.resolve(); - }); - }); - - return ( - <> - { - setShowDebug(false); - }} - > - -
- -
-
-
- { - setShowQr(false); - }} - > - - - - - - {({ open }): JSX.Element => ( - <> - - <> -
-
role === channel.role) - ? 'bg-green-500' - : 'bg-gray-400' - }`} - /> -
- {channel.settings?.name.length - ? channel.settings.name - : channel.role === Protobuf.Channel_Role.PRIMARY - ? 'Primary' - : `Channel: ${channel.index}`} -
-
-
- {open && ( - <> - { - e.stopPropagation(); - reset(); - }} - disabled={loading || !formState.isDirty} - icon={} - /> - => { - e.stopPropagation(); - await onSubmit(); - }} - disabled={loading || !formState.isDirty} - icon={} - /> - - )} - { - e.stopPropagation(); - setShowDebug(true); - }} - icon={} - /> - - { - e.stopPropagation(); - setShowQr(true); - }} - icon={} - /> - : } - /> -
- - - - {loading && } -
-
- {channel.index !== 0 && ( - <> - - - - )} - - - { - setPskHidden(!pskHidden); - }} - icon={ - pskHidden ? : - } - /> - { - const key = new Uint8Array(keySize); - crypto.getRandomValues(key); - setValue('psk', fromByteArray(key)); - }} - icon={} - /> - - } - {...register('psk')} - /> - - - -
-
- - )} - - - ); -}; diff --git a/src/components/generic/Sidebar.tsx b/src/components/generic/Sidebar.tsx new file mode 100644 index 00000000..2b415504 --- /dev/null +++ b/src/components/generic/Sidebar.tsx @@ -0,0 +1,46 @@ +import type React from 'react'; + +import { FiX } from 'react-icons/fi'; + +import { IconButton } from '@meshtastic/components'; + +export interface SidebarProps { + title: string; + tagline: string; + footer?: JSX.Element; + closeSidebar: () => void; + children: React.ReactNode; +} + +export const Sidebar = ({ + title, + tagline, + closeSidebar, + children, +}: SidebarProps): JSX.Element => { + return ( +
+
+
+
+

{title}

+

{tagline}

+
+
+ { + closeSidebar(); + }} + icon={} + /> +
+
+
+ {children ?? ( +
+
Please select item
+
+ )} +
+ ); +}; diff --git a/src/components/menu/buttons/CopyButton.tsx b/src/components/menu/buttons/CopyButton.tsx new file mode 100644 index 00000000..3b2de877 --- /dev/null +++ b/src/components/menu/buttons/CopyButton.tsx @@ -0,0 +1,31 @@ +import type React from 'react'; + +import { FiCheck, FiClipboard } from 'react-icons/fi'; +import useCopyClipboard from 'react-use-clipboard'; + +import type { ButtonProps } from '@meshtastic/components'; +import { IconButton } from '@meshtastic/components'; + +export interface CopyButtonProps extends ButtonProps { + data: string; +} + +export const CopyButton = ({ + data, + ...props +}: CopyButtonProps): JSX.Element => { + const [isCopied, setCopied] = useCopyClipboard(data, { + successDuration: 1000, + }); + + return ( + { + setCopied(); + }} + icon={isCopied ? : } + {...props} + /> + ); +}; diff --git a/src/pages/Nodes/NodeCard.tsx b/src/components/pages/nodes/NodeCard.tsx similarity index 100% rename from src/pages/Nodes/NodeCard.tsx rename to src/components/pages/nodes/NodeCard.tsx diff --git a/src/components/pages/nodes/NodeSidebar.tsx b/src/components/pages/nodes/NodeSidebar.tsx new file mode 100644 index 00000000..b163cff0 --- /dev/null +++ b/src/components/pages/nodes/NodeSidebar.tsx @@ -0,0 +1,59 @@ +import React from 'react'; + +import { FiCode, FiMapPin, FiSliders, FiUser } from 'react-icons/fi'; +import { IoTelescope } from 'react-icons/io5'; + +import { DebugPanel } from '@app/components/pages/nodes/panels/DebugPanel'; +import { InfoPanel } from '@app/components/pages/nodes/panels/InfoPanel'; +import { PositionPanel } from '@app/components/pages/nodes/panels/PositionPanel'; +import { Sidebar } from '@components/generic/Sidebar'; +import { TabButton } from '@components/TabButton'; +import type { Node } from '@core/slices/meshtasticSlice'; +import { Tab } from '@headlessui/react'; + +export interface NodeSidebarProps { + node: Node; + closeSidebar: () => void; +} + +export const NodeSidebar = ({ + node, + closeSidebar, +}: NodeSidebarProps): JSX.Element => { + return ( + + +
+ + + + + + + + + + + + + + + + + +
+ + + + Content 3 + Remote Administration + + +
+
+ ); +}; diff --git a/src/components/pages/nodes/panels/DebugPanel.tsx b/src/components/pages/nodes/panels/DebugPanel.tsx new file mode 100644 index 00000000..ac6c4a25 --- /dev/null +++ b/src/components/pages/nodes/panels/DebugPanel.tsx @@ -0,0 +1,22 @@ +import type React from 'react'; + +import JSONPretty from 'react-json-pretty'; + +import { CopyButton } from '@components/menu/buttons/CopyButton'; +import type { Node } from '@core/slices/meshtasticSlice'; +import { Tab } from '@headlessui/react'; + +export interface DebugPanelProps { + node: Node; +} + +export const DebugPanel = ({ node }: DebugPanelProps): JSX.Element => { + return ( + +
+ +
+ +
+ ); +}; diff --git a/src/components/pages/nodes/panels/InfoPanel.tsx b/src/components/pages/nodes/panels/InfoPanel.tsx new file mode 100644 index 00000000..107dda2f --- /dev/null +++ b/src/components/pages/nodes/panels/InfoPanel.tsx @@ -0,0 +1,7 @@ +import type React from 'react'; + +import { Tab } from '@headlessui/react'; + +export const InfoPanel = (): JSX.Element => { + return Info; +}; diff --git a/src/components/pages/nodes/panels/PositionPanel.tsx b/src/components/pages/nodes/panels/PositionPanel.tsx new file mode 100644 index 00000000..7249b84c --- /dev/null +++ b/src/components/pages/nodes/panels/PositionPanel.tsx @@ -0,0 +1,33 @@ +import type React from 'react'; + +import { CopyButton } from '@components/menu/buttons/CopyButton'; +import type { Node } from '@core/slices/meshtasticSlice'; +import { Tab } from '@headlessui/react'; + +export interface PositionPanelProps { + node: Node; +} + +export const PositionPanel = ({ node }: PositionPanelProps): JSX.Element => { + return ( + + {node.currentPosition && ( +
+
+ {(node.currentPosition.latitudeI / 1e7).toPrecision(6)},  + {(node.currentPosition?.longitudeI / 1e7).toPrecision(6)} +
+ +
+ )} +
+ ); +}; diff --git a/src/components/pages/settings/channels/ChannelsSidebar.tsx b/src/components/pages/settings/channels/ChannelsSidebar.tsx new file mode 100644 index 00000000..c44b032d --- /dev/null +++ b/src/components/pages/settings/channels/ChannelsSidebar.tsx @@ -0,0 +1,60 @@ +import type React from 'react'; + +import { FaQrcode } from 'react-icons/fa'; +import { FiCode, FiSliders } from 'react-icons/fi'; + +import { TabButton } from '@app/components/TabButton'; +import { Sidebar } from '@components/generic/Sidebar'; +import { Tab } from '@headlessui/react'; +import { Protobuf } from '@meshtastic/meshtasticjs'; + +import { DebugPanel } from './panels/DebugPanel'; +import { QRCodePanel } from './panels/QRCodePanel'; +import { SettingsPanel } from './panels/SettingsPanel'; + +export interface ChannelsSidebarProps { + channel?: Protobuf.Channel; + closeSidebar: () => void; +} + +export const ChannelsSidebar = ({ + channel, + closeSidebar, +}: ChannelsSidebarProps): JSX.Element => { + return ( + + {channel && ( + +
+ + + + + + + + + + + +
+ + + + + +
+ )} +
+ ); +}; diff --git a/src/components/pages/settings/channels/panels/DebugPanel.tsx b/src/components/pages/settings/channels/panels/DebugPanel.tsx new file mode 100644 index 00000000..a6b2e493 --- /dev/null +++ b/src/components/pages/settings/channels/panels/DebugPanel.tsx @@ -0,0 +1,22 @@ +import type React from 'react'; + +import JSONPretty from 'react-json-pretty'; + +import { CopyButton } from '@components/menu/buttons/CopyButton'; +import { Tab } from '@headlessui/react'; +import type { Protobuf } from '@meshtastic/meshtasticjs'; + +export interface DebugPanelProps { + channel: Protobuf.Channel; +} + +export const DebugPanel = ({ channel }: DebugPanelProps): JSX.Element => { + return ( + +
+ +
+ +
+ ); +}; diff --git a/src/components/pages/settings/channels/panels/QRCodePanel.tsx b/src/components/pages/settings/channels/panels/QRCodePanel.tsx new file mode 100644 index 00000000..e87ff66f --- /dev/null +++ b/src/components/pages/settings/channels/panels/QRCodePanel.tsx @@ -0,0 +1,23 @@ +import type React from 'react'; + +import QRCode from 'react-qr-code'; + +import { Tab } from '@headlessui/react'; +import type { Protobuf } from '@meshtastic/meshtasticjs'; + +export interface QRCodePanelProps { + channel: Protobuf.Channel; +} + +export const QRCodePanel = ({ channel }: QRCodePanelProps): JSX.Element => { + return ( + +
+ +
+
+ ); +}; diff --git a/src/components/pages/settings/channels/panels/SettingsPanel.tsx b/src/components/pages/settings/channels/panels/SettingsPanel.tsx new file mode 100644 index 00000000..677f3999 --- /dev/null +++ b/src/components/pages/settings/channels/panels/SettingsPanel.tsx @@ -0,0 +1,139 @@ +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 { Loading } from '@app/components/generic/Loading'; +import { connection } from '@app/core/connection'; +import { Tab } from '@headlessui/react'; +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 & { 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 => { + reset({ ...data }); + setLoading(false); + return Promise.resolve(); + }); + }); + + return ( + + {loading && } +
+ {channel?.index !== 0 && ( + <> + + + + )} + + + { + setPskHidden(!pskHidden); + }} + icon={pskHidden ? : } + /> + { + const key = new Uint8Array(keySize); + crypto.getRandomValues(key); + setValue('psk', fromByteArray(key)); + }} + icon={} + /> + + } + {...register('psk')} + /> + + + +
+
+ => { + await onSubmit(); + }} + icon={} + /> +
+
+
+ ); +}; diff --git a/src/components/templates/PrimaryTemplate.tsx b/src/components/templates/PrimaryTemplate.tsx index f82c5520..82c70287 100644 --- a/src/components/templates/PrimaryTemplate.tsx +++ b/src/components/templates/PrimaryTemplate.tsx @@ -19,7 +19,7 @@ export const PrimaryTemplate = ({ }: PrimaryTemplateProps): JSX.Element => { return (
-
+
{tagline} diff --git a/src/pages/Nodes/Index.tsx b/src/pages/Nodes.tsx similarity index 94% rename from src/pages/Nodes/Index.tsx rename to src/pages/Nodes.tsx index eb80b14a..8f9d379b 100644 --- a/src/pages/Nodes/Index.tsx +++ b/src/pages/Nodes.tsx @@ -4,17 +4,15 @@ import mapbox from 'mapbox-gl'; import { FiMapPin, FiXCircle } from 'react-icons/fi'; import { Marker } from '@app/components/Map/Marker'; -import type { Node } from '@app/core/slices/meshtasticSlice.js'; +import type { Node } from '@app/core/slices/meshtasticSlice'; import { Drawer } from '@components/generic/Drawer'; import { Map } from '@components/Map'; +import { NodeSidebar } from '@components/pages/nodes/NodeSidebar'; import { useAppSelector } from '@hooks/useAppSelector'; import { useBreakpoint } from '@hooks/useBreakpoint'; import { IconButton } from '@meshtastic/components'; -import { NodeCard } from './NodeCard'; -import { Sidebar } from './Sidebar'; - -// const getMarkerRadius = ():number => {} +import { NodeCard } from '../components/pages/nodes/NodeCard'; export const Nodes = (): JSX.Element => { const myNodeInfo = useAppSelector((state) => state.meshtastic.radio.hardware); @@ -28,9 +26,9 @@ export const Nodes = (): JSX.Element => { ); const myNode = nodes.find((node) => node.number === myNodeInfo.myNodeNum); - const [navOpen, setNavOpen] = React.useState(false); const { breakpoint } = useBreakpoint(); + const [navOpen, setNavOpen] = React.useState(false); const [sidebarOpen, setSidebarOpen] = React.useState(false); const [selectedNode, setSelectedNode] = React.useState(); @@ -119,7 +117,7 @@ export const Nodes = (): JSX.Element => { {sidebarOpen && selectedNode && ( - { setSidebarOpen(false); }} diff --git a/src/pages/Nodes/Sidebar.tsx b/src/pages/Nodes/Sidebar.tsx deleted file mode 100644 index b2b7a0ad..00000000 --- a/src/pages/Nodes/Sidebar.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import React from 'react'; - -import { - FiCheck, - FiClipboard, - FiCode, - FiMapPin, - FiSliders, - FiUser, - FiX, -} from 'react-icons/fi'; -import { IoTelescope } from 'react-icons/io5'; -import JSONPretty from 'react-json-pretty'; -import useCopyClipboard from 'react-use-clipboard'; - -import { TabButton } from '@app/components/TabButton'; -import type { Node } from '@app/core/slices/meshtasticSlice'; -import { Tab } from '@headlessui/react'; -import { IconButton } from '@meshtastic/components'; - -export interface SidebarProps { - node: Node; - closeSidebar: () => void; -} - -export const Sidebar = ({ node, closeSidebar }: SidebarProps): JSX.Element => { - const [toCopy, setToCopy] = React.useState(''); - const [isCopied, setCopied] = useCopyClipboard(toCopy, { - successDuration: 1000, - }); - - return ( -
- -
-
-
-
-

- {node.number} -

-

- {node.user?.longName}({node.user?.shortName}) -

-
-
- { - closeSidebar(); - }} - icon={} - /> -
-
-
- - - - - - - - - - - - - - - - - - -
- - Content 1 - - {node.currentPosition && ( -
-
- {(node.currentPosition.latitudeI / 1e7).toPrecision(6)},  - {(node.currentPosition?.longitudeI / 1e7).toPrecision(6)} -
- { - setToCopy( - node.currentPosition - ? `${node.currentPosition.latitudeI / 1e7},${ - node.currentPosition.longitudeI / 1e7 - }` - : '', - ); - setCopied(); - }} - icon={isCopied ? : } - /> -
- )} -
- Content 3 - Remote Administration - -
- { - setToCopy(JSON.stringify(node)); - setCopied(); - }} - icon={isCopied ? : } - /> -
- -
-
-
-
- ); -}; diff --git a/src/pages/settings/Channels.tsx b/src/pages/settings/Channels.tsx index 808d253a..d0222ecc 100644 --- a/src/pages/settings/Channels.tsx +++ b/src/pages/settings/Channels.tsx @@ -1,16 +1,21 @@ import React from 'react'; import { useForm } from 'react-hook-form'; -import { FiCode, FiMenu } from 'react-icons/fi'; -import JSONPretty from 'react-json-pretty'; +import { FiExternalLink, FiMenu, FiX } from 'react-icons/fi'; +import { + RiArrowDownLine, + RiArrowUpDownLine, + RiArrowUpLine, +} from 'react-icons/ri'; -import { Channel } from '@components/Channel'; +import { Tooltip } from '@app/components/generic/Tooltip'; +import type { ChannelData } from '@app/core/slices/meshtasticSlice'; import { FormFooter } from '@components/FormFooter'; -import { Cover } from '@components/generic/Cover'; import { Loading } from '@components/generic/Loading'; import { PrimaryTemplate } from '@components/templates/PrimaryTemplate'; import { connection } from '@core/connection'; import { useAppSelector } from '@hooks/useAppSelector'; +import { useBreakpoint } from '@hooks/useBreakpoint'; import { Card, Checkbox, @@ -20,6 +25,8 @@ import { } from '@meshtastic/components'; import { Protobuf } from '@meshtastic/meshtasticjs'; +import { ChannelsSidebar } from '../../components/pages/settings/channels/ChannelsSidebar'; + export interface ChannelsProps { navOpen?: boolean; setNavOpen?: React.Dispatch>; @@ -34,10 +41,13 @@ export const Channels = ({ channels.find( (channel) => channel.channel.role === Protobuf.Channel_Role.PRIMARY, ) ?? channels[0]; - + const { breakpoint } = useBreakpoint(); const [usePreset, setUsePreset] = React.useState(true); - const [debug, setDebug] = React.useState(false); const [loading, setLoading] = React.useState(false); + const [sidebarOpen, setSidebarOpen] = React.useState(breakpoint !== 'sm'); + const [selectedChannel, setSelectedChannel] = React.useState< + ChannelData | undefined + >(); const { register, handleSubmit, reset, formState } = useForm< DeepOmit @@ -66,102 +76,155 @@ export const Channels = ({ }); return ( - } - onClick={(): void => { - setNavOpen && setNavOpen(!navOpen); - }} - /> - } - rightButton={ - } - active={debug} - onClick={(): void => { - setDebug(!debug); - }} - /> - } - footer={ - - } - > -
- {adminChannel && ( - - {loading && } -
- {/* TODO: get gap working */} - setUsePreset(e.target.checked)} - /> -
- {usePreset ? ( - - - - - )} - + } + onClick={(): void => { + setNavOpen && setNavOpen(!navOpen); + }} + /> + } + footer={ + + } + > +
+ {adminChannel && ( + + {loading && } +
+ {/* TODO: get gap working */} + setUsePreset(e.target.checked)} /> - +
+ {usePreset ? ( + + + + + )} + +
+
+
+ )} + +
+ {channels.map((channel) => ( +
{ + setSelectedChannel(channel); + setSidebarOpen(true); + }} + className={`flex justify-between p-2 border border-gray-300 dark:border-gray-600 bg-gray-100 rounded-md dark:bg-secondaryDark shadow-md ${ + selectedChannel?.channel.index === channel.channel.index + ? 'border-primary dark:border-primary' + : '' + }`} + > +
+
role === channel.channel.role) + ? 'bg-green-500' + : 'bg-gray-400' + }`} + /> +
+ {channel.channel.settings?.name.length + ? channel.channel.settings.name + : channel.channel.role === Protobuf.Channel_Role.PRIMARY + ? 'Primary' + : `Channel: ${channel.channel.index}`} +
+
+
+ +
+ {channel.channel.settings?.uplinkEnabled && + channel.channel.settings?.downlinkEnabled ? ( + + ) : channel.channel.settings?.uplinkEnabled ? ( + + ) : channel.channel.settings?.downlinkEnabled ? ( + + ) : ( + + )} +
+
+ } + /> +
+
+ ))}
- )} - - } /> -
- {channels.map((channel) => ( - - ))} -
-
-
- +
+
+ {sidebarOpen && ( + { + setSidebarOpen(false); + }} + channel={selectedChannel?.channel} + /> + )} + ); };