19 changed files with 388 additions and 259 deletions
@ -6,13 +6,13 @@ specifiers: |
|||
'@heroicons/react': ^2.0.13 |
|||
'@hookform/error-message': ^2.0.1 |
|||
'@hookform/resolvers': ^2.9.10 |
|||
'@meshtastic/meshtasticjs': ^0.7.2 |
|||
'@meshtastic/meshtasticjs': ^0.7.3 |
|||
'@tailwindcss/forms': ^0.5.3 |
|||
'@tailwindcss/line-clamp': ^0.4.2 |
|||
'@tailwindcss/typography': ^0.5.8 |
|||
'@types/chrome': ^0.0.203 |
|||
'@types/chrome': ^0.0.204 |
|||
'@types/geodesy': ^2.2.3 |
|||
'@types/node': ^18.11.12 |
|||
'@types/node': ^18.11.13 |
|||
'@types/react': ^18.0.26 |
|||
'@types/react-dom': ^18.0.9 |
|||
'@types/w3c-web-serial': ^1.0.3 |
|||
@ -23,7 +23,7 @@ specifiers: |
|||
autoprefixer: ^10.4.13 |
|||
base64-js: ^1.5.1 |
|||
chart.js: ^4.0.1 |
|||
chartjs-adapter-date-fns: ^2.0.1 |
|||
chartjs-adapter-date-fns: ^3.0.0 |
|||
class-transformer: ^0.5.1 |
|||
class-validator: ^0.14.0 |
|||
date-fns: ^2.29.3 |
|||
@ -69,12 +69,12 @@ dependencies: |
|||
'@heroicons/react': 2.0[email protected] |
|||
'@hookform/error-message': 2.0.1_nrnvpvixg5xdweezd67llqjwze |
|||
'@hookform/resolvers': 2.9[email protected] |
|||
'@meshtastic/meshtasticjs': 0.7.2 |
|||
'@meshtastic/meshtasticjs': 0.7.3 |
|||
'@tailwindcss/line-clamp': 0.4[email protected] |
|||
'@tailwindcss/typography': 0.5[email protected] |
|||
base64-js: 1.5.1 |
|||
chart.js: 4.0.1 |
|||
chartjs-adapter-date-fns: 2.0[email protected] |
|||
chartjs-adapter-date-fns: 3.0.0_thp3sedjxvmiqxrdsmekdnimom |
|||
class-transformer: 0.5.1 |
|||
class-validator: 0.14.0 |
|||
date-fns: 2.29.3 |
|||
@ -98,9 +98,9 @@ dependencies: |
|||
|
|||
devDependencies: |
|||
'@tailwindcss/forms': 0.5[email protected] |
|||
'@types/chrome': 0.0.203 |
|||
'@types/chrome': 0.0.204 |
|||
'@types/geodesy': 2.2.3 |
|||
'@types/node': 18.11.12 |
|||
'@types/node': 18.11.13 |
|||
'@types/react': 18.0.26 |
|||
'@types/react-dom': 18.0.9 |
|||
'@types/w3c-web-serial': 1.0.3 |
|||
@ -125,7 +125,7 @@ devDependencies: |
|||
tslib: 2.4.1 |
|||
typescript: 4.9.4 |
|||
unimported: 1.23.0 |
|||
vite: 4.0.0_@[email protected]2 |
|||
vite: 4.0.0_@[email protected]3 |
|||
vite-plugin-environment: 1.1[email protected] |
|||
|
|||
packages: |
|||
@ -785,8 +785,8 @@ packages: |
|||
engines: {node: '>=6.0.0'} |
|||
dev: false |
|||
|
|||
/@meshtastic/meshtasticjs/0.7.2: |
|||
resolution: {integrity: sha512-RJCJLtlGUn7To+I4MnxuSDxwWK/MgAAfbfq+Naib6fve2VQEBu2CPF2Iy0A+wAHxR1ygOKcS8VbyS2D1FAQhcw==} |
|||
/@meshtastic/meshtasticjs/0.7.3: |
|||
resolution: {integrity: sha512-9NvWvTQSqCRNhYrE7FnhW9/bVSmnZ9LYGy04P+8IwhQzlYduhnaIb5vDKgLSURl5YhpnHlWOQFJVXs8tuZv2CQ==} |
|||
dependencies: |
|||
'@protobuf-ts/runtime': 2.8.2 |
|||
glob: 8.0.3 |
|||
@ -887,8 +887,8 @@ packages: |
|||
resolution: {integrity: sha512-oYO/U4VD1DavwrKuCSQWdLG+5K22SLPem2OQaHmFcQuwHoVeGC+JGVRji2MUqZUAIQZHEonOeVfAX09hYiLsdg==} |
|||
dev: false |
|||
|
|||
/@types/chrome/0.0.203: |
|||
resolution: {integrity: sha512-JlQNebwpBETVc8U1Rr2inDFuOTtn0lahRAhnddx1dd0S5RrLAFJEEsyIu7AXI14mkCgSunksnuLGioH8kvBqRA==} |
|||
/@types/chrome/0.0.204: |
|||
resolution: {integrity: sha512-EvnHfxMHUWP5EAlRMK66uIEUiy36t72vg5RwmzQv9tdIl2ZmAp92NwvmEZJKpbRnIMTEc2BmSmtrFiEISUJ0Sw==} |
|||
dependencies: |
|||
'@types/filesystem': 0.0.32 |
|||
'@types/har-format': 1.2.10 |
|||
@ -946,8 +946,8 @@ packages: |
|||
'@types/pbf': 3.0.2 |
|||
dev: false |
|||
|
|||
/@types/node/18.11.12: |
|||
resolution: {integrity: sha512-FgD3NtTAKvyMmD44T07zz2fEf+OKwutgBCEVM8GcvMGVGaDktiLNTDvPwC/LUe3PinMW+X6CuLOF2Ui1mAlSXg==} |
|||
/@types/node/18.11.13: |
|||
resolution: {integrity: sha512-IASpMGVcWpUsx5xBOrxMj7Bl8lqfuTY7FKAnPmu5cHkfQVWF8GulWS1jbRqA934qZL35xh5xN/+Xe/i26Bod4w==} |
|||
dev: true |
|||
|
|||
/@types/normalize-package-data/2.4.1: |
|||
@ -1153,7 +1153,7 @@ packages: |
|||
'@babel/plugin-transform-react-jsx-source': 7.19.6_@[email protected] |
|||
magic-string: 0.27.0 |
|||
react-refresh: 0.14.0 |
|||
vite: 4.0.0_@[email protected]2 |
|||
vite: 4.0.0_@[email protected]3 |
|||
transitivePeerDependencies: |
|||
- supports-color |
|||
dev: true |
|||
@ -1435,12 +1435,14 @@ packages: |
|||
engines: {pnpm: ^7.0.0} |
|||
dev: false |
|||
|
|||
/chartjs-adapter-date-fns/[email protected]: |
|||
resolution: {integrity: sha512-v3WV9rdnQ05ce3A0ZCjzUekJCAbfm6+3HqSoeY2BIkdMYZoYr/4T+ril1tZyDl869lz6xdNVMXejUFT9YKpw4A==} |
|||
/chartjs-adapter-date-fns/3.0.0_thp3sedjxvmiqxrdsmekdnimom: |
|||
resolution: {integrity: sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==} |
|||
peerDependencies: |
|||
chart.js: '>=2.8.0' |
|||
date-fns: '>=2.0.0' |
|||
dependencies: |
|||
chart.js: 4.0.1 |
|||
date-fns: 2.29.3 |
|||
dev: false |
|||
|
|||
/chokidar/3.5.3: |
|||
@ -3758,8 +3760,8 @@ packages: |
|||
yargs: 17.6.2 |
|||
dev: true |
|||
|
|||
/rollup/3.7.1: |
|||
resolution: {integrity: sha512-ek6+FORvI79VQTNlIYtXpIrGEPRlYSNZO+5EcmaozKkRL5L6KLvGDUbM5E+bd6jnHW9fgcK0DKTdWjIsEmNb4g==} |
|||
/rollup/3.7.2: |
|||
resolution: {integrity: sha512-orqIX5zkHyHKVsIBl8J5a2tnVikOAMte0DgOLViyW6McYuj45FG+cQPrXILhaifBSmy0D0hKbHg2RbgzFJcwTg==} |
|||
engines: {node: '>=14.18.0', npm: '>=8.0.0'} |
|||
hasBin: true |
|||
optionalDependencies: |
|||
@ -4288,10 +4290,10 @@ packages: |
|||
peerDependencies: |
|||
vite: '>= 2.7' |
|||
dependencies: |
|||
vite: 4.0.0_@[email protected]2 |
|||
vite: 4.0.0_@[email protected]3 |
|||
dev: true |
|||
|
|||
/vite/4.0.0_@[email protected]2: |
|||
/vite/4.0.0_@[email protected]3: |
|||
resolution: {integrity: sha512-ynad+4kYs8Jcnn8J7SacS9vAbk7eMy0xWg6E7bAhS1s79TK+D7tVFGXVZ55S7RNLRROU1rxoKlvZ/qjaB41DGA==} |
|||
engines: {node: ^14.18.0 || >=16.0.0} |
|||
hasBin: true |
|||
@ -4316,11 +4318,11 @@ packages: |
|||
terser: |
|||
optional: true |
|||
dependencies: |
|||
'@types/node': 18.11.12 |
|||
'@types/node': 18.11.13 |
|||
esbuild: 0.16.4 |
|||
postcss: 8.4.19 |
|||
resolve: 1.22.1 |
|||
rollup: 3.7.1 |
|||
rollup: 3.7.2 |
|||
optionalDependencies: |
|||
fsevents: 2.3.2 |
|||
dev: true |
|||
|
|||
@ -1,125 +0,0 @@ |
|||
import type React from "react"; |
|||
import { useEffect, useState } from "react"; |
|||
|
|||
import { fromByteArray } from "base64-js"; |
|||
import { toast } from "react-hot-toast"; |
|||
import { QRCode } from "react-qrcode-logo"; |
|||
|
|||
import { Checkbox } from "@components/form/Checkbox.js"; |
|||
import { IconButton } from "@components/form/IconButton.js"; |
|||
import { Input } from "@components/form/Input.js"; |
|||
import { Dialog } from "@headlessui/react"; |
|||
import { ClipboardIcon, XMarkIcon } from "@heroicons/react/24/outline"; |
|||
import { Protobuf } from "@meshtastic/meshtasticjs"; |
|||
|
|||
export interface ImportDialogProps { |
|||
isOpen: boolean; |
|||
close: () => void; |
|||
loraConfig?: Protobuf.Config_LoRaConfig; |
|||
channels: Protobuf.Channel[]; |
|||
} |
|||
|
|||
export const ImportDialog = ({ |
|||
isOpen, |
|||
close, |
|||
loraConfig, |
|||
channels |
|||
}: ImportDialogProps): JSX.Element => { |
|||
const [selectedChannels, setSelectedChannels] = useState<number[]>([0]); |
|||
const [QRCodeURL, setQRCodeURL] = useState<string>(""); |
|||
|
|||
useEffect(() => { |
|||
const channelsToEncode = channels |
|||
.filter((channel) => selectedChannels.includes(channel.index)) |
|||
.map((channel) => channel.settings) |
|||
.filter((ch): ch is Protobuf.ChannelSettings => !!ch); |
|||
const encoded = Protobuf.ChannelSet.toBinary({ |
|||
loraConfig, |
|||
settings: channelsToEncode |
|||
}); |
|||
const base64 = fromByteArray(encoded) |
|||
.replace(/=/g, "") |
|||
.replace(/\+/g, "-") |
|||
.replace(/\//g, "_"); |
|||
|
|||
setQRCodeURL(`https://meshtastic.org/e/#${base64}`); |
|||
}, [channels, selectedChannels, loraConfig]); |
|||
|
|||
return ( |
|||
<Dialog open={isOpen} onClose={close}> |
|||
<div className="fixed inset-0 bg-black/30" aria-hidden="true" /> |
|||
<div className="fixed inset-0 flex items-center justify-center p-4"> |
|||
<Dialog.Panel> |
|||
<div className="divide-y divide-gray-200 overflow-hidden rounded-lg bg-white shadow"> |
|||
<div className="flex px-4 py-5 sm:px-6"> |
|||
<div> |
|||
<h1 className="text-lg font-bold">Generate QR Code</h1> |
|||
<h5 className="text-sm text-slate-600"> |
|||
The current LoRa configuration will also be shared. |
|||
</h5> |
|||
</div> |
|||
<IconButton |
|||
onClick={close} |
|||
className="my-auto ml-auto" |
|||
size="sm" |
|||
variant="secondary" |
|||
icon={<XMarkIcon className="h-4" />} |
|||
/> |
|||
</div> |
|||
<div className="flex gap-3 px-4 py-5 sm:p-6"> |
|||
<div className="flex w-40 flex-col gap-1"> |
|||
{channels.map((channel) => ( |
|||
<Checkbox |
|||
key={channel.index} |
|||
disabled={ |
|||
channel.index === 0 || |
|||
channel.role === Protobuf.Channel_Role.DISABLED |
|||
} |
|||
label={ |
|||
channel.settings?.name.length |
|||
? channel.settings.name |
|||
: channel.role === Protobuf.Channel_Role.PRIMARY |
|||
? "Primary" |
|||
: `Channel: ${channel.index}` |
|||
} |
|||
checked={selectedChannels.includes(channel.index)} |
|||
onChange={() => { |
|||
if (selectedChannels.includes(channel.index)) { |
|||
setSelectedChannels( |
|||
selectedChannels.filter((c) => c !== channel.index) |
|||
); |
|||
} else { |
|||
setSelectedChannels([ |
|||
...selectedChannels, |
|||
channel.index |
|||
]); |
|||
} |
|||
}} |
|||
/> |
|||
))} |
|||
</div> |
|||
<QRCode value={QRCodeURL} size={200} qrStyle="dots" /> |
|||
</div> |
|||
|
|||
<div className="px-4 py-4 sm:px-6"> |
|||
<Input |
|||
label="Sharable URL" |
|||
value={QRCodeURL} |
|||
disabled |
|||
action={{ |
|||
icon: <ClipboardIcon className="h-4" />, |
|||
action() { |
|||
void navigator.clipboard.writeText(QRCodeURL); |
|||
toast.success("Copied URL to Clipboard"); |
|||
} |
|||
}} |
|||
/> |
|||
</div> |
|||
|
|||
{/* </Card> */} |
|||
</div> |
|||
</Dialog.Panel> |
|||
</div> |
|||
</Dialog> |
|||
); |
|||
}; |
|||
@ -0,0 +1,68 @@ |
|||
import type React from "react"; |
|||
import { useState } from "react"; |
|||
|
|||
import { useDevice } from "@app/core/providers/useDevice.js"; |
|||
import { Dialog } from "@components/generic/Dialog.js"; |
|||
import { ArrowPathIcon, ClockIcon } from "@heroicons/react/24/outline"; |
|||
|
|||
import { Button } from "../form/Button.js"; |
|||
import { Input } from "../form/Input.js"; |
|||
|
|||
export interface RebootDialogProps { |
|||
isOpen: boolean; |
|||
close: () => void; |
|||
} |
|||
|
|||
export const RebootDialog = ({ |
|||
isOpen, |
|||
close |
|||
}: RebootDialogProps): JSX.Element => { |
|||
const { connection, setRebootDialogOpen } = useDevice(); |
|||
|
|||
const [time, setTime] = useState<number>(5); |
|||
|
|||
return ( |
|||
<Dialog |
|||
title={"Schedule Reboot"} |
|||
description={"Reboot the connected node after x minutes."} |
|||
isOpen={isOpen} |
|||
close={close} |
|||
> |
|||
<div className="flex gap-2 p-4"> |
|||
<Input |
|||
type="number" |
|||
value={time} |
|||
onChange={(e) => setTime(parseInt(e.target.value))} |
|||
action={{ |
|||
icon: <ClockIcon className="w-4" />, |
|||
action() { |
|||
connection?.reboot({ |
|||
time: time * 60, |
|||
callback: async () => { |
|||
setRebootDialogOpen(false); |
|||
await Promise.resolve(); |
|||
} |
|||
}); |
|||
} |
|||
}} |
|||
/> |
|||
<Button |
|||
className="w-24" |
|||
variant="secondary" |
|||
iconAfter={<ArrowPathIcon className="w-4" />} |
|||
onClick={() => { |
|||
connection?.reboot({ |
|||
time: 0, |
|||
callback: async () => { |
|||
setRebootDialogOpen(false); |
|||
await Promise.resolve(); |
|||
} |
|||
}); |
|||
}} |
|||
> |
|||
Now |
|||
</Button> |
|||
</div> |
|||
</Dialog> |
|||
); |
|||
}; |
|||
@ -0,0 +1,68 @@ |
|||
import type React from "react"; |
|||
import { useState } from "react"; |
|||
|
|||
import { useDevice } from "@app/core/providers/useDevice.js"; |
|||
import { Dialog } from "@components/generic/Dialog.js"; |
|||
import { ClockIcon, PowerIcon } from "@heroicons/react/24/outline"; |
|||
|
|||
import { Button } from "../form/Button.js"; |
|||
import { Input } from "../form/Input.js"; |
|||
|
|||
export interface ShutdownDialogProps { |
|||
isOpen: boolean; |
|||
close: () => void; |
|||
} |
|||
|
|||
export const ShutdownDialog = ({ |
|||
isOpen, |
|||
close |
|||
}: ShutdownDialogProps): JSX.Element => { |
|||
const { connection, setShutdownDialogOpen } = useDevice(); |
|||
|
|||
const [time, setTime] = useState<number>(5); |
|||
|
|||
return ( |
|||
<Dialog |
|||
title={"Schedule Shutdown"} |
|||
description={"Turn off the connected node after x minutes."} |
|||
isOpen={isOpen} |
|||
close={close} |
|||
> |
|||
<div className="flex gap-2 p-4"> |
|||
<Input |
|||
type="number" |
|||
value={time} |
|||
onChange={(e) => setTime(parseInt(e.target.value))} |
|||
action={{ |
|||
icon: <ClockIcon className="w-4" />, |
|||
action() { |
|||
connection?.shutdown({ |
|||
time: time * 60, |
|||
callback: async () => { |
|||
setShutdownDialogOpen(false); |
|||
await Promise.resolve(); |
|||
} |
|||
}); |
|||
} |
|||
}} |
|||
/> |
|||
<Button |
|||
className="w-24" |
|||
variant="secondary" |
|||
iconAfter={<PowerIcon className="w-4" />} |
|||
onClick={() => { |
|||
connection?.shutdown({ |
|||
time: 0, |
|||
callback: async () => { |
|||
setShutdownDialogOpen(false); |
|||
await Promise.resolve(); |
|||
} |
|||
}); |
|||
}} |
|||
> |
|||
Now |
|||
</Button> |
|||
</div> |
|||
</Dialog> |
|||
); |
|||
}; |
|||
@ -0,0 +1,48 @@ |
|||
import type React from "react"; |
|||
|
|||
import { IconButton } from "@components/form/IconButton.js"; |
|||
import { Dialog as DialogUI } from "@headlessui/react"; |
|||
import { XMarkIcon } from "@heroicons/react/24/outline"; |
|||
|
|||
export interface DialogProps { |
|||
title: string; |
|||
description: string; |
|||
isOpen: boolean; |
|||
close: () => void; |
|||
children: React.ReactNode; |
|||
} |
|||
|
|||
export const Dialog = ({ |
|||
title, |
|||
description, |
|||
isOpen, |
|||
close, |
|||
children |
|||
}: DialogProps): JSX.Element => { |
|||
return ( |
|||
<DialogUI open={isOpen} onClose={close}> |
|||
<div className="fixed inset-0 bg-black/30" aria-hidden="true" /> |
|||
<div className="fixed inset-0 flex items-center justify-center p-4"> |
|||
<DialogUI.Panel> |
|||
<div className="divide-y divide-gray-200 overflow-hidden rounded-lg bg-white shadow"> |
|||
<div className="flex px-4 py-5 sm:px-6"> |
|||
<div> |
|||
<h1 className="text-lg font-bold">{title}</h1> |
|||
<h5 className="text-sm text-slate-600">{description}</h5> |
|||
</div> |
|||
<IconButton |
|||
onClick={close} |
|||
className="my-auto ml-auto" |
|||
size="sm" |
|||
variant="secondary" |
|||
icon={<XMarkIcon className="h-4" />} |
|||
/> |
|||
</div> |
|||
|
|||
<div className="p-4">{children}</div> |
|||
</div> |
|||
</DialogUI.Panel> |
|||
</div> |
|||
</DialogUI> |
|||
); |
|||
}; |
|||
Loading…
Reference in new issue