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-qrcode-logo": "^3.0.0",
|
||||
"rfc4648": "^1.5.4",
|
||||
"timeago-react": "^3.0.6",
|
||||
"vite-plugin-node-polyfills": "^0.23.0",
|
||||
"zustand": "5.0.3"
|
||||
},
|
||||
|
||||
@@ -113,9 +113,6 @@ importers:
|
||||
rfc4648:
|
||||
specifier: ^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:
|
||||
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))
|
||||
@@ -2860,14 +2857,6 @@ packages:
|
||||
through2@4.0.2:
|
||||
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:
|
||||
resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==}
|
||||
engines: {node: '>=0.6.0'}
|
||||
@@ -6629,13 +6618,6 @@ snapshots:
|
||||
dependencies:
|
||||
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:
|
||||
dependencies:
|
||||
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 { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import type { JSX } from "react";
|
||||
import { NodeDetailsDialog } from "./NodeDetailsDialog";
|
||||
|
||||
export const DialogManager = (): JSX.Element => {
|
||||
const { channels, config, dialog, setDialogOpen } = useDevice();
|
||||
@@ -56,6 +58,12 @@ export const DialogManager = (): JSX.Element => {
|
||||
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,
|
||||
)
|
||||
? "meshtastic.local"
|
||||
: window.location.hostname,
|
||||
: window.location.host,
|
||||
tls: location.protocol === "https:",
|
||||
},
|
||||
});
|
||||
|
||||
const tlsEnabled = useWatch({
|
||||
control,
|
||||
name: "tls",
|
||||
defaultValue: location.protocol === "https:",
|
||||
});
|
||||
|
||||
const [connectionInProgress, setConnectionInProgress] = useState(false);
|
||||
const [https, setHTTPS] = useState(false);
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
setConnectionInProgress(true);
|
||||
@@ -46,7 +41,7 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
||||
await connection.connect({
|
||||
address: data.ip,
|
||||
fetchInterval: 2000,
|
||||
tls: data.tls,
|
||||
tls: https,
|
||||
});
|
||||
|
||||
setSelectedDevice(id);
|
||||
@@ -60,8 +55,7 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
||||
<div className="flex h-48 flex-col gap-2">
|
||||
<Label>IP Address/Hostname</Label>
|
||||
<Input
|
||||
// label="IP Address/Hostname"
|
||||
prefix={tlsEnabled ? "https://" : "http://"}
|
||||
prefix={https ? "https://" : "http://"}
|
||||
placeholder="000.000.000.000 / meshtastic.local"
|
||||
disabled={connectionInProgress}
|
||||
{...register("ip")}
|
||||
@@ -71,8 +65,12 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<>
|
||||
<Label>Use TLS</Label>
|
||||
<Label>Use HTTPS</Label>
|
||||
<Switch
|
||||
onCheckedChange={(checked) => {
|
||||
checked ? setHTTPS(true) : setHTTPS(false);
|
||||
}}
|
||||
|
||||
// label="Use TLS"
|
||||
// description="Description"
|
||||
disabled={
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Separator } from "@app/components/UI/Seperator";
|
||||
import { H5 } from "@app/components/UI/Typography/H5.tsx";
|
||||
import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
|
||||
import { formatQuantity } from "@app/core/utils/string";
|
||||
import { Avatar } from "@components/UI/Avatar";
|
||||
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 type { Protobuf as ProtobufType } from "@meshtastic/js";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
@@ -25,12 +26,12 @@ export interface NodeDetailProps {
|
||||
|
||||
export const NodeDetail = ({ node }: NodeDetailProps) => {
|
||||
const name = node.user?.longName || `!${numberToHexUnpadded(node.num)}`;
|
||||
const hardwareType = Protobuf.Mesh.HardwareModel[
|
||||
node.user?.hwModel ?? 0
|
||||
].replaceAll("_", " ");
|
||||
const hwModel = node.user?.hwModel ?? 0;
|
||||
const hardwareType =
|
||||
Protobuf.Mesh.HardwareModel[hwModel]?.replaceAll("_", " ") ?? `${hwModel}`;
|
||||
|
||||
return (
|
||||
<div className="dark:text-black">
|
||||
<div className="dark:text-black p-1">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col items-center gap-2 min-w-6 pt-1">
|
||||
<Avatar text={node.user?.shortName} />
|
||||
@@ -132,7 +133,12 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
|
||||
className="ml-2 mr-1"
|
||||
aria-label="Elevation"
|
||||
/>
|
||||
<div>{node.position?.altitude} ft</div>
|
||||
<div>
|
||||
{formatQuantity(node.position?.altitude, {
|
||||
one: "meter",
|
||||
other: "meters",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -51,11 +51,11 @@ export const ChannelChat = ({
|
||||
|
||||
if (!messages?.length) {
|
||||
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">
|
||||
<EmptyState />
|
||||
</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} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,23 +63,25 @@ export const ChannelChat = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full w-full">
|
||||
<div className="flex-1 overflow-y-scroll w-full" ref={scrollContainerRef}>
|
||||
<div className="w-full h-full flex flex-col justify-end">
|
||||
{messages.map((message, index) => (
|
||||
<Message
|
||||
key={message.id}
|
||||
message={message}
|
||||
lastMsgSameUser={
|
||||
index > 0 && messages[index - 1].from === message.from
|
||||
}
|
||||
sender={nodes.get(message.from)}
|
||||
/>
|
||||
))}
|
||||
<div className="flex flex-col h-full w-full container mx-auto">
|
||||
<div className="flex-1 overflow-y-auto" ref={scrollContainerRef}>
|
||||
<div className="w-full h-full flex flex-col justify-end pl-4 pr-44">
|
||||
{messages.map((message, index) => {
|
||||
return (
|
||||
<Message
|
||||
key={message.id}
|
||||
message={message}
|
||||
sender={nodes.get(message.from)}
|
||||
lastMsgSameUser={
|
||||
index > 0 && messages[index - 1].from === message.from
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div ref={messagesEndRef} className="w-full" />
|
||||
</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} />
|
||||
</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 { Avatar } from "@components/UI/Avatar";
|
||||
import type { Protobuf } from "@meshtastic/js";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
const MESSAGE_STATES = {
|
||||
ACK: "ack",
|
||||
@@ -17,7 +28,7 @@ type MessageState = MessageWithState["state"];
|
||||
interface MessageProps {
|
||||
lastMsgSameUser: boolean;
|
||||
message: MessageWithState;
|
||||
sender?: Protobuf.Mesh.NodeInfo;
|
||||
sender: Protobuf.Mesh.NodeInfo;
|
||||
}
|
||||
|
||||
interface StatusTooltipProps {
|
||||
@@ -45,22 +56,20 @@ const STATUS_ICON_MAP: Record<MessageState, LucideIcon> = {
|
||||
const getStatusText = (state: MessageState): string => STATUS_TEXT_MAP[state];
|
||||
|
||||
const StatusTooltip = ({ state, children }: StatusTooltipProps) => (
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>{children}</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<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"
|
||||
side="top"
|
||||
align="center"
|
||||
sideOffset={5}
|
||||
>
|
||||
{getStatusText(state)}
|
||||
<Tooltip.Arrow className="fill-slate-800" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||
<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}
|
||||
>
|
||||
{getStatusText(state)}
|
||||
<TooltipArrow className="fill-slate-800" />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
const StatusIcon = ({ state, className, ...otherProps }: StatusIconProps) => {
|
||||
@@ -88,7 +97,7 @@ const getMessageTextStyles = (state: MessageState) => {
|
||||
const isWaiting = state === MESSAGE_STATES.WAITING;
|
||||
|
||||
return cn(
|
||||
"pl-2 break-words overflow-hidden",
|
||||
"break-words overflow-hidden",
|
||||
isAcknowledged
|
||||
? "text-black dark:text-white"
|
||||
: "text-black dark:text-gray-400",
|
||||
@@ -96,8 +105,11 @@ const getMessageTextStyles = (state: MessageState) => {
|
||||
);
|
||||
};
|
||||
|
||||
const TimeDisplay = ({ date }: { date: Date }) => (
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
const TimeDisplay = ({
|
||||
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">
|
||||
{date.toLocaleDateString()}
|
||||
</span>
|
||||
@@ -111,44 +123,46 @@ const TimeDisplay = ({ date }: { date: Date }) => (
|
||||
);
|
||||
|
||||
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 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 (
|
||||
<div className={containerClass}>
|
||||
{!lastMsgSameUser && (
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Avatar
|
||||
text={sender?.user?.shortName ?? "UNK"}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
<span className="font-medium text-gray-900 dark:text-white truncate">
|
||||
{sender?.user?.longName ?? "UNK"}
|
||||
</span>
|
||||
<div className="flex flex-col w-full px-4 justify-start">
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col flex-wrap items-start py-1",
|
||||
isDeviceUser && "items-end",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{!lastMsgSameUser ? (
|
||||
<div className="flex place-items-center gap-2 mb-1">
|
||||
<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>
|
||||
<TimeDisplay date={message.rxTime} />
|
||||
<StatusIcon state={message.state} />
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,13 @@ import { Input } from "@components/UI/Input.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import type { Types } from "@meshtastic/js";
|
||||
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 {
|
||||
to: Types.Destination;
|
||||
@@ -26,7 +32,7 @@ export const MessageInput = ({
|
||||
} = useDevice();
|
||||
const myNodeNum = hardware.myNodeNum;
|
||||
const [localDraft, setLocalDraft] = useState(messageDraft);
|
||||
const [messageBytes, setMessageBytes] = useState(maxBytes);
|
||||
const [messageBytes, setMessageBytes] = useState(0);
|
||||
|
||||
const debouncedSetMessageDraft = useMemo(
|
||||
() => debounce(setMessageDraft, 300),
|
||||
@@ -64,10 +70,11 @@ export const MessageInput = ({
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = e.target.value;
|
||||
const byteLength = new Blob([newValue]).size;
|
||||
|
||||
if (byteLength <= maxBytes) {
|
||||
setLocalDraft(newValue);
|
||||
debouncedSetMessageDraft(newValue);
|
||||
setMessageBytes(maxBytes - byteLength);
|
||||
setMessageBytes(byteLength);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -75,11 +82,16 @@ export const MessageInput = ({
|
||||
<div className="flex gap-2">
|
||||
<form
|
||||
className="w-full"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
sendText(localDraft);
|
||||
setLocalDraft("");
|
||||
setMessageDraft("");
|
||||
action={async (formData: FormData) => {
|
||||
// prevent user from sending blank/empty message
|
||||
if (localDraft === "") return;
|
||||
const message = formData.get("messageInput") as string;
|
||||
startTransition(() => {
|
||||
sendText(message);
|
||||
setLocalDraft("");
|
||||
setMessageDraft("");
|
||||
setMessageBytes(0);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-grow gap-2">
|
||||
@@ -87,14 +99,16 @@ export const MessageInput = ({
|
||||
<Input
|
||||
autoFocus={true}
|
||||
minLength={1}
|
||||
name="messageInput"
|
||||
placeholder="Enter Message"
|
||||
value={localDraft}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</span>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center w-24 p-2 place-content-end">
|
||||
{messageBytes}/{maxBytes}
|
||||
</div>
|
||||
|
||||
<Button type="submit">
|
||||
<SendIcon size={16} />
|
||||
</Button>
|
||||
|
||||
@@ -1,36 +1,60 @@
|
||||
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||
import type { Protobuf } from "@meshtastic/js";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import type { JSX } from "react";
|
||||
|
||||
export interface TraceRouteProps {
|
||||
from?: Protobuf.Mesh.NodeInfo;
|
||||
to?: Protobuf.Mesh.NodeInfo;
|
||||
route: Array<number>;
|
||||
routeBack?: Array<number>;
|
||||
snrTowards?: Array<number>;
|
||||
snrBack?: Array<number>;
|
||||
}
|
||||
|
||||
export const TraceRoute = ({
|
||||
from,
|
||||
to,
|
||||
route,
|
||||
routeBack,
|
||||
snrTowards,
|
||||
snrBack,
|
||||
}: TraceRouteProps): JSX.Element => {
|
||||
const { nodes } = useDevice();
|
||||
|
||||
return route.length === 0 ? (
|
||||
return (
|
||||
<div className="ml-5 flex">
|
||||
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
|
||||
{to?.user?.longName}↔{from?.user?.longName}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="ml-5 flex">
|
||||
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
|
||||
{to?.user?.longName}↔
|
||||
{route.map((hop) => {
|
||||
const node = nodes.get(hop);
|
||||
return `${node?.user?.longName ?? (node?.num ? numberToHexUnpadded(node.num) : "Unknown")}↔`;
|
||||
})}
|
||||
<p className="font-semibold">Route to destination:</p>
|
||||
<p>{to?.user?.longName}</p>
|
||||
<p>↓ {snrTowards?.[0] ? snrTowards[0] : "??"}dB</p>
|
||||
{route.map((hop, i) => (
|
||||
<span key={nodes.get(hop)?.num}>
|
||||
<p>
|
||||
{nodes.get(hop)?.user?.longName ?? `!${numberToHexUnpadded(hop)}`}
|
||||
</p>
|
||||
<p>↓ {snrTowards?.[i + 1] ? snrTowards[i + 1] : "??"}dB</p>
|
||||
</span>
|
||||
))}
|
||||
{from?.user?.longName}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
"bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600",
|
||||
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:
|
||||
"bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-700 dark:text-slate-100",
|
||||
ghost:
|
||||
|
||||
@@ -44,7 +44,7 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
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",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -28,7 +28,7 @@ const toastVariants = cva(
|
||||
variants: {
|
||||
variant: {
|
||||
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:
|
||||
"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;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
const TooltipArrow = TooltipPrimitive.Arrow;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
@@ -26,4 +27,10 @@ const TooltipContent = React.forwardRef<
|
||||
));
|
||||
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 { create } from "zustand";
|
||||
|
||||
@@ -29,6 +30,9 @@ interface AppState {
|
||||
nodeNumToBeRemoved: number;
|
||||
accent: AccentColor;
|
||||
connectDialogOpen: boolean;
|
||||
nodeNumDetails: number;
|
||||
activeChat: number;
|
||||
chatType: "broadcast" | "direct";
|
||||
|
||||
setRasterSources: (sources: RasterSource[]) => void;
|
||||
addRasterSource: (source: RasterSource) => void;
|
||||
@@ -42,6 +46,9 @@ interface AppState {
|
||||
setNodeNumToBeRemoved: (nodeNum: number) => void;
|
||||
setAccent: (color: AccentColor) => void;
|
||||
setConnectDialogOpen: (open: boolean) => void;
|
||||
setNodeNumDetails: (nodeNum: number) => void;
|
||||
setActiveChat: (chat: number) => void;
|
||||
setChatType: (type: "broadcast" | "direct") => void;
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppState>()((set) => ({
|
||||
@@ -57,6 +64,9 @@ export const useAppStore = create<AppState>()((set) => ({
|
||||
accent: "orange",
|
||||
connectDialogOpen: false,
|
||||
nodeNumToBeRemoved: 0,
|
||||
nodeNumDetails: 0,
|
||||
activeChat: Types.ChannelNumber.Primary,
|
||||
chatType: "broadcast",
|
||||
|
||||
setRasterSources: (sources: RasterSource[]) => {
|
||||
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"
|
||||
| "deviceName"
|
||||
| "nodeRemoval"
|
||||
| "pkiBackup";
|
||||
| "pkiBackup"
|
||||
| "nodeDetails";
|
||||
|
||||
export interface Device {
|
||||
id: number;
|
||||
@@ -62,6 +63,7 @@ export interface Device {
|
||||
deviceName: boolean;
|
||||
nodeRemoval: boolean;
|
||||
pkiBackup: boolean;
|
||||
nodeDetails: boolean;
|
||||
};
|
||||
|
||||
setStatus: (status: Types.DeviceStatusEnum) => void;
|
||||
@@ -145,6 +147,7 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
||||
deviceName: false,
|
||||
nodeRemoval: false,
|
||||
pkiBackup: false,
|
||||
nodeDetails: false,
|
||||
},
|
||||
pendingSettingsChanges: false,
|
||||
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 { 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 { 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 { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import type { Protobuf } from "@meshtastic/js";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import { bbox, lineString } from "@turf/turf";
|
||||
import {
|
||||
BoxSelectIcon,
|
||||
MapPinIcon,
|
||||
ZoomInIcon,
|
||||
ZoomOutIcon,
|
||||
} from "lucide-react";
|
||||
import { MapPinIcon } from "lucide-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";
|
||||
|
||||
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 { nodes, waypoints } = useDevice();
|
||||
const { rasterSources, darkMode } = useAppStore();
|
||||
const { darkMode } = useAppStore();
|
||||
const { default: map } = useMap();
|
||||
|
||||
const [zoom, setZoom] = useState(0);
|
||||
const [selectedNode, setSelectedNode] =
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
const nodesWithPosition = allNodes.filter(
|
||||
(node) => node.position?.latitudeI,
|
||||
);
|
||||
if (!nodesWithPosition.length) {
|
||||
|
||||
if (!validNodes.length) {
|
||||
return;
|
||||
}
|
||||
if (nodesWithPosition.length === 1) {
|
||||
if (validNodes.length === 1) {
|
||||
map.easeTo({
|
||||
zoom: 12,
|
||||
zoom: map.getZoom(),
|
||||
center: [
|
||||
(nodesWithPosition[0].position?.longitudeI ?? 0) / 1e7,
|
||||
(nodesWithPosition[0].position?.latitudeI ?? 0) / 1e7,
|
||||
(validNodes[0].position?.longitudeI ?? 0) / 1e7,
|
||||
(validNodes[0].position?.latitudeI ?? 0) / 1e7,
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
const line = lineString(
|
||||
nodesWithPosition.map((n) => [
|
||||
validNodes.map((n) => [
|
||||
(n.position?.latitudeI ?? 0) / 1e7,
|
||||
(n.position?.longitudeI ?? 0) / 1e7,
|
||||
]),
|
||||
@@ -69,78 +103,54 @@ const MapPage = (): JSX.Element => {
|
||||
if (center) {
|
||||
map.easeTo(center);
|
||||
}
|
||||
}, [allNodes, map]);
|
||||
}, [validNodes, map]);
|
||||
|
||||
useEffect(() => {
|
||||
map?.on("zoom", () => {
|
||||
setZoom(map?.getZoom() ?? 0);
|
||||
});
|
||||
}, [map]);
|
||||
// Generate all markers
|
||||
const markers = useMemo(
|
||||
() =>
|
||||
validNodes.map((node) => {
|
||||
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(() => {
|
||||
map?.on("load", () => {
|
||||
getBBox();
|
||||
getMapBounds();
|
||||
});
|
||||
}, [map, getBBox]);
|
||||
}, [map, getMapBounds]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sidebar>
|
||||
<SidebarSection label="Sources">
|
||||
{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();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Sidebar />
|
||||
<PageLayout label="Map" noPadding={true} actions={[]}>
|
||||
<MapGl
|
||||
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}
|
||||
renderWorldCopies={false}
|
||||
maxPitch={0}
|
||||
antialias={true}
|
||||
style={{
|
||||
filter: darkMode ? "brightness(0.8)" : "",
|
||||
filter: darkMode ? "brightness(0.9)" : "",
|
||||
}}
|
||||
dragRotate={false}
|
||||
touchZoomRotate={false}
|
||||
initialViewState={{
|
||||
zoom: 1.6,
|
||||
zoom: 1.8,
|
||||
latitude: 35,
|
||||
longitude: 0,
|
||||
}}
|
||||
@@ -151,6 +161,14 @@ const MapPage = (): JSX.Element => {
|
||||
color: darkMode ? "black" : "",
|
||||
}}
|
||||
/>
|
||||
<GeolocateControl
|
||||
position="top-right"
|
||||
positionOptions={{ enableHighAccuracy: true }}
|
||||
trackUserLocation
|
||||
/>
|
||||
<NavigationControl position="top-right" showCompass={false} />
|
||||
|
||||
<ScaleControl />
|
||||
{waypoints.map((wp) => (
|
||||
<Marker
|
||||
key={wp.id}
|
||||
@@ -163,58 +181,17 @@ const MapPage = (): JSX.Element => {
|
||||
</div>
|
||||
</Marker>
|
||||
))}
|
||||
{/* {rasterSources.map((source, index) => (
|
||||
<Source key={index} type="raster" {...source}>
|
||||
<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 && (
|
||||
{markers}
|
||||
{selectedNode ? (
|
||||
<Popup
|
||||
longitude={(selectedNode.position.longitudeI ?? 0) / 1e7}
|
||||
latitude={(selectedNode.position.latitudeI ?? 0) / 1e7}
|
||||
anchor="left"
|
||||
closeOnClick={false}
|
||||
anchor="top"
|
||||
longitude={convertToLatLng(selectedNode.position).longitude}
|
||||
latitude={convertToLatLng(selectedNode.position).latitude}
|
||||
onClose={() => setSelectedNode(null)}
|
||||
>
|
||||
<NodeDetail node={selectedNode} />
|
||||
</Popup>
|
||||
)}
|
||||
) : null}
|
||||
</MapGl>
|
||||
</PageLayout>
|
||||
</>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useAppStore } from "@app/core/stores/appStore";
|
||||
import { ChannelChat } from "@components/PageComponents/Messages/ChannelChat.tsx";
|
||||
import { PageLayout } from "@components/PageLayout.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 { SidebarButton } from "@components/UI/Sidebar/sidebarButton.tsx";
|
||||
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 { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import { getChannelName } from "@pages/Channels.tsx";
|
||||
import { HashIcon, LockIcon, LockOpenIcon, WaypointsIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
const MessagesPage = () => {
|
||||
export const MessagesPage = () => {
|
||||
const { channels, nodes, hardware, messages, traceroutes, connection } =
|
||||
useDevice();
|
||||
const [chatType, setChatType] =
|
||||
useState<Types.PacketDestination>("broadcast");
|
||||
const [activeChat, setActiveChat] = useState<number>(
|
||||
Types.ChannelNumber.Primary,
|
||||
);
|
||||
const { activeChat, chatType, setActiveChat, setChatType } = useAppStore();
|
||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||
const filteredNodes = Array.from(nodes.values()).filter((node) => {
|
||||
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 { useAppStore } from "@app/core/stores/appStore";
|
||||
import { Sidebar } from "@components/Sidebar.tsx";
|
||||
import { Avatar } from "@components/UI/Avatar.tsx";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { Mono } from "@components/generic/Mono.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 { Protobuf } from "@meshtastic/js";
|
||||
import { Protobuf, type Types } from "@meshtastic/js";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import { LockIcon, LockOpenIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Fragment, type JSX } from "react";
|
||||
import { LockIcon, LockOpenIcon } from "lucide-react";
|
||||
import { Fragment, type JSX, useCallback, useEffect, useState } from "react";
|
||||
import { base16 } from "rfc4648";
|
||||
|
||||
export interface DeleteNoteDialogProps {
|
||||
@@ -19,8 +21,16 @@ export interface DeleteNoteDialogProps {
|
||||
}
|
||||
|
||||
const NodesPage = (): JSX.Element => {
|
||||
const { nodes, hardware, setDialogOpen } = useDevice();
|
||||
const { setNodeNumToBeRemoved } = useAppStore();
|
||||
const { nodes, hardware, connection } = useDevice();
|
||||
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 filteredNodes = Array.from(nodes.values()).filter((node) => {
|
||||
@@ -29,6 +39,36 @@ const NodesPage = (): JSX.Element => {
|
||||
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 (
|
||||
<>
|
||||
<Sidebar />
|
||||
@@ -46,22 +86,38 @@ const NodesPage = (): JSX.Element => {
|
||||
<Table
|
||||
headings={[
|
||||
{ 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: "MAC Address", type: "normal", sortable: true },
|
||||
{ title: "Last Heard", type: "normal", sortable: true },
|
||||
{ title: "SNR", type: "normal", sortable: true },
|
||||
{ title: "Encryption", type: "normal", sortable: false },
|
||||
{ title: "Connection", type: "normal", sortable: true },
|
||||
{ title: "Remove", type: "normal", sortable: false },
|
||||
]}
|
||||
rows={filteredNodes.map((node) => [
|
||||
<span
|
||||
key={node.num}
|
||||
className="h-3 w-3 rounded-full bg-accent"
|
||||
/>,
|
||||
<div key={node.num}>
|
||||
<Avatar text={node.user?.shortName.toString() ?? "UNK"} />
|
||||
</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?.macaddr
|
||||
? `Meshtastic ${base16
|
||||
@@ -95,7 +151,7 @@ const NodesPage = (): JSX.Element => {
|
||||
{node.user?.publicKey && node.user?.publicKey.length > 0 ? (
|
||||
<LockIcon className="text-green-600" />
|
||||
) : (
|
||||
<LockOpenIcon className="text-yellow-300" />
|
||||
<LockOpenIcon className="text-yellow-300 mx-auto" />
|
||||
)}
|
||||
</Mono>,
|
||||
<Mono key="hops">
|
||||
@@ -108,19 +164,23 @@ const NodesPage = (): JSX.Element => {
|
||||
: "-"}
|
||||
{node.viaMqtt === true ? ", via MQTT" : ""}
|
||||
</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>
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||