add new dialogs & cleanup
This commit is contained in:
+4
-4
@@ -27,12 +27,12 @@
|
||||
"@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/line-clamp": "^0.4.2",
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"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",
|
||||
@@ -56,9 +56,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@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",
|
||||
|
||||
Generated
+26
-24
@@ -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.13_react@18.2.0
|
||||
'@hookform/error-message': 2.0.1_nrnvpvixg5xdweezd67llqjwze
|
||||
'@hookform/resolvers': 2.9.10_react-hook-form@7.40.0
|
||||
'@meshtastic/meshtasticjs': 0.7.2
|
||||
'@meshtastic/meshtasticjs': 0.7.3
|
||||
'@tailwindcss/line-clamp': 0.4.2_tailwindcss@3.2.4
|
||||
'@tailwindcss/typography': 0.5.8_tailwindcss@3.2.4
|
||||
base64-js: 1.5.1
|
||||
chart.js: 4.0.1
|
||||
chartjs-adapter-date-fns: 2.0.1_chart.js@4.0.1
|
||||
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.3_tailwindcss@3.2.4
|
||||
'@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_@types+node@18.11.12
|
||||
vite: 4.0.0_@types+node@18.11.13
|
||||
vite-plugin-environment: 1.1.3_vite@4.0.0
|
||||
|
||||
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_@babel+core@7.20.5
|
||||
magic-string: 0.27.0
|
||||
react-refresh: 0.14.0
|
||||
vite: 4.0.0_@types+node@18.11.12
|
||||
vite: 4.0.0_@types+node@18.11.13
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
@@ -1435,12 +1435,14 @@ packages:
|
||||
engines: {pnpm: ^7.0.0}
|
||||
dev: false
|
||||
|
||||
/chartjs-adapter-date-fns/2.0.1_chart.js@4.0.1:
|
||||
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_@types+node@18.11.12
|
||||
vite: 4.0.0_@types+node@18.11.13
|
||||
dev: true
|
||||
|
||||
/vite/4.0.0_@types+node@18.11.12:
|
||||
/vite/4.0.0_@types+node@18.11.13:
|
||||
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
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
MapIcon,
|
||||
MoonIcon,
|
||||
PlusIcon,
|
||||
PowerIcon,
|
||||
QrCodeIcon,
|
||||
Square3Stack3DIcon,
|
||||
TrashIcon,
|
||||
@@ -61,9 +62,16 @@ export interface Command {
|
||||
|
||||
export const CommandPalette = (): JSX.Element => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
const { commandPaletteOpen, setCommandPaletteOpen } = useAppStore();
|
||||
// const [open, setOpen] = useState(false);
|
||||
|
||||
const { setQRDialogOpen, setActivePage, connection } = useDevice();
|
||||
const {
|
||||
setQRDialogOpen,
|
||||
setShutdownDialogOpen,
|
||||
setRebootDialogOpen,
|
||||
setActivePage,
|
||||
connection
|
||||
} = useDevice();
|
||||
const { setSelectedDevice, removeDevice, selectedDevice } = useAppStore();
|
||||
|
||||
const groups: Group[] = [
|
||||
@@ -181,6 +189,20 @@ export const CommandPalette = (): JSX.Element => {
|
||||
setSelectedDevice(0);
|
||||
removeDevice(selectedDevice ?? 0);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Schedule Shutdown",
|
||||
icon: PowerIcon,
|
||||
action() {
|
||||
setShutdownDialogOpen(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Schedule Reboot",
|
||||
icon: ArrowPathIcon,
|
||||
action() {
|
||||
setRebootDialogOpen(true);
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -222,7 +244,7 @@ export const CommandPalette = (): JSX.Element => {
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
setOpen(true);
|
||||
setCommandPaletteOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -249,12 +271,16 @@ export const CommandPalette = (): JSX.Element => {
|
||||
|
||||
return (
|
||||
<Transition.Root
|
||||
show={open}
|
||||
show={commandPaletteOpen}
|
||||
as={Fragment}
|
||||
afterLeave={() => setQuery("")}
|
||||
appear
|
||||
>
|
||||
<Dialog as="div" className="relative z-10" onClose={setOpen}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={setCommandPaletteOpen}
|
||||
>
|
||||
<PaletteTransition>
|
||||
<Dialog.Panel className="mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 overflow-hidden rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
|
||||
<Combobox<Command | string>
|
||||
@@ -262,7 +288,7 @@ export const CommandPalette = (): JSX.Element => {
|
||||
if (typeof input === "string") {
|
||||
setQuery(input);
|
||||
} else if (input.action) {
|
||||
setOpen(false);
|
||||
setCommandPaletteOpen(false);
|
||||
input.action();
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useAppStore } from "@app/core/stores/appStore.js";
|
||||
import { useDeviceStore } from "@app/core/stores/deviceStore.js";
|
||||
import { Mono } from "@components/generic/Mono.js";
|
||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||
import { CommandLineIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
export const DeviceSelector = (): JSX.Element => {
|
||||
const { getDevices } = useDeviceStore();
|
||||
@@ -43,14 +43,7 @@ export const DeviceSelector = (): JSX.Element => {
|
||||
<PlusIcon />
|
||||
</div>
|
||||
</span>
|
||||
<img src="Logo_Black.svg" className="mt-auto px-3" />
|
||||
<div className="my-4 flex flex-col gap-2 [writing-mode:horizontal-tb]">
|
||||
<CommandLineIcon className="h-6 text-slate-400" />
|
||||
<Mono className="text-xs">
|
||||
<kbd className="rounded-md bg-slate-200 p-0.5 pr-1 italic">Ctrl</kbd>+
|
||||
<kbd className="rounded-md bg-slate-200 p-0.5 pr-1 italic">K</kbd>
|
||||
</Mono>
|
||||
</div>
|
||||
<img src="Logo_Black.svg" className="mt-auto px-3 py-4" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,8 +3,20 @@ import type React from "react";
|
||||
import { useDevice } from "@app/core/providers/useDevice.js";
|
||||
import { QRDialog } from "@components/Dialog/QRDialog.js";
|
||||
|
||||
import { RebootDialog } from "./RebootDialog.js";
|
||||
import { ShutdownDialog } from "./ShutdownDialog.js";
|
||||
|
||||
export const DialogManager = (): JSX.Element => {
|
||||
const { channels, config, QRDialogOpen, setQRDialogOpen } = useDevice();
|
||||
const {
|
||||
channels,
|
||||
config,
|
||||
QRDialogOpen,
|
||||
setQRDialogOpen,
|
||||
shutdownDialogOpen,
|
||||
setShutdownDialogOpen,
|
||||
rebootDialogOpen,
|
||||
setRebootDialogOpen
|
||||
} = useDevice();
|
||||
return (
|
||||
<>
|
||||
<QRDialog
|
||||
@@ -15,6 +27,18 @@ export const DialogManager = (): JSX.Element => {
|
||||
channels={channels.map((ch) => ch.config)}
|
||||
loraConfig={config.lora}
|
||||
/>
|
||||
<ShutdownDialog
|
||||
isOpen={shutdownDialogOpen}
|
||||
close={() => {
|
||||
setShutdownDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
<RebootDialog
|
||||
isOpen={rebootDialogOpen}
|
||||
close={() => {
|
||||
setRebootDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -6,10 +6,9 @@ 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 { Dialog } from "@components/generic/Dialog.js";
|
||||
import { ClipboardIcon } from "@heroicons/react/24/outline";
|
||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||
|
||||
export interface QRDialogProps {
|
||||
@@ -48,79 +47,57 @@ export const QRDialog = ({
|
||||
}, [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>
|
||||
<Dialog
|
||||
title={"Generate QR Code"}
|
||||
description={"The current LoRa configuration will also be shared."}
|
||||
isOpen={isOpen}
|
||||
close={close}
|
||||
>
|
||||
<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 className="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>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -10,8 +10,8 @@ export const MapControlls = (): JSX.Element => {
|
||||
const { current: map } = useMap();
|
||||
|
||||
return (
|
||||
<div className="absolute right-0 top-0 z-10 m-2">
|
||||
<div className="divide-y overflow-hidden rounded-md bg-white">
|
||||
<div className="absolute right-0 top-0 z-10 m-2 ">
|
||||
<div className="divide-y divide-orange-300 overflow-hidden rounded-md bg-white shadow-md">
|
||||
<div
|
||||
className="cursor-pointer p-3 hover:bg-orange-200 hover:text-orange-700"
|
||||
onClick={() => {
|
||||
|
||||
@@ -8,12 +8,16 @@ import { PeersWidget } from "@components/Widgets/PeersWidget.js";
|
||||
import { PositionWidget } from "@components/Widgets/PositionWidget.js";
|
||||
import { useAppStore } from "@core/stores/appStore.js";
|
||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
||||
import { CommandLineIcon } from "@heroicons/react/24/outline";
|
||||
import { Types } from "@meshtastic/meshtasticjs";
|
||||
|
||||
import { Input } from "./form/Input.js";
|
||||
|
||||
export const Sidebar = (): JSX.Element => {
|
||||
const { removeDevice } = useDeviceStore();
|
||||
const { connection, hardware, nodes, status, currentMetrics } = useDevice();
|
||||
const { selectedDevice, setSelectedDevice } = useAppStore();
|
||||
const { selectedDevice, setSelectedDevice, setCommandPaletteOpen } =
|
||||
useAppStore();
|
||||
const myNode = nodes.find((n) => n.data.num === hardware.myNodeNum);
|
||||
|
||||
return (
|
||||
@@ -35,7 +39,7 @@ export const Sidebar = (): JSX.Element => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-grow flex-col gap-3">
|
||||
<BatteryWidget
|
||||
batteryLevel={currentMetrics.batteryLevel}
|
||||
voltage={currentMetrics.voltage}
|
||||
@@ -51,6 +55,18 @@ export const Sidebar = (): JSX.Element => {
|
||||
myNode?.data.position?.longitudeI
|
||||
)}
|
||||
/>
|
||||
<div className="mt-auto">
|
||||
<Input
|
||||
placeholder={"Search for a command"}
|
||||
onClick={() => setCommandPaletteOpen(true)}
|
||||
action={{
|
||||
icon: <CommandLineIcon className="w-4" />,
|
||||
action() {
|
||||
setCommandPaletteOpen(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ export const Button = ({
|
||||
iconAfter,
|
||||
children,
|
||||
disabled,
|
||||
className,
|
||||
...rest
|
||||
}: ButtonProps): JSX.Element => {
|
||||
return (
|
||||
@@ -22,7 +23,7 @@ export const Button = ({
|
||||
className={`flex w-full rounded-md border border-transparent px-3 focus:outline-none focus:ring-2 focus:ring-orange-500 ${
|
||||
variant === "primary"
|
||||
? "bg-orange-600 text-white shadow-sm hover:bg-orange-700"
|
||||
: "bg-orange-200 text-orange-700 hover:bg-orange-200"
|
||||
: "bg-orange-200 text-orange-700 hover:bg-orange-300"
|
||||
} ${
|
||||
size === "sm"
|
||||
? "h-8 text-sm"
|
||||
@@ -33,7 +34,7 @@ export const Button = ({
|
||||
disabled
|
||||
? "cursor-not-allowed bg-gray-400 hover:bg-gray-400 focus:ring-gray-500"
|
||||
: ""
|
||||
}`}
|
||||
} ${className}`}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
>
|
||||
|
||||
@@ -21,7 +21,7 @@ export const IconButton = ({
|
||||
className={`flex rounded-md border border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 ${
|
||||
variant === "primary"
|
||||
? "bg-orange-600 text-white shadow-sm hover:bg-orange-700"
|
||||
: "bg-orange-100 text-orange-700 hover:bg-orange-200"
|
||||
: "bg-orange-200 text-orange-700 hover:bg-orange-300"
|
||||
} ${
|
||||
size === "sm" ? "h-8 w-8" : size === "md" ? "h-10 w-10" : "h-12 w-12"
|
||||
} ${disabled ? "cursor-not-allowed bg-red-400 focus:ring-red-500" : ""} ${
|
||||
|
||||
@@ -56,7 +56,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
||||
<button
|
||||
type="button"
|
||||
onClick={action.action}
|
||||
className="relative -ml-px inline-flex items-center space-x-2 rounded-r-md bg-orange-200 px-4 py-2 text-sm font-medium hover:bg-orange-300 focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
className="relative -ml-px inline-flex items-center space-x-2 rounded-r-md bg-orange-200 px-4 py-2 text-sm font-medium text-orange-700 hover:bg-orange-300 focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
>
|
||||
{action.icon}
|
||||
</button>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -15,6 +15,7 @@ interface AppState {
|
||||
num: number;
|
||||
}[];
|
||||
rasterSources: RasterSource[];
|
||||
commandPaletteOpen: boolean;
|
||||
|
||||
setRasterSources: (sources: RasterSource[]) => void;
|
||||
addRasterSource: (source: RasterSource) => void;
|
||||
@@ -23,6 +24,7 @@ interface AppState {
|
||||
setSelectedDevice: (deviceId: number) => void;
|
||||
addDevice: (device: { id: number; num: number }) => void;
|
||||
removeDevice: (deviceId: number) => void;
|
||||
setCommandPaletteOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppState>()((set) => ({
|
||||
@@ -30,6 +32,7 @@ export const useAppStore = create<AppState>()((set) => ({
|
||||
devices: [],
|
||||
currentPage: "messages",
|
||||
rasterSources: [],
|
||||
commandPaletteOpen: false,
|
||||
|
||||
setRasterSources: (sources: RasterSource[]) => {
|
||||
set(
|
||||
@@ -52,7 +55,6 @@ export const useAppStore = create<AppState>()((set) => ({
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
setSelectedDevice: (deviceId) =>
|
||||
set(() => ({
|
||||
selectedDevice: deviceId
|
||||
@@ -64,5 +66,12 @@ export const useAppStore = create<AppState>()((set) => ({
|
||||
removeDevice: (deviceId) =>
|
||||
set((state) => ({
|
||||
devices: state.devices.filter((device) => device.id !== deviceId)
|
||||
}))
|
||||
})),
|
||||
setCommandPaletteOpen: (open: boolean) => {
|
||||
set(
|
||||
produce<AppState>((draft) => {
|
||||
draft.commandPaletteOpen = open;
|
||||
})
|
||||
);
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -56,6 +56,8 @@ export interface Device {
|
||||
regionUnset: boolean;
|
||||
currentMetrics: Protobuf.DeviceMetrics;
|
||||
QRDialogOpen: boolean;
|
||||
shutdownDialogOpen: boolean;
|
||||
rebootDialogOpen: boolean;
|
||||
pendingSettingsChanges: boolean;
|
||||
|
||||
setReady(ready: boolean): void;
|
||||
@@ -79,6 +81,8 @@ export interface Device {
|
||||
addDeviceMetadataMessage: (metadata: Types.DeviceMetadataPacket) => void;
|
||||
ackMessage: (channelIndex: number, messageId: number) => void;
|
||||
setQRDialogOpen: (open: boolean) => void;
|
||||
setShutdownDialogOpen: (open: boolean) => void;
|
||||
setRebootDialogOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export interface DeviceState {
|
||||
@@ -115,6 +119,8 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
||||
regionUnset: false,
|
||||
currentMetrics: Protobuf.DeviceMetrics.create(),
|
||||
QRDialogOpen: false,
|
||||
shutdownDialogOpen: false,
|
||||
rebootDialogOpen: false,
|
||||
pendingSettingsChanges: false,
|
||||
|
||||
setReady: (ready: boolean) => {
|
||||
@@ -539,6 +545,26 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
setShutdownDialogOpen: (open: boolean) => {
|
||||
set(
|
||||
produce<DeviceState>((draft) => {
|
||||
const device = draft.devices.get(id);
|
||||
if (device) {
|
||||
device.shutdownDialogOpen = open;
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
setRebootDialogOpen: (open: boolean) => {
|
||||
set(
|
||||
produce<DeviceState>((draft) => {
|
||||
const device = draft.devices.get(id);
|
||||
if (device) {
|
||||
device.rebootDialogOpen = open;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
@@ -45,24 +45,19 @@ export const subscribeAll = (
|
||||
});
|
||||
|
||||
connection.onMyNodeInfo.subscribe((nodeInfo) => {
|
||||
console.log("^^^^^^^ GOT MY NODE INFO");
|
||||
|
||||
device.setHardware(nodeInfo);
|
||||
myNodeNum = nodeInfo.myNodeNum;
|
||||
});
|
||||
|
||||
connection.onUserPacket.subscribe((user) => {
|
||||
console.log("^^^^^^^ GOT USER");
|
||||
device.addUser(user);
|
||||
});
|
||||
|
||||
connection.onPositionPacket.subscribe((position) => {
|
||||
console.log("^^^^^^^ GOT POSITION");
|
||||
device.addPosition(position);
|
||||
});
|
||||
|
||||
connection.onNodeInfoPacket.subscribe((nodeInfo) => {
|
||||
console.log("^^^^^^^ GOT NODE INFO");
|
||||
toast(`New Node Discovered: ${nodeInfo.data.user?.shortName ?? "UNK"}`, {
|
||||
icon: "🔎"
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ export class NetworkValidation implements Protobuf.Config_NetworkConfig {
|
||||
ethEnabled: boolean;
|
||||
|
||||
@IsEnum(Protobuf.Config_NetworkConfig_EthMode)
|
||||
@IsOptional()
|
||||
ethMode: Protobuf.Config_NetworkConfig_EthMode;
|
||||
|
||||
ipv4Config: NetworkValidation_IpV4Config;
|
||||
|
||||
Reference in New Issue
Block a user