Merge branch 'master' into switch-rsbuild-to-vite
@@ -0,0 +1,48 @@
|
|||||||
|
name: Code Formatting
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, master ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
format:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.head_ref }}
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install Biome
|
||||||
|
run: npm install --global @biomejs/biome
|
||||||
|
|
||||||
|
- name: Format with Biome
|
||||||
|
run: biome format --write .
|
||||||
|
|
||||||
|
- name: Check for changes
|
||||||
|
id: git-check
|
||||||
|
run: |
|
||||||
|
git diff --quiet || echo "changes=true" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Commit and push changes
|
||||||
|
if: steps.git-check.outputs.changes == 'true'
|
||||||
|
run: |
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
|
||||||
|
git add -A
|
||||||
|
git commit -m "chore: format code"
|
||||||
|
git push
|
||||||
@@ -68,7 +68,6 @@
|
|||||||
"react-map-gl": "7.1.9",
|
"react-map-gl": "7.1.9",
|
||||||
"react-qrcode-logo": "^3.0.0",
|
"react-qrcode-logo": "^3.0.0",
|
||||||
"rfc4648": "^1.5.4",
|
"rfc4648": "^1.5.4",
|
||||||
"timeago-react": "^3.0.6",
|
|
||||||
"vite-plugin-node-polyfills": "^0.23.0",
|
"vite-plugin-node-polyfills": "^0.23.0",
|
||||||
"zustand": "5.0.3"
|
"zustand": "5.0.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -113,9 +113,6 @@ importers:
|
|||||||
rfc4648:
|
rfc4648:
|
||||||
specifier: ^1.5.4
|
specifier: ^1.5.4
|
||||||
version: 1.5.4
|
version: 1.5.4
|
||||||
timeago-react:
|
|
||||||
specifier: ^3.0.6
|
|
||||||
version: 3.0.6(react@19.0.0)
|
|
||||||
vite-plugin-node-polyfills:
|
vite-plugin-node-polyfills:
|
||||||
specifier: ^0.23.0
|
specifier: ^0.23.0
|
||||||
version: 0.23.0(rollup@4.34.6)(vite@6.1.0(@types/node@22.12.0)(jiti@1.21.6)(yaml@2.4.5))
|
version: 0.23.0(rollup@4.34.6)(vite@6.1.0(@types/node@22.12.0)(jiti@1.21.6)(yaml@2.4.5))
|
||||||
@@ -2860,14 +2857,6 @@ packages:
|
|||||||
through2@4.0.2:
|
through2@4.0.2:
|
||||||
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
|
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
|
||||||
|
|
||||||
timeago-react@3.0.6:
|
|
||||||
resolution: {integrity: sha512-4ywnCX3iFjdp84WPK7gt8s4n0FxXbYM+xv8hYL73p83dpcMxzmO+0W4xJuxflnkWNvum5aEaqTe6LZ3lUIudjQ==}
|
|
||||||
peerDependencies:
|
|
||||||
react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
|
||||||
|
|
||||||
timeago.js@4.0.2:
|
|
||||||
resolution: {integrity: sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==}
|
|
||||||
|
|
||||||
timers-browserify@2.0.12:
|
timers-browserify@2.0.12:
|
||||||
resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==}
|
resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==}
|
||||||
engines: {node: '>=0.6.0'}
|
engines: {node: '>=0.6.0'}
|
||||||
@@ -6629,13 +6618,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
timeago-react@3.0.6(react@19.0.0):
|
|
||||||
dependencies:
|
|
||||||
react: 19.0.0
|
|
||||||
timeago.js: 4.0.2
|
|
||||||
|
|
||||||
timeago.js@4.0.2: {}
|
|
||||||
|
|
||||||
timers-browserify@2.0.12:
|
timers-browserify@2.0.12:
|
||||||
dependencies:
|
dependencies:
|
||||||
setimmediate: 1.0.5
|
setimmediate: 1.0.5
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Copyright Notice
|
||||||
|
Copyright © 2024 Meshtastic LLC. All Rights Reserved.
|
||||||
|
|
||||||
|
## In reference to the GNU GPLv3 License terms defined in Section 7e
|
||||||
|
Images (or assets) in this directory are protected under international copyright laws and treaties. Unauthorized reproduction, distribution, modification, or use of these images in any form, commercial or otherwise, outside of official Meshtastic creative works or its Backers and Partners is strictly prohibited without prior written consent from the copyright holder (Meshtastic LLC).
|
||||||
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 62 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="795.27 277.13 409.46 1319.35"><defs><style>.cls-1{fill:#353535;}.cls-2{fill:#1e1e1d;}.cls-3{fill:#b1a368;}.cls-10,.cls-11,.cls-4,.cls-6,.cls-8,.cls-9{fill:none;}.cls-4,.cls-6{stroke:#050606;}.cls-10,.cls-11,.cls-4,.cls-6,.cls-8{stroke-miterlimit:10;}.cls-4{stroke-width:2.41px;}.cls-5{fill:#30c2db;}.cls-6{stroke-width:3.91px;}.cls-7{fill:#dcf0f2;}.cls-10,.cls-11,.cls-8{stroke:#dcf0f2;}.cls-8{stroke-width:1.81px;}.cls-9{stroke:#17afbf;stroke-linecap:round;stroke-linejoin:round;stroke-width:7.23px;}.cls-10{stroke-width:1.78px;}.cls-11{stroke-width:1.81px;}</style></defs><g id="Layer_7" data-name="Layer 7"><path class="cls-1" d="M915.62,278.34h22.61a35,35,0,0,1,35,35V715.74a0,0,0,0,1,0,0H880.6a0,0,0,0,1,0,0V313.36A35,35,0,0,1,915.62,278.34Z"></path><rect class="cls-2" x="880.6" y="340.15" width="92.65" height="7.54"></rect><rect class="cls-2" x="880.6" y="356.68" width="92.65" height="7.54"></rect><rect class="cls-3" x="885.8" y="844.3" width="84.14" height="19.02"></rect><rect class="cls-3" x="880.6" y="819.07" width="92.65" height="25.23"></rect><rect class="cls-3" x="885.8" y="790.65" width="84.14" height="28.41"></rect><rect class="cls-3" x="880.6" y="723.02" width="92.65" height="67.63"></rect><rect class="cls-3" x="885.8" y="715.74" width="84.14" height="7.28"></rect><rect class="cls-4" x="885.8" y="844.3" width="84.14" height="19.02"></rect><rect class="cls-4" x="880.6" y="819.07" width="92.65" height="25.23"></rect><rect class="cls-4" x="885.8" y="790.65" width="84.14" height="28.41"></rect><rect class="cls-4" x="880.6" y="723.02" width="92.65" height="67.63"></rect><rect class="cls-4" x="885.8" y="715.74" width="84.14" height="7.28"></rect><path class="cls-4" d="M915.62,278.34h22.61a35,35,0,0,1,35,35V715.74a0,0,0,0,1,0,0H880.6a0,0,0,0,1,0,0V313.36A35,35,0,0,1,915.62,278.34Z"></path><rect class="cls-4" x="880.6" y="340.15" width="92.65" height="7.54"></rect><rect class="cls-4" x="880.6" y="356.68" width="92.65" height="7.54"></rect><rect class="cls-5" x="796.48" y="856.3" width="407.05" height="738.98" rx="47.74"></rect><rect class="cls-1" x="900.05" y="973.19" width="202.03" height="354.65" rx="16.4"></rect><rect class="cls-6" x="900.05" y="973.19" width="202.03" height="354.65" rx="16.4"></rect><rect class="cls-7" x="871.51" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><rect class="cls-7" x="1070.16" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><rect class="cls-4" x="871.51" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><rect class="cls-4" x="1070.16" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><circle class="cls-8" cx="841.7" cy="1537.01" r="16.25"></circle><circle class="cls-8" cx="841.7" cy="913.26" r="16.25"></circle><circle class="cls-8" cx="1157.32" cy="913.26" r="16.25"></circle><circle class="cls-8" cx="1157.32" cy="1504.51" r="16.25"></circle><line class="cls-9" x1="942.51" y1="1592.42" x2="942.51" y2="1381.55"></line><line class="cls-9" x1="966.52" y1="1592.42" x2="966.52" y2="1381.55"></line><line class="cls-9" x1="990.57" y1="1592.42" x2="990.57" y2="1381.55"></line><line class="cls-9" x1="1014.59" y1="1592.42" x2="1014.59" y2="1381.55"></line><line class="cls-9" x1="1038.63" y1="1592.42" x2="1038.63" y2="1381.55"></line><line class="cls-9" x1="1062.65" y1="1592.42" x2="1062.65" y2="1381.55"></line><rect class="cls-4" x="796.48" y="856.3" width="407.05" height="738.98" rx="47.74"></rect><path class="cls-10" d="M1040.1,947.74H960.65A13.93,13.93,0,0,1,947,936.64l-10.23-49.2a13.93,13.93,0,0,1,13.64-16.77h97.72a13.93,13.93,0,0,1,13.75,16.18l-8,49.2A13.94,13.94,0,0,1,1040.1,947.74Z"></path><rect class="cls-11" x="816.35" y="870.67" width="365.51" height="703.12" rx="32.37"></rect><rect class="cls-11" x="888.77" y="963.84" width="223.2" height="374.66" rx="25.21"></rect></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="404.68 390.65 1217.15 959.26"><defs><style>.cls-1{fill:#dfeaf7;}.cls-2{fill:#17907f;}.cls-3{fill:#2b2b2b;}.cls-4,.cls-5{fill:none;stroke:#050606;stroke-miterlimit:10;}.cls-4{stroke-width:2.25px;}.cls-5{stroke-width:4px;}.cls-6{fill:#050606;}</style></defs><g id="Layer_5" data-name="Layer 5"><path class="cls-1" d="M1517.73,392.65h0a102.1,102.1,0,0,0-102.1,102.1V770.37A40.62,40.62,0,0,1,1375,811H455.16a48.49,48.49,0,0,0-48.48,48.48v126a11.85,11.85,0,0,0,3.46,8.37l15.34,15.34a11.81,11.81,0,0,1,3.47,8.37v137.16a11.81,11.81,0,0,1-3.47,8.37l-15.34,15.34a11.85,11.85,0,0,0-3.46,8.37v112.67a48.49,48.49,0,0,0,48.48,48.49H1571.34a48.51,48.51,0,0,0,48.49-48.5V494.75A102.1,102.1,0,0,0,1517.73,392.65Zm-110.61,815V954a33.14,33.14,0,0,1,66.27,0v253.65a33.14,33.14,0,0,1-66.27,0Z"></path><path class="cls-2" d="M1516,439.16c-30.23.91-53.92,26.54-53.92,56.79V770.37A87.11,87.11,0,0,1,1375,857.48H732.31A27.51,27.51,0,0,0,704.8,885v388.93a27.51,27.51,0,0,0,27.51,27.51h828.38a12.7,12.7,0,0,0,12.65-12.65v-794A55.69,55.69,0,0,0,1516,439.16Zm-108.9,768.47V954a33.14,33.14,0,0,1,66.27,0v253.65a33.14,33.14,0,0,1-66.27,0Z"></path><rect class="cls-3" x="787.14" y="943.38" width="429.45" height="224.42"></rect><path class="cls-1" d="M1478.6,915.35A54.23,54.23,0,0,0,1386,953.69v254.23a54.23,54.23,0,1,0,108.45,0V953.69A54,54,0,0,0,1478.6,915.35Zm-5.21,292.28a33.14,33.14,0,0,1-66.27,0V954a33.14,33.14,0,0,1,66.27,0Z"></path></g><g id="Layer_2" data-name="Layer 2"><path class="cls-4" d="M1573.34,494.75v794a12.68,12.68,0,0,1-12.65,12.65H732.31a27.51,27.51,0,0,1-27.51-27.51V885a27.51,27.51,0,0,1,27.51-27.51H1375a87.11,87.11,0,0,0,87.11-87.11V496c0-30.25,23.69-55.88,53.92-56.79A55.69,55.69,0,0,1,1573.34,494.75Z"></path><path class="cls-5" d="M410.14,1178.39,425.49,1163a11.78,11.78,0,0,0,3.46-8.35V1017.5a11.8,11.8,0,0,0-3.46-8.35l-15.35-15.36a11.77,11.77,0,0,1-3.46-8.35v-126A48.47,48.47,0,0,1,455.16,811H1375a40.63,40.63,0,0,0,40.63-40.63V494.75a102.1,102.1,0,0,1,102.1-102.1h0a102.1,102.1,0,0,1,102.1,102.1v804.66a48.51,48.51,0,0,1-48.49,48.5H455.16a48.48,48.48,0,0,1-48.48-48.49V1186.74A11.78,11.78,0,0,1,410.14,1178.39Z"></path><rect class="cls-4" x="1407.12" y="920.85" width="66.26" height="319.9" rx="33.13"></rect><rect class="cls-4" x="1386.03" y="899.46" width="108.46" height="362.69" rx="54.23"></rect><path class="cls-6" d="M639.76,1070.55a2.91,2.91,0,0,1-2.91-2.91v-30.53a5.42,5.42,0,0,0-1.6-3.86l-32.44-32.44a11.86,11.86,0,0,1-3.5-8.44V901a12.52,12.52,0,0,0-12.51-12.51H483.92a12.7,12.7,0,0,0-12.68,12.69v76.78a24.13,24.13,0,0,0,7.11,17.18l14.33,14.33a24.13,24.13,0,0,0,17.18,7.11h50.75a11.86,11.86,0,0,1,8.44,3.5l24.26,24.26a12.47,12.47,0,0,1,3.68,8.88v14.46a2.91,2.91,0,1,1-5.81,0v-14.46a6.72,6.72,0,0,0-2-4.77l-24.26-24.26a6.09,6.09,0,0,0-4.33-1.8H509.86a29.91,29.91,0,0,1-21.29-8.81l-14.33-14.33a29.87,29.87,0,0,1-8.81-21.29V901.14a18.51,18.51,0,0,1,18.49-18.5H586.8A18.34,18.34,0,0,1,605.12,901v91.41a6.09,6.09,0,0,0,1.8,4.33l32.44,32.44a11.19,11.19,0,0,1,3.3,8v30.53A2.9,2.9,0,0,1,639.76,1070.55Z"></path><path class="cls-6" d="M586.8,1289.46H483.92a18.51,18.51,0,0,1-18.49-18.5v-76.78a29.87,29.87,0,0,1,8.81-21.29l14.33-14.34a29.91,29.91,0,0,1,21.29-8.81h50.75a6.09,6.09,0,0,0,4.33-1.8l24.26-24.25a6.74,6.74,0,0,0,2-4.78v-14.46a2.91,2.91,0,0,1,5.81,0v14.46a12.51,12.51,0,0,1-3.68,8.89l-24.26,24.25a11.86,11.86,0,0,1-8.44,3.5H509.86a24.17,24.17,0,0,0-17.18,7.11L478.35,1177a24.09,24.09,0,0,0-7.11,17.18V1271a12.71,12.71,0,0,0,12.68,12.69H586.8a12.53,12.53,0,0,0,12.51-12.52v-91.4a11.83,11.83,0,0,1,3.5-8.44l32.44-32.44a5.46,5.46,0,0,0,1.6-3.87v-30.53a2.91,2.91,0,0,1,5.81,0V1135a11.19,11.19,0,0,1-3.3,8l-32.44,32.45a6.06,6.06,0,0,0-1.8,4.33v91.4A18.35,18.35,0,0,1,586.8,1289.46Z"></path><rect class="cls-4" x="787.14" y="943.38" width="429.45" height="224.42"></rect></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="528.89 806.04 942.22 446.84"><defs><style>.cls-1{fill:#e8eae8;}.cls-2{fill:#dbdddb;}.cls-3{fill:#c6842a;}.cls-4,.cls-5,.cls-7,.cls-9{fill:none;stroke-miterlimit:10;}.cls-4,.cls-5{stroke:#050606;}.cls-4,.cls-9{stroke-width:2.44px;}.cls-5,.cls-7{stroke-width:1.22px;}.cls-6{fill:#b7b7b7;}.cls-7,.cls-9{stroke:#b7b7b7;}.cls-8{fill:#cbcccb;}.cls-10{fill:#434543;}</style></defs><g id="Layer_4" data-name="Layer 4"><path class="cls-1" d="M1469.9,826.68v390.94a19.43,19.43,0,0,1-19.43,19.43H549.53a19.43,19.43,0,0,1-19.43-19.43V826.68a19.42,19.42,0,0,1,19.43-19.42h900.94A19.42,19.42,0,0,1,1469.9,826.68Z"></path><path class="cls-2" d="M574.23,807.26v429.79h-24.7a19.43,19.43,0,0,1-19.43-19.43V826.68a19.42,19.42,0,0,1,19.43-19.42Z"></path><path class="cls-2" d="M1469.9,826.68v390.94a19.43,19.43,0,0,1-19.43,19.43h-37.56V807.26h37.56A19.42,19.42,0,0,1,1469.9,826.68Z"></path><path class="cls-3" d="M574.23,1129.8h-7.47a4.55,4.55,0,0,1-4.55-4.54V919.05a4.55,4.55,0,0,1,4.55-4.55h7.47"></path><rect class="cls-4" x="530.11" y="807.26" width="939.78" height="429.79" rx="19.42"></rect><line class="cls-5" x1="574.23" y1="807.26" x2="574.23" y2="1237.05"></line><path class="cls-5" d="M574.23,1129.8h-7.47a4.55,4.55,0,0,1-4.55-4.54V919.05a4.55,4.55,0,0,1,4.55-4.55h7.47"></path><rect class="cls-6" x="599.01" y="970.1" width="11.52" height="104.11"></rect><rect class="cls-5" x="599.01" y="970.1" width="11.52" height="104.11"></rect><path class="cls-2" d="M610.53,816.78V935.32l38.37,11.55a7.85,7.85,0,0,1,5.6,7.53V1107a7.87,7.87,0,0,1-7.87,7.87h-36.1v110.58H1406V816.78Zm775.41,384.75H631.43a3.66,3.66,0,0,1-3.66-3.66V1138a3.65,3.65,0,0,1,3.66-3.65h28.3a14.7,14.7,0,0,0,14.71-14.71V931a14.7,14.7,0,0,0-14.71-14.71h-28.3a3.65,3.65,0,0,1-3.66-3.66V844.11a3.66,3.66,0,0,1,3.66-3.66h754.51Z"></path><path class="cls-1" d="M1385.94,840.45v361.08H631.43a3.66,3.66,0,0,1-3.66-3.66V1138a3.65,3.65,0,0,1,3.66-3.65h28.3a14.7,14.7,0,0,0,14.71-14.71V931a14.7,14.7,0,0,0-14.71-14.71h-28.3a3.65,3.65,0,0,1-3.66-3.66V844.11a3.66,3.66,0,0,1,3.66-3.66Z"></path><path class="cls-7" d="M610.53,816.78V935.32l38.37,11.55a7.85,7.85,0,0,1,5.6,7.53V1107a7.87,7.87,0,0,1-7.87,7.87h-36.1v110.58H1406V816.78Zm775.41,384.75H631.43a3.66,3.66,0,0,1-3.66-3.66V1138a3.65,3.65,0,0,1,3.66-3.65h28.3a14.7,14.7,0,0,0,14.71-14.71V931a14.7,14.7,0,0,0-14.71-14.71h-28.3a3.65,3.65,0,0,1-3.66-3.66V844.11a3.66,3.66,0,0,1,3.66-3.66h754.51Z"></path><path class="cls-7" d="M1385.94,840.45v361.08H631.43a3.66,3.66,0,0,1-3.66-3.66V1138a3.65,3.65,0,0,1,3.66-3.65h28.3a14.7,14.7,0,0,0,14.71-14.71V931a14.7,14.7,0,0,0-14.71-14.71h-28.3a3.65,3.65,0,0,1-3.66-3.66V844.11a3.66,3.66,0,0,1,3.66-3.66Z"></path><path class="cls-7" d="M1385.94,840.45v361.08H631.43a3.66,3.66,0,0,1-3.66-3.66V1138a3.65,3.65,0,0,1,3.66-3.65h28.3a14.7,14.7,0,0,0,14.71-14.71V931a14.7,14.7,0,0,0-14.71-14.71h-28.3a3.65,3.65,0,0,1-3.66-3.66V844.11a3.66,3.66,0,0,1,3.66-3.66Z"></path><line class="cls-7" x1="666.4" y1="1132.79" x2="666.4" y2="1201.53"></line><line class="cls-7" x1="663.66" y1="916.86" x2="663.66" y2="840.45"></line><circle class="cls-8" cx="644.99" cy="878.66" r="10.7"></circle><circle class="cls-8" cx="644.99" cy="1171.55" r="10.7"></circle><circle class="cls-9" cx="644.99" cy="878.66" r="10.7"></circle><circle class="cls-9" cx="644.99" cy="1171.55" r="10.7"></circle><path class="cls-10" d="M1225,1237.05l2.23,11.19a4.25,4.25,0,0,0,4.17,3.42H1249a4.26,4.26,0,0,0,4.18-3.42l2.22-11.19"></path><path class="cls-10" d="M1306.87,1237.05l2.22,11.19a4.26,4.26,0,0,0,4.18,3.42h17.62a4.25,4.25,0,0,0,4.17-3.42l2.22-11.19"></path><path class="cls-10" d="M1388.77,1237.05l2.23,11.19a4.24,4.24,0,0,0,4.17,3.42h17.62a4.25,4.25,0,0,0,4.17-3.42l2.23-11.19"></path><path class="cls-4" d="M1225,1237.05l2.23,11.19a4.25,4.25,0,0,0,4.17,3.42H1249a4.26,4.26,0,0,0,4.18-3.42l2.22-11.19"></path><path class="cls-4" d="M1306.87,1237.05l2.22,11.19a4.26,4.26,0,0,0,4.18,3.42h17.62a4.25,4.25,0,0,0,4.17-3.42l2.22-11.19"></path><path class="cls-4" d="M1388.77,1237.05l2.23,11.19a4.24,4.24,0,0,0,4.17,3.42h17.62a4.25,4.25,0,0,0,4.17-3.42l2.23-11.19"></path><line class="cls-4" x1="1412.9" y1="807.26" x2="1412.9" y2="1237.05"></line></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="479.57 786.58 1040.84 433.17"><defs><style>.cls-1{fill:#cccccb;}.cls-2{fill:#2b2b2b;}.cls-3,.cls-6,.cls-7,.cls-8{fill:none;stroke-miterlimit:10;}.cls-3,.cls-6,.cls-7{stroke:#050606;}.cls-3,.cls-8{stroke-width:1.65px;}.cls-4{fill:#40403f;}.cls-5{fill:#ddd;}.cls-6{stroke-width:1.62px;}.cls-7{stroke-width:1.64px;}.cls-8{stroke:#fff;}.cls-9{fill:#353535;}.cls-10{fill:#c08c2d;}</style></defs><g id="Layer_4" data-name="Layer 4"><path class="cls-1" d="M595,923.44H519.6A10.46,10.46,0,0,1,509.14,913V802.7a15.28,15.28,0,0,1,15.28-15.28h979.89a15.29,15.29,0,0,1,15.29,15.29v400.93a15.3,15.3,0,0,1-15.29,15.29H524.42a15.28,15.28,0,0,1-15.28-15.28V1102.1a10.46,10.46,0,0,1,10.46-10.46H595"></path><rect class="cls-2" x="611.37" y="796.48" width="819.83" height="411.47"></rect><line class="cls-3" x1="1441.99" y1="787.41" x2="1441.99" y2="1218.92"></line><path class="cls-4" d="M620.91,851.7v302.78a1.87,1.87,0,0,1-1.87,1.87h-13.8a8.7,8.7,0,0,1-.89,0,10.23,10.23,0,0,1-9.35-10.2v-286a10.24,10.24,0,0,1,9.35-10.2,8.7,8.7,0,0,1,.89,0H619A1.87,1.87,0,0,1,620.91,851.7Z"></path><rect class="cls-5" x="480.4" y="942.42" width="114.6" height="127.58"></rect><rect class="cls-3" x="480.4" y="942.42" width="114.6" height="127.58"></rect><path class="cls-6" d="M595,923.44H519.6A10.46,10.46,0,0,1,509.14,913V802.7a15.28,15.28,0,0,1,15.28-15.28h979.89a15.29,15.29,0,0,1,15.29,15.29v400.93a15.3,15.3,0,0,1-15.29,15.29H524.42a15.28,15.28,0,0,1-15.28-15.28V1102.1a10.46,10.46,0,0,1,10.46-10.46H595"></path><path class="cls-2" d="M584.65,970.14H595a0,0,0,0,1,0,0v24.44a0,0,0,0,1,0,0H584.65a3,3,0,0,1-3-3V973.11A3,3,0,0,1,584.65,970.14Z"></path><rect class="cls-2" x="560.58" y="976.5" width="9.04" height="16.58" rx="2.72"></rect><path class="cls-2" d="M584.65,1026.63H595a0,0,0,0,1,0,0v24.44a0,0,0,0,1,0,0H584.65a3,3,0,0,1-3-3v-18.49A3,3,0,0,1,584.65,1026.63Z"></path><rect class="cls-2" x="560.58" y="1028.91" width="9.04" height="16.58" rx="2.72"></rect><path class="cls-3" d="M584.65,970.14H595a0,0,0,0,1,0,0v24.44a0,0,0,0,1,0,0H584.65a3,3,0,0,1-3-3V973.11A3,3,0,0,1,584.65,970.14Z"></path><rect class="cls-3" x="560.58" y="976.5" width="9.04" height="16.58" rx="2.72"></rect><path class="cls-3" d="M584.65,1026.63H595a0,0,0,0,1,0,0v24.44a0,0,0,0,1,0,0H584.65a3,3,0,0,1-3-3v-18.49A3,3,0,0,1,584.65,1026.63Z"></path><rect class="cls-3" x="560.58" y="1028.91" width="9.04" height="16.58" rx="2.72"></rect><polyline class="cls-7" points="611.37 1156.35 611.37 1207.95 1431.2 1207.95 1431.2 796.48 611.37 796.48 611.37 849.83"></polyline><line class="cls-3" x1="611.37" y1="1207.95" x2="611.37" y2="1218.93"></line><line class="cls-3" x1="611.37" y1="796.48" x2="611.37" y2="787.42"></line><rect class="cls-8" x="560.58" y="1107.17" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="528.51" y="1107.17" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="560.58" y="1166.28" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="528.51" y="1166.28" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="560.58" y="804.46" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="528.51" y="804.46" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="560.58" y="863.57" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="528.51" y="863.57" width="18.37" height="41.67" rx="4.91"></rect><circle class="cls-8" cx="1476.63" cy="831.74" r="27.15"></circle><circle class="cls-8" cx="1476.63" cy="1173.49" r="27.15"></circle><rect class="cls-9" x="676.15" y="804.6" width="742.79" height="396.03"></rect><path class="cls-10" d="M604.35,849.87v306.44a10.23,10.23,0,0,1-9.35-10.2v-286A10.24,10.24,0,0,1,604.35,849.87Z"></path><path class="cls-3" d="M605.24,849.83H619a1.87,1.87,0,0,1,1.87,1.87v302.78a1.87,1.87,0,0,1-1.87,1.87h-13.8A10.24,10.24,0,0,1,595,1146.11v-286A10.24,10.24,0,0,1,605.24,849.83Z"></path><rect class="cls-6" x="676.15" y="804.6" width="742.79" height="396.03"></rect></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="733.42 451.82 573.87 931.48"><defs><style>.cls-1{fill:#8e8d8e;}.cls-2{fill:#383839;}.cls-3{fill:#cccccb;}.cls-4{fill:#222226;}.cls-5,.cls-6{fill:none;stroke:#050606;stroke-miterlimit:10;}.cls-5{stroke-width:1.87px;}.cls-6{stroke-width:3.77px;}.cls-7{fill:#4c4c4d;}</style></defs><g id="Layer_4" data-name="Layer 4"><path class="cls-1" d="M1277.27,847.59h4.35a8.09,8.09,0,0,1,8.09,8.08v138a8.09,8.09,0,0,1-8.09,8.09h-4.35"></path><path class="cls-1" d="M1277.27,732.73h18a10.14,10.14,0,0,1,10.14,10.14v43A10.14,10.14,0,0,1,1295.26,796h-18a0,0,0,0,1,0,0V732.73A0,0,0,0,1,1277.27,732.73Z"></path><path class="cls-2" d="M1256.49,1200.6h0a14.19,14.19,0,0,1-2.83,12.5c-8.13,9.86-19.94,18.58-46,30.75-19.15,9-28.65,16-38.35,29.6a93.15,93.15,0,0,0-8.7,14.61c-6.95,15.17-11.77,44.44-11.77,65.66v3.61a24.09,24.09,0,0,1-24.1,24.09H887.83a24.09,24.09,0,0,1-24.1-24.09v-3.61c0-21.22-4.82-50.49-11.77-65.66a93.15,93.15,0,0,0-8.7-14.61c-9.7-13.63-19.2-20.65-38.35-29.6-26.06-12.17-37.87-20.89-46-30.75a14.22,14.22,0,0,1-2.82-12.5h0"></path><path class="cls-2" d="M756.09,634.53h0a14.19,14.19,0,0,1,2.83-12.5c8.12-9.86,19.93-18.58,46-30.75,19.15-8.95,28.65-16,38.35-29.6a93.15,93.15,0,0,0,8.7-14.61c6.95-15.17,11.77-44.44,11.77-65.66V477.8a24.09,24.09,0,0,1,24.1-24.09h236.92a24.09,24.09,0,0,1,24.1,24.09v3.61c0,21.22,4.82,50.49,11.77,65.66a93.15,93.15,0,0,0,8.7,14.61c9.7,13.63,19.2,20.65,38.35,29.6,26,12.17,37.86,20.89,46,30.75a14.19,14.19,0,0,1,2.83,12.5h0"></path><rect class="cls-3" x="735.31" y="598.25" width="541.96" height="638.99" rx="96.44"></rect><path class="cls-2" d="M1247.38,694.68v446.11a66.63,66.63,0,0,1-66.54,66.56H831.75a66.62,66.62,0,0,1-66.56-66.56V694.68a66.63,66.63,0,0,1,66.56-66.55h349.09A66.64,66.64,0,0,1,1247.38,694.68Z"></path><rect class="cls-4" x="817.71" y="721.76" width="379.03" height="388.6"></rect><path class="cls-5" d="M1247.38,694.68v446.11a66.63,66.63,0,0,1-66.54,66.56H831.75a66.62,66.62,0,0,1-66.56-66.56V694.68a66.63,66.63,0,0,1,66.56-66.55h349.09A66.64,66.64,0,0,1,1247.38,694.68Z"></path><rect class="cls-6" x="735.31" y="598.25" width="541.96" height="638.99" rx="96.44"></rect><path class="cls-6" d="M1256.49,1200.6h0a14.19,14.19,0,0,1-2.83,12.5c-8.13,9.86-19.94,18.58-46,30.75-19.15,9-28.65,16-38.35,29.6a93.15,93.15,0,0,0-8.7,14.61c-6.95,15.17-11.77,44.44-11.77,65.66v3.61a24.09,24.09,0,0,1-24.1,24.09H887.83a24.09,24.09,0,0,1-24.1-24.09v-3.61c0-21.22-4.82-50.49-11.77-65.66a93.15,93.15,0,0,0-8.7-14.61c-9.7-13.63-19.2-20.65-38.35-29.6-26.06-12.17-37.87-20.89-46-30.75a14.22,14.22,0,0,1-2.82-12.5h0"></path><path class="cls-6" d="M756.09,634.53h0a14.19,14.19,0,0,1,2.83-12.5c8.12-9.86,19.93-18.58,46-30.75,19.15-8.95,28.65-16,38.35-29.6a93.15,93.15,0,0,0,8.7-14.61c6.95-15.17,11.77-44.44,11.77-65.66V477.8a24.09,24.09,0,0,1,24.1-24.09h236.92a24.09,24.09,0,0,1,24.1,24.09v3.61c0,21.22,4.82,50.49,11.77,65.66a93.15,93.15,0,0,0,8.7,14.61c9.7,13.63,19.2,20.65,38.35,29.6,26,12.17,37.86,20.89,46,30.75a14.19,14.19,0,0,1,2.83,12.5h0"></path><rect class="cls-5" x="817.71" y="721.76" width="379.03" height="388.6"></rect><path class="cls-6" d="M1277.27,847.59h4.35a8.09,8.09,0,0,1,8.09,8.08v138a8.09,8.09,0,0,1-8.09,8.09h-4.35"></path><path class="cls-6" d="M1277.27,732.73h18a10.14,10.14,0,0,1,10.14,10.14v43A10.14,10.14,0,0,1,1295.26,796h-18a0,0,0,0,1,0,0V732.73A0,0,0,0,1,1277.27,732.73Z"></path><circle class="cls-7" cx="1083.08" cy="1177.35" r="16.6"></circle><rect class="cls-2" x="1280.24" y="739.77" width="16.77" height="4.59" rx="2.29"></rect><rect class="cls-2" x="1280.24" y="750.91" width="16.77" height="4.59" rx="2.29"></rect><rect class="cls-2" x="1280.24" y="762.06" width="16.77" height="4.59" rx="2.29"></rect><rect class="cls-2" x="1280.24" y="773.2" width="16.77" height="4.59" rx="2.29"></rect><rect class="cls-2" x="1280.24" y="784.34" width="16.77" height="4.59" rx="2.29"></rect></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
@@ -0,0 +1,160 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
class="svg-icon"
|
||||||
|
style="overflow:hidden;fill:currentColor"
|
||||||
|
viewBox="0 0 909.87988 546.85529"
|
||||||
|
version="1.1"
|
||||||
|
id="svg3"
|
||||||
|
xml:space="preserve"
|
||||||
|
width="909.87988"
|
||||||
|
height="546.85529"
|
||||||
|
sodipodi:docname="unknown.svg"
|
||||||
|
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="0.57169944"
|
||||||
|
inkscape:cx="291.23695"
|
||||||
|
inkscape:cy="107.57401"
|
||||||
|
inkscape:window-width="1472"
|
||||||
|
inkscape:window-height="890"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="38"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="Layer_7" /><defs
|
||||||
|
id="defs3"><style
|
||||||
|
id="style1">.cls-1{fill:#383838;}.cls-2{fill:#9f9f9e;}.cls-3{fill:#cbcccb;}.cls-4{fill:#b7b7b7;}.cls-5{fill:#353535;}.cls-6{fill:#b1a368;}.cls-7{fill:#2c2d2d;}.cls-10,.cls-11,.cls-8,.cls-9{fill:none;stroke:#050606;}.cls-10,.cls-11,.cls-8{stroke-miterlimit:10;}.cls-8,.cls-9{stroke-width:2px;}.cls-9{stroke-linecap:round;stroke-linejoin:round;}.cls-10{stroke-width:2.04px;}.cls-11{stroke-width:1.99px;}.cls-12{fill:#c08c2d;}.cls-13{fill:#af7a2b;}</style></defs><g
|
||||||
|
id="Layer_7"
|
||||||
|
data-name="Layer 7"
|
||||||
|
transform="translate(-646.6554,-758.05941)"><path
|
||||||
|
class="cls-2"
|
||||||
|
d="m 1545.1753,893.49468 h 4.69 a 5.67,5.67 0 0 1 5.67,5.67 v 84.64998 a 5.67,5.67 0 0 1 -5.67,5.67 h -4.69"
|
||||||
|
id="path1-4" /><rect
|
||||||
|
class="cls-3"
|
||||||
|
x="647.6554"
|
||||||
|
y="862.80469"
|
||||||
|
width="897.52002"
|
||||||
|
height="441.10999"
|
||||||
|
rx="11.7"
|
||||||
|
id="rect2" /><path
|
||||||
|
class="cls-2"
|
||||||
|
d="m 681.12532,862.80468 v 113.47998 a 3.67,3.67 0 0 0 3.67,3.67 h 41 a 2.35,2.35 0 0 1 2.35,2.35 V 1303.9147 H 1517.6053 V 862.80468 Z M 1492.6453,1278.9147 H 753.18532 V 972.01466 a 17.06,17.06 0 0 0 -17.06,-17.06 h -27.5 a 2.5,2.5 0 0 1 -2.5,-2.5 v -62.14998 a 2.5,2.5 0 0 1 2.5,-2.5 h 783.99998 z"
|
||||||
|
id="path2-7" /><path
|
||||||
|
class="cls-3"
|
||||||
|
d="M 1492.6453,887.80468 V 1278.9147 H 753.18532 V 972.01466 a 17,17 0 0 0 -7.2,-13.92 v -70.28998 z"
|
||||||
|
id="path3-7"
|
||||||
|
style="fill:#ffffff" /><path
|
||||||
|
class="cls-4"
|
||||||
|
d="m 745.98532,887.80468 v 70.28998 a 17,17 0 0 0 -9.86,-3.14 h -27.5 a 2.5,2.5 0 0 1 -2.5,-2.5 v -62.14998 a 2.5,2.5 0 0 1 2.5,-2.5 z"
|
||||||
|
id="path4" /><rect
|
||||||
|
class="cls-2"
|
||||||
|
x="672.10535"
|
||||||
|
y="1011.4448"
|
||||||
|
width="13.53"
|
||||||
|
height="148.39999"
|
||||||
|
id="rect4" /><path
|
||||||
|
class="cls-6"
|
||||||
|
d="m 1077.2923,853.76468 h 71.71 a 2.55,2.55 0 0 1 2.55,2.55 v 6.48 h -76.8 v -6.48 a 2.55,2.55 0 0 1 2.54,-2.55 z"
|
||||||
|
id="path7" /><path
|
||||||
|
class="cls-8"
|
||||||
|
d="m 1082.9205,761.22647 h 60.8838 a 6.1958134,4.8451518 0 0 1 6.1958,4.84516 v 77.32638 h -73.2754 v -77.32638 a 6.1958134,4.8451518 0 0 1 6.1958,-4.84516 z"
|
||||||
|
id="path39"
|
||||||
|
style="fill:#b1a368" /><rect
|
||||||
|
class="cls-8"
|
||||||
|
x="1066.9833"
|
||||||
|
y="778.19855"
|
||||||
|
width="91.504646"
|
||||||
|
height="55.957298"
|
||||||
|
rx="5.5511622"
|
||||||
|
id="rect39"
|
||||||
|
style="fill:#b1a368" /><path
|
||||||
|
class="cls-2"
|
||||||
|
d="m 1158.4522,782.53954 v 47.24724 a 5.5153484,4.3130254 0 0 1 -5.5512,4.34102 h -80.3665 a 5.5511623,4.341032 0 0 1 -5.587,-4.34102 v -47.24724 a 5.5511623,4.341032 0 0 1 5.587,-4.34103 h 80.5098 a 5.5153484,4.3130254 0 0 1 5.4079,4.34103 z"
|
||||||
|
id="path41-4"
|
||||||
|
style="fill:none;stroke:#050606;stroke-width:3.16706;stroke-miterlimit:10" /><rect
|
||||||
|
class="cls-6"
|
||||||
|
x="1079.9424"
|
||||||
|
y="843.73468"
|
||||||
|
width="65.989998"
|
||||||
|
height="10.03"
|
||||||
|
id="rect8" /><path
|
||||||
|
class="cls-8"
|
||||||
|
d="M 1492.6453,887.80468 V 1278.9147 H 753.18532 V 972.01466 a 17.06,17.06 0 0 0 -17.06,-17.06 h -27.5 a 2.5,2.5 0 0 1 -2.5,-2.5 v -62.14998 a 2.5,2.5 0 0 1 2.5,-2.5 h 783.99998 m 25,-25 H 681.12532 v 113.47998 a 3.68,3.68 0 0 0 3.67,3.67 h 41 a 2.35,2.35 0 0 1 2.35,2.35 V 1303.9147 H 1517.6053 V 862.80468 Z"
|
||||||
|
id="path10" /><line
|
||||||
|
class="cls-8"
|
||||||
|
x1="745.99536"
|
||||||
|
y1="958.09467"
|
||||||
|
x2="745.99536"
|
||||||
|
y2="887.80469"
|
||||||
|
id="line10" /><rect
|
||||||
|
class="cls-8"
|
||||||
|
x="672.10535"
|
||||||
|
y="1011.4448"
|
||||||
|
width="13.53"
|
||||||
|
height="148.39999"
|
||||||
|
id="rect11" /><path
|
||||||
|
class="cls-8"
|
||||||
|
d="m 1545.1753,893.49468 h 4.69 a 5.67,5.67 0 0 1 5.67,5.67 v 84.64998 a 5.67,5.67 0 0 1 -5.67,5.67 h -4.69"
|
||||||
|
id="path14" /><path
|
||||||
|
class="cls-10"
|
||||||
|
d="m 1077.2923,853.76468 h 71.71 a 2.55,2.55 0 0 1 2.55,2.55 v 6.48 h -76.8 v -6.48 a 2.55,2.55 0 0 1 2.54,-2.55 z"
|
||||||
|
id="path16" /><rect
|
||||||
|
class="cls-11"
|
||||||
|
x="1079.9424"
|
||||||
|
y="843.73468"
|
||||||
|
width="65.989998"
|
||||||
|
height="10.03"
|
||||||
|
id="rect17" /><path
|
||||||
|
class="cls-2"
|
||||||
|
d="m 725.27532,910.38466 a 14,14 0 1 0 14,14 13.95,13.95 0 0 0 -14,-14 z m 0,21.5 a 7.55,7.55 0 1 1 7.54,-7.55 7.55,7.55 0 0 1 -7.54,7.55 z"
|
||||||
|
id="path19" /><circle
|
||||||
|
class="cls-8"
|
||||||
|
cx="725.27539"
|
||||||
|
cy="924.33466"
|
||||||
|
r="7.5500002"
|
||||||
|
id="circle19" /><circle
|
||||||
|
class="cls-8"
|
||||||
|
cx="725.27539"
|
||||||
|
cy="924.33466"
|
||||||
|
r="13.95"
|
||||||
|
id="circle20" /><path
|
||||||
|
d="m 445.36309,440.05365 c 0,11.52004 10.38375,20.85861 23.19309,20.85861 12.80937,0 23.19311,-9.33857 23.19311,-20.85861 0,-11.52005 -10.38374,-20.85861 -23.19311,-20.85861 -12.80934,0 -23.19309,9.33856 -23.19309,20.85861 z"
|
||||||
|
fill="#ccc"
|
||||||
|
id="path1"
|
||||||
|
style="overflow:hidden;fill:#4d4d4d;stroke-width:0.458227"
|
||||||
|
transform="translate(646.6554,758.05941)" /><path
|
||||||
|
d="m 469.40305,538.40107 c -119.83415,0 -217.31582,-93.40624 -217.31582,-208.23067 0,-114.82425 97.48167,-208.23058 217.31582,-208.23058 119.83417,0 217.31585,93.40633 217.31585,208.23058 0,114.82443 -97.48168,208.23067 -217.31585,208.23067 z m 0,-386.58065 c -102.63515,0 -186.13149,80.00572 -186.13149,178.34998 0,98.32948 83.49634,178.34997 186.13149,178.34997 102.61966,0 186.13151,-80.01997 186.13151,-178.34997 0,-98.34426 -83.51185,-178.34998 -186.13151,-178.34998 z"
|
||||||
|
fill="#ccc"
|
||||||
|
id="path2"
|
||||||
|
style="overflow:hidden;fill:#4d4d4d;stroke-width:0.474832"
|
||||||
|
transform="translate(646.6554,758.05941)" /><path
|
||||||
|
d="m 468.55618,391.96713 c -8.53552,0 -15.46205,-6.22977 -15.46205,-13.90533 v -23.51468 c 0,-22.75028 19.32709,-40.13201 36.39722,-55.47009 12.50833,-11.26363 25.45056,-22.88885 25.45056,-32.16398 0,-23.18095 -20.81195,-42.03718 -46.38573,-42.03718 -26.0067,0 -46.38619,18.0497 -46.38619,41.09158 0,7.67594 -6.92654,13.90533 -15.46208,13.90533 -8.53554,0 -15.46207,-6.22977 -15.46207,-13.9058 0,-37.99002 34.68046,-68.90262 77.31034,-68.90262 42.62989,0 77.31034,31.32967 77.31034,69.84869 0,20.81694 -17.54944,36.5856 -34.51132,51.84064 -13.452,12.07016 -27.33645,24.55758 -27.33645,35.77907 v 23.51468 c 0,7.6764 -6.92702,13.91969 -15.46257,13.91969 z"
|
||||||
|
fill="#ccc"
|
||||||
|
id="path3"
|
||||||
|
style="overflow:hidden;fill:#4d4d4d;stroke-width:0.458227;stroke:#000000;stroke-opacity:1"
|
||||||
|
transform="translate(646.6554,758.05941)" /><rect
|
||||||
|
class="cls-8"
|
||||||
|
x="647.6554"
|
||||||
|
y="862.80469"
|
||||||
|
width="897.52002"
|
||||||
|
height="441.10999"
|
||||||
|
rx="11.7"
|
||||||
|
id="rect28" /></g><path
|
||||||
|
style="fill:#ffffff;fill-opacity:0;stroke-width:0.92"
|
||||||
|
d="m 107.41785,363.03448 -0.23786,-156.30546 -2.99,-3.72057 -2.99,-3.72058 v -34.09394 -34.09394 l 150.64998,0.048 150.64999,0.048 -8.28,3.06943 c -19.31509,7.16019 -34.46167,14.82453 -50.21721,25.41044 -50.57644,33.98158 -84.35747,88.86991 -91.06203,147.96009 -1.43336,12.63279 -0.63536,44.7022 1.3876,55.76392 7.76201,42.44321 25.98398,77.92651 55.67763,108.41989 17.37837,17.84644 33.98994,30.29944 55.42867,41.55255 l 11.30534,5.93414 -134.54212,0.0167 -134.54213,0.0167 z"
|
||||||
|
id="path11" /><path
|
||||||
|
style="fill:#ffffff;fill-opacity:0;stroke-width:0.92"
|
||||||
|
d="m 107.41785,363.03448 -0.23786,-156.30546 -2.99,-3.72057 -2.99,-3.72058 v -34.09394 -34.09394 l 150.64998,0.048 150.64999,0.048 -8.28,3.06943 c -19.31509,7.16019 -34.46167,14.82453 -50.21721,25.41044 -50.57644,33.98158 -84.35747,88.86991 -91.06203,147.96009 -1.43336,12.63279 -0.63536,44.7022 1.3876,55.76392 7.76201,42.44321 25.98398,77.92651 55.67763,108.41989 17.37837,17.84644 33.98994,30.29944 55.42867,41.55255 l 11.30534,5.93414 -134.54212,0.0167 -134.54213,0.0167 z"
|
||||||
|
id="path12" /><path
|
||||||
|
style="fill:#ffffff;fill-opacity:0;stroke-width:0.92"
|
||||||
|
d="m 107.41785,363.03448 -0.23786,-156.30546 -2.99,-3.72057 -2.99,-3.72058 v -34.09394 -34.09394 l 150.64998,0.048 150.64999,0.048 -8.28,3.06943 c -19.31509,7.16019 -34.46167,14.82453 -50.21721,25.41044 -50.57644,33.98158 -84.35747,88.86991 -91.06203,147.96009 -1.43336,12.63279 -0.63536,44.7022 1.3876,55.76392 7.76201,42.44321 25.98398,77.92651 55.67763,108.41989 17.37837,17.84644 33.98994,30.29944 55.42867,41.55255 l 11.30534,5.93414 -134.54212,0.0167 -134.54213,0.0167 z"
|
||||||
|
id="path13" /></svg>
|
||||||
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 120 KiB |
@@ -6,6 +6,8 @@ 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 => {
|
export const DialogManager = (): JSX.Element => {
|
||||||
const { channels, config, dialog, setDialogOpen } = useDevice();
|
const { channels, config, dialog, setDialogOpen } = useDevice();
|
||||||
@@ -56,6 +58,12 @@ export const DialogManager = (): JSX.Element => {
|
|||||||
setDialogOpen("pkiBackup", open);
|
setDialogOpen("pkiBackup", open);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<NodeDetailsDialog
|
||||||
|
open={dialog.nodeDetails}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setDialogOpen("nodeDetails", open);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { useDevice } from "@app/core/stores/deviceStore";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@components/UI/Dialog";
|
||||||
|
import type { Protobuf, Types } from "@meshtastic/js";
|
||||||
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||||
|
import type { JSX } from "react";
|
||||||
|
|
||||||
|
export interface LocationResponseDialogProps {
|
||||||
|
location: Types.PacketMetadata<Protobuf.Mesh.location> | undefined;
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LocationResponseDialog = ({
|
||||||
|
location,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: LocationResponseDialogProps): JSX.Element => {
|
||||||
|
const { nodes } = useDevice();
|
||||||
|
|
||||||
|
const from = nodes.get(location?.from ?? 0);
|
||||||
|
const longName =
|
||||||
|
from?.user?.longName ??
|
||||||
|
(from ? `!${numberToHexUnpadded(from?.num)}` : "Unknown");
|
||||||
|
const shortName =
|
||||||
|
from?.user?.shortName ??
|
||||||
|
(from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : "UNK");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{`Location: ${longName} (${shortName})`}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogDescription>
|
||||||
|
<div className="ml-5 flex">
|
||||||
|
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
|
||||||
|
<p>
|
||||||
|
Coordinates:{" "}
|
||||||
|
<a
|
||||||
|
className="text-blue-500 dark:text-blue-400"
|
||||||
|
href={`https://www.openstreetmap.org/?mlat=${location?.data.latitudeI / 1e7}&mlon=${location?.data.longitudeI / 1e7}&layers=N`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{location?.data.latitudeI / 1e7},{" "}
|
||||||
|
{location?.data.longitudeI / 1e7}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>Altitude: {location?.data.altitude}m</p>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import { useAppStore } from "@app/core/stores/appStore";
|
||||||
|
import { useDevice } from "@app/core/stores/deviceStore";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "@components/UI/Accordion";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@components/UI/Dialog";
|
||||||
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||||
|
import { DeviceImage } from "../generic/DeviceImage";
|
||||||
|
import { TimeAgo } from "../generic/TimeAgo";
|
||||||
|
import { Uptime } from "../generic/Uptime";
|
||||||
|
|
||||||
|
export interface NodeDetailsDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NodeDetailsDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: NodeDetailsDialogProps) => {
|
||||||
|
const { nodes } = useDevice();
|
||||||
|
const { nodeNumDetails } = useAppStore();
|
||||||
|
const device: Protobuf.Mesh.NodeInfo = nodes.get(nodeNumDetails);
|
||||||
|
|
||||||
|
return device ? (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
Node Details for {device.user?.longName ?? "UNKNOWN"} (
|
||||||
|
{device.user?.shortName ?? "UNK"})
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<div className="w-full">
|
||||||
|
<DeviceImage
|
||||||
|
className="w-32 h-32 mx-auto rounded-lg border-4 border-gray-200 dark:border-gray-800"
|
||||||
|
deviceType={
|
||||||
|
Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="mt-5 bg-gray-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||||
|
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
||||||
|
Details:
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Hardware:{" "}
|
||||||
|
{Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]}
|
||||||
|
</p>
|
||||||
|
<p>Node Number: {device.num}</p>
|
||||||
|
<p>Node HEX: !{numberToHexUnpadded(device.num)}</p>
|
||||||
|
<p>
|
||||||
|
Role:{" "}
|
||||||
|
{
|
||||||
|
Protobuf.Config.Config_DeviceConfig_Role[
|
||||||
|
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-gray-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-gray-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-gray-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)}
|
||||||
|
</pre>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { toast } from "@app/core/hooks/useToast";
|
||||||
|
import { useAppStore } from "@app/core/stores/appStore";
|
||||||
|
import { useDevice } from "@app/core/stores/deviceStore";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@components/UI/Dialog";
|
||||||
|
import type { Protobuf } from "@meshtastic/js";
|
||||||
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||||
|
import { TrashIcon } from "lucide-react";
|
||||||
|
import type { JSX } from "react";
|
||||||
|
import { Button } from "../UI/Button";
|
||||||
|
|
||||||
|
export interface NodeOptionsDialogProps {
|
||||||
|
node: Protobuf.Mesh.NodeInfo | undefined;
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NodeOptionsDialog = ({
|
||||||
|
node,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: NodeOptionsDialogProps): JSX.Element => {
|
||||||
|
const { setDialogOpen, connection, setActivePage } = useDevice();
|
||||||
|
const {
|
||||||
|
setNodeNumToBeRemoved,
|
||||||
|
setNodeNumDetails,
|
||||||
|
setChatType,
|
||||||
|
setActiveChat,
|
||||||
|
} = useAppStore();
|
||||||
|
const longName =
|
||||||
|
node?.user?.longName ??
|
||||||
|
(node ? `!${numberToHexUnpadded(node?.num)}` : "Unknown");
|
||||||
|
const shortName =
|
||||||
|
node?.user?.shortName ??
|
||||||
|
(node ? `${numberToHexUnpadded(node?.num).substring(0, 4)}` : "UNK");
|
||||||
|
|
||||||
|
function handleDirectMessage() {
|
||||||
|
if (!node) return;
|
||||||
|
setChatType("direct");
|
||||||
|
setActiveChat(node.num);
|
||||||
|
setActivePage("messages");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRequestPosition() {
|
||||||
|
if (!node) return;
|
||||||
|
toast({
|
||||||
|
title: "Requesting position, please wait...",
|
||||||
|
});
|
||||||
|
connection?.requestPosition(node.num).then(() =>
|
||||||
|
toast({
|
||||||
|
title: "Position request sent.",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
onOpenChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTraceroute() {
|
||||||
|
if (!node) return;
|
||||||
|
toast({
|
||||||
|
title: "Sending Traceroute, please wait...",
|
||||||
|
});
|
||||||
|
connection?.traceRoute(node.num).then(() =>
|
||||||
|
toast({
|
||||||
|
title: "Traceroute sent.",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
onOpenChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{`${longName} (${shortName})`}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<div>
|
||||||
|
<Button onClick={handleDirectMessage}>Direct Message</Button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button onClick={handleRequestPosition}>Request Position</Button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button onClick={handleTraceroute}>Trace Route</Button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
key="remove"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => {
|
||||||
|
setNodeNumToBeRemoved(node.num);
|
||||||
|
setDialogOpen("nodeRemoval", true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setNodeNumDetails(node.num);
|
||||||
|
setDialogOpen("nodeDetails", true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
More Details
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { useDevice } from "@app/core/stores/deviceStore";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@components/UI/Dialog";
|
||||||
|
import type { Protobuf, Types } from "@meshtastic/js";
|
||||||
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||||
|
import type { JSX } from "react";
|
||||||
|
import { TraceRoute } from "../PageComponents/Messages/TraceRoute";
|
||||||
|
|
||||||
|
export interface TracerouteResponseDialogProps {
|
||||||
|
traceroute: Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery> | undefined;
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TracerouteResponseDialog = ({
|
||||||
|
traceroute,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: TracerouteResponseDialogProps): JSX.Element => {
|
||||||
|
const { nodes } = useDevice();
|
||||||
|
const route: number[] = traceroute?.data.route ?? [];
|
||||||
|
const routeBack: number[] = traceroute?.data.routeBack ?? [];
|
||||||
|
const snrTowards = traceroute?.data.snrTowards ?? [];
|
||||||
|
const snrBack = traceroute?.data.snrBack ?? [];
|
||||||
|
const from = nodes.get(traceroute?.from ?? 0);
|
||||||
|
const longName =
|
||||||
|
from?.user?.longName ??
|
||||||
|
(from ? `!${numberToHexUnpadded(from?.num)}` : "Unknown");
|
||||||
|
const shortName =
|
||||||
|
from?.user?.shortName ??
|
||||||
|
(from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : "UNK");
|
||||||
|
const to = nodes.get(traceroute?.to ?? 0);
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{`Traceroute: ${longName} (${shortName})`}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogDescription>
|
||||||
|
<TraceRoute
|
||||||
|
route={route}
|
||||||
|
routeBack={routeBack}
|
||||||
|
from={from}
|
||||||
|
to={to}
|
||||||
|
snrTowards={snrTowards}
|
||||||
|
snrBack={snrBack}
|
||||||
|
/>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -23,18 +23,13 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
|||||||
window.location.hostname,
|
window.location.hostname,
|
||||||
)
|
)
|
||||||
? "meshtastic.local"
|
? "meshtastic.local"
|
||||||
: window.location.hostname,
|
: window.location.host,
|
||||||
tls: location.protocol === "https:",
|
tls: location.protocol === "https:",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const tlsEnabled = useWatch({
|
|
||||||
control,
|
|
||||||
name: "tls",
|
|
||||||
defaultValue: location.protocol === "https:",
|
|
||||||
});
|
|
||||||
|
|
||||||
const [connectionInProgress, setConnectionInProgress] = useState(false);
|
const [connectionInProgress, setConnectionInProgress] = useState(false);
|
||||||
|
const [https, setHTTPS] = useState(false);
|
||||||
|
|
||||||
const onSubmit = handleSubmit(async (data) => {
|
const onSubmit = handleSubmit(async (data) => {
|
||||||
setConnectionInProgress(true);
|
setConnectionInProgress(true);
|
||||||
@@ -46,7 +41,7 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
|||||||
await connection.connect({
|
await connection.connect({
|
||||||
address: data.ip,
|
address: data.ip,
|
||||||
fetchInterval: 2000,
|
fetchInterval: 2000,
|
||||||
tls: data.tls,
|
tls: https,
|
||||||
});
|
});
|
||||||
|
|
||||||
setSelectedDevice(id);
|
setSelectedDevice(id);
|
||||||
@@ -60,8 +55,7 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
|||||||
<div className="flex h-48 flex-col gap-2">
|
<div className="flex h-48 flex-col gap-2">
|
||||||
<Label>IP Address/Hostname</Label>
|
<Label>IP Address/Hostname</Label>
|
||||||
<Input
|
<Input
|
||||||
// label="IP Address/Hostname"
|
prefix={https ? "https://" : "http://"}
|
||||||
prefix={tlsEnabled ? "https://" : "http://"}
|
|
||||||
placeholder="000.000.000.000 / meshtastic.local"
|
placeholder="000.000.000.000 / meshtastic.local"
|
||||||
disabled={connectionInProgress}
|
disabled={connectionInProgress}
|
||||||
{...register("ip")}
|
{...register("ip")}
|
||||||
@@ -71,8 +65,12 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value, ...rest } }) => (
|
render={({ field: { value, ...rest } }) => (
|
||||||
<>
|
<>
|
||||||
<Label>Use TLS</Label>
|
<Label>Use HTTPS</Label>
|
||||||
<Switch
|
<Switch
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
checked ? setHTTPS(true) : setHTTPS(false);
|
||||||
|
}}
|
||||||
|
|
||||||
// label="Use TLS"
|
// label="Use TLS"
|
||||||
// description="Description"
|
// description="Description"
|
||||||
disabled={
|
disabled={
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Separator } from "@app/components/UI/Seperator";
|
import { Separator } from "@app/components/UI/Seperator";
|
||||||
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 { Avatar } from "@components/UI/Avatar";
|
import { Avatar } from "@components/UI/Avatar";
|
||||||
import { Mono } from "@components/generic/Mono.tsx";
|
import { Mono } from "@components/generic/Mono.tsx";
|
||||||
import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.tsx";
|
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
import type { Protobuf as ProtobufType } from "@meshtastic/js";
|
import type { Protobuf as ProtobufType } from "@meshtastic/js";
|
||||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||||
@@ -25,12 +26,12 @@ export interface NodeDetailProps {
|
|||||||
|
|
||||||
export const NodeDetail = ({ node }: NodeDetailProps) => {
|
export const NodeDetail = ({ node }: NodeDetailProps) => {
|
||||||
const name = node.user?.longName || `!${numberToHexUnpadded(node.num)}`;
|
const name = node.user?.longName || `!${numberToHexUnpadded(node.num)}`;
|
||||||
const hardwareType = Protobuf.Mesh.HardwareModel[
|
const hwModel = node.user?.hwModel ?? 0;
|
||||||
node.user?.hwModel ?? 0
|
const hardwareType =
|
||||||
].replaceAll("_", " ");
|
Protobuf.Mesh.HardwareModel[hwModel]?.replaceAll("_", " ") ?? `${hwModel}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark:text-black">
|
<div className="dark:text-black p-1">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="flex flex-col items-center gap-2 min-w-6 pt-1">
|
<div className="flex flex-col items-center gap-2 min-w-6 pt-1">
|
||||||
<Avatar text={node.user?.shortName} />
|
<Avatar text={node.user?.shortName} />
|
||||||
@@ -132,7 +133,12 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
|
|||||||
className="ml-2 mr-1"
|
className="ml-2 mr-1"
|
||||||
aria-label="Elevation"
|
aria-label="Elevation"
|
||||||
/>
|
/>
|
||||||
<div>{node.position?.altitude} ft</div>
|
<div>
|
||||||
|
{formatQuantity(node.position?.altitude, {
|
||||||
|
one: "meter",
|
||||||
|
other: "meters",
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,11 +51,11 @@ export const ChannelChat = ({
|
|||||||
|
|
||||||
if (!messages?.length) {
|
if (!messages?.length) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full w-full">
|
<div className="flex flex-col h-full w-full container mx-auto">
|
||||||
<div className="flex-1 flex items-center justify-center">
|
<div className="flex-1 flex items-center justify-center">
|
||||||
<EmptyState />
|
<EmptyState />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0 p-4 w-full bg-gray-900">
|
<div className="flex-shrink-0 p-4 w-full dark:bg-gray-900">
|
||||||
<MessageInput to={to} channel={channel} maxBytes={200} />
|
<MessageInput to={to} channel={channel} maxBytes={200} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,23 +63,25 @@ export const ChannelChat = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full w-full">
|
<div className="flex flex-col h-full w-full container mx-auto">
|
||||||
<div className="flex-1 overflow-y-scroll w-full" ref={scrollContainerRef}>
|
<div className="flex-1 overflow-y-auto" ref={scrollContainerRef}>
|
||||||
<div className="w-full h-full flex flex-col justify-end">
|
<div className="w-full h-full flex flex-col justify-end pl-4 pr-44">
|
||||||
{messages.map((message, index) => (
|
{messages.map((message, index) => {
|
||||||
<Message
|
return (
|
||||||
key={message.id}
|
<Message
|
||||||
message={message}
|
key={message.id}
|
||||||
lastMsgSameUser={
|
message={message}
|
||||||
index > 0 && messages[index - 1].from === message.from
|
sender={nodes.get(message.from)}
|
||||||
}
|
lastMsgSameUser={
|
||||||
sender={nodes.get(message.from)}
|
index > 0 && messages[index - 1].from === message.from
|
||||||
/>
|
}
|
||||||
))}
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
<div ref={messagesEndRef} className="w-full" />
|
<div ref={messagesEndRef} className="w-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0 mt-2 p-4 w-full bg-gray-900">
|
<div className="flex-shrink-0 mt-2 p-4 w-full dark:bg-gray-900">
|
||||||
<MessageInput to={to} channel={channel} maxBytes={200} />
|
<MessageInput to={to} channel={channel} maxBytes={200} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
import type { MessageWithState } from "@app/core/stores/deviceStore.ts";
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipArrow,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@app/components/UI/Tooltip";
|
||||||
|
import { useAppStore } from "@app/core/stores/appStore";
|
||||||
|
import {
|
||||||
|
type MessageWithState,
|
||||||
|
useDeviceStore,
|
||||||
|
} from "@app/core/stores/deviceStore.ts";
|
||||||
import { cn } from "@app/core/utils/cn";
|
import { cn } from "@app/core/utils/cn";
|
||||||
import { Avatar } from "@components/UI/Avatar";
|
import { Avatar } from "@components/UI/Avatar";
|
||||||
import type { Protobuf } from "@meshtastic/js";
|
import type { Protobuf } from "@meshtastic/js";
|
||||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
|
||||||
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";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
const MESSAGE_STATES = {
|
const MESSAGE_STATES = {
|
||||||
ACK: "ack",
|
ACK: "ack",
|
||||||
@@ -17,7 +28,7 @@ type MessageState = MessageWithState["state"];
|
|||||||
interface MessageProps {
|
interface MessageProps {
|
||||||
lastMsgSameUser: boolean;
|
lastMsgSameUser: boolean;
|
||||||
message: MessageWithState;
|
message: MessageWithState;
|
||||||
sender?: Protobuf.Mesh.NodeInfo;
|
sender: Protobuf.Mesh.NodeInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StatusTooltipProps {
|
interface StatusTooltipProps {
|
||||||
@@ -45,22 +56,20 @@ const STATUS_ICON_MAP: Record<MessageState, LucideIcon> = {
|
|||||||
const getStatusText = (state: MessageState): string => STATUS_TEXT_MAP[state];
|
const getStatusText = (state: MessageState): string => STATUS_TEXT_MAP[state];
|
||||||
|
|
||||||
const StatusTooltip = ({ state, children }: StatusTooltipProps) => (
|
const StatusTooltip = ({ state, children }: StatusTooltipProps) => (
|
||||||
<Tooltip.Provider>
|
<TooltipProvider>
|
||||||
<Tooltip.Root>
|
<Tooltip>
|
||||||
<Tooltip.Trigger asChild>{children}</Tooltip.Trigger>
|
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||||
<Tooltip.Portal>
|
<TooltipContent
|
||||||
<Tooltip.Content
|
className="rounded-md bg-slate-800 px-3 py-1.5 text-sm text-white shadow-md animate-in fade-in-0 zoom-in-95"
|
||||||
className="rounded-md bg-slate-800 px-3 py-1.5 text-sm text-white shadow-md animate-in fade-in-0 zoom-in-95"
|
side="top"
|
||||||
side="top"
|
align="center"
|
||||||
align="center"
|
sideOffset={5}
|
||||||
sideOffset={5}
|
>
|
||||||
>
|
{getStatusText(state)}
|
||||||
{getStatusText(state)}
|
<TooltipArrow className="fill-slate-800" />
|
||||||
<Tooltip.Arrow className="fill-slate-800" />
|
</TooltipContent>
|
||||||
</Tooltip.Content>
|
</Tooltip>
|
||||||
</Tooltip.Portal>
|
</TooltipProvider>
|
||||||
</Tooltip.Root>
|
|
||||||
</Tooltip.Provider>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const StatusIcon = ({ state, className, ...otherProps }: StatusIconProps) => {
|
const StatusIcon = ({ state, className, ...otherProps }: StatusIconProps) => {
|
||||||
@@ -88,7 +97,7 @@ const getMessageTextStyles = (state: MessageState) => {
|
|||||||
const isWaiting = state === MESSAGE_STATES.WAITING;
|
const isWaiting = state === MESSAGE_STATES.WAITING;
|
||||||
|
|
||||||
return cn(
|
return cn(
|
||||||
"pl-2 break-words overflow-hidden",
|
"break-words overflow-hidden",
|
||||||
isAcknowledged
|
isAcknowledged
|
||||||
? "text-black dark:text-white"
|
? "text-black dark:text-white"
|
||||||
: "text-black dark:text-gray-400",
|
: "text-black dark:text-gray-400",
|
||||||
@@ -96,8 +105,11 @@ const getMessageTextStyles = (state: MessageState) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const TimeDisplay = ({ date }: { date: Date }) => (
|
const TimeDisplay = ({
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
date,
|
||||||
|
className,
|
||||||
|
}: { date: Date; className?: string }) => (
|
||||||
|
<div className={cn("flex items-center gap-2 flex-shrink-0", className)}>
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">
|
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">
|
||||||
{date.toLocaleDateString()}
|
{date.toLocaleDateString()}
|
||||||
</span>
|
</span>
|
||||||
@@ -111,44 +123,46 @@ const TimeDisplay = ({ date }: { date: Date }) => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const Message = ({ lastMsgSameUser, message, sender }: MessageProps) => {
|
export const Message = ({ lastMsgSameUser, message, sender }: MessageProps) => {
|
||||||
|
const { getDevices } = useDeviceStore();
|
||||||
|
|
||||||
|
const isDeviceUser = useMemo(
|
||||||
|
() =>
|
||||||
|
getDevices()
|
||||||
|
.map((device) => device.nodes.get(device.hardware.myNodeNum)?.num)
|
||||||
|
.includes(message.from),
|
||||||
|
[getDevices, message.from],
|
||||||
|
);
|
||||||
|
const messageUser = sender?.user;
|
||||||
|
|
||||||
const messageTextClass = getMessageTextStyles(message.state);
|
const messageTextClass = getMessageTextStyles(message.state);
|
||||||
const isFailed = message.state === MESSAGE_STATES.ACK;
|
|
||||||
|
|
||||||
const baseMessageWrapper = cn(
|
|
||||||
"flex items-center gap-2 w-full max-w-full pl-11",
|
|
||||||
!lastMsgSameUser && "flex-wrap flex-grow",
|
|
||||||
);
|
|
||||||
|
|
||||||
const containerClass = cn(
|
|
||||||
"w-full px-4 relative",
|
|
||||||
lastMsgSameUser ? "mt-1" : "mt-2",
|
|
||||||
!lastMsgSameUser && "pt-2",
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClass}>
|
<div className="flex flex-col w-full px-4 justify-start">
|
||||||
{!lastMsgSameUser && (
|
<div
|
||||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
className={cn(
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
"flex flex-col flex-wrap items-start py-1",
|
||||||
<Avatar
|
isDeviceUser && "items-end",
|
||||||
text={sender?.user?.shortName ?? "UNK"}
|
)}
|
||||||
className="flex-shrink-0"
|
>
|
||||||
/>
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<span className="font-medium text-gray-900 dark:text-white truncate">
|
{!lastMsgSameUser ? (
|
||||||
{sender?.user?.longName ?? "UNK"}
|
<div className="flex place-items-center gap-2 mb-1">
|
||||||
</span>
|
<Avatar text={messageUser?.shortName} />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium text-gray-900 dark:text-white truncate">
|
||||||
|
{messageUser?.longName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<TimeDisplay date={message.rxTime} />
|
||||||
|
<div className="flex place-items-center gap-2 pb-2">
|
||||||
|
<div className={cn(isDeviceUser && "pl-11", messageTextClass)}>
|
||||||
|
{message.data}
|
||||||
</div>
|
</div>
|
||||||
<TimeDisplay date={message.rxTime} />
|
<StatusIcon state={message.state} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div className={baseMessageWrapper}>
|
|
||||||
<div className="flex-1 min-w-0 max-w-full">
|
|
||||||
<div className={messageTextClass}>{message.data}</div>
|
|
||||||
</div>
|
|
||||||
<StatusIcon
|
|
||||||
state={message.state}
|
|
||||||
className="ml-auto mr-6 flex-shrink-0"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ 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/js";
|
import type { Types } from "@meshtastic/js";
|
||||||
import { SendIcon } from "lucide-react";
|
import { SendIcon } from "lucide-react";
|
||||||
import { type JSX, useCallback, useMemo, useState } from "react";
|
import {
|
||||||
|
type JSX,
|
||||||
|
startTransition,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
export interface MessageInputProps {
|
export interface MessageInputProps {
|
||||||
to: Types.Destination;
|
to: Types.Destination;
|
||||||
@@ -26,7 +32,7 @@ export const MessageInput = ({
|
|||||||
} = useDevice();
|
} = useDevice();
|
||||||
const myNodeNum = hardware.myNodeNum;
|
const myNodeNum = hardware.myNodeNum;
|
||||||
const [localDraft, setLocalDraft] = useState(messageDraft);
|
const [localDraft, setLocalDraft] = useState(messageDraft);
|
||||||
const [messageBytes, setMessageBytes] = useState(maxBytes);
|
const [messageBytes, setMessageBytes] = useState(0);
|
||||||
|
|
||||||
const debouncedSetMessageDraft = useMemo(
|
const debouncedSetMessageDraft = useMemo(
|
||||||
() => debounce(setMessageDraft, 300),
|
() => debounce(setMessageDraft, 300),
|
||||||
@@ -64,10 +70,11 @@ export const MessageInput = ({
|
|||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
const byteLength = new Blob([newValue]).size;
|
const byteLength = new Blob([newValue]).size;
|
||||||
|
|
||||||
if (byteLength <= maxBytes) {
|
if (byteLength <= maxBytes) {
|
||||||
setLocalDraft(newValue);
|
setLocalDraft(newValue);
|
||||||
debouncedSetMessageDraft(newValue);
|
debouncedSetMessageDraft(newValue);
|
||||||
setMessageBytes(maxBytes - byteLength);
|
setMessageBytes(byteLength);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,11 +82,16 @@ export const MessageInput = ({
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<form
|
<form
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onSubmit={(e) => {
|
action={async (formData: FormData) => {
|
||||||
e.preventDefault();
|
// prevent user from sending blank/empty message
|
||||||
sendText(localDraft);
|
if (localDraft === "") return;
|
||||||
setLocalDraft("");
|
const message = formData.get("messageInput") as string;
|
||||||
setMessageDraft("");
|
startTransition(() => {
|
||||||
|
sendText(message);
|
||||||
|
setLocalDraft("");
|
||||||
|
setMessageDraft("");
|
||||||
|
setMessageBytes(0);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-grow gap-2">
|
<div className="flex flex-grow gap-2">
|
||||||
@@ -87,14 +99,16 @@ export const MessageInput = ({
|
|||||||
<Input
|
<Input
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
minLength={1}
|
minLength={1}
|
||||||
|
name="messageInput"
|
||||||
placeholder="Enter Message"
|
placeholder="Enter Message"
|
||||||
value={localDraft}
|
value={localDraft}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center w-24 p-2 place-content-end">
|
||||||
{messageBytes}/{maxBytes}
|
{messageBytes}/{maxBytes}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button type="submit">
|
<Button type="submit">
|
||||||
<SendIcon size={16} />
|
<SendIcon size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,36 +1,60 @@
|
|||||||
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||||
import type { Protobuf } from "@meshtastic/js";
|
import type { Protobuf } from "@meshtastic/js";
|
||||||
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;
|
||||||
to?: Protobuf.Mesh.NodeInfo;
|
to?: Protobuf.Mesh.NodeInfo;
|
||||||
route: Array<number>;
|
route: Array<number>;
|
||||||
|
routeBack?: Array<number>;
|
||||||
|
snrTowards?: Array<number>;
|
||||||
|
snrBack?: Array<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TraceRoute = ({
|
export const TraceRoute = ({
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
route,
|
route,
|
||||||
|
routeBack,
|
||||||
|
snrTowards,
|
||||||
|
snrBack,
|
||||||
}: TraceRouteProps): JSX.Element => {
|
}: TraceRouteProps): JSX.Element => {
|
||||||
const { nodes } = useDevice();
|
const { nodes } = useDevice();
|
||||||
|
|
||||||
return route.length === 0 ? (
|
return (
|
||||||
<div className="ml-5 flex">
|
<div className="ml-5 flex">
|
||||||
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
|
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
|
||||||
{to?.user?.longName}↔{from?.user?.longName}
|
<p className="font-semibold">Route to destination:</p>
|
||||||
</span>
|
<p>{to?.user?.longName}</p>
|
||||||
</div>
|
<p>↓ {snrTowards?.[0] ? snrTowards[0] : "??"}dB</p>
|
||||||
) : (
|
{route.map((hop, i) => (
|
||||||
<div className="ml-5 flex">
|
<span key={nodes.get(hop)?.num}>
|
||||||
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
|
<p>
|
||||||
{to?.user?.longName}↔
|
{nodes.get(hop)?.user?.longName ?? `!${numberToHexUnpadded(hop)}`}
|
||||||
{route.map((hop) => {
|
</p>
|
||||||
const node = nodes.get(hop);
|
<p>↓ {snrTowards?.[i + 1] ? snrTowards[i + 1] : "??"}dB</p>
|
||||||
return `${node?.user?.longName ?? (node?.num ? numberToHexUnpadded(node.num) : "Unknown")}↔`;
|
</span>
|
||||||
})}
|
))}
|
||||||
{from?.user?.longName}
|
{from?.user?.longName}
|
||||||
</span>
|
</span>
|
||||||
|
{routeBack ? (
|
||||||
|
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
|
||||||
|
<p className="font-semibold">Route back:</p>
|
||||||
|
<p>{from?.user?.longName}</p>
|
||||||
|
<p>↓ {snrBack?.[0] ? snrBack[0] : "??"}dB</p>
|
||||||
|
{routeBack.map((hop, i) => (
|
||||||
|
<span key={nodes.get(hop)?.num}>
|
||||||
|
<p>
|
||||||
|
{nodes.get(hop)?.user?.longName ??
|
||||||
|
`!${numberToHexUnpadded(hop)}`}
|
||||||
|
</p>
|
||||||
|
<p>↓ {snrBack?.[i + 1] ? snrBack[i + 1] : "??"}dB</p>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{to?.user?.longName}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||||
|
import { ChevronDownIcon } from "lucide-react";
|
||||||
|
import { type ComponentRef, forwardRef } from "react";
|
||||||
|
|
||||||
|
export const Accordion = AccordionPrimitive.Root;
|
||||||
|
|
||||||
|
export const AccordionHeader = AccordionPrimitive.Header;
|
||||||
|
|
||||||
|
export const AccordionItem = AccordionPrimitive.Item;
|
||||||
|
|
||||||
|
export const AccordionTrigger = forwardRef<
|
||||||
|
ComponentRef<typeof AccordionPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex justify-between items-center w-full p-4 border-b border-gray-200 dark:border-gray-800 group",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
<ChevronDownIcon
|
||||||
|
className="h-5 w-5 transition-transform duration-300 group-data-[state=open]:rotate-180"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
));
|
||||||
|
|
||||||
|
export const AccordionContent = forwardRef<
|
||||||
|
ComponentRef<typeof AccordionPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"p-4 border-b border-gray-200 dark:border-gray-800",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
@@ -15,7 +15,7 @@ const buttonVariants = cva(
|
|||||||
success:
|
success:
|
||||||
"bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600",
|
"bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600",
|
||||||
outline:
|
outline:
|
||||||
"bg-transparent border border-slate-200 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-100",
|
"bg-transparent border border-slate-200 hover:bg-slate-100 dark:border-slate-400 dark:text-slate-100",
|
||||||
subtle:
|
subtle:
|
||||||
"bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-700 dark:text-slate-100",
|
"bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-700 dark:text-slate-100",
|
||||||
ghost:
|
ghost:
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const DialogContent = React.forwardRef<
|
|||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed z-50 grid w-full scale-100 gap-4 bg-white p-6 opacity-100 animate-in fade-in-90 slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 sm:slide-in-from-bottom-0",
|
"fixed z-50 grid w-full max-w-[512px] max-h-[100vh] overflow-y-scroll scale-100 gap-4 bg-white p-6 opacity-100 animate-in fade-in-90 slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 sm:slide-in-from-bottom-0",
|
||||||
"dark:bg-slate-900",
|
"dark:bg-slate-900",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const toastVariants = cva(
|
|||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"border bg-background text-foreground dark:bg-slate-700 dark:border-slate-600 dark:text-slate-50",
|
"border bg-backgroundPrimary text-foreground dark:bg-slate-700 dark:border-slate-600 dark:text-slate-50",
|
||||||
destructive:
|
destructive:
|
||||||
"group destructive bg-red-600 text-white dark:border-red-900 dark:bg-red-900 dark:text-red-50",
|
"group destructive bg-red-600 text-white dark:border-red-900 dark:bg-red-900 dark:text-red-50",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const Tooltip = ({ ...props }) => <TooltipPrimitive.Root {...props} />;
|
|||||||
Tooltip.displayName = TooltipPrimitive.Tooltip.displayName;
|
Tooltip.displayName = TooltipPrimitive.Tooltip.displayName;
|
||||||
|
|
||||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||||
|
const TooltipArrow = TooltipPrimitive.Arrow;
|
||||||
|
|
||||||
const TooltipContent = React.forwardRef<
|
const TooltipContent = React.forwardRef<
|
||||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||||
@@ -26,4 +27,10 @@ const TooltipContent = React.forwardRef<
|
|||||||
));
|
));
|
||||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||||
|
|
||||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
export {
|
||||||
|
Tooltip,
|
||||||
|
TooltipTrigger,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipArrow,
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
export interface DeviceImageProps {
|
||||||
|
deviceType: string;
|
||||||
|
className?: React.HTMLAttributes<HTMLImageElement>["className"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const hardwareModelToFilename: { [key: string]: string } = {
|
||||||
|
DIY_V1: "diy.svg",
|
||||||
|
NANO_G2_ULTRA: "nano-g2-ultra.svg",
|
||||||
|
TBEAM: "tbeam.svg",
|
||||||
|
HELTEC_HT62: "heltec-ht62-esp32c3-sx1262.svg",
|
||||||
|
RPI_PICO: "pico.svg",
|
||||||
|
T_DECK: "t-deck.svg",
|
||||||
|
HELTEC_MESH_NODE_T114: "heltec-mesh-node-t114.svg",
|
||||||
|
HELTEC_MESH_NODE_T114_CASE: "heltec-mesh-node-t114-case.svg",
|
||||||
|
HELTEC_V3: "heltec-v3.svg",
|
||||||
|
HELTEC_V3_CASE: "heltec-v3-case.svg",
|
||||||
|
HELTEC_VISION_MASTER_E213: "heltec-vision-master-e213.svg",
|
||||||
|
HELTEC_VISION_MASTER_E290: "heltec-vision-master-e290.svg",
|
||||||
|
HELTEC_VISION_MASTER_T190: "heltec-vision-master-t190.svg",
|
||||||
|
HELTEC_WIRELESS_PAPER: "heltec-wireless-paper.svg",
|
||||||
|
HELTEC_WIRELESS_PAPER_V1_0: "heltec-wireless-paper-V1_0.svg",
|
||||||
|
HELTEC_WIRELESS_TRACKER: "heltec-wireless-tracker.svg",
|
||||||
|
HELTEC_WIRELESS_TRACKER_V1_0: "heltec-wireless-tracker-V1-0.svg",
|
||||||
|
HELTEC_WSL_V3: "heltec-wsl-v3.svg",
|
||||||
|
TLORA_C6: "tlora-c6.svg",
|
||||||
|
TLORA_T3_S3: "tlora-t3s3-v1.svg",
|
||||||
|
TLORA_T3_S3_EPAPER: "tlora-t3s3-epaper.svg",
|
||||||
|
TLORA_V2: "tlora-v2-1-1_6.svg",
|
||||||
|
TLORA_V2_1_1P6: "tlora-v2-1-1_6.svg",
|
||||||
|
TLORA_V2_1_1P8: "tlora-v2-1-1_8.svg",
|
||||||
|
RAK11310: "rak11310.svg",
|
||||||
|
RAK2560: "rak2560.svg",
|
||||||
|
RAK4631: "rak4631.svg",
|
||||||
|
RAK4631_CASE: "rak4631_case.svg",
|
||||||
|
WIO_WM1110: "wio-tracker-wm1110.svg",
|
||||||
|
WM1110_DEV_KIT: "wm1110_dev_kit.svg",
|
||||||
|
STATION_G2: "station-g2.svg",
|
||||||
|
TBEAM_V0P7: "tbeam-s3-core.svg",
|
||||||
|
T_ECHO: "t-echo.svg",
|
||||||
|
TRACKER_T1000_E: "tracker-t1000-e.svg",
|
||||||
|
T_WATCH_S3: "t-watch-s3.svg",
|
||||||
|
SEEED_XIAO_S3: "seeed-xiao-s3.svg",
|
||||||
|
SENSECAP_INDICATOR: "seeed-sensecap-indicator.svg",
|
||||||
|
PROMICRO: "promicro.svg",
|
||||||
|
RPIPICOW: "rpipicow.svg",
|
||||||
|
UNKNOWN: "unknown.svg",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DeviceImage = ({ deviceType, className }: DeviceImageProps) => {
|
||||||
|
const getPath = (device: string) => `/devices/${device}`;
|
||||||
|
const device = hardwareModelToFilename[deviceType] || "unknown.svg";
|
||||||
|
return <img className={className} src={getPath(device)} alt={device} />;
|
||||||
|
};
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import TimeAgoReact from "timeago-react";
|
|
||||||
|
|
||||||
export interface TimeAgoProps {
|
|
||||||
timestamp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TimeAgo = ({ timestamp }: TimeAgoProps): JSX.Element => {
|
|
||||||
return <TimeAgoReact datetime={timestamp} opts={{ minInterval: 10 }} />;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipPortal,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@radix-ui/react-tooltip";
|
||||||
|
import type { JSX } from "react";
|
||||||
|
|
||||||
|
export interface TimeAgoProps {
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTimeAgo = (
|
||||||
|
unixTimestamp: number,
|
||||||
|
locale: Intl.LocalesArgument = "en",
|
||||||
|
): string => {
|
||||||
|
const timestamp = new Date(unixTimestamp);
|
||||||
|
const diff = (new Date().getTime() - timestamp.getTime()) / 1000;
|
||||||
|
|
||||||
|
const minutes = Math.floor(diff / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
const months = Math.floor(days / 30);
|
||||||
|
const years = Math.floor(months / 12);
|
||||||
|
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
|
||||||
|
|
||||||
|
if (years > 0) {
|
||||||
|
return rtf.format(0 - years, "year");
|
||||||
|
}
|
||||||
|
if (months > 0) {
|
||||||
|
return rtf.format(0 - months, "month");
|
||||||
|
}
|
||||||
|
if (days > 0) {
|
||||||
|
return rtf.format(0 - days, "day");
|
||||||
|
}
|
||||||
|
if (hours > 0) {
|
||||||
|
return rtf.format(0 - hours, "hour");
|
||||||
|
}
|
||||||
|
if (minutes > 0) {
|
||||||
|
return rtf.format(0 - minutes, "minute");
|
||||||
|
}
|
||||||
|
return rtf.format(Math.floor(0 - diff), "second");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TimeAgo = ({ timestamp }: TimeAgoProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<span>{getTimeAgo(timestamp)}</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPortal>
|
||||||
|
<TooltipContent
|
||||||
|
className="rounded-md bg-slate-800 px-3 py-1.5 text-sm text-white shadow-md animate-in fade-in-0 zoom-in-95"
|
||||||
|
side="top"
|
||||||
|
align="center"
|
||||||
|
sideOffset={5}
|
||||||
|
>
|
||||||
|
{new Date(timestamp).toLocaleString()}
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPortal>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { JSX } from "react";
|
||||||
|
|
||||||
|
export interface UptimeProps {
|
||||||
|
seconds: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUptime = (seconds: number): string => {
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||||||
|
const minutes = Math.floor(((seconds % 86400) % 3600) / 60);
|
||||||
|
const secondsLeft = Math.floor(((seconds % 86400) % 3600) % 60);
|
||||||
|
return `${days}d ${hours}h ${minutes}m ${secondsLeft}s`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Uptime = ({ seconds }: UptimeProps): JSX.Element => {
|
||||||
|
return <span>{getUptime(seconds)}</span>;
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Types } from "@meshtastic/js";
|
||||||
import { produce } from "immer";
|
import { produce } from "immer";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
@@ -29,6 +30,9 @@ interface AppState {
|
|||||||
nodeNumToBeRemoved: number;
|
nodeNumToBeRemoved: number;
|
||||||
accent: AccentColor;
|
accent: AccentColor;
|
||||||
connectDialogOpen: boolean;
|
connectDialogOpen: boolean;
|
||||||
|
nodeNumDetails: number;
|
||||||
|
activeChat: number;
|
||||||
|
chatType: "broadcast" | "direct";
|
||||||
|
|
||||||
setRasterSources: (sources: RasterSource[]) => void;
|
setRasterSources: (sources: RasterSource[]) => void;
|
||||||
addRasterSource: (source: RasterSource) => void;
|
addRasterSource: (source: RasterSource) => void;
|
||||||
@@ -42,6 +46,9 @@ interface AppState {
|
|||||||
setNodeNumToBeRemoved: (nodeNum: number) => void;
|
setNodeNumToBeRemoved: (nodeNum: number) => void;
|
||||||
setAccent: (color: AccentColor) => void;
|
setAccent: (color: AccentColor) => void;
|
||||||
setConnectDialogOpen: (open: boolean) => void;
|
setConnectDialogOpen: (open: boolean) => void;
|
||||||
|
setNodeNumDetails: (nodeNum: number) => void;
|
||||||
|
setActiveChat: (chat: number) => void;
|
||||||
|
setChatType: (type: "broadcast" | "direct") => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAppStore = create<AppState>()((set) => ({
|
export const useAppStore = create<AppState>()((set) => ({
|
||||||
@@ -57,6 +64,9 @@ export const useAppStore = create<AppState>()((set) => ({
|
|||||||
accent: "orange",
|
accent: "orange",
|
||||||
connectDialogOpen: false,
|
connectDialogOpen: false,
|
||||||
nodeNumToBeRemoved: 0,
|
nodeNumToBeRemoved: 0,
|
||||||
|
nodeNumDetails: 0,
|
||||||
|
activeChat: Types.ChannelNumber.Primary,
|
||||||
|
chatType: "broadcast",
|
||||||
|
|
||||||
setRasterSources: (sources: RasterSource[]) => {
|
setRasterSources: (sources: RasterSource[]) => {
|
||||||
set(
|
set(
|
||||||
@@ -124,4 +134,16 @@ export const useAppStore = create<AppState>()((set) => ({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
setNodeNumDetails: (nodeNum) =>
|
||||||
|
set((state) => ({
|
||||||
|
nodeNumDetails: nodeNum,
|
||||||
|
})),
|
||||||
|
setActiveChat: (chat) =>
|
||||||
|
set(() => ({
|
||||||
|
activeChat: chat,
|
||||||
|
})),
|
||||||
|
setChatType: (type) =>
|
||||||
|
set(() => ({
|
||||||
|
chatType: type,
|
||||||
|
})),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ export type DialogVariant =
|
|||||||
| "reboot"
|
| "reboot"
|
||||||
| "deviceName"
|
| "deviceName"
|
||||||
| "nodeRemoval"
|
| "nodeRemoval"
|
||||||
| "pkiBackup";
|
| "pkiBackup"
|
||||||
|
| "nodeDetails";
|
||||||
|
|
||||||
export interface Device {
|
export interface Device {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -62,6 +63,7 @@ export interface Device {
|
|||||||
deviceName: boolean;
|
deviceName: boolean;
|
||||||
nodeRemoval: boolean;
|
nodeRemoval: boolean;
|
||||||
pkiBackup: boolean;
|
pkiBackup: boolean;
|
||||||
|
nodeDetails: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
setStatus: (status: Types.DeviceStatusEnum) => void;
|
setStatus: (status: Types.DeviceStatusEnum) => void;
|
||||||
@@ -145,6 +147,7 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
|||||||
deviceName: false,
|
deviceName: false,
|
||||||
nodeRemoval: false,
|
nodeRemoval: false,
|
||||||
pkiBackup: false,
|
pkiBackup: false,
|
||||||
|
nodeDetails: false,
|
||||||
},
|
},
|
||||||
pendingSettingsChanges: false,
|
pendingSettingsChanges: false,
|
||||||
messageDraft: "",
|
messageDraft: "",
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
interface PluralForms {
|
||||||
|
one: string;
|
||||||
|
other: string;
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormatOptions {
|
||||||
|
locale?: string;
|
||||||
|
pluralRules?: Intl.PluralRulesOptions;
|
||||||
|
numberFormat?: Intl.NumberFormatOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatQuantity(
|
||||||
|
value: number,
|
||||||
|
forms: PluralForms,
|
||||||
|
options: FormatOptions = {},
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
locale = "en-US",
|
||||||
|
pluralRules: pluralOptions = { type: "cardinal" },
|
||||||
|
numberFormat: numberOptions = {},
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const pluralRules = new Intl.PluralRules(locale, pluralOptions);
|
||||||
|
const numberFormat = new Intl.NumberFormat(locale, numberOptions);
|
||||||
|
|
||||||
|
const pluralCategory = pluralRules.select(value);
|
||||||
|
const word = forms[pluralCategory];
|
||||||
|
|
||||||
|
return `${numberFormat.format(value)} ${word}`;
|
||||||
|
}
|
||||||
@@ -1,59 +1,93 @@
|
|||||||
import { NodeDetail } from "@app/components/PageComponents/Map/NodeDetail";
|
import { NodeDetail } from "@app/components/PageComponents/Map/NodeDetail";
|
||||||
import { Avatar } from "@app/components/UI/Avatar";
|
import { Avatar } from "@app/components/UI/Avatar";
|
||||||
import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
|
|
||||||
import { cn } from "@app/core/utils/cn.ts";
|
|
||||||
import { PageLayout } from "@components/PageLayout.tsx";
|
import { PageLayout } from "@components/PageLayout.tsx";
|
||||||
import { Sidebar } from "@components/Sidebar.tsx";
|
import { Sidebar } from "@components/Sidebar.tsx";
|
||||||
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
|
|
||||||
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.tsx";
|
|
||||||
import { useAppStore } from "@core/stores/appStore.ts";
|
import { useAppStore } from "@core/stores/appStore.ts";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import type { Protobuf } from "@meshtastic/js";
|
import type { Protobuf } from "@meshtastic/js";
|
||||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
|
||||||
import { bbox, lineString } from "@turf/turf";
|
import { bbox, lineString } from "@turf/turf";
|
||||||
import {
|
import { MapPinIcon } from "lucide-react";
|
||||||
BoxSelectIcon,
|
|
||||||
MapPinIcon,
|
|
||||||
ZoomInIcon,
|
|
||||||
ZoomOutIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { type JSX, useCallback, useEffect, useMemo, useState } from "react";
|
import { type JSX, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { AttributionControl, Marker, Popup, useMap } from "react-map-gl";
|
import {
|
||||||
|
AttributionControl,
|
||||||
|
GeolocateControl,
|
||||||
|
Marker,
|
||||||
|
NavigationControl,
|
||||||
|
Popup,
|
||||||
|
ScaleControl,
|
||||||
|
useMap,
|
||||||
|
} from "react-map-gl";
|
||||||
import MapGl from "react-map-gl/maplibre";
|
import MapGl from "react-map-gl/maplibre";
|
||||||
|
|
||||||
|
type NodePosition = {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertToLatLng = (position: {
|
||||||
|
latitudeI?: number;
|
||||||
|
longitudeI?: number;
|
||||||
|
}): NodePosition => ({
|
||||||
|
latitude: (position.latitudeI ?? 0) / 1e7,
|
||||||
|
longitude: (position.longitudeI ?? 0) / 1e7,
|
||||||
|
});
|
||||||
|
|
||||||
const MapPage = (): JSX.Element => {
|
const MapPage = (): JSX.Element => {
|
||||||
const { nodes, waypoints } = useDevice();
|
const { nodes, waypoints } = useDevice();
|
||||||
const { rasterSources, darkMode } = useAppStore();
|
const { darkMode } = useAppStore();
|
||||||
const { default: map } = useMap();
|
const { default: map } = useMap();
|
||||||
|
|
||||||
const [zoom, setZoom] = useState(0);
|
|
||||||
const [selectedNode, setSelectedNode] =
|
const [selectedNode, setSelectedNode] =
|
||||||
useState<Protobuf.Mesh.NodeInfo | null>(null);
|
useState<Protobuf.Mesh.NodeInfo | null>(null);
|
||||||
|
|
||||||
const allNodes = useMemo(() => Array.from(nodes.values()), [nodes]);
|
// Filter out nodes without a valid position
|
||||||
|
const validNodes = useMemo(
|
||||||
|
() =>
|
||||||
|
Array.from(nodes.values()).filter(
|
||||||
|
(node): node is Protobuf.Mesh.NodeInfo =>
|
||||||
|
Boolean(node.position?.latitudeI),
|
||||||
|
),
|
||||||
|
[nodes],
|
||||||
|
);
|
||||||
|
|
||||||
const getBBox = useCallback(() => {
|
const handleMarkerClick = useCallback(
|
||||||
|
(node: Protobuf.Mesh.NodeInfo, event: { originalEvent: MouseEvent }) => {
|
||||||
|
event?.originalEvent?.stopPropagation();
|
||||||
|
|
||||||
|
setSelectedNode(node);
|
||||||
|
|
||||||
|
if (map) {
|
||||||
|
const position = convertToLatLng(node.position);
|
||||||
|
map.easeTo({
|
||||||
|
center: [position.longitude, position.latitude],
|
||||||
|
zoom: map?.getZoom(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[map],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the bounds of the map based on the nodes furtherest away from center
|
||||||
|
const getMapBounds = useCallback(() => {
|
||||||
if (!map) {
|
if (!map) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nodesWithPosition = allNodes.filter(
|
|
||||||
(node) => node.position?.latitudeI,
|
if (!validNodes.length) {
|
||||||
);
|
|
||||||
if (!nodesWithPosition.length) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (nodesWithPosition.length === 1) {
|
if (validNodes.length === 1) {
|
||||||
map.easeTo({
|
map.easeTo({
|
||||||
zoom: 12,
|
zoom: map.getZoom(),
|
||||||
center: [
|
center: [
|
||||||
(nodesWithPosition[0].position?.longitudeI ?? 0) / 1e7,
|
(validNodes[0].position?.longitudeI ?? 0) / 1e7,
|
||||||
(nodesWithPosition[0].position?.latitudeI ?? 0) / 1e7,
|
(validNodes[0].position?.latitudeI ?? 0) / 1e7,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const line = lineString(
|
const line = lineString(
|
||||||
nodesWithPosition.map((n) => [
|
validNodes.map((n) => [
|
||||||
(n.position?.latitudeI ?? 0) / 1e7,
|
(n.position?.latitudeI ?? 0) / 1e7,
|
||||||
(n.position?.longitudeI ?? 0) / 1e7,
|
(n.position?.longitudeI ?? 0) / 1e7,
|
||||||
]),
|
]),
|
||||||
@@ -69,78 +103,54 @@ const MapPage = (): JSX.Element => {
|
|||||||
if (center) {
|
if (center) {
|
||||||
map.easeTo(center);
|
map.easeTo(center);
|
||||||
}
|
}
|
||||||
}, [allNodes, map]);
|
}, [validNodes, map]);
|
||||||
|
|
||||||
useEffect(() => {
|
// Generate all markers
|
||||||
map?.on("zoom", () => {
|
const markers = useMemo(
|
||||||
setZoom(map?.getZoom() ?? 0);
|
() =>
|
||||||
});
|
validNodes.map((node) => {
|
||||||
}, [map]);
|
const position = convertToLatLng(node.position);
|
||||||
|
return (
|
||||||
|
<Marker
|
||||||
|
key={`marker-${node.num}`}
|
||||||
|
longitude={position.longitude}
|
||||||
|
latitude={position.latitude}
|
||||||
|
anchor="bottom"
|
||||||
|
onClick={(e) => handleMarkerClick(node, e)}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
text={node.user?.shortName?.toString() ?? node.num.toString()}
|
||||||
|
className="border-[1.5px] border-slate-600 shadow-xl shadow-slate-600"
|
||||||
|
/>
|
||||||
|
</Marker>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
[validNodes, handleMarkerClick],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
map?.on("load", () => {
|
map?.on("load", () => {
|
||||||
getBBox();
|
getMapBounds();
|
||||||
});
|
});
|
||||||
}, [map, getBBox]);
|
}, [map, getMapBounds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Sidebar>
|
<Sidebar />
|
||||||
<SidebarSection label="Sources">
|
<PageLayout label="Map" noPadding={true} actions={[]}>
|
||||||
{rasterSources.map((source) => (
|
|
||||||
<SidebarButton key={source.title} label={source.title} />
|
|
||||||
))}
|
|
||||||
</SidebarSection>
|
|
||||||
</Sidebar>
|
|
||||||
<PageLayout
|
|
||||||
label="Map"
|
|
||||||
noPadding={true}
|
|
||||||
actions={[
|
|
||||||
{
|
|
||||||
icon: ZoomInIcon,
|
|
||||||
onClick() {
|
|
||||||
map?.zoomIn();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: ZoomOutIcon,
|
|
||||||
onClick() {
|
|
||||||
map?.zoomOut();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: BoxSelectIcon,
|
|
||||||
onClick() {
|
|
||||||
getBBox();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<MapGl
|
<MapGl
|
||||||
mapStyle="https://raw.githubusercontent.com/hc-oss/maplibre-gl-styles/master/styles/osm-mapnik/v8/default.json"
|
mapStyle="https://raw.githubusercontent.com/hc-oss/maplibre-gl-styles/master/styles/osm-mapnik/v8/default.json"
|
||||||
// onClick={(e) => {
|
|
||||||
// const waypoint = new Protobuf.Waypoint({
|
|
||||||
// name: "test",
|
|
||||||
// description: "test description",
|
|
||||||
// latitudeI: Math.trunc(e.lngLat.lat * 1e7),
|
|
||||||
// longitudeI: Math.trunc(e.lngLat.lng * 1e7)
|
|
||||||
// });
|
|
||||||
// addWaypoint(waypoint);
|
|
||||||
// connection?.sendWaypoint(waypoint, "broadcast");
|
|
||||||
// }}
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
|
|
||||||
attributionControl={false}
|
attributionControl={false}
|
||||||
renderWorldCopies={false}
|
renderWorldCopies={false}
|
||||||
maxPitch={0}
|
maxPitch={0}
|
||||||
|
antialias={true}
|
||||||
style={{
|
style={{
|
||||||
filter: darkMode ? "brightness(0.8)" : "",
|
filter: darkMode ? "brightness(0.9)" : "",
|
||||||
}}
|
}}
|
||||||
dragRotate={false}
|
dragRotate={false}
|
||||||
touchZoomRotate={false}
|
touchZoomRotate={false}
|
||||||
initialViewState={{
|
initialViewState={{
|
||||||
zoom: 1.6,
|
zoom: 1.8,
|
||||||
latitude: 35,
|
latitude: 35,
|
||||||
longitude: 0,
|
longitude: 0,
|
||||||
}}
|
}}
|
||||||
@@ -151,6 +161,14 @@ const MapPage = (): JSX.Element => {
|
|||||||
color: darkMode ? "black" : "",
|
color: darkMode ? "black" : "",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<GeolocateControl
|
||||||
|
position="top-right"
|
||||||
|
positionOptions={{ enableHighAccuracy: true }}
|
||||||
|
trackUserLocation
|
||||||
|
/>
|
||||||
|
<NavigationControl position="top-right" showCompass={false} />
|
||||||
|
|
||||||
|
<ScaleControl />
|
||||||
{waypoints.map((wp) => (
|
{waypoints.map((wp) => (
|
||||||
<Marker
|
<Marker
|
||||||
key={wp.id}
|
key={wp.id}
|
||||||
@@ -163,58 +181,17 @@ const MapPage = (): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
</Marker>
|
</Marker>
|
||||||
))}
|
))}
|
||||||
{/* {rasterSources.map((source, index) => (
|
{markers}
|
||||||
<Source key={index} type="raster" {...source}>
|
{selectedNode ? (
|
||||||
<Layer type="raster" />
|
|
||||||
</Source>
|
|
||||||
))} */}
|
|
||||||
{allNodes.map((node) => {
|
|
||||||
if (node.position?.latitudeI && node.num !== selectedNode?.num) {
|
|
||||||
return (
|
|
||||||
<Marker
|
|
||||||
key={node.num}
|
|
||||||
longitude={(node.position.longitudeI ?? 0) / 1e7}
|
|
||||||
latitude={(node.position.latitudeI ?? 0) / 1e7}
|
|
||||||
// style={{ filter: darkMode ? "invert(1)" : "" }}
|
|
||||||
anchor="bottom"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedNode(node);
|
|
||||||
map?.easeTo({
|
|
||||||
zoom: 12,
|
|
||||||
center: [
|
|
||||||
(node.position?.longitudeI ?? 0) / 1e7,
|
|
||||||
(node.position?.latitudeI ?? 0) / 1e7,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex cursor-pointer gap-2 rounded-md bg-transparent p-1.5">
|
|
||||||
<Avatar
|
|
||||||
text={
|
|
||||||
node.user?.shortName.toString() ?? node.num.toString()
|
|
||||||
}
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
<Subtle className={cn(zoom < 12 && "hidden")}>
|
|
||||||
{node.user?.longName ||
|
|
||||||
`!${numberToHexUnpadded(node.num)}`}
|
|
||||||
</Subtle>
|
|
||||||
</div>
|
|
||||||
</Marker>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
{selectedNode?.position && (
|
|
||||||
<Popup
|
<Popup
|
||||||
longitude={(selectedNode.position.longitudeI ?? 0) / 1e7}
|
anchor="top"
|
||||||
latitude={(selectedNode.position.latitudeI ?? 0) / 1e7}
|
longitude={convertToLatLng(selectedNode.position).longitude}
|
||||||
anchor="left"
|
latitude={convertToLatLng(selectedNode.position).latitude}
|
||||||
closeOnClick={false}
|
|
||||||
onClose={() => setSelectedNode(null)}
|
onClose={() => setSelectedNode(null)}
|
||||||
>
|
>
|
||||||
<NodeDetail node={selectedNode} />
|
<NodeDetail node={selectedNode} />
|
||||||
</Popup>
|
</Popup>
|
||||||
)}
|
) : null}
|
||||||
</MapGl>
|
</MapGl>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useAppStore } from "@app/core/stores/appStore";
|
||||||
import { ChannelChat } from "@components/PageComponents/Messages/ChannelChat.tsx";
|
import { ChannelChat } from "@components/PageComponents/Messages/ChannelChat.tsx";
|
||||||
import { PageLayout } from "@components/PageLayout.tsx";
|
import { PageLayout } from "@components/PageLayout.tsx";
|
||||||
import { Sidebar } from "@components/Sidebar.tsx";
|
import { Sidebar } from "@components/Sidebar.tsx";
|
||||||
@@ -5,21 +6,17 @@ import { Avatar } from "@components/UI/Avatar.tsx";
|
|||||||
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
|
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
|
||||||
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.tsx";
|
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.tsx";
|
||||||
import { useToast } from "@core/hooks/useToast.ts";
|
import { useToast } from "@core/hooks/useToast.ts";
|
||||||
import { Device, useDevice, useDeviceStore } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf, Types } from "@meshtastic/js";
|
import { Protobuf, Types } from "@meshtastic/js";
|
||||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||||
import { getChannelName } from "@pages/Channels.tsx";
|
import { getChannelName } from "@pages/Channels.tsx";
|
||||||
import { HashIcon, LockIcon, LockOpenIcon, WaypointsIcon } from "lucide-react";
|
import { HashIcon, LockIcon, LockOpenIcon, WaypointsIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
const MessagesPage = () => {
|
export const MessagesPage = () => {
|
||||||
const { channels, nodes, hardware, messages, traceroutes, connection } =
|
const { channels, nodes, hardware, messages, traceroutes, connection } =
|
||||||
useDevice();
|
useDevice();
|
||||||
const [chatType, setChatType] =
|
const { activeChat, chatType, setActiveChat, setChatType } = useAppStore();
|
||||||
useState<Types.PacketDestination>("broadcast");
|
|
||||||
const [activeChat, setActiveChat] = useState<number>(
|
|
||||||
Types.ChannelNumber.Primary,
|
|
||||||
);
|
|
||||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||||
const filteredNodes = Array.from(nodes.values()).filter((node) => {
|
const filteredNodes = Array.from(nodes.values()).filter((node) => {
|
||||||
if (node.num === hardware.myNodeNum) return false;
|
if (node.num === hardware.myNodeNum) return false;
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
|
import { LocationResponseDialog } from "@app/components/Dialog/LocationResponseDialog";
|
||||||
|
import { NodeOptionsDialog } from "@app/components/Dialog/NodeOptionsDialog";
|
||||||
|
import { TracerouteResponseDialog } from "@app/components/Dialog/TracerouteResponseDialog";
|
||||||
import Footer from "@app/components/UI/Footer";
|
import Footer from "@app/components/UI/Footer";
|
||||||
import { useAppStore } from "@app/core/stores/appStore";
|
|
||||||
import { Sidebar } from "@components/Sidebar.tsx";
|
import { Sidebar } from "@components/Sidebar.tsx";
|
||||||
|
import { Avatar } from "@components/UI/Avatar.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 { Table } from "@components/generic/Table/index.tsx";
|
import { Table } from "@components/generic/Table/index.tsx";
|
||||||
import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.tsx";
|
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf, type Types } from "@meshtastic/js";
|
||||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||||
import { LockIcon, LockOpenIcon, TrashIcon } from "lucide-react";
|
import { LockIcon, LockOpenIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { Fragment, type JSX, useCallback, useEffect, useState } from "react";
|
||||||
import { Fragment, type JSX } from "react";
|
|
||||||
import { base16 } from "rfc4648";
|
import { base16 } from "rfc4648";
|
||||||
|
|
||||||
export interface DeleteNoteDialogProps {
|
export interface DeleteNoteDialogProps {
|
||||||
@@ -19,8 +21,16 @@ export interface DeleteNoteDialogProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const NodesPage = (): JSX.Element => {
|
const NodesPage = (): JSX.Element => {
|
||||||
const { nodes, hardware, setDialogOpen } = useDevice();
|
const { nodes, hardware, connection } = useDevice();
|
||||||
const { setNodeNumToBeRemoved } = useAppStore();
|
const [selectedNode, setSelectedNode] = useState<
|
||||||
|
Protobuf.Mesh.NodeInfo | undefined
|
||||||
|
>(undefined);
|
||||||
|
const [selectedTraceroute, setSelectedTraceroute] = useState<
|
||||||
|
Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery> | undefined
|
||||||
|
>();
|
||||||
|
const [selectedLocation, setSelectedLocation] = useState<
|
||||||
|
Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery> | undefined
|
||||||
|
>();
|
||||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||||
|
|
||||||
const filteredNodes = Array.from(nodes.values()).filter((node) => {
|
const filteredNodes = Array.from(nodes.values()).filter((node) => {
|
||||||
@@ -29,6 +39,36 @@ const NodesPage = (): JSX.Element => {
|
|||||||
return nodeName.toLowerCase().includes(searchTerm.toLowerCase());
|
return nodeName.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!connection) return;
|
||||||
|
connection.events.onTraceRoutePacket.subscribe(handleTraceroute);
|
||||||
|
return () => {
|
||||||
|
connection.events.onTraceRoutePacket.unsubscribe(handleTraceroute);
|
||||||
|
};
|
||||||
|
}, [connection]);
|
||||||
|
|
||||||
|
const handleTraceroute = useCallback(
|
||||||
|
(traceroute: Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>) => {
|
||||||
|
setSelectedTraceroute(traceroute);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!connection) return;
|
||||||
|
connection.events.onPositionPacket.subscribe(handleLocation);
|
||||||
|
return () => {
|
||||||
|
connection.events.onPositionPacket.subscribe(handleLocation);
|
||||||
|
};
|
||||||
|
}, [connection]);
|
||||||
|
|
||||||
|
const handleLocation = useCallback(
|
||||||
|
(location: Types.PacketMetadata<Protobuf.Mesh.Position>) => {
|
||||||
|
setSelectedLocation(location);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
@@ -46,22 +86,38 @@ const NodesPage = (): JSX.Element => {
|
|||||||
<Table
|
<Table
|
||||||
headings={[
|
headings={[
|
||||||
{ title: "", type: "blank", sortable: false },
|
{ title: "", type: "blank", sortable: false },
|
||||||
{ title: "Name", type: "normal", sortable: true },
|
{ title: "Short Name", type: "normal", sortable: true },
|
||||||
|
{ title: "Long Name", type: "normal", sortable: true },
|
||||||
{ title: "Model", type: "normal", sortable: true },
|
{ title: "Model", type: "normal", sortable: true },
|
||||||
{ title: "MAC Address", type: "normal", sortable: true },
|
{ title: "MAC Address", type: "normal", sortable: true },
|
||||||
{ title: "Last Heard", type: "normal", sortable: true },
|
{ title: "Last Heard", type: "normal", sortable: true },
|
||||||
{ title: "SNR", type: "normal", sortable: true },
|
{ title: "SNR", type: "normal", sortable: true },
|
||||||
{ title: "Encryption", type: "normal", sortable: false },
|
{ title: "Encryption", type: "normal", sortable: false },
|
||||||
{ title: "Connection", type: "normal", sortable: true },
|
{ title: "Connection", type: "normal", sortable: true },
|
||||||
{ title: "Remove", type: "normal", sortable: false },
|
|
||||||
]}
|
]}
|
||||||
rows={filteredNodes.map((node) => [
|
rows={filteredNodes.map((node) => [
|
||||||
<span
|
<div key={node.num}>
|
||||||
key={node.num}
|
<Avatar text={node.user?.shortName.toString() ?? "UNK"} />
|
||||||
className="h-3 w-3 rounded-full bg-accent"
|
</div>,
|
||||||
/>,
|
|
||||||
|
|
||||||
<h1 key="header">
|
<h1
|
||||||
|
key="shortName"
|
||||||
|
onMouseDown={() => setSelectedNode(node)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
{node.user?.shortName ??
|
||||||
|
(node.user?.macaddr
|
||||||
|
? `${base16
|
||||||
|
.stringify(node.user?.macaddr.subarray(4, 6) ?? [])
|
||||||
|
.toLowerCase()}`
|
||||||
|
: `${numberToHexUnpadded(node.num).slice(-4)}`)}
|
||||||
|
</h1>,
|
||||||
|
|
||||||
|
<h1
|
||||||
|
key="longName"
|
||||||
|
onMouseDown={() => setSelectedNode(node)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
{node.user?.longName ??
|
{node.user?.longName ??
|
||||||
(node.user?.macaddr
|
(node.user?.macaddr
|
||||||
? `Meshtastic ${base16
|
? `Meshtastic ${base16
|
||||||
@@ -95,7 +151,7 @@ const NodesPage = (): JSX.Element => {
|
|||||||
{node.user?.publicKey && node.user?.publicKey.length > 0 ? (
|
{node.user?.publicKey && node.user?.publicKey.length > 0 ? (
|
||||||
<LockIcon className="text-green-600" />
|
<LockIcon className="text-green-600" />
|
||||||
) : (
|
) : (
|
||||||
<LockOpenIcon className="text-yellow-300" />
|
<LockOpenIcon className="text-yellow-300 mx-auto" />
|
||||||
)}
|
)}
|
||||||
</Mono>,
|
</Mono>,
|
||||||
<Mono key="hops">
|
<Mono key="hops">
|
||||||
@@ -108,19 +164,23 @@ const NodesPage = (): JSX.Element => {
|
|||||||
: "-"}
|
: "-"}
|
||||||
{node.viaMqtt === true ? ", via MQTT" : ""}
|
{node.viaMqtt === true ? ", via MQTT" : ""}
|
||||||
</Mono>,
|
</Mono>,
|
||||||
<Button
|
|
||||||
key="remove"
|
|
||||||
variant="destructive"
|
|
||||||
onClick={() => {
|
|
||||||
setNodeNumToBeRemoved(node.num);
|
|
||||||
setDialogOpen("nodeRemoval", true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TrashIcon />
|
|
||||||
Remove
|
|
||||||
</Button>,
|
|
||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
|
<NodeOptionsDialog
|
||||||
|
node={selectedNode}
|
||||||
|
open={!!selectedNode}
|
||||||
|
onOpenChange={() => setSelectedNode(undefined)}
|
||||||
|
/>
|
||||||
|
<TracerouteResponseDialog
|
||||||
|
traceroute={selectedTraceroute}
|
||||||
|
open={!!selectedTraceroute}
|
||||||
|
onOpenChange={() => setSelectedTraceroute(undefined)}
|
||||||
|
/>
|
||||||
|
<LocationResponseDialog
|
||||||
|
location={selectedLocation}
|
||||||
|
open={!!selectedLocation}
|
||||||
|
onOpenChange={() => setSelectedLocation(undefined)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||