chore: fixes from deno linting/formatting
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"@meshtastic/core": "jsr:@meshtastic/core@^2.6.0",
|
||||||
|
"@meshtastic/js": "jsr:@meshtastic/js@^2.3.4",
|
||||||
|
"@meshtastic/transport-http": "jsr:@meshtastic/transport-http@^0.2.1",
|
||||||
|
"@meshtastic/transport-web-serial": "jsr:@meshtastic/transport-web-serial@^0.2.1",
|
||||||
|
"@app/": "./src/",
|
||||||
|
"@pages/": "./src/pages/",
|
||||||
|
"@components/": "./src/components/",
|
||||||
|
"@core/": "./src/core/",
|
||||||
|
"@layouts/": "./src/layouts/"
|
||||||
|
},
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable",
|
||||||
|
"ESNext"
|
||||||
|
],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"types": [
|
||||||
|
"vite/client",
|
||||||
|
"node",
|
||||||
|
"@types/web-bluetooth",
|
||||||
|
"@types/w3c-web-serial"
|
||||||
|
],
|
||||||
|
"strictPropertyInitialization": false
|
||||||
|
},
|
||||||
|
"unstable": [
|
||||||
|
"sloppy-imports"
|
||||||
|
]
|
||||||
|
}
|
||||||
+11
-3
@@ -8,8 +8,8 @@
|
|||||||
"build": "deno run -A npm:vite build",
|
"build": "deno run -A npm:vite build",
|
||||||
"build:analyze": "BUNDLE_ANALYZE=true deno task build",
|
"build:analyze": "BUNDLE_ANALYZE=true deno task build",
|
||||||
"lint": "deno lint src/",
|
"lint": "deno lint src/",
|
||||||
"lint:fix": "deno lint --write src/",
|
"lint:fix": "deno lint --fix src/",
|
||||||
"format": "deno fmt --write src/",
|
"format": "deno fmt src/",
|
||||||
"dev": "deno task dev:ui",
|
"dev": "deno task dev:ui",
|
||||||
"dev:ui": "deno run -A npm:vite dev",
|
"dev:ui": "deno run -A npm:vite dev",
|
||||||
"dev:scan": "VITE_DEBUG_SCAN=true deno task dev:ui",
|
"dev:scan": "VITE_DEBUG_SCAN=true deno task dev:ui",
|
||||||
@@ -18,7 +18,6 @@
|
|||||||
"preview": "deno run --allow-net npm:vite preview",
|
"preview": "deno run --allow-net npm:vite preview",
|
||||||
"package": "deno run -A npm:gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && deno run -A --allow-run tar -cvf dist/build.tar -C ./dist/output/ ."
|
"package": "deno run -A npm:gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && deno run -A --allow-run tar -cvf dist/build.tar -C ./dist/output/ ."
|
||||||
},
|
},
|
||||||
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/meshtastic/web.git"
|
"url": "git+https://github.com/meshtastic/web.git"
|
||||||
@@ -26,6 +25,15 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/meshtastic/web/issues"
|
"url": "https://github.com/meshtastic/web/issues"
|
||||||
},
|
},
|
||||||
|
"simple-git-hooks": {
|
||||||
|
"pre-commit": "deno task lint:fix && deno task format"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{ts,tsx}": [
|
||||||
|
"deno task lint:fix",
|
||||||
|
"deno task format"
|
||||||
|
]
|
||||||
|
},
|
||||||
"homepage": "https://meshtastic.org",
|
"homepage": "https://meshtastic.org",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bufbuild/protobuf": "^2.2.3",
|
"@bufbuild/protobuf": "^2.2.3",
|
||||||
|
|||||||
+19
-18
@@ -1,20 +1,19 @@
|
|||||||
import { DeviceWrapper } from "@app/DeviceWrapper.tsx";
|
import { DeviceWrapper } from "@app/DeviceWrapper.tsx";
|
||||||
import { PageRouter } from "@app/PageRouter.tsx";
|
import { PageRouter } from "@app/PageRouter.tsx";
|
||||||
import { ThemeProvider } from "@app/components/generic/ThemeProvider";
|
|
||||||
import { CommandPalette } from "@components/CommandPalette.tsx";
|
import { CommandPalette } from "@components/CommandPalette.tsx";
|
||||||
import { DeviceSelector } from "@components/DeviceSelector.tsx";
|
import { DeviceSelector } from "@components/DeviceSelector.tsx";
|
||||||
import { DialogManager } from "@components/Dialog/DialogManager";
|
import { DialogManager } from "@components/Dialog/DialogManager.tsx";
|
||||||
import { NewDeviceDialog } from "@components/Dialog/NewDeviceDialog.tsx";
|
import { NewDeviceDialog } from "@components/Dialog/NewDeviceDialog.tsx";
|
||||||
import { KeyBackupReminder } from "@components/KeyBackupReminder";
|
import { KeyBackupReminder } from "@components/KeyBackupReminder.tsx";
|
||||||
import { Toaster } from "@components/Toaster.tsx";
|
import { Toaster } from "@components/Toaster.tsx";
|
||||||
import Footer from "@components/UI/Footer.tsx";
|
import Footer from "@components/UI/Footer.tsx";
|
||||||
import { useAppStore } from "@core/stores/appStore.ts";
|
import { useAppStore } from "@core/stores/appStore.ts";
|
||||||
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||||
import { Dashboard } from "@pages/Dashboard/index.tsx";
|
import { Dashboard } from "@pages/Dashboard/index.tsx";
|
||||||
import type { JSX } from "react";
|
|
||||||
import { MapProvider } from "react-map-gl/maplibre";
|
import { MapProvider } from "react-map-gl/maplibre";
|
||||||
|
|
||||||
export const App = (): JSX.Element => {
|
export const App = () => {
|
||||||
const { getDevice } = useDeviceStore();
|
const { getDevice } = useDeviceStore();
|
||||||
const { selectedDevice, setConnectDialogOpen, connectDialogOpen } =
|
const { selectedDevice, setConnectDialogOpen, connectDialogOpen } =
|
||||||
useAppStore();
|
useAppStore();
|
||||||
@@ -36,19 +35,21 @@ export const App = (): JSX.Element => {
|
|||||||
<div className="flex grow">
|
<div className="flex grow">
|
||||||
<DeviceSelector />
|
<DeviceSelector />
|
||||||
<div className="flex grow flex-col">
|
<div className="flex grow flex-col">
|
||||||
{device ? (
|
{device
|
||||||
<div className="flex h-screen">
|
? (
|
||||||
<DialogManager />
|
<div className="flex h-screen">
|
||||||
<KeyBackupReminder />
|
<DialogManager />
|
||||||
<CommandPalette />
|
<KeyBackupReminder />
|
||||||
<PageRouter />
|
<CommandPalette />
|
||||||
</div>
|
<PageRouter />
|
||||||
) : (
|
</div>
|
||||||
<>
|
)
|
||||||
<Dashboard />
|
: (
|
||||||
<Footer />
|
<>
|
||||||
</>
|
<Dashboard />
|
||||||
)}
|
<Footer />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Avatar } from "@components/UI/Avatar";
|
import { Avatar } from "./UI/Avatar.tsx";
|
||||||
import {
|
import {
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@@ -117,13 +117,11 @@ export const CommandPalette = () => {
|
|||||||
return {
|
return {
|
||||||
label:
|
label:
|
||||||
device.nodes.get(device.hardware.myNodeNum)?.user?.longName ??
|
device.nodes.get(device.hardware.myNodeNum)?.user?.longName ??
|
||||||
device.hardware.myNodeNum.toString(),
|
device.hardware.myNodeNum.toString(),
|
||||||
icon: (
|
icon: (
|
||||||
<Avatar
|
<Avatar
|
||||||
text={
|
text={device.nodes.get(device.hardware.myNodeNum)?.user
|
||||||
device.nodes.get(device.hardware.myNodeNum)?.user
|
?.shortName ?? device.hardware.myNodeNum.toString()}
|
||||||
?.shortName ?? device.hardware.myNodeNum.toString()
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
action() {
|
action() {
|
||||||
@@ -241,8 +239,8 @@ export const CommandPalette = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeydown);
|
globalThis.addEventListener("keydown", handleKeydown);
|
||||||
return () => window.removeEventListener("keydown", handleKeydown);
|
return () => globalThis.removeEventListener("keydown", handleKeydown);
|
||||||
}, [setCommandPaletteOpen]);
|
}, [setCommandPaletteOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import { Code } from "@components/UI/Typography/Code.tsx";
|
|||||||
import { useAppStore } from "@core/stores/appStore.ts";
|
import { useAppStore } from "@core/stores/appStore.ts";
|
||||||
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||||
import { HomeIcon, PlusIcon, SearchIcon } from "lucide-react";
|
import { HomeIcon, PlusIcon, SearchIcon } from "lucide-react";
|
||||||
import type { JSX } from "react";
|
|
||||||
import { Avatar } from "./UI/Avatar";
|
|
||||||
|
|
||||||
export const DeviceSelector = (): JSX.Element => {
|
import { Avatar } from "./UI/Avatar.tsx";
|
||||||
|
import process from "node:process";
|
||||||
|
|
||||||
|
export const DeviceSelector = () => {
|
||||||
const { getDevices } = useDeviceStore();
|
const { getDevices } = useDeviceStore();
|
||||||
const {
|
const {
|
||||||
selectedDevice,
|
selectedDevice,
|
||||||
@@ -38,11 +39,9 @@ export const DeviceSelector = (): JSX.Element => {
|
|||||||
active={selectedDevice === device.id}
|
active={selectedDevice === device.id}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
text={
|
text={device.nodes
|
||||||
device.nodes
|
.get(device.hardware.myNodeNum)
|
||||||
.get(device.hardware.myNodeNum)
|
?.user?.shortName.toString() ?? "UNK"}
|
||||||
?.user?.shortName.toString() ?? "UNK"
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</DeviceSelectorButton>
|
</DeviceSelectorButton>
|
||||||
))}
|
))}
|
||||||
@@ -66,9 +65,11 @@ export const DeviceSelector = (): JSX.Element => {
|
|||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
</button>
|
</button>
|
||||||
{/* TODO: This is being commented out until its fixed */}
|
{/* TODO: This is being commented out until its fixed */}
|
||||||
{/* <button type="button" className="transition-all hover:text-accent">
|
{
|
||||||
|
/* <button type="button" className="transition-all hover:text-accent">
|
||||||
<LanguagesIcon />
|
<LanguagesIcon />
|
||||||
</button> */}
|
</button> */
|
||||||
|
}
|
||||||
<Separator />
|
<Separator />
|
||||||
<Code>{process.env.COMMIT_HASH}</Code>
|
<Code>{process.env.COMMIT_HASH}</Code>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export interface DeviceSelectorButtonProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DeviceSelectorButton = ({
|
export const DeviceSelectorButton = ({
|
||||||
active,
|
|
||||||
onClick,
|
onClick,
|
||||||
children,
|
children,
|
||||||
}: DeviceSelectorButtonProps) => (
|
}: DeviceSelectorButtonProps) => (
|
||||||
@@ -14,9 +13,11 @@ export const DeviceSelectorButton = ({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onKeyDown={onClick}
|
onKeyDown={onClick}
|
||||||
>
|
>
|
||||||
{/* {active && (
|
{
|
||||||
|
/* {active && (
|
||||||
<div className="absolute -left-2 h-10 w-1.5 rounded-full bg-accent" />
|
<div className="absolute -left-2 h-10 w-1.5 rounded-full bg-accent" />
|
||||||
)} */}
|
)} */
|
||||||
|
}
|
||||||
<div className="flex aspect-square cursor-pointer flex-col items-center justify-center">
|
<div className="flex aspect-square cursor-pointer flex-col items-center justify-center">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { RemoveNodeDialog } from "@app/components/Dialog/RemoveNodeDialog.tsx";
|
import { RemoveNodeDialog } from "@app/components/Dialog/RemoveNodeDialog.tsx";
|
||||||
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.tsx";
|
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.tsx";
|
||||||
import { ImportDialog } from "@components/Dialog/ImportDialog.tsx";
|
import { ImportDialog } from "@components/Dialog/ImportDialog.tsx";
|
||||||
import { PkiBackupDialog } from "@components/Dialog/PKIBackupDialog";
|
import { PkiBackupDialog } from "./PKIBackupDialog.tsx";
|
||||||
import { QRDialog } from "@components/Dialog/QRDialog.tsx";
|
import { QRDialog } from "@components/Dialog/QRDialog.tsx";
|
||||||
import { RebootDialog } from "@components/Dialog/RebootDialog.tsx";
|
import { RebootDialog } from "@components/Dialog/RebootDialog.tsx";
|
||||||
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
|
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import type { JSX } from "react";
|
|
||||||
import { NodeDetailsDialog } from "./NodeDetailsDialog";
|
|
||||||
|
|
||||||
export const DialogManager = (): JSX.Element => {
|
import { NodeDetailsDialog } from "./NodeDetailsDialog.tsx";
|
||||||
|
|
||||||
|
export const DialogManager = () => {
|
||||||
const { channels, config, dialog, setDialogOpen } = useDevice();
|
const { channels, config, dialog, setDialogOpen } = useDevice();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { Switch } from "@components/UI/Switch.tsx";
|
|||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { toByteArray } from "base64-js";
|
import { toByteArray } from "base64-js";
|
||||||
import { type JSX, useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export interface ImportDialogProps {
|
export interface ImportDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -26,7 +26,7 @@ export interface ImportDialogProps {
|
|||||||
export const ImportDialog = ({
|
export const ImportDialog = ({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: ImportDialogProps): JSX.Element => {
|
}: ImportDialogProps) => {
|
||||||
const [importDialogInput, setImportDialogInput] = useState<string>("");
|
const [importDialogInput, setImportDialogInput] = useState<string>("");
|
||||||
const [channelSet, setChannelSet] = useState<Protobuf.AppOnly.ChannelSet>();
|
const [channelSet, setChannelSet] = useState<Protobuf.AppOnly.ChannelSet>();
|
||||||
const [validUrl, setValidUrl] = useState<boolean>(false);
|
const [validUrl, setValidUrl] = useState<boolean>(false);
|
||||||
@@ -62,7 +62,7 @@ export const ImportDialog = ({
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
setValidUrl(true);
|
setValidUrl(true);
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
setValidUrl(false);
|
setValidUrl(false);
|
||||||
setChannelSet(undefined);
|
setChannelSet(undefined);
|
||||||
}
|
}
|
||||||
@@ -73,10 +73,9 @@ export const ImportDialog = ({
|
|||||||
connection?.setChannel(
|
connection?.setChannel(
|
||||||
create(Protobuf.Channel.ChannelSchema, {
|
create(Protobuf.Channel.ChannelSchema, {
|
||||||
index,
|
index,
|
||||||
role:
|
role: index === 0
|
||||||
index === 0
|
? Protobuf.Channel.Channel_Role.PRIMARY
|
||||||
? Protobuf.Channel.Channel_Role.PRIMARY
|
: Protobuf.Channel.Channel_Role.SECONDARY,
|
||||||
: Protobuf.Channel.Channel_Role.SECONDARY,
|
|
||||||
settings: ch,
|
settings: ch,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -119,25 +118,29 @@ export const ImportDialog = ({
|
|||||||
<div className="w-36">
|
<div className="w-36">
|
||||||
<Label>Use Preset?</Label>
|
<Label>Use Preset?</Label>
|
||||||
<Switch
|
<Switch
|
||||||
disabled={true}
|
disabled
|
||||||
checked={channelSet?.loraConfig?.usePreset ?? true}
|
checked={channelSet?.loraConfig?.usePreset ?? true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* <Select
|
{
|
||||||
|
/* <Select
|
||||||
label="Modem Preset"
|
label="Modem Preset"
|
||||||
disabled
|
disabled
|
||||||
value={channelSet?.loraConfig?.modemPreset}
|
value={channelSet?.loraConfig?.modemPreset}
|
||||||
>
|
>
|
||||||
{renderOptions(Protobuf.Config_LoRaConfig_ModemPreset)}
|
{renderOptions(Protobuf.Config_LoRaConfig_ModemPreset)}
|
||||||
</Select> */}
|
</Select> */
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
{/* <Select
|
{
|
||||||
|
/* <Select
|
||||||
label="Region"
|
label="Region"
|
||||||
disabled
|
disabled
|
||||||
value={channelSet?.loraConfig?.region}
|
value={channelSet?.loraConfig?.region}
|
||||||
>
|
>
|
||||||
{renderOptions(Protobuf.Config_LoRaConfig_RegionCode)}
|
{renderOptions(Protobuf.Config_LoRaConfig_RegionCode)}
|
||||||
</Select> */}
|
</Select> */
|
||||||
|
}
|
||||||
|
|
||||||
<span className="text-md block font-medium text-text-primary">
|
<span className="text-md block font-medium text-text-primary">
|
||||||
Channels:
|
Channels:
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { useDevice } from "@app/core/stores/deviceStore";
|
import { useDevice } from "../../core/stores/deviceStore.ts";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@components/UI/Dialog";
|
} from "../UI/Dialog.tsx";
|
||||||
import type { Protobuf, Types } from "@meshtastic/core";
|
import type { Protobuf, Types } from "@meshtastic/core";
|
||||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||||
import type { JSX } from "react";
|
|
||||||
|
|
||||||
export interface LocationResponseDialogProps {
|
export interface LocationResponseDialogProps {
|
||||||
location: Types.PacketMetadata<Protobuf.Mesh.location> | undefined;
|
location: Types.PacketMetadata<Protobuf.Mesh.location> | undefined;
|
||||||
@@ -20,15 +19,13 @@ export const LocationResponseDialog = ({
|
|||||||
location,
|
location,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: LocationResponseDialogProps): JSX.Element => {
|
}: LocationResponseDialogProps) => {
|
||||||
const { nodes } = useDevice();
|
const { nodes } = useDevice();
|
||||||
|
|
||||||
const from = nodes.get(location?.from ?? 0);
|
const from = nodes.get(location?.from ?? 0);
|
||||||
const longName =
|
const longName = from?.user?.longName ??
|
||||||
from?.user?.longName ??
|
|
||||||
(from ? `!${numberToHexUnpadded(from?.num)}` : "Unknown");
|
(from ? `!${numberToHexUnpadded(from?.num)}` : "Unknown");
|
||||||
const shortName =
|
const shortName = from?.user?.shortName ??
|
||||||
from?.user?.shortName ??
|
|
||||||
(from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : "UNK");
|
(from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : "UNK");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
type BrowserFeature,
|
type BrowserFeature,
|
||||||
useBrowserFeatureDetection,
|
useBrowserFeatureDetection,
|
||||||
} from "@app/core/hooks/useBrowserFeatureDetection";
|
} from "../../core/hooks/useBrowserFeatureDetection.ts";
|
||||||
import { BLE } from "@components/PageComponents/Connect/BLE.tsx";
|
import { BLE } from "@components/PageComponents/Connect/BLE.tsx";
|
||||||
import { HTTP } from "@components/PageComponents/Connect/HTTP.tsx";
|
import { HTTP } from "@components/PageComponents/Connect/HTTP.tsx";
|
||||||
import { Serial } from "@components/PageComponents/Connect/Serial.tsx";
|
import { Serial } from "@components/PageComponents/Connect/Serial.tsx";
|
||||||
@@ -18,9 +18,9 @@ import {
|
|||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from "@components/UI/Tabs.tsx";
|
} from "@components/UI/Tabs.tsx";
|
||||||
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
||||||
import { AlertCircle, InfoIcon } from "lucide-react";
|
import { AlertCircle } from "lucide-react";
|
||||||
import { Fragment, type JSX } from "react/jsx-runtime";
|
import { Link } from "../UI/Typography/Link.tsx";
|
||||||
import { Link } from "../UI/Typography/Link";
|
import { Fragment } from "react/jsx-runtime";
|
||||||
|
|
||||||
export interface TabElementProps {
|
export interface TabElementProps {
|
||||||
closeDialog: () => void;
|
closeDialog: () => void;
|
||||||
@@ -85,14 +85,16 @@ const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => {
|
|||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
{browserFeatures.length > 0 && (
|
{browserFeatures.length > 0 && (
|
||||||
<>
|
<>
|
||||||
This application requires {formatFeatureList(browserFeatures)}.
|
This application requires{" "}
|
||||||
Please use a Chromium-based browser like Chrome or Edge.
|
{formatFeatureList(browserFeatures)}. Please use a
|
||||||
|
Chromium-based browser like Chrome or Edge.
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{needsSecureContext && (
|
{needsSecureContext && (
|
||||||
<>
|
<>
|
||||||
{browserFeatures.length > 0 && " Additionally, it"}
|
{browserFeatures.length > 0 && " Additionally, it"}
|
||||||
{browserFeatures.length === 0 && "This application"} requires a{" "}
|
{browserFeatures.length === 0 && "This application"} requires a
|
||||||
|
{" "}
|
||||||
<Link href={links["Secure Context"]}>secure context</Link>.
|
<Link href={links["Secure Context"]}>secure context</Link>.
|
||||||
Please connect using HTTPS or localhost.
|
Please connect using HTTPS or localhost.
|
||||||
</>
|
</>
|
||||||
@@ -107,7 +109,7 @@ const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => {
|
|||||||
export const NewDeviceDialog = ({
|
export const NewDeviceDialog = ({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: NewDeviceProps): JSX.Element => {
|
}: NewDeviceProps) => {
|
||||||
const { unsupported } = useBrowserFeatureDetection();
|
const { unsupported } = useBrowserFeatureDetection();
|
||||||
|
|
||||||
const tabs: TabManifest[] = [
|
const tabs: TabManifest[] = [
|
||||||
@@ -119,15 +121,13 @@ export const NewDeviceDialog = ({
|
|||||||
{
|
{
|
||||||
label: "Bluetooth",
|
label: "Bluetooth",
|
||||||
element: BLE,
|
element: BLE,
|
||||||
isDisabled:
|
isDisabled: unsupported.includes("Web Bluetooth") ||
|
||||||
unsupported.includes("Web Bluetooth") ||
|
|
||||||
unsupported.includes("Secure Context"),
|
unsupported.includes("Secure Context"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Serial",
|
label: "Serial",
|
||||||
element: Serial,
|
element: Serial,
|
||||||
isDisabled:
|
isDisabled: unsupported.includes("Web Serial") ||
|
||||||
unsupported.includes("Web Serial") ||
|
|
||||||
unsupported.includes("Secure Context"),
|
unsupported.includes("Secure Context"),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -149,9 +149,9 @@ export const NewDeviceDialog = ({
|
|||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<TabsContent key={tab.label} value={tab.label}>
|
<TabsContent key={tab.label} value={tab.label}>
|
||||||
<fieldset disabled={tab.isDisabled}>
|
<fieldset disabled={tab.isDisabled}>
|
||||||
{tab.isDisabled ? (
|
{tab.isDisabled
|
||||||
<ErrorMessage missingFeatures={unsupported} />
|
? <ErrorMessage missingFeatures={unsupported} />
|
||||||
) : null}
|
: null}
|
||||||
<tab.element closeDialog={() => onOpenChange(false)} />
|
<tab.element closeDialog={() => onOpenChange(false)} />
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import { useAppStore } from "@app/core/stores/appStore";
|
import { useAppStore } from "../../core/stores/appStore.ts";
|
||||||
import { useDevice } from "@app/core/stores/deviceStore";
|
import { useDevice } from "../../core/stores/deviceStore.ts";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
AccordionItem,
|
AccordionItem,
|
||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from "@components/UI/Accordion";
|
} from "../UI/Accordion.tsx";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@components/UI/Dialog";
|
} from "../UI/Dialog.tsx";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||||
import { DeviceImage } from "../generic/DeviceImage";
|
import { DeviceImage } from "../generic/DeviceImage.tsx";
|
||||||
import { TimeAgo } from "../generic/TimeAgo";
|
import { TimeAgo } from "../generic/TimeAgo.tsx";
|
||||||
import { Uptime } from "../generic/Uptime";
|
import { Uptime } from "../generic/Uptime.tsx";
|
||||||
|
|
||||||
export interface NodeDetailsDialogProps {
|
export interface NodeDetailsDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -32,134 +32,159 @@ export const NodeDetailsDialog = ({
|
|||||||
const { nodeNumDetails } = useAppStore();
|
const { nodeNumDetails } = useAppStore();
|
||||||
const device: Protobuf.Mesh.NodeInfo = nodes.get(nodeNumDetails);
|
const device: Protobuf.Mesh.NodeInfo = nodes.get(nodeNumDetails);
|
||||||
|
|
||||||
return device ? (
|
return device
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
? (
|
||||||
<DialogContent>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogHeader>
|
<DialogContent>
|
||||||
<DialogTitle>
|
<DialogHeader>
|
||||||
Node Details for {device.user?.longName ?? "UNKNOWN"} (
|
<DialogTitle>
|
||||||
{device.user?.shortName ?? "UNK"})
|
Node Details for {device.user?.longName ?? "UNKNOWN"} (
|
||||||
</DialogTitle>
|
{device.user?.shortName ?? "UNK"})
|
||||||
</DialogHeader>
|
</DialogTitle>
|
||||||
<DialogFooter>
|
</DialogHeader>
|
||||||
<div className="w-full">
|
<DialogFooter>
|
||||||
<DeviceImage
|
<div className="w-full">
|
||||||
className="w-32 h-32 mx-auto rounded-lg border-4 border-slate-200 dark:border-slate-800"
|
<DeviceImage
|
||||||
deviceType={
|
className="w-32 h-32 mx-auto rounded-lg border-4 border-slate-200 dark:border-slate-800"
|
||||||
Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]
|
deviceType={Protobuf.Mesh
|
||||||
}
|
.HardwareModel[device.user?.hwModel ?? 0]}
|
||||||
/>
|
/>
|
||||||
<div className="bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
<div className="bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
||||||
Details:
|
Details:
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Hardware:{" "}
|
Hardware:{" "}
|
||||||
{Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]}
|
{Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]}
|
||||||
</p>
|
</p>
|
||||||
<p>Node Number: {device.num}</p>
|
<p>Node Number: {device.num}</p>
|
||||||
<p>Node HEX: !{numberToHexUnpadded(device.num)}</p>
|
<p>Node HEX: !{numberToHexUnpadded(device.num)}</p>
|
||||||
<p>
|
<p>
|
||||||
Role:{" "}
|
Role: {Protobuf.Config.Config_DeviceConfig_Role[
|
||||||
{
|
|
||||||
Protobuf.Config.Config_DeviceConfig_Role[
|
|
||||||
device.user?.role ?? 0
|
device.user?.role ?? 0
|
||||||
]
|
]}
|
||||||
}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Last Heard:{" "}
|
|
||||||
{device.lastHeard === 0 ? (
|
|
||||||
"Never"
|
|
||||||
) : (
|
|
||||||
<TimeAgo timestamp={device.lastHeard * 1000} />
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{device.position ? (
|
|
||||||
<div className="mt-5 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
|
||||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
|
||||||
Position:
|
|
||||||
</p>
|
</p>
|
||||||
{device.position.latitudeI && device.position.longitudeI ? (
|
<p>
|
||||||
<p>
|
Last Heard: {device.lastHeard === 0
|
||||||
Coordinates:{" "}
|
? (
|
||||||
<a
|
"Never"
|
||||||
className="text-blue-500 dark:text-blue-400"
|
)
|
||||||
href={`https://www.openstreetmap.org/?mlat=${
|
: <TimeAgo timestamp={device.lastHeard * 1000} />}
|
||||||
device.position.latitudeI / 1e7
|
</p>
|
||||||
}&mlon=${device.position.longitudeI / 1e7}&layers=N`}
|
</div>
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
{device.position
|
||||||
|
? (
|
||||||
|
<div className="mt-5 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||||
|
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
||||||
|
Position:
|
||||||
|
</p>
|
||||||
|
{device.position.latitudeI && device.position.longitudeI
|
||||||
|
? (
|
||||||
|
<p>
|
||||||
|
Coordinates:{" "}
|
||||||
|
<a
|
||||||
|
className="text-blue-500 dark:text-blue-400"
|
||||||
|
href={`https://www.openstreetmap.org/?mlat=${
|
||||||
|
device.position.latitudeI / 1e7
|
||||||
|
}&mlon=${
|
||||||
|
device.position.longitudeI / 1e7
|
||||||
|
}&layers=N`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{device.position.latitudeI / 1e7},{" "}
|
||||||
|
{device.position.longitudeI / 1e7}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
{device.position.altitude
|
||||||
|
? <p>Altitude: {device.position.altitude}m</p>
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
|
||||||
|
{device.deviceMetrics
|
||||||
|
? (
|
||||||
|
<div className="mt-5 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||||
|
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
||||||
|
Device Metrics:
|
||||||
|
</p>
|
||||||
|
{device.deviceMetrics.airUtilTx
|
||||||
|
? (
|
||||||
|
<p>
|
||||||
|
Air TX utilization:{" "}
|
||||||
|
{device.deviceMetrics.airUtilTx.toFixed(2)}%
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
{device.deviceMetrics.channelUtilization
|
||||||
|
? (
|
||||||
|
<p>
|
||||||
|
Channel utilization:{" "}
|
||||||
|
{device.deviceMetrics.channelUtilization.toFixed(2)}%
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
{device.deviceMetrics.batteryLevel
|
||||||
|
? (
|
||||||
|
<p>
|
||||||
|
Battery level:{" "}
|
||||||
|
{device.deviceMetrics.batteryLevel.toFixed(2)}%
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
{device.deviceMetrics.voltage
|
||||||
|
? (
|
||||||
|
<p>
|
||||||
|
Voltage: {device.deviceMetrics.voltage.toFixed(2)}V
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
{device.deviceMetrics.uptimeSeconds
|
||||||
|
? (
|
||||||
|
<p>
|
||||||
|
Uptime:{" "}
|
||||||
|
<Uptime
|
||||||
|
seconds={device.deviceMetrics.uptimeSeconds}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
|
||||||
|
{device
|
||||||
|
? (
|
||||||
|
<div className="mt-5 w-full max-w-[464px] bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||||
|
<Accordion
|
||||||
|
className="AccordionRoot"
|
||||||
|
type="single"
|
||||||
|
collapsible
|
||||||
>
|
>
|
||||||
{device.position.latitudeI / 1e7},{" "}
|
<AccordionItem className="AccordionItem" value="item-1">
|
||||||
{device.position.longitudeI / 1e7}
|
<AccordionTrigger>
|
||||||
</a>
|
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
||||||
</p>
|
All Raw Metrics:
|
||||||
) : null}
|
</p>
|
||||||
{device.position.altitude ? (
|
</AccordionTrigger>
|
||||||
<p>Altitude: {device.position.altitude}m</p>
|
<AccordionContent className="overflow-x-scroll">
|
||||||
) : null}
|
<pre className="text-xs w-full">
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{device.deviceMetrics ? (
|
|
||||||
<div className="mt-5 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
|
||||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
|
||||||
Device Metrics:
|
|
||||||
</p>
|
|
||||||
{device.deviceMetrics.airUtilTx ? (
|
|
||||||
<p>
|
|
||||||
Air TX utilization:{" "}
|
|
||||||
{device.deviceMetrics.airUtilTx.toFixed(2)}%
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
{device.deviceMetrics.channelUtilization ? (
|
|
||||||
<p>
|
|
||||||
Channel utilization:{" "}
|
|
||||||
{device.deviceMetrics.channelUtilization.toFixed(2)}%
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
{device.deviceMetrics.batteryLevel ? (
|
|
||||||
<p>
|
|
||||||
Battery level:{" "}
|
|
||||||
{device.deviceMetrics.batteryLevel.toFixed(2)}%
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
{device.deviceMetrics.voltage ? (
|
|
||||||
<p>Voltage: {device.deviceMetrics.voltage.toFixed(2)}V</p>
|
|
||||||
) : null}
|
|
||||||
{device.deviceMetrics.uptimeSeconds ? (
|
|
||||||
<p>
|
|
||||||
Uptime:{" "}
|
|
||||||
<Uptime seconds={device.deviceMetrics.uptimeSeconds} />
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{device ? (
|
|
||||||
<div className="mt-5 w-full max-w-[464px] bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
|
||||||
<Accordion className="AccordionRoot" type="single" collapsible>
|
|
||||||
<AccordionItem className="AccordionItem" value="item-1">
|
|
||||||
<AccordionTrigger>
|
|
||||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
|
||||||
All Raw Metrics:
|
|
||||||
</p>
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="overflow-x-scroll">
|
|
||||||
<pre className="text-xs w-full">
|
|
||||||
{JSON.stringify(device, null, 2)}
|
{JSON.stringify(device, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
)
|
||||||
</div>
|
: null}
|
||||||
</DialogFooter>
|
</div>
|
||||||
</DialogContent>
|
</DialogFooter>
|
||||||
</Dialog>
|
</DialogContent>
|
||||||
) : null;
|
</Dialog>
|
||||||
|
)
|
||||||
|
: null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { toast } from "@app/core/hooks/useToast";
|
import { toast } from "../../core/hooks/useToast.ts";
|
||||||
import { useAppStore } from "@app/core/stores/appStore";
|
import { useAppStore } from "../../core/stores/appStore.ts";
|
||||||
import { useDevice } from "@app/core/stores/deviceStore";
|
import { useDevice } from "../../core/stores/deviceStore.ts";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@components/UI/Dialog";
|
} from "../UI/Dialog.tsx";
|
||||||
import type { Protobuf } from "@meshtastic/core";
|
import type { Protobuf } from "@meshtastic/core";
|
||||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||||
import { TrashIcon } from "lucide-react";
|
import { TrashIcon } from "lucide-react";
|
||||||
import type { JSX } from "react";
|
|
||||||
import { Button } from "../UI/Button";
|
import { Button } from "../UI/Button.tsx";
|
||||||
|
|
||||||
export interface NodeOptionsDialogProps {
|
export interface NodeOptionsDialogProps {
|
||||||
node: Protobuf.Mesh.NodeInfo | undefined;
|
node: Protobuf.Mesh.NodeInfo | undefined;
|
||||||
@@ -23,7 +23,7 @@ export const NodeOptionsDialog = ({
|
|||||||
node,
|
node,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: NodeOptionsDialogProps): JSX.Element => {
|
}: NodeOptionsDialogProps) => {
|
||||||
const { setDialogOpen, connection, setActivePage } = useDevice();
|
const { setDialogOpen, connection, setActivePage } = useDevice();
|
||||||
const {
|
const {
|
||||||
setNodeNumToBeRemoved,
|
setNodeNumToBeRemoved,
|
||||||
@@ -31,11 +31,9 @@ export const NodeOptionsDialog = ({
|
|||||||
setChatType,
|
setChatType,
|
||||||
setActiveChat,
|
setActiveChat,
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
const longName =
|
const longName = node?.user?.longName ??
|
||||||
node?.user?.longName ??
|
|
||||||
(node ? `!${numberToHexUnpadded(node?.num)}` : "Unknown");
|
(node ? `!${numberToHexUnpadded(node?.num)}` : "Unknown");
|
||||||
const shortName =
|
const shortName = node?.user?.shortName ??
|
||||||
node?.user?.shortName ??
|
|
||||||
(node ? `${numberToHexUnpadded(node?.num).substring(0, 4)}` : "UNK");
|
(node ? `${numberToHexUnpadded(node?.num).substring(0, 4)}` : "UNK");
|
||||||
|
|
||||||
function handleDirectMessage() {
|
function handleDirectMessage() {
|
||||||
@@ -53,7 +51,7 @@ export const NodeOptionsDialog = ({
|
|||||||
connection?.requestPosition(node.num).then(() =>
|
connection?.requestPosition(node.num).then(() =>
|
||||||
toast({
|
toast({
|
||||||
title: "Position request sent.",
|
title: "Position request sent.",
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
onOpenChange();
|
onOpenChange();
|
||||||
}
|
}
|
||||||
@@ -66,7 +64,7 @@ export const NodeOptionsDialog = ({
|
|||||||
connection?.traceRoute(node.num).then(() =>
|
connection?.traceRoute(node.num).then(() =>
|
||||||
toast({
|
toast({
|
||||||
title: "Traceroute sent.",
|
title: "Traceroute sent.",
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
onOpenChange();
|
onOpenChange();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useDevice } from "@app/core/stores/deviceStore";
|
import { useDevice } from "../../core/stores/deviceStore.ts";
|
||||||
import { Button } from "@components/UI/Button";
|
import { Button } from "../UI/Button.tsx";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -40,7 +40,7 @@ export const PkiBackupDialog = ({
|
|||||||
const renderPrintWindow = React.useCallback(() => {
|
const renderPrintWindow = React.useCallback(() => {
|
||||||
if (!privateKey || !publicKey) return;
|
if (!privateKey || !publicKey) return;
|
||||||
|
|
||||||
const printWindow = window.open("", "_blank");
|
const printWindow = globalThis.open("", "_blank");
|
||||||
if (printWindow) {
|
if (printWindow) {
|
||||||
printWindow.document.write(`
|
printWindow.document.write(`
|
||||||
<html>
|
<html>
|
||||||
@@ -116,14 +116,14 @@ export const PkiBackupDialog = ({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter className="mt-6">
|
<DialogFooter className="mt-6">
|
||||||
<Button
|
<Button
|
||||||
variant={"default"}
|
variant="default"
|
||||||
onClick={() => createDownloadKeyFile()}
|
onClick={() => createDownloadKeyFile()}
|
||||||
className=""
|
className=""
|
||||||
>
|
>
|
||||||
<DownloadIcon size={20} className="mr-2" />
|
<DownloadIcon size={20} className="mr-2" />
|
||||||
Download
|
Download
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant={"default"} onClick={() => renderPrintWindow()}>
|
<Button variant="default" onClick={() => renderPrintWindow()}>
|
||||||
<PrinterIcon size={20} className="mr-2" />
|
<PrinterIcon size={20} className="mr-2" />
|
||||||
Print
|
Print
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const PkiRegenerateDialog = ({
|
|||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: PkiRegenerateDialogProps): JSX.Element => {
|
}: PkiRegenerateDialogProps) => {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const QRDialog = ({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
loraConfig,
|
loraConfig,
|
||||||
channels,
|
channels,
|
||||||
}: QRDialogProps): JSX.Element => {
|
}: QRDialogProps) => {
|
||||||
const [selectedChannels, setSelectedChannels] = useState<number[]>([0]);
|
const [selectedChannels, setSelectedChannels] = useState<number[]>([0]);
|
||||||
const [qrCodeUrl, setQrCodeUrl] = useState<string>("");
|
const [qrCodeUrl, setQrCodeUrl] = useState<string>("");
|
||||||
const [qrCodeAdd, setQrCodeAdd] = useState<boolean>();
|
const [qrCodeAdd, setQrCodeAdd] = useState<boolean>();
|
||||||
@@ -77,8 +77,8 @@ export const QRDialog = ({
|
|||||||
{channel.settings?.name.length
|
{channel.settings?.name.length
|
||||||
? channel.settings.name
|
? channel.settings.name
|
||||||
: channel.role === Protobuf.Channel.Channel_Role.PRIMARY
|
: channel.role === Protobuf.Channel.Channel_Role.PRIMARY
|
||||||
? "Primary"
|
? "Primary"
|
||||||
: `Channel: ${channel.index}`}
|
: `Channel: ${channel.index}`}
|
||||||
</Label>
|
</Label>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
key={channel.index}
|
key={channel.index}
|
||||||
@@ -86,7 +86,9 @@ export const QRDialog = ({
|
|||||||
onCheckedChange={() => {
|
onCheckedChange={() => {
|
||||||
if (selectedChannels.includes(channel.index)) {
|
if (selectedChannels.includes(channel.index)) {
|
||||||
setSelectedChannels(
|
setSelectedChannels(
|
||||||
selectedChannels.filter((c) => c !== channel.index),
|
selectedChannels.filter((c) =>
|
||||||
|
c !== channel.index
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setSelectedChannels([
|
setSelectedChannels([
|
||||||
@@ -130,7 +132,7 @@ export const QRDialog = ({
|
|||||||
<Label>Sharable URL</Label>
|
<Label>Sharable URL</Label>
|
||||||
<Input
|
<Input
|
||||||
value={qrCodeUrl}
|
value={qrCodeUrl}
|
||||||
disabled={true}
|
disabled
|
||||||
className="dark:text-slate-900"
|
className="dark:text-slate-900"
|
||||||
action={{
|
action={{
|
||||||
icon: ClipboardIcon,
|
icon: ClipboardIcon,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export interface RebootDialogProps {
|
|||||||
export const RebootDialog = ({
|
export const RebootDialog = ({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: RebootDialogProps): JSX.Element => {
|
}: RebootDialogProps) => {
|
||||||
const { connection } = useDevice();
|
const { connection } = useDevice();
|
||||||
|
|
||||||
const [time, setTime] = useState<number>(5);
|
const [time, setTime] = useState<number>(5);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useAppStore } from "@app/core/stores/appStore";
|
import { useAppStore } from "../../core/stores/appStore.ts";
|
||||||
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||||
import { Button } from "@components/UI/Button.tsx";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import {
|
import {
|
||||||
@@ -19,7 +19,7 @@ export interface RemoveNodeDialogProps {
|
|||||||
export const RemoveNodeDialog = ({
|
export const RemoveNodeDialog = ({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: RemoveNodeDialogProps): JSX.Element => {
|
}: RemoveNodeDialogProps) => {
|
||||||
const { connection, nodes, removeNode } = useDevice();
|
const { connection, nodes, removeNode } = useDevice();
|
||||||
const { nodeNumToBeRemoved } = useAppStore();
|
const { nodeNumToBeRemoved } = useAppStore();
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export interface ShutdownDialogProps {
|
|||||||
export const ShutdownDialog = ({
|
export const ShutdownDialog = ({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: ShutdownDialogProps): JSX.Element => {
|
}: ShutdownDialogProps) => {
|
||||||
const { connection } = useDevice();
|
const { connection } = useDevice();
|
||||||
|
|
||||||
const [time, setTime] = useState<number>(5);
|
const [time, setTime] = useState<number>(5);
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { useDevice } from "@app/core/stores/deviceStore";
|
import { useDevice } from "../../core/stores/deviceStore.ts";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@components/UI/Dialog";
|
} from "../UI/Dialog.tsx";
|
||||||
import type { Protobuf, Types } from "@meshtastic/core";
|
import type { Protobuf, Types } from "@meshtastic/core";
|
||||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||||
import type { JSX } from "react";
|
|
||||||
import { TraceRoute } from "../PageComponents/Messages/TraceRoute";
|
import { TraceRoute } from "../PageComponents/Messages/TraceRoute.tsx";
|
||||||
|
|
||||||
export interface TracerouteResponseDialogProps {
|
export interface TracerouteResponseDialogProps {
|
||||||
traceroute: Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery> | undefined;
|
traceroute: Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery> | undefined;
|
||||||
@@ -21,18 +21,16 @@ export const TracerouteResponseDialog = ({
|
|||||||
traceroute,
|
traceroute,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: TracerouteResponseDialogProps): JSX.Element => {
|
}: TracerouteResponseDialogProps) => {
|
||||||
const { nodes } = useDevice();
|
const { nodes } = useDevice();
|
||||||
const route: number[] = traceroute?.data.route ?? [];
|
const route: number[] = traceroute?.data.route ?? [];
|
||||||
const routeBack: number[] = traceroute?.data.routeBack ?? [];
|
const routeBack: number[] = traceroute?.data.routeBack ?? [];
|
||||||
const snrTowards = traceroute?.data.snrTowards ?? [];
|
const snrTowards = traceroute?.data.snrTowards ?? [];
|
||||||
const snrBack = traceroute?.data.snrBack ?? [];
|
const snrBack = traceroute?.data.snrBack ?? [];
|
||||||
const from = nodes.get(traceroute?.from ?? 0);
|
const from = nodes.get(traceroute?.from ?? 0);
|
||||||
const longName =
|
const longName = from?.user?.longName ??
|
||||||
from?.user?.longName ??
|
|
||||||
(from ? `!${numberToHexUnpadded(from?.num)}` : "Unknown");
|
(from ? `!${numberToHexUnpadded(from?.num)}` : "Unknown");
|
||||||
const shortName =
|
const shortName = from?.user?.shortName ??
|
||||||
from?.user?.shortName ??
|
|
||||||
(from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : "UNK");
|
(from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : "UNK");
|
||||||
const to = nodes.get(traceroute?.to ?? 0);
|
const to = nodes.get(traceroute?.to ?? 0);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -74,10 +74,11 @@ export function DynamicForm<T extends FieldValues>({
|
|||||||
const value = getValues(field.fieldName);
|
const value = getValues(field.fieldName);
|
||||||
if (value === "always") return true;
|
if (value === "always") return true;
|
||||||
if (typeof value === "boolean") return field.invert ? value : !value;
|
if (typeof value === "boolean") return field.invert ? value : !value;
|
||||||
if (typeof value === "number")
|
if (typeof value === "number") {
|
||||||
return field.invert
|
return field.invert
|
||||||
? field.selector !== value
|
? field.selector !== value
|
||||||
: field.selector === value;
|
: field.selector === value;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -85,11 +86,9 @@ export function DynamicForm<T extends FieldValues>({
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="space-y-8"
|
className="space-y-8"
|
||||||
{...(submitType === "onSubmit"
|
{...(submitType === "onSubmit" ? { onSubmit: handleSubmit(onSubmit) } : {
|
||||||
? { onSubmit: handleSubmit(onSubmit) }
|
onChange: handleSubmit(onSubmit),
|
||||||
: {
|
})}
|
||||||
onChange: handleSubmit(onSubmit),
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{fieldGroups.map((fieldGroup) => (
|
{fieldGroups.map((fieldGroup) => (
|
||||||
<div key={fieldGroup.label} className="space-y-8 sm:space-y-5">
|
<div key={fieldGroup.label} className="space-y-8 sm:space-y-5">
|
||||||
@@ -105,10 +104,8 @@ export function DynamicForm<T extends FieldValues>({
|
|||||||
label={field.label}
|
label={field.label}
|
||||||
fieldName={field.name}
|
fieldName={field.name}
|
||||||
description={field.description}
|
description={field.description}
|
||||||
valid={
|
valid={field.validationText === undefined ||
|
||||||
field.validationText === undefined ||
|
field.validationText === ""}
|
||||||
field.validationText === ""
|
|
||||||
}
|
|
||||||
validationText={field.validationText}
|
validationText={field.validationText}
|
||||||
>
|
>
|
||||||
<DynamicFormField
|
<DynamicFormField
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
type MultiSelectFieldProps,
|
type MultiSelectFieldProps,
|
||||||
MultiSelectInput,
|
MultiSelectInput,
|
||||||
} from "@app/components/Form/FormMultiSelect";
|
} from "./FormMultiSelect.tsx";
|
||||||
import {
|
import {
|
||||||
GenericInput,
|
GenericInput,
|
||||||
type InputFieldProps,
|
type InputFieldProps,
|
||||||
@@ -48,11 +48,19 @@ export function DynamicFormField<T extends FieldValues>({
|
|||||||
|
|
||||||
case "toggle":
|
case "toggle":
|
||||||
return (
|
return (
|
||||||
<ToggleInput field={field} control={control} disabled={disabled} />
|
<ToggleInput
|
||||||
|
field={field}
|
||||||
|
control={control}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
case "select":
|
case "select":
|
||||||
return (
|
return (
|
||||||
<SelectInput field={field} control={control} disabled={disabled} />
|
<SelectInput
|
||||||
|
field={field}
|
||||||
|
control={control}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
case "passwordGenerator":
|
case "passwordGenerator":
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -40,17 +40,15 @@ export function GenericInput<T extends FieldValues>({
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value, onChange, ...rest } }) => (
|
render={({ field: { value, onChange, ...rest } }) => (
|
||||||
<Input
|
<Input
|
||||||
type={
|
type={field.type === "password" && passwordShown
|
||||||
field.type === "password" && passwordShown ? "text" : field.type
|
? "text"
|
||||||
}
|
: field.type}
|
||||||
action={
|
action={field.type === "password"
|
||||||
field.type === "password"
|
? {
|
||||||
? {
|
icon: passwordShown ? EyeOff : Eye,
|
||||||
icon: passwordShown ? EyeOff : Eye,
|
onClick: togglePasswordVisiblity,
|
||||||
onClick: togglePasswordVisiblity,
|
}
|
||||||
}
|
: undefined}
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
step={field.properties?.step}
|
step={field.properties?.step}
|
||||||
value={field.type === "number" ? Number.parseFloat(value) : value}
|
value={field.type === "number" ? Number.parseFloat(value) : value}
|
||||||
id={field.name}
|
id={field.name}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type {
|
|||||||
GenericFormElementProps,
|
GenericFormElementProps,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import type { FieldValues } from "react-hook-form";
|
import type { FieldValues } from "react-hook-form";
|
||||||
import { MultiSelect, MultiSelectItem } from "../UI/MultiSelect";
|
import { MultiSelect, MultiSelectItem } from "../UI/MultiSelect.tsx";
|
||||||
|
|
||||||
export interface MultiSelectFieldProps<T> extends BaseFormBuilderProps<T> {
|
export interface MultiSelectFieldProps<T> extends BaseFormBuilderProps<T> {
|
||||||
type: "multiSelect";
|
type: "multiSelect";
|
||||||
@@ -28,8 +28,8 @@ export function MultiSelectInput<T extends FieldValues>({
|
|||||||
// Make sure to filter out the UNSET value, as it shouldn't be shown in the UI
|
// Make sure to filter out the UNSET value, as it shouldn't be shown in the UI
|
||||||
const optionsEnumValues = enumValue
|
const optionsEnumValues = enumValue
|
||||||
? Object.entries(enumValue)
|
? Object.entries(enumValue)
|
||||||
.filter((value) => typeof value[1] === "number")
|
.filter((value) => typeof value[1] === "number")
|
||||||
.filter((value) => value[0] !== "UNSET")
|
.filter((value) => value[0] !== "UNSET")
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const formatName = (name: string) => {
|
const formatName = (name: string) => {
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import type {
|
|||||||
BaseFormBuilderProps,
|
BaseFormBuilderProps,
|
||||||
GenericFormElementProps,
|
GenericFormElementProps,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import type { ButtonVariant } from "@components/UI/Button";
|
import type { ButtonVariant } from "../UI/Button.tsx";
|
||||||
import { Generator } from "@components/UI/Generator.tsx";
|
import { Generator } from "@components/UI/Generator.tsx";
|
||||||
import { Eye, EyeOff } from "lucide-react";
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
import type { ChangeEventHandler, MouseEventHandler } from "react";
|
import type { ChangeEventHandler } from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, type FieldValues } from "react-hook-form";
|
import { Controller, type FieldValues } from "react-hook-form";
|
||||||
|
|
||||||
@@ -43,14 +43,12 @@ export function PasswordGenerator<T extends FieldValues>({
|
|||||||
<Generator
|
<Generator
|
||||||
type={field.hide && !passwordShown ? "password" : "text"}
|
type={field.hide && !passwordShown ? "password" : "text"}
|
||||||
id={field.id}
|
id={field.id}
|
||||||
action={
|
action={field.hide
|
||||||
field.hide
|
? {
|
||||||
? {
|
icon: passwordShown ? EyeOff : Eye,
|
||||||
icon: passwordShown ? EyeOff : Eye,
|
onClick: togglePasswordVisiblity,
|
||||||
onClick: togglePasswordVisiblity,
|
}
|
||||||
}
|
: undefined}
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
devicePSKBitCount={field.devicePSKBitCount}
|
devicePSKBitCount={field.devicePSKBitCount}
|
||||||
bits={field.bits}
|
bits={field.bits}
|
||||||
inputChange={field.inputChange}
|
inputChange={field.inputChange}
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ export function SelectInput<T extends FieldValues>({
|
|||||||
field.properties;
|
field.properties;
|
||||||
const optionsEnumValues = enumValue
|
const optionsEnumValues = enumValue
|
||||||
? Object.entries(enumValue).filter(
|
? Object.entries(enumValue).filter(
|
||||||
(value) => typeof value[1] === "number",
|
(value) => typeof value[1] === "number",
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
@@ -58,11 +58,13 @@ export function SelectInput<T extends FieldValues>({
|
|||||||
<SelectItem key={name} value={value.toString()}>
|
<SelectItem key={name} value={value.toString()}>
|
||||||
{formatEnumName
|
{formatEnumName
|
||||||
? name
|
? name
|
||||||
.replace(/_/g, " ")
|
.replace(/_/g, " ")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.map((s) => s.charAt(0).toUpperCase() + s.substring(1))
|
.map((s) =>
|
||||||
.join(" ")
|
s.charAt(0).toUpperCase() + s.substring(1)
|
||||||
|
)
|
||||||
|
.join(" ")
|
||||||
: name}
|
: name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useBackupReminder } from "@app/core/hooks/useKeyBackupReminder";
|
import { useBackupReminder } from "@core/hooks/useKeyBackupReminder.tsx";
|
||||||
import { useDevice } from "@app/core/stores/deviceStore";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
|
|
||||||
export const KeyBackupReminder = (): JSX.Element => {
|
export const KeyBackupReminder = () => {
|
||||||
const { setDialogOpen } = useDevice();
|
const { setDialogOpen } = useDevice();
|
||||||
|
|
||||||
useBackupReminder({
|
useBackupReminder({
|
||||||
@@ -15,5 +15,6 @@ export const KeyBackupReminder = (): JSX.Element => {
|
|||||||
sameSite: "strict",
|
sameSite: "strict",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// deno-lint-ignore jsx-no-useless-fragment
|
||||||
return <></>;
|
return <></>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import { Protobuf } from "@meshtastic/core";
|
|||||||
import { fromByteArray, toByteArray } from "base64-js";
|
import { fromByteArray, toByteArray } from "base64-js";
|
||||||
import cryptoRandomString from "crypto-random-string";
|
import cryptoRandomString from "crypto-random-string";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { PkiRegenerateDialog } from "../Dialog/PkiRegenerateDialog";
|
import { PkiRegenerateDialog } from "../Dialog/PkiRegenerateDialog.tsx";
|
||||||
|
|
||||||
export interface SettingsPanelProps {
|
export interface SettingsPanelProps {
|
||||||
channel: Protobuf.Channel.Channel;
|
channel: Protobuf.Channel.Channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
export const Channel = ({ channel }: SettingsPanelProps) => {
|
||||||
const { config, connection, addChannel } = useDevice();
|
const { config, connection, addChannel } = useDevice();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@@ -24,8 +24,9 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
channel?.settings?.psk.length ?? 16,
|
channel?.settings?.psk.length ?? 16,
|
||||||
);
|
);
|
||||||
const [validationText, setValidationText] = useState<string>();
|
const [validationText, setValidationText] = useState<string>();
|
||||||
const [preSharedDialogOpen, setPreSharedDialogOpen] =
|
const [preSharedDialogOpen, setPreSharedDialogOpen] = useState<boolean>(
|
||||||
useState<boolean>(false);
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
const onSubmit = (data: ChannelValidation) => {
|
const onSubmit = (data: ChannelValidation) => {
|
||||||
const channel = create(Protobuf.Channel.ChannelSchema, {
|
const channel = create(Protobuf.Channel.ChannelSchema, {
|
||||||
@@ -92,7 +93,7 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
<DynamicForm<ChannelValidation>
|
<DynamicForm<ChannelValidation>
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
submitType="onSubmit"
|
submitType="onSubmit"
|
||||||
hasSubmitButton={true}
|
hasSubmitButton
|
||||||
defaultValues={{
|
defaultValues={{
|
||||||
...channel,
|
...channel,
|
||||||
...{
|
...{
|
||||||
@@ -107,7 +108,7 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
channel?.settings?.moduleSettings?.positionPrecision === 32,
|
channel?.settings?.moduleSettings?.positionPrecision === 32,
|
||||||
positionPrecision:
|
positionPrecision:
|
||||||
channel?.settings?.moduleSettings?.positionPrecision ===
|
channel?.settings?.moduleSettings?.positionPrecision ===
|
||||||
undefined
|
undefined
|
||||||
? 10
|
? 10
|
||||||
: channel?.settings?.moduleSettings?.positionPrecision,
|
: channel?.settings?.moduleSettings?.positionPrecision,
|
||||||
},
|
},
|
||||||
@@ -126,10 +127,9 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
description:
|
description:
|
||||||
"Device telemetry is sent over PRIMARY. Only one PRIMARY allowed",
|
"Device telemetry is sent over PRIMARY. Only one PRIMARY allowed",
|
||||||
properties: {
|
properties: {
|
||||||
enumValue:
|
enumValue: channel.index === 0
|
||||||
channel.index === 0
|
? { PRIMARY: 1 }
|
||||||
? { PRIMARY: 1 }
|
: { DISABLED: 0, SECONDARY: 2 },
|
||||||
: { DISABLED: 0, SECONDARY: 2 },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -192,32 +192,31 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
description:
|
description:
|
||||||
"If not sharing precise location, position shared on channel will be accurate within this distance",
|
"If not sharing precise location, position shared on channel will be accurate within this distance",
|
||||||
properties: {
|
properties: {
|
||||||
enumValue:
|
enumValue: config.display?.units === 0
|
||||||
config.display?.units === 0
|
? {
|
||||||
? {
|
"Within 23 km": 10,
|
||||||
"Within 23 km": 10,
|
"Within 12 km": 11,
|
||||||
"Within 12 km": 11,
|
"Within 5.8 km": 12,
|
||||||
"Within 5.8 km": 12,
|
"Within 2.9 km": 13,
|
||||||
"Within 2.9 km": 13,
|
"Within 1.5 km": 14,
|
||||||
"Within 1.5 km": 14,
|
"Within 700 m": 15,
|
||||||
"Within 700 m": 15,
|
"Within 350 m": 16,
|
||||||
"Within 350 m": 16,
|
"Within 200 m": 17,
|
||||||
"Within 200 m": 17,
|
"Within 90 m": 18,
|
||||||
"Within 90 m": 18,
|
"Within 50 m": 19,
|
||||||
"Within 50 m": 19,
|
}
|
||||||
}
|
: {
|
||||||
: {
|
"Within 15 miles": 10,
|
||||||
"Within 15 miles": 10,
|
"Within 7.3 miles": 11,
|
||||||
"Within 7.3 miles": 11,
|
"Within 3.6 miles": 12,
|
||||||
"Within 3.6 miles": 12,
|
"Within 1.8 miles": 13,
|
||||||
"Within 1.8 miles": 13,
|
"Within 0.9 miles": 14,
|
||||||
"Within 0.9 miles": 14,
|
"Within 0.5 miles": 15,
|
||||||
"Within 0.5 miles": 15,
|
"Within 0.2 miles": 16,
|
||||||
"Within 0.2 miles": 16,
|
"Within 600 feet": 17,
|
||||||
"Within 600 feet": 17,
|
"Within 300 feet": 18,
|
||||||
"Within 300 feet": 18,
|
"Within 150 feet": 19,
|
||||||
"Within 150 feet": 19,
|
},
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useAppStore } from "@app/core/stores/appStore";
|
import { useAppStore } from "../../../core/stores/appStore.ts";
|
||||||
import type { BluetoothValidation } from "@app/validation/config/bluetooth.tsx";
|
import type { BluetoothValidation } from "@app/validation/config/bluetooth.tsx";
|
||||||
import { create } from "@bufbuild/protobuf";
|
import { create } from "@bufbuild/protobuf";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
@@ -111,9 +111,8 @@ export const Bluetooth = () => {
|
|||||||
disabledBy: [
|
disabledBy: [
|
||||||
{
|
{
|
||||||
fieldName: "mode",
|
fieldName: "mode",
|
||||||
selector:
|
selector: Protobuf.Config.Config_BluetoothConfig_PairingMode
|
||||||
Protobuf.Config.Config_BluetoothConfig_PairingMode
|
.FIXED_PIN,
|
||||||
.FIXED_PIN,
|
|
||||||
invert: true,
|
invert: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
|||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
|
|
||||||
export const Device = (): JSX.Element => {
|
export const Device = () => {
|
||||||
const { config, setWorkingConfig } = useDevice();
|
const { config, setWorkingConfig } = useDevice();
|
||||||
|
|
||||||
const onSubmit = (data: DeviceValidation) => {
|
const onSubmit = (data: DeviceValidation) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
|||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
|
|
||||||
export const Display = (): JSX.Element => {
|
export const Display = () => {
|
||||||
const { config, setWorkingConfig } = useDevice();
|
const { config, setWorkingConfig } = useDevice();
|
||||||
|
|
||||||
const onSubmit = (data: DisplayValidation) => {
|
const onSubmit = (data: DisplayValidation) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
|||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
|
|
||||||
export const LoRa = (): JSX.Element => {
|
export const LoRa = () => {
|
||||||
const { config, setWorkingConfig } = useDevice();
|
const { config, setWorkingConfig } = useDevice();
|
||||||
|
|
||||||
const onSubmit = (data: LoRaValidation) => {
|
const onSubmit = (data: LoRaValidation) => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "@core/utils/ip.ts";
|
} from "@core/utils/ip.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
|
|
||||||
export const Network = (): JSX.Element => {
|
export const Network = () => {
|
||||||
const { config, setWorkingConfig } = useDevice();
|
const { config, setWorkingConfig } = useDevice();
|
||||||
|
|
||||||
const onSubmit = (data: NetworkValidation) => {
|
const onSubmit = (data: NetworkValidation) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
type FlagName,
|
type FlagName,
|
||||||
usePositionFlags,
|
usePositionFlags,
|
||||||
} from "@app/core/hooks/usePositionFlags";
|
} from "../../../core/hooks/usePositionFlags.ts";
|
||||||
import type { PositionValidation } from "@app/validation/config/position.tsx";
|
import type { PositionValidation } from "@app/validation/config/position.tsx";
|
||||||
import { create } from "@bufbuild/protobuf";
|
import { create } from "@bufbuild/protobuf";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
|||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
|
|
||||||
export const Power = (): JSX.Element => {
|
export const Power = () => {
|
||||||
const { config, setWorkingConfig } = useDevice();
|
const { config, setWorkingConfig } = useDevice();
|
||||||
|
|
||||||
const onSubmit = (data: PowerValidation) => {
|
const onSubmit = (data: PowerValidation) => {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { PkiRegenerateDialog } from "@app/components/Dialog/PkiRegenerateDialog";
|
import { PkiRegenerateDialog } from "../../../Dialog/PkiRegenerateDialog.tsx";
|
||||||
import { DynamicForm } from "@app/components/Form/DynamicForm.tsx";
|
import { DynamicForm } from "@app/components/Form/DynamicForm.tsx";
|
||||||
import { useAppStore } from "@app/core/stores/appStore";
|
import { useAppStore } from "../../../../core/stores/appStore.ts";
|
||||||
import {
|
import {
|
||||||
getX25519PrivateKey,
|
getX25519PrivateKey,
|
||||||
getX25519PublicKey,
|
getX25519PublicKey,
|
||||||
} from "@app/core/utils/x25519";
|
} from "../../../../core/utils/x25519.ts";
|
||||||
import type { SecurityValidation } from "@app/validation/config/security.tsx";
|
import type { SecurityValidation } from "@app/validation/config/security.tsx";
|
||||||
import { create } from "@bufbuild/protobuf";
|
import { create } from "@bufbuild/protobuf";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
@@ -12,7 +12,7 @@ import { Protobuf } from "@meshtastic/core";
|
|||||||
import { fromByteArray, toByteArray } from "base64-js";
|
import { fromByteArray, toByteArray } from "base64-js";
|
||||||
import { Eye, EyeOff } from "lucide-react";
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
import { useReducer } from "react";
|
import { useReducer } from "react";
|
||||||
import { securityReducer } from "./securityReducer";
|
import { securityReducer } from "./securityReducer.tsx";
|
||||||
|
|
||||||
export const Security = () => {
|
export const Security = () => {
|
||||||
const { config, setWorkingConfig, setDialogOpen } = useDevice();
|
const { config, setWorkingConfig, setDialogOpen } = useDevice();
|
||||||
@@ -308,8 +308,7 @@ export const Security = () => {
|
|||||||
<PkiRegenerateDialog
|
<PkiRegenerateDialog
|
||||||
open={state.privateKeyDialogOpen}
|
open={state.privateKeyDialogOpen}
|
||||||
onOpenChange={() =>
|
onOpenChange={() =>
|
||||||
dispatch({ type: "SHOW_PRIVATE_KEY_DIALOG", payload: false })
|
dispatch({ type: "SHOW_PRIVATE_KEY_DIALOG", payload: false })}
|
||||||
}
|
|
||||||
onSubmit={pkiRegenerate}
|
onSubmit={pkiRegenerate}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { SecurityAction, SecurityState } from "./types";
|
import type { SecurityAction, SecurityState } from "./types.ts";
|
||||||
|
|
||||||
export function securityReducer(
|
export function securityReducer(
|
||||||
state: SecurityState,
|
state: SecurityState,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export type SecurityAction =
|
|||||||
| { type: "SET_ADMIN_KEY"; payload: string }
|
| { type: "SET_ADMIN_KEY"; payload: string }
|
||||||
| { type: "SHOW_PRIVATE_KEY_DIALOG"; payload: boolean }
|
| { type: "SHOW_PRIVATE_KEY_DIALOG"; payload: boolean }
|
||||||
| {
|
| {
|
||||||
type: "REGENERATE_PRIV_PUB_KEY";
|
type: "REGENERATE_PRIV_PUB_KEY";
|
||||||
payload: { privateKey: string; publicKey: string };
|
payload: { privateKey: string; publicKey: string };
|
||||||
}
|
}
|
||||||
| { type: "REGENERATE_ADMIN_KEY"; payload: { adminKey: string } };
|
| { type: "REGENERATE_ADMIN_KEY"; payload: { adminKey: string } };
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
|
import type { TabElementProps } from "../../Dialog/NewDeviceDialog.tsx";
|
||||||
import { Button } from "@components/UI/Button.tsx";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { Mono } from "@components/generic/Mono.tsx";
|
import { Mono } from "@components/generic/Mono.tsx";
|
||||||
import { useAppStore } from "@core/stores/appStore.ts";
|
import { useAppStore } from "@core/stores/appStore.ts";
|
||||||
@@ -8,7 +8,7 @@ import { randId } from "@core/utils/randId.ts";
|
|||||||
import { BleConnection, Constants } from "@meshtastic/js";
|
import { BleConnection, Constants } from "@meshtastic/js";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
export const BLE = ({ closeDialog }: TabElementProps): JSX.Element => {
|
export const BLE = ({ closeDialog }: TabElementProps) => {
|
||||||
const [bleDevices, setBleDevices] = useState<BluetoothDevice[]>([]);
|
const [bleDevices, setBleDevices] = useState<BluetoothDevice[]>([]);
|
||||||
const { addDevice } = useDeviceStore();
|
const { addDevice } = useDeviceStore();
|
||||||
const { setSelectedDevice } = useAppStore();
|
const { setSelectedDevice } = useAppStore();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
|
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog.tsx";
|
||||||
import { Button } from "@components/UI/Button.tsx";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { Input } from "@components/UI/Input.tsx";
|
import { Input } from "@components/UI/Input.tsx";
|
||||||
import { Label } from "@components/UI/Label.tsx";
|
import { Label } from "@components/UI/Label.tsx";
|
||||||
@@ -9,23 +9,23 @@ import { subscribeAll } from "@core/subscriptions.ts";
|
|||||||
import { randId } from "@core/utils/randId.ts";
|
import { randId } from "@core/utils/randId.ts";
|
||||||
import { MeshDevice } from "@meshtastic/core";
|
import { MeshDevice } from "@meshtastic/core";
|
||||||
import { TransportHTTP } from "@meshtastic/transport-http";
|
import { TransportHTTP } from "@meshtastic/transport-http";
|
||||||
import { type JSX, useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
export const HTTP = ({ closeDialog }: TabElementProps) => {
|
||||||
const [https, setHTTPS] = useState(false);
|
const [https, setHTTPS] = useState(false);
|
||||||
const { addDevice } = useDeviceStore();
|
const { addDevice } = useDeviceStore();
|
||||||
const { setSelectedDevice } = useAppStore();
|
const { setSelectedDevice } = useAppStore();
|
||||||
const { register, handleSubmit, control, watch } = useForm<{
|
const { register, handleSubmit, control } = useForm<{
|
||||||
ip: string;
|
ip: string;
|
||||||
tls: boolean;
|
tls: boolean;
|
||||||
}>({
|
}>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
ip: ["client.meshtastic.org", "localhost"].includes(
|
ip: ["client.meshtastic.org", "localhost"].includes(
|
||||||
window.location.hostname,
|
globalThis.location.hostname,
|
||||||
)
|
)
|
||||||
? "meshtastic.local"
|
? "meshtastic.local"
|
||||||
: window.location.host,
|
: globalThis.location.host,
|
||||||
tls: location.protocol === "https:",
|
tls: location.protocol === "https:",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -61,16 +61,15 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
|||||||
<Controller
|
<Controller
|
||||||
name="tls"
|
name="tls"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value, onChange, ...rest } }) => (
|
render={({ field: { ...rest } }) => (
|
||||||
<>
|
<>
|
||||||
<Label>Use HTTPS</Label>
|
<Label>Use HTTPS</Label>
|
||||||
<Switch
|
<Switch
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked: boolean) => {
|
||||||
checked ? setHTTPS(true) : setHTTPS(false);
|
checked ? setHTTPS(true) : setHTTPS(false);
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={location.protocol === "https:" ||
|
||||||
location.protocol === "https:" || connectionInProgress
|
connectionInProgress}
|
||||||
}
|
|
||||||
checked={https}
|
checked={https}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
|
import type { TabElementProps } from "../../Dialog/NewDeviceDialog.tsx";
|
||||||
import { Button } from "@components/UI/Button.tsx";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { Mono } from "@components/generic/Mono.tsx";
|
import { Mono } from "@components/generic/Mono.tsx";
|
||||||
import { useAppStore } from "@core/stores/appStore.ts";
|
import { useAppStore } from "@core/stores/appStore.ts";
|
||||||
@@ -9,7 +9,7 @@ import { MeshDevice } from "@meshtastic/core";
|
|||||||
import { TransportWebSerial } from "@meshtastic/transport-web-serial";
|
import { TransportWebSerial } from "@meshtastic/transport-web-serial";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
export const Serial = ({ closeDialog }: TabElementProps): JSX.Element => {
|
export const Serial = ({ closeDialog }: TabElementProps) => {
|
||||||
const [serialPorts, setSerialPorts] = useState<SerialPort[]>([]);
|
const [serialPorts, setSerialPorts] = useState<SerialPort[]>([]);
|
||||||
const { addDevice } = useDeviceStore();
|
const { addDevice } = useDeviceStore();
|
||||||
const { setSelectedDevice } = useAppStore();
|
const { setSelectedDevice } = useAppStore();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Separator } from "@app/components/UI/Seperator";
|
import { Separator } from "../../UI/Seperator.tsx";
|
||||||
import { H5 } from "@app/components/UI/Typography/H5.tsx";
|
import { H5 } from "@app/components/UI/Typography/H5.tsx";
|
||||||
import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
|
import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
|
||||||
import { formatQuantity } from "@app/core/utils/string";
|
import { formatQuantity } from "../../../core/utils/string.ts";
|
||||||
import { Avatar } from "@components/UI/Avatar";
|
import { Avatar } from "../../UI/Avatar.tsx";
|
||||||
import { Mono } from "@components/generic/Mono.tsx";
|
import { Mono } from "@components/generic/Mono.tsx";
|
||||||
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
|
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
@@ -37,21 +37,23 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
|
|||||||
<Avatar text={node.user?.shortName} />
|
<Avatar text={node.user?.shortName} />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{node.user?.publicKey && node.user?.publicKey.length > 0 ? (
|
{node.user?.publicKey && node.user?.publicKey.length > 0
|
||||||
<LockIcon
|
? (
|
||||||
className="text-green-600"
|
<LockIcon
|
||||||
size={12}
|
className="text-green-600"
|
||||||
strokeWidth={3}
|
size={12}
|
||||||
aria-label="Public Key Enabled"
|
strokeWidth={3}
|
||||||
/>
|
aria-label="Public Key Enabled"
|
||||||
) : (
|
/>
|
||||||
<LockOpenIcon
|
)
|
||||||
className="text-yellow-500"
|
: (
|
||||||
size={12}
|
<LockOpenIcon
|
||||||
strokeWidth={3}
|
className="text-yellow-500"
|
||||||
aria-label="No Public Key"
|
size={12}
|
||||||
/>
|
strokeWidth={3}
|
||||||
)}
|
aria-label="No Public Key"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Star
|
<Star
|
||||||
@@ -73,15 +75,13 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
|
|||||||
node.deviceMetrics?.voltage?.toPrecision(3) ?? "Unknown"
|
node.deviceMetrics?.voltage?.toPrecision(3) ?? "Unknown"
|
||||||
} volts`}
|
} volts`}
|
||||||
>
|
>
|
||||||
{node.deviceMetrics?.batteryLevel > 100 ? (
|
{node.deviceMetrics?.batteryLevel > 100
|
||||||
<BatteryChargingIcon size={22} />
|
? <BatteryChargingIcon size={22} />
|
||||||
) : node.deviceMetrics?.batteryLevel > 80 ? (
|
: node.deviceMetrics?.batteryLevel > 80
|
||||||
<BatteryFullIcon size={22} />
|
? <BatteryFullIcon size={22} />
|
||||||
) : node.deviceMetrics?.batteryLevel > 20 ? (
|
: node.deviceMetrics?.batteryLevel > 20
|
||||||
<BatteryMediumIcon size={22} />
|
? <BatteryMediumIcon size={22} />
|
||||||
) : (
|
: <BatteryLowIcon size={22} />}
|
||||||
<BatteryLowIcon size={22} />
|
|
||||||
)}
|
|
||||||
<Subtle aria-label="Battery">
|
<Subtle aria-label="Battery">
|
||||||
{node.deviceMetrics?.batteryLevel > 100
|
{node.deviceMetrics?.batteryLevel > 100
|
||||||
? "Charging"
|
? "Charging"
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { MessageInput } from "@components/PageComponents/Messages/MessageInput.t
|
|||||||
import type { Types } from "@meshtastic/core";
|
import type { Types } from "@meshtastic/core";
|
||||||
import { InboxIcon } from "lucide-react";
|
import { InboxIcon } from "lucide-react";
|
||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
import type { JSX } from "react";
|
|
||||||
|
|
||||||
export interface ChannelChatProps {
|
export interface ChannelChatProps {
|
||||||
messages?: MessageWithState[];
|
messages?: MessageWithState[];
|
||||||
@@ -26,7 +25,7 @@ export const ChannelChat = ({
|
|||||||
messages,
|
messages,
|
||||||
channel,
|
channel,
|
||||||
to,
|
to,
|
||||||
}: ChannelChatProps): JSX.Element => {
|
}: ChannelChatProps) => {
|
||||||
const { nodes } = useDevice();
|
const { nodes } = useDevice();
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -34,8 +33,7 @@ export const ChannelChat = ({
|
|||||||
const scrollToBottom = useCallback(() => {
|
const scrollToBottom = useCallback(() => {
|
||||||
const scrollContainer = scrollContainerRef.current;
|
const scrollContainer = scrollContainerRef.current;
|
||||||
if (scrollContainer) {
|
if (scrollContainer) {
|
||||||
const isNearBottom =
|
const isNearBottom = scrollContainer.scrollHeight -
|
||||||
scrollContainer.scrollHeight -
|
|
||||||
scrollContainer.scrollTop -
|
scrollContainer.scrollTop -
|
||||||
scrollContainer.clientHeight <
|
scrollContainer.clientHeight <
|
||||||
100;
|
100;
|
||||||
@@ -72,9 +70,8 @@ export const ChannelChat = ({
|
|||||||
key={message.id}
|
key={message.id}
|
||||||
message={message}
|
message={message}
|
||||||
sender={nodes.get(message.from)}
|
sender={nodes.get(message.from)}
|
||||||
lastMsgSameUser={
|
lastMsgSameUser={index > 0 &&
|
||||||
index > 0 && messages[index - 1].from === message.from
|
messages[index - 1].from === message.from}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -4,14 +4,13 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@app/components/UI/Tooltip";
|
} from "@components/UI/Tooltip.tsx";
|
||||||
import { useAppStore } from "@app/core/stores/appStore";
|
|
||||||
import {
|
import {
|
||||||
type MessageWithState,
|
type MessageWithState,
|
||||||
useDeviceStore,
|
useDeviceStore,
|
||||||
} from "@app/core/stores/deviceStore.ts";
|
} from "@app/core/stores/deviceStore.ts";
|
||||||
import { cn } from "@app/core/utils/cn";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
import { Avatar } from "@components/UI/Avatar";
|
import { Avatar } from "@components/UI/Avatar.tsx";
|
||||||
import type { Protobuf } from "@meshtastic/core";
|
import type { Protobuf } from "@meshtastic/core";
|
||||||
import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react";
|
import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
@@ -94,7 +93,6 @@ const StatusIcon = ({ state, className, ...otherProps }: StatusIconProps) => {
|
|||||||
const getMessageTextStyles = (state: MessageState) => {
|
const getMessageTextStyles = (state: MessageState) => {
|
||||||
const isAcknowledged = state === MESSAGE_STATES.ACK;
|
const isAcknowledged = state === MESSAGE_STATES.ACK;
|
||||||
const isFailed = state === MESSAGE_STATES.FAILED;
|
const isFailed = state === MESSAGE_STATES.FAILED;
|
||||||
const isWaiting = state === MESSAGE_STATES.WAITING;
|
|
||||||
|
|
||||||
return cn(
|
return cn(
|
||||||
"break-words overflow-hidden",
|
"break-words overflow-hidden",
|
||||||
@@ -145,16 +143,18 @@ export const Message = ({ lastMsgSameUser, message, sender }: MessageProps) => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
{!lastMsgSameUser ? (
|
{!lastMsgSameUser
|
||||||
<div className="flex place-items-center gap-2 mb-1">
|
? (
|
||||||
<Avatar text={messageUser?.shortName} />
|
<div className="flex place-items-center gap-2 mb-1">
|
||||||
<div className="flex flex-col">
|
<Avatar text={messageUser?.shortName} />
|
||||||
<span className="font-medium text-slate-900 dark:text-white truncate">
|
<div className="flex flex-col">
|
||||||
{messageUser?.longName}
|
<span className="font-medium text-slate-900 dark:text-white truncate">
|
||||||
</span>
|
{messageUser?.longName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
) : null}
|
: null}
|
||||||
</div>
|
</div>
|
||||||
<TimeDisplay date={message.rxTime} />
|
<TimeDisplay date={message.rxTime} />
|
||||||
<div className="flex place-items-center gap-2 pb-2">
|
<div className="flex place-items-center gap-2 pb-2">
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
import { debounce } from "@app/core/utils/debounce";
|
import { debounce } from "../../../core/utils/debounce.ts";
|
||||||
import { Button } from "@components/UI/Button.tsx";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { Input } from "@components/UI/Input.tsx";
|
import { Input } from "@components/UI/Input.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import type { Types } from "@meshtastic/core";
|
import type { Types } from "@meshtastic/core";
|
||||||
import { SendIcon } from "lucide-react";
|
import { SendIcon } from "lucide-react";
|
||||||
import {
|
import { startTransition, useCallback, useMemo, useState } from "react";
|
||||||
type JSX,
|
|
||||||
startTransition,
|
|
||||||
useCallback,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
|
|
||||||
export interface MessageInputProps {
|
export interface MessageInputProps {
|
||||||
to: Types.Destination;
|
to: Types.Destination;
|
||||||
@@ -22,7 +16,7 @@ export const MessageInput = ({
|
|||||||
to,
|
to,
|
||||||
channel,
|
channel,
|
||||||
maxBytes,
|
maxBytes,
|
||||||
}: MessageInputProps): JSX.Element => {
|
}: MessageInputProps) => {
|
||||||
const {
|
const {
|
||||||
connection,
|
connection,
|
||||||
setMessageState,
|
setMessageState,
|
||||||
@@ -43,7 +37,7 @@ export const MessageInput = ({
|
|||||||
async (message: string) => {
|
async (message: string) => {
|
||||||
await connection
|
await connection
|
||||||
?.sendText(message, to, true, channel)
|
?.sendText(message, to, true, channel)
|
||||||
.then((id) =>
|
.then((id: number) =>
|
||||||
setMessageState(
|
setMessageState(
|
||||||
to === "broadcast" ? "broadcast" : "direct",
|
to === "broadcast" ? "broadcast" : "direct",
|
||||||
channel,
|
channel,
|
||||||
@@ -51,7 +45,7 @@ export const MessageInput = ({
|
|||||||
myNodeNum,
|
myNodeNum,
|
||||||
id,
|
id,
|
||||||
"ack",
|
"ack",
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
.catch((e: Types.PacketError) =>
|
.catch((e: Types.PacketError) =>
|
||||||
setMessageState(
|
setMessageState(
|
||||||
@@ -61,7 +55,7 @@ export const MessageInput = ({
|
|||||||
myNodeNum,
|
myNodeNum,
|
||||||
e.id,
|
e.id,
|
||||||
e.error,
|
e.error,
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[channel, connection, myNodeNum, setMessageState, to],
|
[channel, connection, myNodeNum, setMessageState, to],
|
||||||
@@ -82,7 +76,7 @@ export const MessageInput = ({
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<form
|
<form
|
||||||
className="w-full"
|
className="w-full"
|
||||||
action={async (formData: FormData) => {
|
action={(formData: FormData) => {
|
||||||
// prevent user from sending blank/empty message
|
// prevent user from sending blank/empty message
|
||||||
if (localDraft === "") return;
|
if (localDraft === "") return;
|
||||||
const message = formData.get("messageInput") as string;
|
const message = formData.get("messageInput") as string;
|
||||||
@@ -97,7 +91,7 @@ export const MessageInput = ({
|
|||||||
<div className="flex grow gap-2">
|
<div className="flex grow gap-2">
|
||||||
<span className="w-full">
|
<span className="w-full">
|
||||||
<Input
|
<Input
|
||||||
autoFocus={true}
|
autoFocus
|
||||||
minLength={1}
|
minLength={1}
|
||||||
name="messageInput"
|
name="messageInput"
|
||||||
placeholder="Enter Message"
|
placeholder="Enter Message"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||||
import type { Protobuf } from "@meshtastic/core";
|
import type { Protobuf } from "@meshtastic/core";
|
||||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||||
import type { JSX } from "react";
|
|
||||||
|
|
||||||
export interface TraceRouteProps {
|
export interface TraceRouteProps {
|
||||||
from?: Protobuf.Mesh.NodeInfo;
|
from?: Protobuf.Mesh.NodeInfo;
|
||||||
@@ -19,7 +18,7 @@ export const TraceRoute = ({
|
|||||||
routeBack,
|
routeBack,
|
||||||
snrTowards,
|
snrTowards,
|
||||||
snrBack,
|
snrBack,
|
||||||
}: TraceRouteProps): JSX.Element => {
|
}: TraceRouteProps) => {
|
||||||
const { nodes } = useDevice();
|
const { nodes } = useDevice();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -38,23 +37,25 @@ export const TraceRoute = ({
|
|||||||
))}
|
))}
|
||||||
{from?.user?.longName}
|
{from?.user?.longName}
|
||||||
</span>
|
</span>
|
||||||
{routeBack ? (
|
{routeBack
|
||||||
<span className="ml-4 border-l-2 border-l-background-primary pl-2 text-text-primary">
|
? (
|
||||||
<p className="font-semibold">Route back:</p>
|
<span className="ml-4 border-l-2 border-l-background-primary pl-2 text-text-primary">
|
||||||
<p>{from?.user?.longName}</p>
|
<p className="font-semibold">Route back:</p>
|
||||||
<p>↓ {snrBack?.[0] ? snrBack[0] : "??"}dB</p>
|
<p>{from?.user?.longName}</p>
|
||||||
{routeBack.map((hop, i) => (
|
<p>↓ {snrBack?.[0] ? snrBack[0] : "??"}dB</p>
|
||||||
<span key={nodes.get(hop)?.num}>
|
{routeBack.map((hop, i) => (
|
||||||
<p>
|
<span key={nodes.get(hop)?.num}>
|
||||||
{nodes.get(hop)?.user?.longName ??
|
<p>
|
||||||
`!${numberToHexUnpadded(hop)}`}
|
{nodes.get(hop)?.user?.longName ??
|
||||||
</p>
|
`!${numberToHexUnpadded(hop)}`}
|
||||||
<p>↓ {snrBack?.[i + 1] ? snrBack[i + 1] : "??"}dB</p>
|
</p>
|
||||||
</span>
|
<p>↓ {snrBack?.[i + 1] ? snrBack[i + 1] : "??"}dB</p>
|
||||||
))}
|
</span>
|
||||||
{to?.user?.longName}
|
))}
|
||||||
</span>
|
{to?.user?.longName}
|
||||||
) : null}
|
</span>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { create } from "@bufbuild/protobuf";
|
|||||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
|
|
||||||
export const AmbientLighting = (): JSX.Element => {
|
export const AmbientLighting = () => {
|
||||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||||
|
|
||||||
const onSubmit = (data: AmbientLightingValidation) => {
|
const onSubmit = (data: AmbientLightingValidation) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
|||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
|
|
||||||
export const Audio = (): JSX.Element => {
|
export const Audio = () => {
|
||||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||||
|
|
||||||
const onSubmit = (data: AudioValidation) => {
|
const onSubmit = (data: AudioValidation) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
|||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
|
|
||||||
export const CannedMessage = (): JSX.Element => {
|
export const CannedMessage = () => {
|
||||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||||
|
|
||||||
const onSubmit = (data: CannedMessageValidation) => {
|
const onSubmit = (data: CannedMessageValidation) => {
|
||||||
@@ -63,9 +63,8 @@ export const CannedMessage = (): JSX.Element => {
|
|||||||
label: "Clockwise event",
|
label: "Clockwise event",
|
||||||
description: "Select input event.",
|
description: "Select input event.",
|
||||||
properties: {
|
properties: {
|
||||||
enumValue:
|
enumValue: Protobuf.ModuleConfig
|
||||||
Protobuf.ModuleConfig
|
.ModuleConfig_CannedMessageConfig_InputEventChar,
|
||||||
.ModuleConfig_CannedMessageConfig_InputEventChar,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -74,9 +73,8 @@ export const CannedMessage = (): JSX.Element => {
|
|||||||
label: "Counter Clockwise event",
|
label: "Counter Clockwise event",
|
||||||
description: "Select input event.",
|
description: "Select input event.",
|
||||||
properties: {
|
properties: {
|
||||||
enumValue:
|
enumValue: Protobuf.ModuleConfig
|
||||||
Protobuf.ModuleConfig
|
.ModuleConfig_CannedMessageConfig_InputEventChar,
|
||||||
.ModuleConfig_CannedMessageConfig_InputEventChar,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -85,9 +83,8 @@ export const CannedMessage = (): JSX.Element => {
|
|||||||
label: "Press event",
|
label: "Press event",
|
||||||
description: "Select input event",
|
description: "Select input event",
|
||||||
properties: {
|
properties: {
|
||||||
enumValue:
|
enumValue: Protobuf.ModuleConfig
|
||||||
Protobuf.ModuleConfig
|
.ModuleConfig_CannedMessageConfig_InputEventChar,
|
||||||
.ModuleConfig_CannedMessageConfig_InputEventChar,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { create } from "@bufbuild/protobuf";
|
|||||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
|
|
||||||
export const DetectionSensor = (): JSX.Element => {
|
export const DetectionSensor = () => {
|
||||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||||
|
|
||||||
const onSubmit = (data: DetectionSensorValidation) => {
|
const onSubmit = (data: DetectionSensorValidation) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
|||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
|
|
||||||
export const ExternalNotification = (): JSX.Element => {
|
export const ExternalNotification = () => {
|
||||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||||
|
|
||||||
const onSubmit = (data: ExternalNotificationValidation) => {
|
const onSubmit = (data: ExternalNotificationValidation) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { create } from "@bufbuild/protobuf";
|
|||||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
|
|
||||||
export const MQTT = (): JSX.Element => {
|
export const MQTT = () => {
|
||||||
const { config, moduleConfig, setWorkingModuleConfig } = useDevice();
|
const { config, moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||||
|
|
||||||
const onSubmit = (data: MqttValidation) => {
|
const onSubmit = (data: MqttValidation) => {
|
||||||
@@ -165,32 +165,31 @@ export const MQTT = (): JSX.Element => {
|
|||||||
description:
|
description:
|
||||||
"Position shared will be accurate within this distance",
|
"Position shared will be accurate within this distance",
|
||||||
properties: {
|
properties: {
|
||||||
enumValue:
|
enumValue: config.display?.units === 0
|
||||||
config.display?.units === 0
|
? {
|
||||||
? {
|
"Within 23 km": 10,
|
||||||
"Within 23 km": 10,
|
"Within 12 km": 11,
|
||||||
"Within 12 km": 11,
|
"Within 5.8 km": 12,
|
||||||
"Within 5.8 km": 12,
|
"Within 2.9 km": 13,
|
||||||
"Within 2.9 km": 13,
|
"Within 1.5 km": 14,
|
||||||
"Within 1.5 km": 14,
|
"Within 700 m": 15,
|
||||||
"Within 700 m": 15,
|
"Within 350 m": 16,
|
||||||
"Within 350 m": 16,
|
"Within 200 m": 17,
|
||||||
"Within 200 m": 17,
|
"Within 90 m": 18,
|
||||||
"Within 90 m": 18,
|
"Within 50 m": 19,
|
||||||
"Within 50 m": 19,
|
}
|
||||||
}
|
: {
|
||||||
: {
|
"Within 15 miles": 10,
|
||||||
"Within 15 miles": 10,
|
"Within 7.3 miles": 11,
|
||||||
"Within 7.3 miles": 11,
|
"Within 3.6 miles": 12,
|
||||||
"Within 3.6 miles": 12,
|
"Within 1.8 miles": 13,
|
||||||
"Within 1.8 miles": 13,
|
"Within 0.9 miles": 14,
|
||||||
"Within 0.9 miles": 14,
|
"Within 0.5 miles": 15,
|
||||||
"Within 0.5 miles": 15,
|
"Within 0.2 miles": 16,
|
||||||
"Within 0.2 miles": 16,
|
"Within 600 feet": 17,
|
||||||
"Within 600 feet": 17,
|
"Within 300 feet": 18,
|
||||||
"Within 300 feet": 18,
|
"Within 150 feet": 19,
|
||||||
"Within 150 feet": 19,
|
},
|
||||||
},
|
|
||||||
},
|
},
|
||||||
disabledBy: [
|
disabledBy: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { create } from "@bufbuild/protobuf";
|
|||||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
|
|
||||||
export const NeighborInfo = (): JSX.Element => {
|
export const NeighborInfo = () => {
|
||||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||||
|
|
||||||
const onSubmit = (data: NeighborInfoValidation) => {
|
const onSubmit = (data: NeighborInfoValidation) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
|||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
|
|
||||||
export const Paxcounter = (): JSX.Element => {
|
export const Paxcounter = () => {
|
||||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||||
|
|
||||||
const onSubmit = (data: PaxcounterValidation) => {
|
const onSubmit = (data: PaxcounterValidation) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
|||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
|
|
||||||
export const RangeTest = (): JSX.Element => {
|
export const RangeTest = () => {
|
||||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||||
|
|
||||||
const onSubmit = (data: RangeTestValidation) => {
|
const onSubmit = (data: RangeTestValidation) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
|||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
|
|
||||||
export const Serial = (): JSX.Element => {
|
export const Serial = () => {
|
||||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||||
|
|
||||||
const onSubmit = (data: SerialValidation) => {
|
const onSubmit = (data: SerialValidation) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
|||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
|
|
||||||
export const StoreForward = (): JSX.Element => {
|
export const StoreForward = () => {
|
||||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||||
|
|
||||||
const onSubmit = (data: StoreForwardValidation) => {
|
const onSubmit = (data: StoreForwardValidation) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
|||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
|
|
||||||
export const Telemetry = (): JSX.Element => {
|
export const Telemetry = () => {
|
||||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||||
|
|
||||||
const onSubmit = (data: TelemetryValidation) => {
|
const onSubmit = (data: TelemetryValidation) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { cn } from "@app/core/utils/cn.ts";
|
import { cn } from "@app/core/utils/cn.ts";
|
||||||
import { AlignLeftIcon, type LucideIcon } from "lucide-react";
|
import { AlignLeftIcon, type LucideIcon } from "lucide-react";
|
||||||
import Footer from "./UI/Footer";
|
import Footer from "./UI/Footer.tsx";
|
||||||
import { Spinner } from "./UI/Spinner";
|
import { Spinner } from "./UI/Spinner.tsx";
|
||||||
|
|
||||||
export interface PageLayoutProps {
|
export interface PageLayoutProps {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -44,9 +44,7 @@ export const PageLayout = ({
|
|||||||
className="transition-all hover:text-accent"
|
className="transition-all hover:text-accent"
|
||||||
onClick={action.onClick}
|
onClick={action.onClick}
|
||||||
>
|
>
|
||||||
{action?.isLoading ? (
|
{action?.isLoading ? <Spinner /> : (
|
||||||
<Spinner />
|
|
||||||
) : (
|
|
||||||
<action.icon
|
<action.icon
|
||||||
className={action.iconClasses}
|
className={action.iconClasses}
|
||||||
aria-disabled={action.disabled}
|
aria-disabled={action.disabled}
|
||||||
|
|||||||
+65
-63
@@ -23,7 +23,7 @@ export interface SidebarProps {
|
|||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
|
export const Sidebar = ({ children }: SidebarProps) => {
|
||||||
const { hardware, nodes, metadata } = useDevice();
|
const { hardware, nodes, metadata } = useDevice();
|
||||||
const myNode = nodes.get(hardware.myNodeNum);
|
const myNode = nodes.get(hardware.myNodeNum);
|
||||||
const myMetadata = metadata.get(0);
|
const myMetadata = metadata.get(0);
|
||||||
@@ -64,69 +64,71 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return showSidebar ? (
|
return showSidebar
|
||||||
<div className="min-w-[280px] max-w-min flex-col overflow-y-auto border-r-[0.5px] bg-background-primary border-slate-300 dark:border-slate-700">
|
? (
|
||||||
<div className="flex justify-between px-8 pt-6">
|
<div className="min-w-[280px] max-w-min flex-col overflow-y-auto border-r-[0.5px] bg-background-primary border-slate-300 dark:border-slate-700">
|
||||||
<div>
|
<div className="flex justify-between px-8 pt-6">
|
||||||
<span className="text-lg font-medium">
|
<div>
|
||||||
{myNode?.user?.shortName ?? "UNK"}
|
<span className="text-lg font-medium">
|
||||||
</span>
|
{myNode?.user?.shortName ?? "UNK"}
|
||||||
<Subtle>{myNode?.user?.longName ?? "UNK"}</Subtle>
|
</span>
|
||||||
|
<Subtle>{myNode?.user?.longName ?? "UNK"}</Subtle>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="transition-all hover:text-accent"
|
||||||
|
onClick={() => setDialogOpen("deviceName", true)}
|
||||||
|
>
|
||||||
|
<EditIcon size={16} />
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => setShowSidebar(false)}>
|
||||||
|
<SidebarCloseIcon size={24} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div className="px-8 pb-6">
|
||||||
type="button"
|
<div className="flex items-center">
|
||||||
className="transition-all hover:text-accent"
|
<BatteryMediumIcon size={24} viewBox="0 0 28 24" />
|
||||||
onClick={() => setDialogOpen("deviceName", true)}
|
<Subtle>
|
||||||
>
|
{myNode?.deviceMetrics?.batteryLevel
|
||||||
<EditIcon size={16} />
|
? myNode?.deviceMetrics?.batteryLevel > 100
|
||||||
</button>
|
? "Charging"
|
||||||
<button type="button" onClick={() => setShowSidebar(false)}>
|
: `${myNode?.deviceMetrics?.batteryLevel}%`
|
||||||
<SidebarCloseIcon size={24} />
|
: "UNK"}
|
||||||
</button>
|
</Subtle>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-8 pb-6">
|
<div className="flex items-center">
|
||||||
<div className="flex items-center">
|
<ZapIcon size={24} viewBox="0 0 36 24" />
|
||||||
<BatteryMediumIcon size={24} viewBox={"0 0 28 24"} />
|
<Subtle>
|
||||||
<Subtle>
|
{myNode?.deviceMetrics?.voltage?.toPrecision(3) ?? "UNK"} volts
|
||||||
{myNode?.deviceMetrics?.batteryLevel
|
</Subtle>
|
||||||
? myNode?.deviceMetrics?.batteryLevel > 100
|
</div>
|
||||||
? "Charging"
|
<div className="flex items-center">
|
||||||
: `${myNode?.deviceMetrics?.batteryLevel}%`
|
<CpuIcon size={24} viewBox="0 0 36 24" />
|
||||||
: "UNK"}
|
<Subtle>v{myMetadata?.firmwareVersion ?? "UNK"}</Subtle>
|
||||||
</Subtle>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
|
||||||
<ZapIcon size={24} viewBox={"0 0 36 24"} />
|
|
||||||
<Subtle>
|
|
||||||
{myNode?.deviceMetrics?.voltage?.toPrecision(3) ?? "UNK"} volts
|
|
||||||
</Subtle>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<CpuIcon size={24} viewBox={"0 0 36 24"} />
|
|
||||||
<Subtle>v{myMetadata?.firmwareVersion ?? "UNK"}</Subtle>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SidebarSection label="Navigation">
|
<SidebarSection label="Navigation">
|
||||||
{pages.map((link) => (
|
{pages.map((link) => (
|
||||||
<SidebarButton
|
<SidebarButton
|
||||||
key={link.name}
|
key={link.name}
|
||||||
label={link.name}
|
label={link.name}
|
||||||
Icon={link.icon}
|
Icon={link.icon}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActivePage(link.page);
|
setActivePage(link.page);
|
||||||
}}
|
}}
|
||||||
active={link.page === activePage}
|
active={link.page === activePage}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
)
|
||||||
<div className="px-1 pt-8 border-r-[0.5px] border-slate-700">
|
: (
|
||||||
<button type="button" onClick={() => setShowSidebar(true)}>
|
<div className="px-1 pt-8 border-r-[0.5px] border-slate-700">
|
||||||
<SidebarOpenIcon size={24} />
|
<button type="button" onClick={() => setShowSidebar(true)}>
|
||||||
</button>
|
<SidebarOpenIcon size={24} />
|
||||||
</div>
|
</button>
|
||||||
);
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useTheme } from "@app/core/hooks/useTheme";
|
import { useTheme } from "../core/hooks/useTheme.ts";
|
||||||
import { cn } from "@app/core/utils/cn";
|
import { cn } from "../core/utils/cn.ts";
|
||||||
import { Monitor, Moon, Sun } from "lucide-react";
|
import { Monitor, Moon, Sun } from "lucide-react";
|
||||||
|
|
||||||
type ThemePreference = "light" | "dark" | "system";
|
type ThemePreference = "light" | "dark" | "system";
|
||||||
@@ -32,11 +32,9 @@ export default function ThemeSwitcher({
|
|||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
onClick={toggleTheme}
|
onClick={toggleTheme}
|
||||||
aria-label={
|
aria-label={preference === "system"
|
||||||
preference === "system"
|
? `System theme (currently ${theme}). Click to change theme.`
|
||||||
? `System theme (currently ${theme}). Click to change theme.`
|
: `Current theme: ${theme}. Click to change theme.`}
|
||||||
: `Current theme: ${theme}. Click to change theme.`
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{themeIcons[preference]}
|
{themeIcons[preference]}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import {
|
|||||||
ToastProvider,
|
ToastProvider,
|
||||||
ToastTitle,
|
ToastTitle,
|
||||||
ToastViewport,
|
ToastViewport,
|
||||||
} from "@components/UI/Toast";
|
} from "./UI/Toast.tsx";
|
||||||
import { useToast } from "@core/hooks/useToast";
|
import { useToast } from "../core/hooks/useToast.ts";
|
||||||
|
|
||||||
export function Toaster() {
|
export function Toaster() {
|
||||||
const { toasts } = useToast();
|
const { toasts } = useToast();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { cn } from "@app/core/utils/cn";
|
import { cn } from "../../core/utils/cn.ts";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
type RGBColor = {
|
type RGBColor = {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type VariantProps, cva } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@core/utils/cn.ts";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
@@ -20,7 +20,8 @@ const buttonVariants = cva(
|
|||||||
"bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-500 dark:text-white dark:hover:bg-slate-400",
|
"bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-500 dark:text-white dark:hover:bg-slate-400",
|
||||||
ghost:
|
ghost:
|
||||||
"bg-transparent hover:bg-slate-100 dark:hover:bg-slate-800 dark:text-slate-100 dark:hover:text-slate-100 data-[state=open]:bg-transparent dark:data-[state=open]:bg-transparent",
|
"bg-transparent hover:bg-slate-100 dark:hover:bg-slate-800 dark:text-slate-100 dark:hover:text-slate-100 data-[state=open]:bg-transparent dark:data-[state=open]:bg-transparent",
|
||||||
link: "bg-transparent underline-offset-4 hover:underline text-slate-900 dark:text-slate-100 hover:bg-transparent dark:hover:bg-transparent",
|
link:
|
||||||
|
"bg-transparent underline-offset-4 hover:underline text-slate-900 dark:text-slate-100 hover:bg-transparent dark:hover:bg-transparent",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-10 py-2 px-4",
|
default: "h-10 py-2 px-4",
|
||||||
@@ -38,7 +39,8 @@ const buttonVariants = cva(
|
|||||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
extends
|
||||||
|
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
VariantProps<typeof buttonVariants> {}
|
VariantProps<typeof buttonVariants> {}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
|||||||
@@ -144,11 +144,11 @@ CommandShortcut.displayName = "CommandShortcut";
|
|||||||
export {
|
export {
|
||||||
Command,
|
Command,
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
CommandInput,
|
|
||||||
CommandList,
|
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandShortcut,
|
CommandList,
|
||||||
CommandSeparator,
|
CommandSeparator,
|
||||||
|
CommandShortcut,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ DialogPortal.displayName = DialogPrimitive.Portal.displayName;
|
|||||||
const DialogOverlay = React.forwardRef<
|
const DialogOverlay = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 bg-black/50 backdrop-blur-xs transition-opacity animate-in fade-in",
|
"fixed inset-0 z-50 bg-black/50 backdrop-blur-xs transition-opacity animate-in fade-in",
|
||||||
@@ -113,10 +113,10 @@ DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTrigger,
|
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
|
||||||
DialogFooter,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -184,18 +184,18 @@ DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuRadioGroup,
|
DropdownMenuTrigger,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { cn } from "@app/core/utils/cn";
|
import { cn } from "../../core/utils/cn.ts";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export interface FooterProps extends React.HTMLAttributes<HTMLElement> {}
|
export interface FooterProps extends React.HTMLAttributes<HTMLElement> {}
|
||||||
|
|
||||||
const Footer = React.forwardRef<HTMLElement, FooterProps>(
|
const Footer = React.forwardRef<HTMLElement, FooterProps>(
|
||||||
({ className, ...props }, ref) => {
|
({ className, ...props }) => {
|
||||||
return (
|
return (
|
||||||
<footer className={cn("flex mt-auto justify-center p-2", className)}>
|
<footer
|
||||||
|
className={cn("flex mt-auto justify-center p-2", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href="https://vercel.com/?utm_source=meshtastic&utm_campaign=oss"
|
href="https://vercel.com/?utm_source=meshtastic&utm_campaign=oss"
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
|
|||||||
disabled,
|
disabled,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref,
|
|
||||||
) => {
|
) => {
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@core/utils/cn.ts";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
import { type VariantProps, cva } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
const inputVariants = cva(
|
const inputVariants = cva(
|
||||||
@@ -20,7 +20,8 @@ const inputVariants = cva(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export interface InputProps
|
export interface InputProps
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement>,
|
extends
|
||||||
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
VariantProps<typeof inputVariants> {
|
VariantProps<typeof inputVariants> {
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
suffix?: string;
|
suffix?: string;
|
||||||
|
|||||||
@@ -216,19 +216,19 @@ MenubarShortcut.displayname = "MenubarShortcut";
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
Menubar,
|
Menubar,
|
||||||
MenubarMenu,
|
|
||||||
MenubarTrigger,
|
|
||||||
MenubarContent,
|
|
||||||
MenubarItem,
|
|
||||||
MenubarSeparator,
|
|
||||||
MenubarLabel,
|
|
||||||
MenubarCheckboxItem,
|
MenubarCheckboxItem,
|
||||||
|
MenubarContent,
|
||||||
|
MenubarGroup,
|
||||||
|
MenubarItem,
|
||||||
|
MenubarLabel,
|
||||||
|
MenubarMenu,
|
||||||
|
MenubarPortal,
|
||||||
MenubarRadioGroup,
|
MenubarRadioGroup,
|
||||||
MenubarRadioItem,
|
MenubarRadioItem,
|
||||||
MenubarPortal,
|
MenubarSeparator,
|
||||||
|
MenubarShortcut,
|
||||||
|
MenubarSub,
|
||||||
MenubarSubContent,
|
MenubarSubContent,
|
||||||
MenubarSubTrigger,
|
MenubarSubTrigger,
|
||||||
MenubarGroup,
|
MenubarTrigger,
|
||||||
MenubarSub,
|
|
||||||
MenubarShortcut,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
export interface ModalProps {
|
|
||||||
title: string;
|
|
||||||
actions?: JSX.Element[];
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Modal = ({
|
|
||||||
title,
|
|
||||||
actions,
|
|
||||||
children,
|
|
||||||
}: ModalProps): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<div className="rounded-md overflow-hidden w-full">
|
|
||||||
<div className="flex h-12 px-3 bg-slate-200 dark:bg-slate-700 justify-between">
|
|
||||||
<h2 className="my-auto font-semibold text-lg">{title}</h2>
|
|
||||||
{actions && (
|
|
||||||
<div className="my-auto">{actions.map((action) => action)}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="h-full border border-slate-200 dark:border-slate-700">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { cn } from "@app/core/utils/cn";
|
import { cn } from "../../core/utils/cn.ts";
|
||||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||||
import { Check } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
|
|
||||||
@@ -8,9 +8,8 @@ interface MultiSelectProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MultiSelect = ({ children, className = "" }: MultiSelectProps) => {
|
const MultiSelect = ({ children, className = "" }: MultiSelectProps) => {
|
||||||
return (
|
return <div className={cn("flex flex-wrap gap-2", className)}>{children}
|
||||||
<div className={cn("flex flex-wrap gap-2", className)}>{children}</div>
|
</div>;
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface MultiSelectItemProps {
|
interface MultiSelectItemProps {
|
||||||
|
|||||||
@@ -26,4 +26,4 @@ const PopoverContent = React.forwardRef<
|
|||||||
));
|
));
|
||||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent };
|
export { Popover, PopoverContent, PopoverTrigger };
|
||||||
|
|||||||
@@ -101,11 +101,11 @@ SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
Select,
|
Select,
|
||||||
SelectGroup,
|
|
||||||
SelectValue,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectLabel,
|
SelectGroup,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
SelectSeparator,
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { Button } from "@components/UI/Button.tsx";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import type { JSX } from "react";
|
|
||||||
|
|
||||||
export interface SidebarButtonProps {
|
export interface SidebarButtonProps {
|
||||||
label: string;
|
label: string;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
Icon?: LucideIcon;
|
Icon?: LucideIcon;
|
||||||
element?: JSX.Element;
|
element?;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,7 +15,7 @@ export const SidebarButton = ({
|
|||||||
Icon,
|
Icon,
|
||||||
element,
|
element,
|
||||||
onClick,
|
onClick,
|
||||||
}: SidebarButtonProps): JSX.Element => (
|
}: SidebarButtonProps) => (
|
||||||
<Button
|
<Button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
variant={active ? "subtle" : "ghost"}
|
variant={active ? "subtle" : "ghost"}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { cn } from "@app/core/utils/cn";
|
import { cn } from "../../core/utils/cn.ts";
|
||||||
|
|
||||||
interface SpinnerProps extends React.HTMLAttributes<HTMLDivElement> {
|
interface SpinnerProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
size?: "sm" | "md" | "lg";
|
size?: "sm" | "md" | "lg";
|
||||||
|
|||||||
@@ -50,4 +50,4 @@ const TabsContent = React.forwardRef<
|
|||||||
));
|
));
|
||||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
export { Tabs, TabsContent, TabsList, TabsTrigger };
|
||||||
|
|||||||
+11
-11
@@ -1,9 +1,9 @@
|
|||||||
import * as ToastPrimitives from "@radix-ui/react-toast";
|
import * as ToastPrimitives from "@radix-ui/react-toast";
|
||||||
import { type VariantProps, cva } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@core/utils/cn";
|
import { cn } from "../../core/utils/cn.ts";
|
||||||
|
|
||||||
const ToastProvider = ToastPrimitives.Provider;
|
const ToastProvider = ToastPrimitives.Provider;
|
||||||
|
|
||||||
@@ -41,8 +41,8 @@ const toastVariants = cva(
|
|||||||
|
|
||||||
const Toast = React.forwardRef<
|
const Toast = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
& React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root>
|
||||||
VariantProps<typeof toastVariants>
|
& VariantProps<typeof toastVariants>
|
||||||
>(({ className, variant, ...props }, ref) => {
|
>(({ className, variant, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<ToastPrimitives.Root
|
<ToastPrimitives.Root
|
||||||
@@ -116,13 +116,13 @@ type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
|||||||
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type ToastProps,
|
|
||||||
type ToastActionElement,
|
|
||||||
ToastProvider,
|
|
||||||
ToastViewport,
|
|
||||||
Toast,
|
Toast,
|
||||||
ToastTitle,
|
|
||||||
ToastDescription,
|
|
||||||
ToastClose,
|
|
||||||
ToastAction,
|
ToastAction,
|
||||||
|
type ToastActionElement,
|
||||||
|
ToastClose,
|
||||||
|
ToastDescription,
|
||||||
|
type ToastProps,
|
||||||
|
ToastProvider,
|
||||||
|
ToastTitle,
|
||||||
|
ToastViewport,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipTrigger,
|
TooltipArrow,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipArrow,
|
TooltipTrigger,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export interface BlockquoteProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BlockQuote = ({ children }: BlockquoteProps): JSX.Element => (
|
export const BlockQuote = ({ children }: BlockquoteProps) => (
|
||||||
<blockquote className="mt-6 border-l-2 border-slate-300 pl-6 italic text-slate-800 dark:border-slate-600 dark:text-slate-200">
|
<blockquote className="mt-6 border-l-2 border-slate-300 pl-6 italic text-slate-800 dark:border-slate-600 dark:text-slate-200">
|
||||||
{children}
|
{children}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export interface CodeProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Code = ({ children }: CodeProps): JSX.Element => (
|
export const Code = ({ children }: CodeProps) => (
|
||||||
<code className="relative rounded-sm bg-slate-100 px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold text-slate-900 dark:bg-slate-800 dark:text-slate-400">
|
<code className="relative rounded-sm bg-slate-100 px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold text-slate-900 dark:bg-slate-800 dark:text-slate-400">
|
||||||
{children}
|
{children}
|
||||||
</code>
|
</code>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export interface H1Props {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const H1 = ({ children }: H1Props): JSX.Element => (
|
export const H1 = ({ children }: H1Props) => (
|
||||||
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
|
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
|
||||||
{children}
|
{children}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export interface H2Props {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const H2 = ({ children }: H2Props): JSX.Element => (
|
export const H2 = ({ children }: H2Props) => (
|
||||||
<h2 className="scroll-m-20 border-b border-b-slate-200 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0 dark:border-b-slate-700">
|
<h2 className="scroll-m-20 border-b border-b-slate-200 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0 dark:border-b-slate-700">
|
||||||
{children}
|
{children}
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export interface H3Props {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const H3 = ({ children }: H3Props): JSX.Element => (
|
export const H3 = ({ children }: H3Props) => (
|
||||||
<h3 className="scroll-m-20 text-2xl font-semibold tracking-tight">
|
<h3 className="scroll-m-20 text-2xl font-semibold tracking-tight">
|
||||||
{children}
|
{children}
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export interface H4Props {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const H4 = ({ className, children }: H4Props): JSX.Element => (
|
export const H4 = ({ className, children }: H4Props) => (
|
||||||
<h4
|
<h4
|
||||||
className={cn(
|
className={cn(
|
||||||
"scroll-m-20 text-xl font-semibold tracking-tight",
|
"scroll-m-20 text-xl font-semibold tracking-tight",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export interface H5Props {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const H5 = ({ className, children }: H5Props): JSX.Element => (
|
export const H5 = ({ className, children }: H5Props) => (
|
||||||
<h5
|
<h5
|
||||||
className={cn("scroll-m-20 text-lg font-medium tracking-tight", className)}
|
className={cn("scroll-m-20 text-lg font-medium tracking-tight", className)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { cn } from "@app/core/utils/cn";
|
import { cn } from "../../../core/utils/cn.ts";
|
||||||
|
|
||||||
export interface LinkProps {
|
export interface LinkProps {
|
||||||
href: string;
|
href: string;
|
||||||
@@ -6,10 +6,10 @@ export interface LinkProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Link = ({ href, children, className }: LinkProps): JSX.Element => (
|
export const Link = ({ href, children, className }: LinkProps) => (
|
||||||
<a
|
<a
|
||||||
href={href}
|
href={href}
|
||||||
target={"_blank"}
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className={cn(
|
className={cn(
|
||||||
"font-medium text-slate-900 underline underline-offset-4 dark:text-slate-50",
|
"font-medium text-slate-900 underline underline-offset-4 dark:text-slate-50",
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ export interface PProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const P = ({ children }: PProps): JSX.Element => (
|
export const P = ({ children }: PProps) => (
|
||||||
<p className="leading-7 not-first:mt-6">{children}</p>
|
<p className="leading-7 not-first:mt-6">{children}</p>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export interface SubtleProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Subtle = ({ className, children }: SubtleProps): JSX.Element => (
|
export const Subtle = ({ className, children }: SubtleProps) => (
|
||||||
<p className={cn("text-sm text-slate-500 dark:text-slate-400", className)}>
|
<p className={cn("text-sm text-slate-500 dark:text-slate-400", className)}>
|
||||||
{children}
|
{children}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export const Blur = (): JSX.Element => {
|
export const Blur = () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 backdrop-blur-md backdrop-brightness-press"
|
className="fixed inset-0 backdrop-blur-md backdrop-brightness-press"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export const Mono = ({
|
|||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...rest
|
...rest
|
||||||
}: JSX.IntrinsicElements["span"]): JSX.Element => {
|
}: JSX.IntrinsicElements["span"]) => {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`font-mono text-sm text-text-secondary ${className ?? ""}`}
|
className={`font-mono text-sm text-text-secondary ${className ?? ""}`}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export interface TableProps {
|
export interface TableProps {
|
||||||
headings: Heading[];
|
headings: Heading[];
|
||||||
rows: JSX.Element[][];
|
rows: [][];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Heading {
|
export interface Heading {
|
||||||
@@ -12,7 +12,7 @@ export interface Heading {
|
|||||||
sortable: boolean;
|
sortable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Table = ({ headings, rows }: TableProps): JSX.Element => {
|
export const Table = ({ headings, rows }: TableProps) => {
|
||||||
const [sortColumn, setSortColumn] = useState<string | null>("Last Heard");
|
const [sortColumn, setSortColumn] = useState<string | null>("Last Heard");
|
||||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
||||||
|
|
||||||
@@ -75,11 +75,9 @@ export const Table = ({ headings, rows }: TableProps): JSX.Element => {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{heading.title}
|
{heading.title}
|
||||||
{sortColumn === heading.title &&
|
{sortColumn === heading.title &&
|
||||||
(sortOrder === "asc" ? (
|
(sortOrder === "asc"
|
||||||
<ChevronUpIcon size={16} />
|
? <ChevronUpIcon size={16} />
|
||||||
) : (
|
: <ChevronDownIcon size={16} />)}
|
||||||
<ChevronDownIcon size={16} />
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
import { createContext, useContext, useEffect, useState } from "react";
|
|
||||||
|
|
||||||
type Theme = "light" | "dark" | "system";
|
|
||||||
|
|
||||||
interface ThemeContextType {
|
|
||||||
theme: Theme;
|
|
||||||
setTheme: (theme: Theme) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
|
||||||
|
|
||||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
||||||
const [theme, setTheme] = useState<Theme>(() => {
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
const savedTheme = localStorage.getItem("theme") as Theme;
|
|
||||||
return savedTheme || "system";
|
|
||||||
}
|
|
||||||
return "system";
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const root = window.document.documentElement;
|
|
||||||
root.classList.remove("light", "dark");
|
|
||||||
|
|
||||||
if (theme === "system") {
|
|
||||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
|
||||||
.matches
|
|
||||||
? "dark"
|
|
||||||
: "light";
|
|
||||||
root.classList.add(systemTheme);
|
|
||||||
} else {
|
|
||||||
root.classList.add(theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem("theme", theme);
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
||||||
|
|
||||||
const handleChange = () => {
|
|
||||||
if (theme === "system") {
|
|
||||||
const root = window.document.documentElement;
|
|
||||||
root.classList.remove("light", "dark");
|
|
||||||
root.classList.add(mediaQuery.matches ? "dark" : "light");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
mediaQuery.addEventListener("change", handleChange);
|
|
||||||
return () => mediaQuery.removeEventListener("change", handleChange);
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ThemeContext.Provider value={{ theme, setTheme }}>
|
|
||||||
{children}
|
|
||||||
</ThemeContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTheme() {
|
|
||||||
const context = useContext(ThemeContext);
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error("useTheme must be used within a ThemeProvider");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@radix-ui/react-tooltip";
|
} from "@radix-ui/react-tooltip";
|
||||||
import type { JSX } from "react";
|
|
||||||
|
|
||||||
export interface TimeAgoProps {
|
export interface TimeAgoProps {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
@@ -43,7 +42,7 @@ const getTimeAgo = (
|
|||||||
return rtf.format(Math.floor(0 - diff), "second");
|
return rtf.format(Math.floor(0 - diff), "second");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TimeAgo = ({ timestamp }: TimeAgoProps): JSX.Element => {
|
export const TimeAgo = ({ timestamp }: TimeAgoProps) => {
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { JSX } from "react";
|
|
||||||
|
|
||||||
export interface UptimeProps {
|
export interface UptimeProps {
|
||||||
seconds: number;
|
seconds: number;
|
||||||
}
|
}
|
||||||
@@ -12,6 +10,6 @@ const getUptime = (seconds: number): string => {
|
|||||||
return `${days}d ${hours}h ${minutes}m ${secondsLeft}s`;
|
return `${days}d ${hours}h ${minutes}m ${secondsLeft}s`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Uptime = ({ seconds }: UptimeProps): JSX.Element => {
|
export const Uptime = ({ seconds }: UptimeProps) => {
|
||||||
return <span>{getUptime(seconds)}</span>;
|
return <span>{getUptime(seconds)}</span>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export function useBrowserFeatureDetection(): BrowserSupport {
|
|||||||
["Web Serial", !!navigator?.serial],
|
["Web Serial", !!navigator?.serial],
|
||||||
[
|
[
|
||||||
"Secure Context",
|
"Secure Context",
|
||||||
window.location.protocol === "https:" ||
|
globalThis.location.protocol === "https:" ||
|
||||||
window.location.hostname === "localhost",
|
globalThis.location.hostname === "localhost",
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Button } from "@app/components/UI/Button";
|
import { Button } from "../../components/UI/Button.tsx";
|
||||||
import type { CookieAttributes } from "js-cookie";
|
import type { CookieAttributes } from "js-cookie";
|
||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
import useCookie from "./useCookie";
|
import useCookie from "./useCookie.ts";
|
||||||
import { useToast } from "./useToast";
|
import { useToast } from "./useToast.ts";
|
||||||
|
|
||||||
interface UseBackupReminderOptions {
|
interface UseBackupReminderOptions {
|
||||||
reminderInDays?: number;
|
reminderInDays?: number;
|
||||||
@@ -26,8 +26,8 @@ const ON_ACCEPT_REMINDER_DAYS = 365;
|
|||||||
function isReminderExpired(lastShown: string): boolean {
|
function isReminderExpired(lastShown: string): boolean {
|
||||||
const lastShownDate = new Date(lastShown);
|
const lastShownDate = new Date(lastShown);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const daysSinceLastShown =
|
const daysSinceLastShown = (now.getTime() - lastShownDate.getTime()) /
|
||||||
(now.getTime() - lastShownDate.getTime()) / (1000 * 60 * 60 * 24);
|
(1000 * 60 * 60 * 24);
|
||||||
return daysSinceLastShown >= 7;
|
return daysSinceLastShown >= 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,8 +63,7 @@ export function useBackupReminder({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled || toastShownRef.current) return;
|
if (!enabled || toastShownRef.current) return;
|
||||||
|
|
||||||
const shouldShowReminder =
|
const shouldShowReminder = !reminderCookie?.suppressed ||
|
||||||
!reminderCookie?.suppressed ||
|
|
||||||
isReminderExpired(reminderCookie.lastShown);
|
isReminderExpired(reminderCookie.lastShown);
|
||||||
if (!shouldShowReminder) return;
|
if (!shouldShowReminder) return;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ type Theme = "light" | "dark" | "system";
|
|||||||
|
|
||||||
export function useTheme() {
|
export function useTheme() {
|
||||||
const getSystemTheme = () =>
|
const getSystemTheme = () =>
|
||||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
globalThis.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
? "dark"
|
? "dark"
|
||||||
: "light";
|
: "light";
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ export function useTheme() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [preference, setPreference] = useState<Theme>(() =>
|
const [preference, setPreference] = useState<Theme>(() =>
|
||||||
typeof window !== "undefined" ? getStoredPreference() : "light",
|
typeof window !== "undefined" ? getStoredPreference() : "light"
|
||||||
);
|
);
|
||||||
|
|
||||||
const theme = preference === "system" ? getSystemTheme() : preference;
|
const theme = preference === "system" ? getSystemTheme() : preference;
|
||||||
@@ -26,7 +26,7 @@ export function useTheme() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (preference !== "system") return;
|
if (preference !== "system") return;
|
||||||
|
|
||||||
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
const media = globalThis.matchMedia("(prefers-color-scheme: dark)");
|
||||||
const updateTheme = () => setPreference(getStoredPreference());
|
const updateTheme = () => setPreference(getStoredPreference());
|
||||||
|
|
||||||
media.addEventListener("change", updateTheme);
|
media.addEventListener("change", updateTheme);
|
||||||
|
|||||||
+17
-17
@@ -31,21 +31,21 @@ type ActionType = typeof actionTypes;
|
|||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| {
|
| {
|
||||||
type: ActionType["ADD_TOAST"];
|
type: ActionType["ADD_TOAST"];
|
||||||
toast: ToasterToast;
|
toast: ToasterToast;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: ActionType["UPDATE_TOAST"];
|
type: ActionType["UPDATE_TOAST"];
|
||||||
toast: Partial<ToasterToast>;
|
toast: Partial<ToasterToast>;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: ActionType["DISMISS_TOAST"];
|
type: ActionType["DISMISS_TOAST"];
|
||||||
toastId?: ToasterToast["id"];
|
toastId?: ToasterToast["id"];
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: ActionType["REMOVE_TOAST"];
|
type: ActionType["REMOVE_TOAST"];
|
||||||
toastId?: ToasterToast["id"];
|
toastId?: ToasterToast["id"];
|
||||||
};
|
};
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
toasts: ToasterToast[];
|
toasts: ToasterToast[];
|
||||||
@@ -81,7 +81,7 @@ export const reducer = (state: State, action: Action): State => {
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
toasts: state.toasts.map((t) =>
|
toasts: state.toasts.map((t) =>
|
||||||
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,10 +103,10 @@ export const reducer = (state: State, action: Action): State => {
|
|||||||
toasts: state.toasts.map((t) =>
|
toasts: state.toasts.map((t) =>
|
||||||
t.id === toastId || toastId === undefined
|
t.id === toastId || toastId === undefined
|
||||||
? {
|
? {
|
||||||
...t,
|
...t,
|
||||||
open: false,
|
open: false,
|
||||||
}
|
}
|
||||||
: t,
|
: t
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export const useAppStore = create<AppState>()((set, get) => ({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
setNodeNumToBeRemoved: (nodeNum) =>
|
setNodeNumToBeRemoved: (nodeNum) =>
|
||||||
set((state) => ({
|
set(() => ({
|
||||||
nodeNumToBeRemoved: nodeNum,
|
nodeNumToBeRemoved: nodeNum,
|
||||||
})),
|
})),
|
||||||
setConnectDialogOpen: (open: boolean) => {
|
setConnectDialogOpen: (open: boolean) => {
|
||||||
@@ -124,7 +124,7 @@ export const useAppStore = create<AppState>()((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
setNodeNumDetails: (nodeNum) =>
|
setNodeNumDetails: (nodeNum) =>
|
||||||
set((state) => ({
|
set(() => ({
|
||||||
nodeNumDetails: nodeNum,
|
nodeNumDetails: nodeNum,
|
||||||
})),
|
})),
|
||||||
setActiveChat: (chat) =>
|
setActiveChat: (chat) =>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user