Compare commits
67 Commits
v0.13.1
...
onboarding
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a088145963 | ||
|
|
181ba089c2 | ||
|
|
394c1dd438 | ||
|
|
2ff6e99385 | ||
|
|
3614612ac2 | ||
|
|
dfeb4e79a4 | ||
|
|
a606c5a375 | ||
|
|
4e698b693c | ||
|
|
0eb81eec04 | ||
|
|
35f5d132c8 | ||
|
|
02cc2496f0 | ||
|
|
d84eaa203a | ||
|
|
e0d372e9bb | ||
|
|
b7a54f6aef | ||
|
|
9d65477af7 | ||
|
|
73cdc890f2 | ||
|
|
75cf328aea | ||
|
|
e8afb49685 | ||
|
|
c2fe9541dc | ||
|
|
9c9c5524a5 | ||
|
|
bc487f8655 | ||
|
|
2708b5a15b | ||
|
|
fa74a748ac | ||
|
|
b52e705dc4 | ||
|
|
1a5584bcfc | ||
|
|
b90da87602 | ||
|
|
40b2da34b2 | ||
|
|
da133c086f | ||
|
|
92b7ca7bda | ||
|
|
8238f569c6 | ||
|
|
1051d0c5e5 | ||
|
|
8fe2540f91 | ||
|
|
6232903691 | ||
|
|
96fa13aea9 | ||
|
|
6a8786b241 | ||
|
|
819d2fc3b8 | ||
|
|
8bdee03164 | ||
|
|
565a64b0e3 | ||
|
|
fa10e2d73a | ||
|
|
18f291345d | ||
|
|
67df399f14 | ||
|
|
222359692e | ||
|
|
288db165a7 | ||
|
|
5c5636aa24 | ||
|
|
8e449763a1 | ||
|
|
002de7bcef | ||
|
|
db616eaa95 | ||
|
|
8eaa59f0ce | ||
|
|
d121973e85 | ||
|
|
30367a9204 | ||
|
|
1385403817 | ||
|
|
5f60e59e51 | ||
|
|
c341534166 | ||
|
|
957e81c37a | ||
|
|
c0269155db | ||
|
|
945b8b392d | ||
|
|
0c9a016598 | ||
|
|
583e335fc7 | ||
|
|
986ef8d4b4 | ||
|
|
97fb9dd098 | ||
|
|
db80609379 | ||
|
|
bb5b9f3bfe | ||
|
|
085ba25559 | ||
|
|
682300e707 | ||
|
|
af7f13e8bd | ||
|
|
e530cb5327 | ||
|
|
6b8e4c961e |
3
.github/labeler.yml
vendored
@@ -13,8 +13,7 @@
|
||||
"Area: GUI":
|
||||
- all:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "gui/**/*"
|
||||
- all-globs-to-all-files: "!gui/public/i18n/**"
|
||||
- all-globs-to-any-file: ["gui/**/*", "!gui/public/i18n/**/*"]
|
||||
"Area: Hardware Protocol":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "server/core/src/main/java/dev/slimevr/tracking/trackers/udp/**"
|
||||
|
||||
19
.github/workflows/gradle.yaml
vendored
@@ -144,7 +144,7 @@ jobs:
|
||||
|
||||
|
||||
bundle-linux:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [build, test]
|
||||
if: contains(fromJSON('["workflow_dispatch", "create"]'), github.event_name)
|
||||
steps:
|
||||
@@ -160,14 +160,25 @@ jobs:
|
||||
- name: Set up Linux dependencies
|
||||
uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
with:
|
||||
packages: libgtk-3-dev webkit2gtk-4.1 libappindicator3-dev librsvg2-dev patchelf libfuse2
|
||||
packages: |
|
||||
build-essential curl wget file libssl-dev libgtk-3-dev libappindicator3-dev librsvg2-dev
|
||||
# Increment to invalidate the cache
|
||||
version: 1.0
|
||||
version: 2.0
|
||||
# Enables a workaround to attempt to run pre and post install scripts
|
||||
execute_install_scripts: true
|
||||
# Disables uploading logs as a build artifact
|
||||
debug: false
|
||||
|
||||
- name: Set up specific Linux versioned dependencies
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y \
|
||||
libwebkit2gtk-4.1-0=2.44.0-2 \
|
||||
libwebkit2gtk-4.1-dev=2.44.0-2 \
|
||||
libjavascriptcoregtk-4.1-0=2.44.0-2 \
|
||||
libjavascriptcoregtk-4.1-dev=2.44.0-2 \
|
||||
gir1.2-javascriptcoregtk-4.1=2.44.0-2 \
|
||||
gir1.2-webkit2-4.1=2.44.0-2;
|
||||
|
||||
- name: Cache cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
@@ -274,7 +285,7 @@ jobs:
|
||||
./bundle_dmg.sh --volname SlimeVR --icon slimevr 180 170 --app-drop-link 480 170 \
|
||||
--window-size 660 400 --hide-extension ../macos/SlimeVR.app \
|
||||
--volicon ../macos/SlimeVR.app/Contents/Resources/icon.icns --skip-jenkins \
|
||||
--eula ../../../../LICENSE-MIT slimevr.dmg ../macos/SlimeVR.app
|
||||
--eula ../../../../../LICENSE-MIT slimevr.dmg ../macos/SlimeVR.app
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
3
.gitignore
vendored
@@ -43,3 +43,6 @@ build/
|
||||
|
||||
# Ignore Android local properties
|
||||
local.properties
|
||||
|
||||
# Ignore temporary config
|
||||
vrconfig.yml.tmp
|
||||
|
||||
189
Cargo.lock
generated
@@ -1837,6 +1837,24 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.9"
|
||||
@@ -2000,6 +2018,15 @@ version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.8"
|
||||
@@ -3124,6 +3151,55 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"rand 0.8.5",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"slab",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d5a626c6807713b15cac82a6acaccd6043c9a5408c24baae07611fec3f243da"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.37"
|
||||
@@ -3283,6 +3359,7 @@ dependencies = [
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
@@ -3291,11 +3368,16 @@ dependencies = [
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower-service",
|
||||
"url",
|
||||
@@ -3303,6 +3385,7 @@ dependencies = [
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
@@ -3329,12 +3412,33 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.15",
|
||||
"libc",
|
||||
"spin",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -3357,6 +3461,46 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.102.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
@@ -3692,6 +3836,7 @@ dependencies = [
|
||||
"discord-sdk",
|
||||
"flexi_logger",
|
||||
"glob",
|
||||
"itertools",
|
||||
"libloading 0.8.5",
|
||||
"log",
|
||||
"log-panics",
|
||||
@@ -3780,6 +3925,12 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
@@ -3824,6 +3975,12 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "swift-rs"
|
||||
version = "1.0.7"
|
||||
@@ -4397,6 +4554,17 @@ dependencies = [
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.12"
|
||||
@@ -4681,6 +4849,12 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.2"
|
||||
@@ -4996,6 +5170,15 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com"
|
||||
version = "0.33.0"
|
||||
@@ -5589,6 +5772,12 @@ dependencies = [
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "4.0.0"
|
||||
|
||||
@@ -65,6 +65,11 @@ work. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
|
||||
</provides>
|
||||
|
||||
<releases>
|
||||
<release version="0.13.2" date="2024-11-06"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.2</url></release>
|
||||
<release version="0.13.1" date="2024-11-05"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.1</url></release>
|
||||
<release version="0.13.1~rc.3" type="development" date="2024-10-31"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.1-rc.3</url></release>
|
||||
<release version="0.13.1~rc.2" type="development" date="2024-10-26"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.1-rc.2</url></release>
|
||||
<release version="0.13.1~rc.1" type="development" date="2024-10-16"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.1-rc.1</url></release>
|
||||
<release version="0.13.0" date="2024-09-20"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.0</url></release>
|
||||
<release version="0.13.0~rc.4" type="development" date="2024-09-13"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.0-rc.4</url></release>
|
||||
<release version="0.13.0~rc.3" type="development" date="2024-08-14"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.0-rc.3</url></release>
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
harfbuzz
|
||||
libffi
|
||||
libsoup_3
|
||||
openssl
|
||||
openssl.dev
|
||||
pango
|
||||
pkg-config
|
||||
treefmt
|
||||
|
||||
8
gui/.env
Normal file
@@ -0,0 +1,8 @@
|
||||
VITE_FIRMWARE_TOOL_URL=https://fw-tool-api.slimevr.io
|
||||
VITE_FIRMWARE_TOOL_S3_URL=https://fw-tool-bucket.slimevr.io
|
||||
FIRMWARE_TOOL_SCHEMA_URL=https://fw-tool-api.slimevr.io/api-json
|
||||
|
||||
|
||||
# VITE_FIRMWARE_TOOL_URL=http://localhost:3000
|
||||
# VITE_FIRMWARE_TOOL_S3_URL=http://localhost:9000
|
||||
# FIRMWARE_TOOL_SCHEMA_URL=http://localhost:3000/api-json
|
||||
@@ -1,51 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"jest": true
|
||||
},
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@dword-design/import-alias/recommended"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["react-hooks", "@typescript-eslint"],
|
||||
"rules": {
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/prop-types": "off",
|
||||
"spaced-comment": "error",
|
||||
"quotes": ["error", "single"],
|
||||
"no-duplicate-imports": "error",
|
||||
"no-inline-styles": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"react/no-unescaped-entities": "off",
|
||||
"camelcase": "error",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"@dword-design/import-alias/prefer-alias": [
|
||||
"error",
|
||||
{
|
||||
"alias": {
|
||||
"@": "./src/"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"typescript": {}
|
||||
},
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
gui/.gitignore
vendored
@@ -28,6 +28,7 @@ yarn-error.log*
|
||||
# vite
|
||||
/dist
|
||||
/stats.html
|
||||
vite.config.ts.timestamp*
|
||||
|
||||
# eslint
|
||||
.eslintcache
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
'**/*.{ts,tsx}': () => 'tsc -p tsconfig.json --noEmit',
|
||||
'**/*.{js,jsx,ts,tsx}': 'eslint --max-warnings=0 --cache --fix',
|
||||
'src/**/*.{js,jsx,ts,tsx}': 'eslint --max-warnings=0 --no-warn-ignored --cache --fix',
|
||||
'**/*.{js,jsx,ts,tsx,css,md,json}': 'prettier --write',
|
||||
};
|
||||
|
||||
79
gui/eslint.config.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import { FlatCompat } from '@eslint/eslintrc';
|
||||
import eslint from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
const compat = new FlatCompat();
|
||||
|
||||
export const gui = [
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...compat.extends('plugin:@dword-design/import-alias/recommended'),
|
||||
...compat.plugins('eslint-plugin-react-hooks'),
|
||||
// Add import-alias rule inside compat because plugin doesn't like flat configs
|
||||
...compat.config({
|
||||
rules: {
|
||||
'@dword-design/import-alias/prefer-alias': [
|
||||
'error',
|
||||
{
|
||||
alias: {
|
||||
'@': './src/',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.jest,
|
||||
},
|
||||
},
|
||||
files: ['src/**/*.{js,jsx,ts,tsx,json}'],
|
||||
plugins: {
|
||||
'@typescript-eslint': tseslint.plugin,
|
||||
},
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'spaced-comment': 'error',
|
||||
quotes: ['error', 'single'],
|
||||
'no-duplicate-imports': 'error',
|
||||
'no-inline-styles': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'react/no-unescaped-entities': 'off',
|
||||
camelcase: 'error',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: {},
|
||||
},
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
},
|
||||
// Global ignore
|
||||
{
|
||||
ignores: ['**/firmware-tool-api/'],
|
||||
},
|
||||
];
|
||||
|
||||
export default gui;
|
||||
28
gui/openapi-codegen.config.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
generateSchemaTypes,
|
||||
generateReactQueryComponents,
|
||||
} from '@openapi-codegen/typescript';
|
||||
import { defineConfig } from '@openapi-codegen/cli';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config()
|
||||
|
||||
export default defineConfig({
|
||||
firmwareTool: {
|
||||
from: {
|
||||
source: 'url',
|
||||
url: process.env.FIRMWARE_TOOL_SCHEMA_URL ?? 'http://localhost:3000/api-json',
|
||||
},
|
||||
outputDir: 'src/firmware-tool-api',
|
||||
to: async (context) => {
|
||||
const filenamePrefix = 'firmwareTool';
|
||||
const { schemasFiles } = await generateSchemaTypes(context, {
|
||||
filenamePrefix,
|
||||
});
|
||||
await generateReactQueryComponents(context, {
|
||||
filenamePrefix,
|
||||
schemasFiles,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -2,13 +2,17 @@
|
||||
"name": "slimevr-ui",
|
||||
"version": "0.5.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@fluent/bundle": "^0.18.0",
|
||||
"@fluent/react": "^0.15.2",
|
||||
"@fontsource/poppins": "^5.1.0",
|
||||
"@formatjs/intl-localematcher": "^0.2.32",
|
||||
"@hookform/resolvers": "^3.6.0",
|
||||
"@react-three/drei": "^9.114.3",
|
||||
"@react-three/fiber": "^8.17.10",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tanstack/react-query": "^5.48.0",
|
||||
"@tauri-apps/api": "^2.0.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||
"@tauri-apps/plugin-fs": "^2.0.0",
|
||||
@@ -26,15 +30,18 @@
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-responsive": "^10.0.0",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"semver": "^7.6.3",
|
||||
"solarxr-protocol": "file:../solarxr-protocol",
|
||||
"three": "^0.163.0",
|
||||
"ts-pattern": "^5.4.0",
|
||||
"typescript": "^5.6.3",
|
||||
"use-double-tap": "^1.3.6"
|
||||
"use-double-tap": "^1.3.6",
|
||||
"yup": "^1.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite --force",
|
||||
@@ -46,10 +53,14 @@
|
||||
"lint:fix": "tsc --noEmit && eslint --fix --max-warnings=0 \"src/**/*.{js,jsx,ts,tsx,json}\" && pnpm run format",
|
||||
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,scss,md,json}\"",
|
||||
"preview-vite": "vite preview",
|
||||
"javaversion-build": "cd src-tauri/src/ && javac JavaVersion.java && jar cvfe JavaVersion.jar JavaVersion JavaVersion.class"
|
||||
"javaversion-build": "cd src-tauri/src/ && javac JavaVersion.java && jar cvfe JavaVersion.jar JavaVersion JavaVersion.class",
|
||||
"gen:javaversion": "cd src-tauri/src/ && javac JavaVersion.java && jar cvfe JavaVersion.jar JavaVersion JavaVersion.class",
|
||||
"gen:firmware-tool": "openapi-codegen gen firmwareTool"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dword-design/eslint-plugin-import-alias": "^4.0.9",
|
||||
"@openapi-codegen/cli": "^2.0.2",
|
||||
"@openapi-codegen/typescript": "^8.0.2",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tauri-apps/cli": "^2.0.2",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
@@ -64,6 +75,7 @@
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-import-resolver-typescript": "^3.6.3",
|
||||
@@ -71,12 +83,15 @@
|
||||
"eslint-plugin-jsx-a11y": "^6.10.0",
|
||||
"eslint-plugin-react": "^7.37.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"globals": "^15.10.0",
|
||||
"prettier": "^3.3.3",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.79.4",
|
||||
"spdx-satisfies": "^5.0.1",
|
||||
"tailwind-gradient-mask-image": "^1.2.0",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"vite": "^5.4.8"
|
||||
"ts-xor": "^1.3.0",
|
||||
"vite": "^5.4.8",
|
||||
"typescript-eslint": "^8.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ body_part-RIGHT_UPPER_ARM = Rechter Oberarm
|
||||
body_part-RIGHT_LOWER_ARM = Rechter Unterarm
|
||||
body_part-RIGHT_HAND = Rechte Hand
|
||||
body_part-RIGHT_UPPER_LEG = Rechter Oberschenkel
|
||||
body_part-RIGHT_LOWER_LEG = Rechter Unterschenkel
|
||||
body_part-RIGHT_LOWER_LEG = Rechter Knöchel
|
||||
body_part-RIGHT_FOOT = Rechter Fuß
|
||||
body_part-UPPER_CHEST = Obere Brust
|
||||
body_part-CHEST = Brust
|
||||
@@ -47,7 +47,7 @@ body_part-LEFT_UPPER_ARM = Linker Oberarm
|
||||
body_part-LEFT_LOWER_ARM = Linker Unterarm
|
||||
body_part-LEFT_HAND = Linke Hand
|
||||
body_part-LEFT_UPPER_LEG = Linker Oberschenkel
|
||||
body_part-LEFT_LOWER_LEG = Linker Unterschenkel
|
||||
body_part-LEFT_LOWER_LEG = Linker Knöchel
|
||||
body_part-LEFT_FOOT = Linker Fuß
|
||||
|
||||
## Proportions
|
||||
@@ -208,6 +208,12 @@ tracker-infos-imu = IMU-Sensor
|
||||
tracker-infos-board_type = Platine
|
||||
tracker-infos-network_version = Protokoll Version
|
||||
tracker-infos-magnetometer = Magnetometer
|
||||
tracker-infos-magnetometer-status-v1 =
|
||||
{ $status ->
|
||||
[DISABLED] Ausgeschalten
|
||||
[ENABLED] Angeschalten
|
||||
*[NOT_SUPPORTED] Nicht unterstützt
|
||||
}
|
||||
|
||||
## Tracker settings
|
||||
|
||||
@@ -223,6 +229,11 @@ tracker-settings-drift_compensation_section = Drift-Kompensierung
|
||||
tracker-settings-drift_compensation_section-description = Soll dieser Tracker Drift kompensieren, wenn die Drift-Kompensierung allgemein aktiviert ist?
|
||||
tracker-settings-drift_compensation_section-edit = Erlaube Drift Kompensierung
|
||||
tracker-settings-use_mag = Magnetometer auf diesem Tracker zulassen
|
||||
# Multiline!
|
||||
tracker-settings-use_mag-description =
|
||||
Soll dieser Tracker das Magnetometer verwenden um Drift zu reduzieren, wenn die Verwendung von Magnetometer erlaubt ist? <b> Bitten schalten Sie den Tracker nicht aus, während Sie diese Einstellung umschalten!</b>
|
||||
|
||||
Sie müssen zuerst die Verwendung des Magnetometers zulassen, <magSetting>klicken Sie hier, um zu den Einstellungen zu gelangen</magSetting>.
|
||||
tracker-settings-use_mag-label = Magnetometer zulassen
|
||||
# The .<name> means it's an attribute and it's related to the top key.
|
||||
# In this case that is the settings for the assignment section.
|
||||
@@ -363,6 +374,18 @@ settings-general-tracker_mechanics-drift_compensation-description =
|
||||
Kompensiert IMU Drift auf der Gier-Achse durch Anwenden einer invertierten Rotation.
|
||||
Ändern Sie die Menge der Kompensierung und die Anzahl der Resets, welche für die Berechnung genutzt werden.
|
||||
settings-general-tracker_mechanics-drift_compensation-enabled-label = Drift-Kompensierung
|
||||
settings-general-tracker_mechanics-drift_compensation-prediction = Prognose der Driftkompensation
|
||||
# This cares about multilines
|
||||
settings-general-tracker_mechanics-drift_compensation-prediction-description =
|
||||
Prognostiziert die Driftkompensation basierend auf dem zuvor gemessenen Drift.
|
||||
Aktivieren Sie diese Funktion, wenn sich der Tracker kontinuierlich um die gier-Achse dreht.
|
||||
settings-general-tracker_mechanics-drift_compensation-prediction-label = Prognose der Driftkompensation
|
||||
settings-general-tracker_mechanics-drift_compensation_warning =
|
||||
<b>Warnung:</b> Verwenden Sie die Driftkompensation nur, wenn sie sehr oft
|
||||
reseten müssen (alle ~5-10 Minuten).
|
||||
|
||||
Zu den IMUs, die häufig einen Reset benötigen, gehören:
|
||||
Joy-Cons, owoTrack und MPUs (ohne aktuelle Firmware).
|
||||
settings-general-tracker_mechanics-drift_compensation_warning-cancel = Abbrechen
|
||||
settings-general-tracker_mechanics-drift_compensation_warning-done = Ich verstehe
|
||||
settings-general-tracker_mechanics-drift_compensation-amount-label = Kompensierungsmenge
|
||||
@@ -372,6 +395,10 @@ settings-general-tracker_mechanics-save_mounting_reset-description =
|
||||
Speichert die automatische Befestigungs-Reset Kalibrierung für die Tracker zwischen den Neustarts. Nützlich
|
||||
wenn Sie einen Anzug tragen, bei dem sich die Tracker zwischen den Sitzungen nicht bewegen. <b>Für normale Benutzer nicht zu empfehlen!</b>
|
||||
settings-general-tracker_mechanics-save_mounting_reset-enabled-label = Befestigungs-Reset speichern
|
||||
settings-general-tracker_mechanics-use_mag_on_all_trackers = Verwende das Magnetometer auf allen IMU-Trackern, die dies unterstützen.
|
||||
settings-general-tracker_mechanics-use_mag_on_all_trackers-description =
|
||||
Verwendet das Magnetometer auf allen Trackern, die über eine kompatible Firmware verfügen, um den Drift in stabilen magnetischen Umgebungen zu reduzieren.
|
||||
Kann pro Tracker in den Einstellungen des Trackers deaktiviert werden. <b>Bitte schalten Sie keinen der Tracker aus, während Sie dies umschalten!</b>
|
||||
settings-general-tracker_mechanics-use_mag_on_all_trackers-label = Magnetometer auf Trackern verwenden
|
||||
|
||||
## FK/Tracking settings
|
||||
@@ -485,6 +512,7 @@ settings-interface-appearance-font-slime_font = Standard-Schriftart
|
||||
settings-interface-appearance-font_size = Standard-Schriftgröße
|
||||
settings-interface-appearance-font_size-description = Verändert die Schriftgröße der gesamten Oberfläche außer diesem Einstellungs-Panel.
|
||||
settings-interface-appearance-decorations = Verwenden Sie die systemeigenen Fensterdekorationen
|
||||
settings-interface-appearance-decorations-description = Dadurch wird die obere Leiste der Benutzeroberfläche nicht gerendert, sondern die des Betriebssystems verwendet.
|
||||
settings-interface-appearance-decorations-label = Verwenden der native Fensterdekorationen
|
||||
|
||||
## Notification settings
|
||||
@@ -632,11 +660,26 @@ settings-osc-vmc-mirror_tracking-label = Tracking spiegeln
|
||||
|
||||
settings-utils-advanced = Erweitert
|
||||
settings-utils-advanced-reset-gui = Einstellungen der Benutzeroberfläche zurücksetzen
|
||||
settings-utils-advanced-reset-gui-description = Stellt die Standardeinstellungen für die Benutzeroberfläche wieder her.
|
||||
settings-utils-advanced-reset-gui-label = Benutzeroberfläche zurücksetzen
|
||||
settings-utils-advanced-reset-server = Tracking-Einstellungen zurücksetzen
|
||||
settings-utils-advanced-reset-server-description = Stellen Sie die Standardeinstellungen für das Tracking wieder her.
|
||||
settings-utils-advanced-reset-server-label = Tracking zurücksetzen
|
||||
settings-utils-advanced-reset-all = Alle Einstellungen zurücksetzen
|
||||
settings-utils-advanced-reset-all-description = Stellt die Standardeinstellungen für die Benutzeroberfläche und das Tracking wieder her.
|
||||
settings-utils-advanced-reset-all-label = Alles zurücksetzen
|
||||
settings-utils-advanced-reset_warning =
|
||||
{ $type ->
|
||||
[gui]
|
||||
<b>Warnung:</b> Dadurch werden Ihre Benutzeroberfläche-Einstellungen auf die Standardeinstellungen zurückgesetzt.
|
||||
Möchten Sie das wirklich tun?
|
||||
[server]
|
||||
<b>Warnung:</b> Dadurch werden Ihre Tracking-Einstellungen auf die Standardeinstellungen zurückgesetzt.
|
||||
Möchten Sie das wirklich tun?
|
||||
*[all]
|
||||
<b>Warnung:</b> Dadurch werden alle Ihre Einstellungen auf die Standardeinstellungen zurückgesetzt.
|
||||
Möchten Sie das wirklich tun?
|
||||
}
|
||||
settings-utils-advanced-reset_warning-reset = Einstellungen zurücksetzen
|
||||
settings-utils-advanced-reset_warning-cancel = Abbrechen
|
||||
settings-utils-advanced-open_data = Daten-Ordner
|
||||
@@ -1014,6 +1057,10 @@ onboarding-automatic_proportions-verify_results-redo = Aufnahme wiederholen
|
||||
onboarding-automatic_proportions-verify_results-confirm = Ergebnisse sind korrekt
|
||||
onboarding-automatic_proportions-done-title = Körper gemessen und gespeichert.
|
||||
onboarding-automatic_proportions-done-description = Ihre Körperproportionen-Kalibrierung ist abgeschlossen!
|
||||
onboarding-automatic_proportions-error_modal-v2 =
|
||||
<b>Warnung:</b> Bei der Schätzung der Proportionen ist ein Fehler aufgetreten!
|
||||
Dies ist wahrscheinlich ein Problem mit der Tracker-Ausrichtung. Vergewissern Sie sich, dass Ihre Tracker ordnungsgemäß funktioniert, bevor Sie es erneut versuchen.
|
||||
Bitte <docs>überprüfen Sie die Dokumentation</docs> oder treten Sie unserem <discord>Discord</discord> bei, um Hilfe zu erhalten ^_^
|
||||
onboarding-automatic_proportions-error_modal-confirm = Verstanden!
|
||||
|
||||
## Home
|
||||
|
||||
@@ -44,6 +44,49 @@ body_part-LEFT_HAND = Left hand
|
||||
body_part-LEFT_UPPER_LEG = Left thigh
|
||||
body_part-LEFT_LOWER_LEG = Left ankle
|
||||
body_part-LEFT_FOOT = Left foot
|
||||
body_part-LEFT_THUMB_METACARPAL = Left thumb metacarpal
|
||||
body_part-LEFT_THUMB_PROXIMAL = Left thumb proximal
|
||||
body_part-LEFT_THUMB_DISTAL = Left thumb distal
|
||||
body_part-LEFT_INDEX_PROXIMAL = Left index proximal
|
||||
body_part-LEFT_INDEX_INTERMEDIATE = Left index intermediate
|
||||
body_part-LEFT_INDEX_DISTAL = Left index distal
|
||||
body_part-LEFT_MIDDLE_PROXIMAL = Left middle proximal
|
||||
body_part-LEFT_MIDDLE_INTERMEDIATE = Left middle intermediate
|
||||
body_part-LEFT_MIDDLE_DISTAL = Left middle distal
|
||||
body_part-LEFT_RING_PROXIMAL = Left ring proximal
|
||||
body_part-LEFT_RING_INTERMEDIATE = Left ring intermediate
|
||||
body_part-LEFT_RING_DISTAL = Left ring distal
|
||||
body_part-LEFT_LITTLE_PROXIMAL = Left little proximal
|
||||
body_part-LEFT_LITTLE_INTERMEDIATE = Left little intermediate
|
||||
body_part-LEFT_LITTLE_DISTAL = Left little distal
|
||||
body_part-RIGHT_THUMB_METACARPAL = Right thumb metacarpal
|
||||
body_part-RIGHT_THUMB_PROXIMAL = Right thumb proximal
|
||||
body_part-RIGHT_THUMB_DISTAL = Right thumb distal
|
||||
body_part-RIGHT_INDEX_PROXIMAL = Right index proximal
|
||||
body_part-RIGHT_INDEX_INTERMEDIATE = Right index intermediate
|
||||
body_part-RIGHT_INDEX_DISTAL = Right index distal
|
||||
body_part-RIGHT_MIDDLE_PROXIMAL = Right middle proximal
|
||||
body_part-RIGHT_MIDDLE_INTERMEDIATE = Right middle intermediate
|
||||
body_part-RIGHT_MIDDLE_DISTAL = Right middle distal
|
||||
body_part-RIGHT_RING_PROXIMAL = Right ring proximal
|
||||
body_part-RIGHT_RING_INTERMEDIATE = Right ring intermediate
|
||||
body_part-RIGHT_RING_DISTAL = Right ring distal
|
||||
body_part-RIGHT_LITTLE_PROXIMAL = Right little proximal
|
||||
body_part-RIGHT_LITTLE_INTERMEDIATE = Right little intermediate
|
||||
body_part-RIGHT_LITTLE_DISTAL = Right little distal
|
||||
|
||||
## BoardType
|
||||
board_type-UNKNOWN = Unknown
|
||||
board_type-NODEMCU = NodeMCU
|
||||
board_type-CUSTOM = Custom Board
|
||||
board_type-WROOM32 = WROOM32
|
||||
board_type-WEMOSD1MINI = Wemos D1 Mini
|
||||
board_type-TTGO_TBASE = TTGO T-Base
|
||||
board_type-ESP01 = ESP-01
|
||||
board_type-SLIMEVR = SlimeVR
|
||||
board_type-LOLIN_C3_MINI = Lolin C3 Mini
|
||||
board_type-BEETLE32C3 = Beetle ESP32-C3
|
||||
board_type-ES32C3DEVKITM1 = Espressif ESP32-C3 DevKitM-1
|
||||
|
||||
## Proportions
|
||||
skeleton_bone-NONE = None
|
||||
@@ -184,6 +227,7 @@ tracker-infos-url = Tracker URL
|
||||
tracker-infos-version = Firmware Version
|
||||
tracker-infos-hardware_rev = Hardware Revision
|
||||
tracker-infos-hardware_identifier = Hardware ID
|
||||
tracker-infos-data_support = Data support
|
||||
tracker-infos-imu = IMU Sensor
|
||||
tracker-infos-board_type = Main board
|
||||
tracker-infos-network_version = Protocol Version
|
||||
@@ -222,6 +266,11 @@ tracker-settings-name_section-label = Tracker name
|
||||
tracker-settings-forget = Forget tracker
|
||||
tracker-settings-forget-description = Removes the tracker from the SlimeVR Server and prevent it from connecting to it until the server is restarted. The configuration of the tracker won't be lost.
|
||||
tracker-settings-forget-label = Forget tracker
|
||||
tracker-settings-update-unavailable = Cannot be updated (DIY)
|
||||
tracker-settings-update-up_to_date = Up to date
|
||||
tracker-settings-update-available = { $versionName } is now available
|
||||
tracker-settings-update = Update now
|
||||
tracker-settings-update-title = Firmware version
|
||||
|
||||
## Tracker part card info
|
||||
tracker-part_card-no_name = No name
|
||||
@@ -294,6 +343,7 @@ settings-sidebar-utils = Utilities
|
||||
settings-sidebar-serial = Serial console
|
||||
settings-sidebar-appearance = Appearance
|
||||
settings-sidebar-notifications = Notifications
|
||||
settings-sidebar-firmware-tool = DIY Firmware Tool
|
||||
settings-sidebar-advanced = Advanced
|
||||
|
||||
## SteamVR settings
|
||||
@@ -560,12 +610,14 @@ settings-osc-router-network-address-placeholder = IPV4 address
|
||||
## OSC VRChat settings
|
||||
settings-osc-vrchat = VRChat OSC Trackers
|
||||
# This cares about multilines
|
||||
settings-osc-vrchat-description-v1 =
|
||||
settings-osc-vrchat-description-v2 =
|
||||
Change settings specific to the OSC Trackers standard used for sending
|
||||
tracking data to applications without SteamVR (ex. Quest standalone).
|
||||
# This cares about multilines
|
||||
settings-osc-vrchat-description-guide =
|
||||
Make sure to enable OSC in VRChat via the Action Menu under OSC > Enabled.
|
||||
To allow receiving HMD and controller data from VRChat, go in your main menu's
|
||||
settings under Tracking & IK > Allow Sending Head and Wrist VR Tracking OSC Data.
|
||||
|
||||
To allow receiving HMD and controller data from VRChat, go in your main menu's settings under Tracking & IK > Allow Sending Head and Wrist VR Tracking OSC Data.
|
||||
settings-osc-vrchat-enable = Enable
|
||||
settings-osc-vrchat-enable-description = Toggle the sending and receiving of data.
|
||||
settings-osc-vrchat-enable-label = Enable
|
||||
@@ -610,11 +662,7 @@ settings-osc-vmc-network-address-description = Choose which address to send out
|
||||
settings-osc-vmc-network-address-placeholder = IPV4 address
|
||||
settings-osc-vmc-vrm = VRM Model
|
||||
settings-osc-vmc-vrm-description = Load a VRM model to allow head anchor and enable a higher compatibility with other applications.
|
||||
settings-osc-vmc-vrm-model_unloaded = No model loaded
|
||||
settings-osc-vmc-vrm-model_loaded = { $titled ->
|
||||
*[false] Untitled model loaded
|
||||
[true] Model loaded: { $name }
|
||||
}
|
||||
settings-osc-vmc-vrm-untitled_model = Untitled model
|
||||
settings-osc-vmc-vrm-file_select = Drag & drop a model to use, or <u>browse</u>
|
||||
settings-osc-vmc-anchor_hip = Anchor at hips
|
||||
settings-osc-vmc-anchor_hip-description = Anchor the tracking at the hips, useful for seated VTubing. If disabling, load a VRM model.
|
||||
@@ -672,6 +720,7 @@ onboarding-wifi_creds-submit = Submit!
|
||||
onboarding-wifi_creds-ssid =
|
||||
.label = Wi-Fi name
|
||||
.placeholder = Enter Wi-Fi name
|
||||
onboarding-wifi_creds-ssid-required = Wi-Fi name is required
|
||||
onboarding-wifi_creds-password =
|
||||
.label = Password
|
||||
.placeholder = Enter password
|
||||
@@ -721,6 +770,7 @@ onboarding-connect_tracker-issue-serial = I'm having trouble connecting!
|
||||
onboarding-connect_tracker-usb = USB Tracker
|
||||
onboarding-connect_tracker-connection_status-none = Looking for trackers
|
||||
onboarding-connect_tracker-connection_status-serial_init = Connecting to serial device
|
||||
onboarding-connect_tracker-connection_status-obtaining_mac_address = Obtaining the tracker mac address
|
||||
onboarding-connect_tracker-connection_status-provisioning = Sending Wi-Fi credentials
|
||||
onboarding-connect_tracker-connection_status-connecting = Trying to connect to Wi-Fi
|
||||
onboarding-connect_tracker-connection_status-looking_for_server = Looking for server
|
||||
@@ -759,6 +809,79 @@ onboarding-assignment_tutorial-second_step-v2 = 2. Attach the strap to your trac
|
||||
onboarding-assignment_tutorial-second_step-continuation-v2 = The velcro side for the extension should be facing up like the following image:
|
||||
onboarding-assignment_tutorial-done = I put stickers and straps!
|
||||
|
||||
## Usage reason choose
|
||||
onboarding-usage-choose = What are you gonna use SlimeVR for?
|
||||
onboarding-usage-choose-description = What are you gonna use SlimeVR for?
|
||||
onboarding-usage-choose-option-title = { $mode ->
|
||||
*[VR] Virtual Reality
|
||||
[VTUBING] VTuber
|
||||
[MOCAP] Motion Capture
|
||||
}
|
||||
onboarding-usage-choose-option-label = { $mode ->
|
||||
*[VR] For use with games and applications that use a headset
|
||||
[VTUBING] For use with VTubing programs that use the VMC protocol
|
||||
[MOCAP] For recording a whole body with precise tracking.
|
||||
}
|
||||
onboarding-usage-choose-option-description = { $mode ->
|
||||
*[VR] Users of SteamVR or VR programs that use OSC can select this option to get right to it.
|
||||
[VTUBING] VTubing programs work with SlimeVR using VMC (Virtual Motion Capture), this is what to pick for that!
|
||||
[MOCAP] Many 3D programs can record live mocap for use in animation, and BVH recording is also supported directly in the app.
|
||||
}
|
||||
|
||||
## VR usage choose
|
||||
onboarding-usage-vr-choose = Choose your VR setup
|
||||
onboarding-usage-vr-choose-description = There are different ways to connect SlimeVR to your virtual reality setup! You can decide which you will use here.
|
||||
onboarding-usage-vr-choose-steamvr = I use SteamVR
|
||||
onboarding-usage-vr-choose-steamvr-label = For PCVR
|
||||
# uses multiline
|
||||
onboarding-usage-vr-choose-steamvr-description =
|
||||
SlimeVR emulates SteamVR trackers using the rotational data of the trackers and a human skeleton model, so SteamVR games and programs can use it for full body tracking.
|
||||
|
||||
SteamVR must be installed and a headset or positional head tracker connected to the SlimeVR server to use this method.
|
||||
onboarding-usage-vr-choose-steamvr-warning = The SteamVR driver is currently not connected, <b>please turn on SteamVR</b> or check <docs>the docs for more info</docs>.
|
||||
onboarding-usage-vr-choose-standalone = I use standalone
|
||||
onboarding-usage-vr-choose-standalone-label = For VRChat Quest/Pico users
|
||||
onboarding-usage-vr-choose-standalone-description =
|
||||
Standalone use connects through OSC instead of SteamVR to provide full body tracking with SlimeVR.
|
||||
Any PC that can run SlimeVR server can function like this, as well as phones, which are the recommended ways for best ergonomics.
|
||||
onboarding-usage-vr-standalone-title = Setting up VRChat
|
||||
onboarding-usage-vr-standalone-next = Done!
|
||||
|
||||
## Mocap head usage choose
|
||||
onboarding-usage-mocap-head_choose = What kind of head tracking do you want?
|
||||
onboarding-usage-mocap-head_choose-description = You can use either a tracker or a headset for the head!
|
||||
|
||||
onboarding-usage-mocap-head_choose-standalone = SlimeVR head tracker
|
||||
onboarding-usage-mocap-head_choose-standalone-label = Use an IMU tracker for tracking position
|
||||
onboarding-usage-mocap-head_choose-standalone-description =
|
||||
This enables head tracking using a head mounted SlimeVR tracker.
|
||||
|
||||
This is much less precise in the way that if you walk and return to your starting point, you won't be on the same place on the recording.
|
||||
onboarding-usage-mocap-head_choose-standalone-button = Use IMU tracker
|
||||
|
||||
onboarding-usage-mocap-head_choose-steamvr = SteamVR head tracking
|
||||
onboarding-usage-mocap-head_choose-steamvr-label = Use an HMD or a positional tracker for precision
|
||||
onboarding-usage-mocap-head_choose-steamvr-description =
|
||||
Most accurate way to track the head, using true positional data as reference.
|
||||
|
||||
This allows for the best quality motion capture recordings as well as movements that require both feet to leave the floor at the same time.
|
||||
onboarding-usage-mocap-head_choose-steamvr-button = Use SteamVR
|
||||
|
||||
## Mocap data mode choose
|
||||
onboarding-usage-mocap-data_choose = What kind of data format to use?
|
||||
onboarding-usage-mocap-data_choose-description = description
|
||||
|
||||
onboarding-usage-mocap-data_choose-option-title = { $mode ->
|
||||
*[BVH] BVH
|
||||
[STEAMVR] SteamVR
|
||||
[VMC] VMC
|
||||
}
|
||||
onboarding-usage-mocap-data_choose-option-label = { $mode ->
|
||||
*[BVH] Natively supported on most animation programs
|
||||
[STEAMVR] For programs that support OpenVR as a source of data
|
||||
[VMC] Popular data protocol for VTubing
|
||||
}
|
||||
|
||||
## Tracker assignment setup
|
||||
onboarding-assign_trackers-back = Go Back to Wi-Fi Credentials
|
||||
onboarding-assign_trackers-title = Assign trackers
|
||||
@@ -1013,6 +1136,165 @@ status_system-StatusSteamVRDisconnected = { $type ->
|
||||
status_system-StatusTrackerError = The { $trackerName } tracker has an error.
|
||||
status_system-StatusUnassignedHMD = The VR headset should be assigned as a head tracker.
|
||||
|
||||
|
||||
## Firmware tool globals
|
||||
firmware_tool-next_step = Next Step
|
||||
firmware_tool-previous_step = Previous Step
|
||||
firmware_tool-ok = Looks good
|
||||
firmware_tool-retry = Retry
|
||||
|
||||
firmware_tool-loading = Loading...
|
||||
|
||||
## Firmware tool Steps
|
||||
firmware_tool = DIY Firmware tool
|
||||
firmware_tool-description =
|
||||
Allows you to configure and flash your DIY trackers
|
||||
firmware_tool-not_available = Oops, the firmware tool is not available at the moment. Come back later!
|
||||
firmware_tool-not_compatible = The firmware tool is not compatible with this version of the server. Please update your server!
|
||||
|
||||
firmware_tool-board_step = Select your Board
|
||||
firmware_tool-board_step-description = Select one of the boards listed below.
|
||||
|
||||
firmware_tool-board_pins_step = Check the pins
|
||||
firmware_tool-board_pins_step-description =
|
||||
Please verify that the selected pins are correct.
|
||||
If you followed the SlimeVR documentation the defaults values should be correct
|
||||
firmware_tool-board_pins_step-enable_led = Enable LED
|
||||
firmware_tool-board_pins_step-led_pin =
|
||||
.label = LED Pin
|
||||
.placeholder = Enter the pin address of the LED
|
||||
|
||||
firmware_tool-board_pins_step-battery_type = Select the battery type
|
||||
firmware_tool-board_pins_step-battery_type-BAT_EXTERNAL = External battery
|
||||
firmware_tool-board_pins_step-battery_type-BAT_INTERNAL = Internal battery
|
||||
firmware_tool-board_pins_step-battery_type-BAT_INTERNAL_MCP3021 = Internal MCP3021
|
||||
firmware_tool-board_pins_step-battery_type-BAT_MCP3021 = MCP3021
|
||||
|
||||
|
||||
firmware_tool-board_pins_step-battery_sensor_pin =
|
||||
.label = Battery sensor Pin
|
||||
.placeholder = Enter the pin address of battery sensor
|
||||
firmware_tool-board_pins_step-battery_resistor =
|
||||
.label = Battery Resistor (Ohms)
|
||||
.placeholder = Enter the value of battery resistor
|
||||
firmware_tool-board_pins_step-battery_shield_resistor-0 =
|
||||
.label = Battery Shield R1 (Ohms)
|
||||
.placeholder = Enter the value of Battery Shield R1
|
||||
firmware_tool-board_pins_step-battery_shield_resistor-1 =
|
||||
.label = Battery Shield R2 (Ohms)
|
||||
.placeholder = Enter the value of Battery Shield R2
|
||||
|
||||
firmware_tool-add_imus_step = Declare your IMUs
|
||||
firmware_tool-add_imus_step-description =
|
||||
Please add the IMUs that your tracker has
|
||||
If you followed the SlimeVR documentation the defaults values should be correct
|
||||
firmware_tool-add_imus_step-imu_type-label = IMU type
|
||||
firmware_tool-add_imus_step-imu_type-placeholder = Select the type of IMU
|
||||
firmware_tool-add_imus_step-imu_rotation =
|
||||
.label = IMU Rotation (deg)
|
||||
.placeholder = Rotation angle of the IMU
|
||||
firmware_tool-add_imus_step-scl_pin =
|
||||
.label = SCL Pin
|
||||
.placeholder = Pin address of SCL
|
||||
firmware_tool-add_imus_step-sda_pin =
|
||||
.label = SDA Pin
|
||||
.placeholder = Pin address of SDA
|
||||
firmware_tool-add_imus_step-int_pin =
|
||||
.label = INT Pin
|
||||
.placeholder = Pin address of INT
|
||||
firmware_tool-add_imus_step-optional_tracker =
|
||||
.label = Optional tracker
|
||||
firmware_tool-add_imus_step-show_less = Show Less
|
||||
firmware_tool-add_imus_step-show_more = Show More
|
||||
firmware_tool-add_imus_step-add_more = Add more IMUs
|
||||
|
||||
firmware_tool-select_firmware_step = Select the firmware version
|
||||
firmware_tool-select_firmware_step-description =
|
||||
Please choose what version of the firmware you want to use
|
||||
firmware_tool-select_firmware_step-show-third-party =
|
||||
.label = Show third party firmwares
|
||||
|
||||
firmware_tool-flash_method_step = Flashing Method
|
||||
firmware_tool-flash_method_step-description =
|
||||
Please select the flashing method you want to use
|
||||
firmware_tool-flash_method_step-ota =
|
||||
.label = OTA
|
||||
.description = Use the over the air method. Your tracker will use the Wi-Fi to update it's firmware. Works only on already setup trackers.
|
||||
firmware_tool-flash_method_step-serial =
|
||||
.label = Serial
|
||||
.description = Use a USB cable to update your tracker.
|
||||
|
||||
firmware_tool-flashbtn_step = Press the boot btn
|
||||
firmware_tool-flashbtn_step-description = Before going into the next step there is a few things you need to do
|
||||
|
||||
firmware_tool-flashbtn_step-board_SLIMEVR = Turn off the tracker, remove the case (if any), connect a USB cable to this computer, then do one of the following steps according to your SlimeVR board revision:
|
||||
firmware_tool-flashbtn_step-board_SLIMEVR-r11 = Turn on the tracker while shorting the second rectangular FLASH pad from the edge on the top side of the board, and the metal shield of the microcontroller
|
||||
firmware_tool-flashbtn_step-board_SLIMEVR-r12 = Turn on the tracker while shorting the circular FLASH pad on the top side of the board, and the metal shield of the microcontroller
|
||||
firmware_tool-flashbtn_step-board_SLIMEVR-r14 = Turn on the tracker while pushing in the FLASH button on the top side of the board
|
||||
|
||||
firmware_tool-flashbtn_step-board_OTHER = Before flashing you will probably need to put the tracker into bootloader mode.
|
||||
Most of the time it means pressing the boot button on the board before the flashing process starts.
|
||||
If the flashing process timeout at the begining of the flashing it probably means that the tracker was not in bootloader mode
|
||||
Please refer to the flashing instructions of your board to know how to turn on the boatloader mode
|
||||
|
||||
|
||||
|
||||
firmware_tool-flash_method_ota-devices = Detected OTA Devices:
|
||||
firmware_tool-flash_method_ota-no_devices = There are no boards that can be updated using OTA, make sure you selected the correct board type
|
||||
firmware_tool-flash_method_serial-wifi = Wi-Fi Credentials:
|
||||
firmware_tool-flash_method_serial-devices-label = Detected Serial Devices:
|
||||
firmware_tool-flash_method_serial-devices-placeholder = Select a serial device
|
||||
firmware_tool-flash_method_serial-no_devices = There are no compatible serial devices detected, make sure the tracker is plugged in
|
||||
|
||||
firmware_tool-build_step = Building
|
||||
firmware_tool-build_step-description =
|
||||
The firmware is building, please wait
|
||||
|
||||
firmware_tool-flashing_step = Flashing
|
||||
firmware_tool-flashing_step-description =
|
||||
Your trackers are flashing, please follow the instructions on the screen
|
||||
firmware_tool-flashing_step-warning = Do not unplug or restart the tracker during the upload process unless told to, it may make your board unusable
|
||||
firmware_tool-flashing_step-flash_more = Flash more trackers
|
||||
firmware_tool-flashing_step-exit = Exit
|
||||
|
||||
## firmware tool build status
|
||||
firmware_tool-build-CREATING_BUILD_FOLDER = Creating the build folder
|
||||
firmware_tool-build-DOWNLOADING_FIRMWARE = Downloading the firmware
|
||||
firmware_tool-build-EXTRACTING_FIRMWARE = Extracting the firmware
|
||||
firmware_tool-build-SETTING_UP_DEFINES = Configuring the defines
|
||||
firmware_tool-build-BUILDING = Building the firmware
|
||||
firmware_tool-build-SAVING = Saving the build
|
||||
firmware_tool-build-DONE = Build Complete
|
||||
firmware_tool-build-ERROR = Unable to build the firmware
|
||||
|
||||
## Firmware update status
|
||||
firmware_update-status-DOWNLOADING = Downloading the firmware
|
||||
firmware_update-status-NEED_MANUAL_REBOOT = Waiting for the user to reboot the tracker
|
||||
firmware_update-status-AUTHENTICATING = Authenticating with the mcu
|
||||
firmware_update-status-UPLOADING = Uploading the firmware
|
||||
firmware_update-status-SYNCING_WITH_MCU = Syncing with the mcu
|
||||
firmware_update-status-REBOOTING = Rebooting the tracker
|
||||
firmware_update-status-PROVISIONING = Setting Wi-Fi credentials
|
||||
firmware_update-status-DONE = Update complete!
|
||||
firmware_update-status-ERROR_DEVICE_NOT_FOUND = Could not find the device
|
||||
firmware_update-status-ERROR_TIMEOUT = The update process timed out
|
||||
firmware_update-status-ERROR_DOWNLOAD_FAILED = Could not download the firmware
|
||||
firmware_update-status-ERROR_AUTHENTICATION_FAILED = Could not authenticate with the mcu
|
||||
firmware_update-status-ERROR_UPLOAD_FAILED = Could not upload the firmware
|
||||
firmware_update-status-ERROR_PROVISIONING_FAILED = Could not set the Wi-Fi credentials
|
||||
firmware_update-status-ERROR_UNSUPPORTED_METHOD = The update method is not supported
|
||||
firmware_update-status-ERROR_UNKNOWN = Unknown error
|
||||
|
||||
## Dedicated Firmware Update Page
|
||||
firmware_update-title = Firmware update
|
||||
firmware_update-devices = Available Devices
|
||||
firmware_update-devices-description = Please select the trackers you want to update to the latest version of SlimeVR firmware
|
||||
firmware_update-no_devices = Plase make sure that the trackers you want to update are ON and connected to the Wi-Fi!
|
||||
firmware_update-changelog-title = Updating to {$version}
|
||||
firmware_update-looking_for_devices = Looking for devices to update...
|
||||
firmware_update-retry = Retry
|
||||
firmware_update-update = Update Selected Trackers
|
||||
|
||||
## Tray Menu
|
||||
tray_menu-show = Show
|
||||
tray_menu-hide = Hide
|
||||
|
||||
@@ -231,9 +231,9 @@ tracker-settings-drift_compensation_section-edit = 允許偏移補償
|
||||
tracker-settings-use_mag = 允許使用這個追蹤器的磁力計
|
||||
# Multiline!
|
||||
tracker-settings-use_mag-description =
|
||||
如果「允許追蹤器使用磁力計」已啟用,是否要在這個追蹤器上啟用它來減緩偏移?<b>切換本選項時請勿關閉追蹤器的電源!</b>
|
||||
如果「在追蹤器上啟用磁力計」功能已開啟,是否要在這個追蹤器上啟用它來減緩偏移?<b>切換本選項時請勿關閉追蹤器的電源!</b>
|
||||
|
||||
請先開啟「允許追蹤器使用磁力計」功能,<magSetting>點選此處以移動至該設定</magSetting>。
|
||||
請先開啟「在追蹤器上啟用磁力計」功能,<magSetting>點選此處以移動至該設定</magSetting>。
|
||||
tracker-settings-use_mag-label = 允許使用這個追蹤器的磁力計
|
||||
# The .<name> means it's an attribute and it's related to the top key.
|
||||
# In this case that is the settings for the assignment section.
|
||||
@@ -397,9 +397,9 @@ settings-general-tracker_mechanics-save_mounting_reset-description =
|
||||
settings-general-tracker_mechanics-save_mounting_reset-enabled-label = 儲存自動配戴重置的校正
|
||||
settings-general-tracker_mechanics-use_mag_on_all_trackers = 在有磁力計支援的 IMU 追蹤器上啟用磁力計
|
||||
settings-general-tracker_mechanics-use_mag_on_all_trackers-description =
|
||||
在所有有磁力計韌體支援的追蹤器上啟用磁力計,在磁場穩定的環境中可以減緩偏移。
|
||||
可以依照個別追蹤器關閉本功能。<b>切換此選項時請勿關閉任何一個追蹤器的電源!</b>
|
||||
settings-general-tracker_mechanics-use_mag_on_all_trackers-label = 允許追蹤器使用磁力計
|
||||
在所有有韌體支援的追蹤器上使用磁力計,在磁場穩定的環境中可以減緩偏移。
|
||||
開啟此選項後,可以個別在追蹤器選項內停用磁力計。<b>切換此選項時請勿關閉任何一個追蹤器的電源!</b>
|
||||
settings-general-tracker_mechanics-use_mag_on_all_trackers-label = 在追蹤器上啟用磁力計
|
||||
|
||||
## FK/Tracking settings
|
||||
|
||||
|
||||
BIN
gui/public/images/R11_board_reset.webp
Normal file
|
After Width: | Height: | Size: 450 KiB |
BIN
gui/public/images/R12_board_reset.webp
Normal file
|
After Width: | Height: | Size: 578 KiB |
BIN
gui/public/images/R14_board_reset_sw.webp
Normal file
|
After Width: | Height: | Size: 428 KiB |
BIN
gui/public/images/nighty-vr-sitting.webp
Normal file
|
After Width: | Height: | Size: 153 KiB |
BIN
gui/public/images/usage-mocap.webp
Normal file
|
After Width: | Height: | Size: 222 KiB |
BIN
gui/public/images/usage-vr.webp
Normal file
|
After Width: | Height: | Size: 287 KiB |
BIN
gui/public/images/usage-vtuber.webp
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
gui/public/images/vrslimes.webp
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
gui/public/videos/vrchatosc.webm
Normal file
@@ -28,7 +28,7 @@ shadow-rs = "0.35"
|
||||
[dependencies]
|
||||
serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tauri = { version = "2.0", features = ["devtools", "tray-icon", "image-png"] }
|
||||
tauri = { version = "2.0", features = ["devtools", "tray-icon", "image-png", "rustls-tls"] }
|
||||
tauri-runtime = "2.0"
|
||||
tauri-plugin-dialog = "2.0"
|
||||
tauri-plugin-fs = "2.0"
|
||||
@@ -53,6 +53,7 @@ rfd = { version = "0.15", features = ["gtk3"], default-features = false }
|
||||
dirs-next = "2.0.0"
|
||||
discord-sdk = "0.3.6"
|
||||
tokio = { version = "1.37.0", features = ["time"] }
|
||||
itertools = "0.13.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
win32job = "1"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#![cfg_attr(all(not(debug_assertions), windows), windows_subsystem = "windows")]
|
||||
use std::env;
|
||||
use std::panic;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -68,84 +69,125 @@ fn main() -> Result<()> {
|
||||
let tauri_context = tauri::generate_context!();
|
||||
|
||||
// Set up loggers and global handlers
|
||||
let _logger = {
|
||||
use flexi_logger::{
|
||||
Age, Cleanup, Criterion, Duplicate, FileSpec, Logger, Naming, WriteMode,
|
||||
};
|
||||
use tauri::Error;
|
||||
|
||||
// Based on https://docs.rs/tauri/2.0.0-alpha.10/src/tauri/path/desktop.rs.html#238-256
|
||||
#[cfg(target_os = "macos")]
|
||||
let path = dirs_next::home_dir().ok_or(Error::UnknownPath).map(|dir| {
|
||||
dir.join("Library/Logs")
|
||||
.join(&tauri_context.config().identifier)
|
||||
});
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let path = dirs_next::data_dir()
|
||||
.ok_or(Error::UnknownPath)
|
||||
.map(|dir| dir.join(&tauri_context.config().identifier).join("logs"));
|
||||
|
||||
Logger::try_with_env_or_str("info")?
|
||||
.log_to_file(
|
||||
FileSpec::default().directory(path.expect("We need a log dir")),
|
||||
)
|
||||
.format_for_files(util::logger_format)
|
||||
.format_for_stderr(util::logger_format)
|
||||
.rotate(
|
||||
Criterion::Age(Age::Day),
|
||||
Naming::Timestamps,
|
||||
Cleanup::KeepLogFiles(2),
|
||||
)
|
||||
.duplicate_to_stderr(Duplicate::All)
|
||||
.write_mode(WriteMode::BufferAndFlush)
|
||||
.start()?
|
||||
};
|
||||
let _logger = setup_logger(&tauri_context);
|
||||
|
||||
// Ensure child processes die when spawned on windows
|
||||
// and then check for WebView2's existence
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use crate::util::webview2_exists;
|
||||
use win32job::{ExtendedLimitInfo, Job};
|
||||
setup_webview2()?;
|
||||
|
||||
let mut info = ExtendedLimitInfo::new();
|
||||
info.limit_kill_on_job_close();
|
||||
let job = Job::create_with_limit_info(&mut info).expect("Failed to create Job");
|
||||
job.assign_current_process()
|
||||
.expect("Failed to assign current process to Job");
|
||||
|
||||
// We don't do anything with the job anymore, but we shouldn't drop it because that would
|
||||
// terminate our process tree. So we intentionally leak it instead.
|
||||
std::mem::forget(job);
|
||||
|
||||
if !webview2_exists() {
|
||||
// This makes a dialog appear which let's you press Ok or Cancel
|
||||
// If you press Ok it will open the SlimeVR installer documentation
|
||||
use rfd::{
|
||||
MessageButtons, MessageDialog, MessageDialogResult, MessageLevel,
|
||||
};
|
||||
|
||||
let confirm = MessageDialog::new()
|
||||
.set_title("SlimeVR")
|
||||
.set_description("Couldn't find WebView2 installed. You can install it with the SlimeVR installer")
|
||||
.set_buttons(MessageButtons::OkCancel)
|
||||
.set_level(MessageLevel::Error)
|
||||
.show();
|
||||
if confirm == MessageDialogResult::Ok {
|
||||
open::that("https://docs.slimevr.dev/server-setup/installing-and-connecting.html#install-the-latest-slimevr-installer").unwrap();
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
// Check for environment variables that can affect the server, and if so, warn in log and GUI
|
||||
check_environment_variables();
|
||||
|
||||
// Spawn server process
|
||||
let exit_flag = Arc::new(AtomicBool::new(false));
|
||||
let backend = Arc::new(Mutex::new(Option::<CommandChild>::None));
|
||||
let backend_termination = backend.clone();
|
||||
let run_path = get_launch_path(cli);
|
||||
|
||||
let server_info = if let Some(p) = run_path {
|
||||
let server_info = execute_server(cli)?;
|
||||
let build_result = setup_tauri(
|
||||
tauri_context,
|
||||
server_info,
|
||||
exit_flag.clone(),
|
||||
backend.clone(),
|
||||
);
|
||||
|
||||
tauri_build_result(build_result, exit_flag, backend);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_logger(context: &tauri::Context) -> Result<flexi_logger::LoggerHandle> {
|
||||
use flexi_logger::{
|
||||
Age, Cleanup, Criterion, Duplicate, FileSpec, Logger, Naming, WriteMode,
|
||||
};
|
||||
use tauri::Error;
|
||||
|
||||
// Based on https://docs.rs/tauri/2.0.0-alpha.10/src/tauri/path/desktop.rs.html#238-256
|
||||
#[cfg(target_os = "macos")]
|
||||
let path = dirs_next::home_dir()
|
||||
.ok_or(Error::UnknownPath)
|
||||
.map(|dir| dir.join("Library/Logs").join(&context.config().identifier));
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let path = dirs_next::data_dir()
|
||||
.ok_or(Error::UnknownPath)
|
||||
.map(|dir| dir.join(&context.config().identifier).join("logs"));
|
||||
|
||||
Ok(Logger::try_with_env_or_str("info")?
|
||||
.log_to_file(FileSpec::default().directory(path.expect("We need a log dir")))
|
||||
.format_for_files(|w, now, record| util::logger_format(w, now, record, false))
|
||||
.format_for_stderr(|w, now, record| util::logger_format(w, now, record, true))
|
||||
.rotate(
|
||||
Criterion::Age(Age::Day),
|
||||
Naming::Timestamps,
|
||||
Cleanup::KeepLogFiles(2),
|
||||
)
|
||||
.duplicate_to_stderr(Duplicate::All)
|
||||
.write_mode(WriteMode::BufferAndFlush)
|
||||
.start()?)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn setup_webview2() -> Result<()> {
|
||||
use crate::util::webview2_exists;
|
||||
use win32job::{ExtendedLimitInfo, Job};
|
||||
|
||||
let mut info = ExtendedLimitInfo::new();
|
||||
info.limit_kill_on_job_close();
|
||||
let job = Job::create_with_limit_info(&mut info).expect("Failed to create Job");
|
||||
job.assign_current_process()
|
||||
.expect("Failed to assign current process to Job");
|
||||
|
||||
// We don't do anything with the job anymore, but we shouldn't drop it because that would
|
||||
// terminate our process tree. So we intentionally leak it instead.
|
||||
std::mem::forget(job);
|
||||
|
||||
if !webview2_exists() {
|
||||
// This makes a dialog appear which let's you press Ok or Cancel
|
||||
// If you press Ok it will open the SlimeVR installer documentation
|
||||
use rfd::{MessageButtons, MessageDialog, MessageDialogResult, MessageLevel};
|
||||
|
||||
let confirm = MessageDialog::new()
|
||||
.set_title("SlimeVR")
|
||||
.set_description("Couldn't find WebView2 installed. You can install it with the SlimeVR installer")
|
||||
.set_buttons(MessageButtons::OkCancel)
|
||||
.set_level(MessageLevel::Error)
|
||||
.show();
|
||||
if confirm == MessageDialogResult::Ok {
|
||||
open::that("https://docs.slimevr.dev/server-setup/installing-and-connecting.html#install-the-latest-slimevr-installer").unwrap();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_environment_variables() {
|
||||
use itertools::Itertools;
|
||||
const ENVS_TO_CHECK: &[&str] = &["_JAVA_OPTIONS", "JAVA_TOOL_OPTIONS"];
|
||||
let checked_envs = ENVS_TO_CHECK
|
||||
.into_iter()
|
||||
.filter_map(|e| {
|
||||
let Ok(data) = env::var(e) else {
|
||||
return None;
|
||||
};
|
||||
log::warn!("{e} is set to: {data}");
|
||||
Some(e)
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
if !checked_envs.is_empty() {
|
||||
rfd::MessageDialog::new()
|
||||
.set_title("SlimeVR")
|
||||
.set_description(format!("You have environment variables {} set, which may cause the SlimeVR Server to fail to launch properly.", checked_envs))
|
||||
.set_level(rfd::MessageLevel::Warning)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_server(
|
||||
cli: Cli,
|
||||
) -> Result<Option<(std::ffi::OsString, std::path::PathBuf)>> {
|
||||
use const_format::formatcp;
|
||||
if let Some(p) = get_launch_path(cli) {
|
||||
log::info!("Server found on path: {}", p.to_str().unwrap());
|
||||
|
||||
// Check if any Java already installed is compatible
|
||||
@@ -155,19 +197,29 @@ fn main() -> Result<()> {
|
||||
.then(|| jre.into_os_string())
|
||||
.or_else(|| valid_java_paths().first().map(|x| x.0.to_owned()));
|
||||
let Some(java_bin) = java_bin else {
|
||||
show_error(&format!("Couldn't find a compatible Java version, please download Java {} or higher", MINIMUM_JAVA_VERSION));
|
||||
return Ok(());
|
||||
show_error(formatcp!(
|
||||
"Couldn't find a compatible Java version, please download Java {} or higher",
|
||||
MINIMUM_JAVA_VERSION
|
||||
));
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
log::info!("Using Java binary: {:?}", java_bin);
|
||||
Some((java_bin, p))
|
||||
Ok(Some((java_bin, p)))
|
||||
} else {
|
||||
log::warn!("No server found. We will not start the server.");
|
||||
None
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_tauri(
|
||||
context: tauri::Context,
|
||||
server_info: Option<(std::ffi::OsString, std::path::PathBuf)>,
|
||||
exit_flag: Arc<AtomicBool>,
|
||||
backend: Arc<Mutex<Option<CommandChild>>>,
|
||||
) -> Result<tauri::App, tauri::Error> {
|
||||
let exit_flag_terminated = exit_flag.clone();
|
||||
let build_result = tauri::Builder::default()
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
@@ -277,7 +329,14 @@ fn main() -> Result<()> {
|
||||
// WindowEvent::Resized(_) => std::thread::sleep(std::time::Duration::from_nanos(1)),
|
||||
_ => (),
|
||||
})
|
||||
.build(tauri_context);
|
||||
.build(context)
|
||||
}
|
||||
|
||||
fn tauri_build_result(
|
||||
build_result: Result<tauri::App, tauri::Error>,
|
||||
exit_flag: Arc<AtomicBool>,
|
||||
backend: Arc<Mutex<Option<CommandChild>>>,
|
||||
) {
|
||||
match build_result {
|
||||
Ok(app) => {
|
||||
app.run(move |app_handle, event| match event {
|
||||
@@ -291,7 +350,7 @@ fn main() -> Result<()> {
|
||||
Err(e) => log::error!("failed to save window state: {}", e),
|
||||
}
|
||||
|
||||
let mut lock = backend_termination.lock().unwrap();
|
||||
let mut lock = backend.lock().unwrap();
|
||||
let Some(ref mut child) = *lock else { return };
|
||||
let write_result = child.write(b"exit\n");
|
||||
match write_result {
|
||||
@@ -335,6 +394,4 @@ fn main() -> Result<()> {
|
||||
show_error(&error.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{collections::HashMap, sync::Mutex};
|
||||
|
||||
use tauri::{
|
||||
image::Image,
|
||||
include_image,
|
||||
menu::{Menu, MenuBuilder, MenuItemBuilder, MenuItemKind},
|
||||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||
AppHandle, Emitter, Manager, Runtime, State,
|
||||
@@ -107,10 +107,10 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
||||
.icon_as_template(true)
|
||||
.menu_on_left_click(false)
|
||||
.icon(if cfg!(target_os = "macos") {
|
||||
Image::from_bytes(include_bytes!("../icons/appleTrayIcon.png"))
|
||||
include_image!("icons/appleTrayIcon.png")
|
||||
} else {
|
||||
Image::from_bytes(include_bytes!("../icons/128x128.png"))
|
||||
}?)
|
||||
include_image!("icons/128x128.png")
|
||||
})
|
||||
.on_menu_event(move |app, event| match event.id.as_ref() {
|
||||
"quit" => app.emit("try-close", "tray").unwrap(),
|
||||
"toggle" => {
|
||||
|
||||
@@ -107,7 +107,8 @@ pub fn show_error(text: &str) -> bool {
|
||||
.set_description(text)
|
||||
.set_buttons(MessageButtons::Ok)
|
||||
.set_level(MessageLevel::Error)
|
||||
.show() == MessageDialogResult::Ok
|
||||
.show()
|
||||
== MessageDialogResult::Ok
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
@@ -222,20 +223,26 @@ pub fn logger_format(
|
||||
w: &mut dyn std::io::Write,
|
||||
_now: &mut DeferredNow,
|
||||
record: &Record,
|
||||
ansi: bool,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let level = record.level();
|
||||
let module_path = record.module_path().unwrap_or("<unnamed>");
|
||||
// optionally print target
|
||||
// Optionally print target
|
||||
let target = if module_path.starts_with(record.target()) {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!(", {}", record.target())
|
||||
};
|
||||
write!(
|
||||
w,
|
||||
"{} [{}{target}] {}",
|
||||
style(level).paint(level.to_string()),
|
||||
record.module_path().unwrap_or("<unnamed>"),
|
||||
style(level).paint(record.args().to_string())
|
||||
)
|
||||
// A toggle for ansi formatting, mainly disabled for file logs, but enabled for terminal logs.
|
||||
if ansi {
|
||||
write!(
|
||||
w,
|
||||
"{} [{}{target}] {}",
|
||||
style(level).paint(level.to_string()),
|
||||
module_path,
|
||||
style(level).paint(record.args().to_string())
|
||||
)
|
||||
} else {
|
||||
write!(w, "{} [{}{target}] {}", level, module_path, record.args())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,11 +51,22 @@ import { useBreakpoint, useIsTauri } from './hooks/breakpoint';
|
||||
import { VRModePage } from './components/vr-mode/VRModePage';
|
||||
import { InterfaceSettings } from './components/settings/pages/InterfaceSettings';
|
||||
import { error, log } from './utils/logging';
|
||||
import { FirmwareToolSettings } from './components/firmware-tool/FirmwareTool';
|
||||
import { AppLayout } from './AppLayout';
|
||||
import { Preload } from './components/Preload';
|
||||
import { UnknownDeviceModal } from './components/UnknownDeviceModal';
|
||||
import { useDiscordPresence } from './hooks/discord-presence';
|
||||
import { EmptyLayout } from './components/EmptyLayout';
|
||||
import { AdvancedSettings } from './components/settings/pages/AdvancedSettings';
|
||||
import { FirmwareUpdate } from './components/firmware-update/FirmwareUpdate';
|
||||
import { UsageChoose } from './components/onboarding/pages/usage-reason/UsageChoose';
|
||||
import { VRUsageChoose } from './components/onboarding/pages/usage-reason/VRUsageChoose';
|
||||
import { StandaloneUsageSetup } from './components/onboarding/pages/usage-reason/StandaloneUsageSetup';
|
||||
import { HeadTrackingChoose } from './components/onboarding/pages/usage-reason/HeadTrackingChoose';
|
||||
import { MocapDataChoose } from './components/onboarding/pages/usage-reason/MocapDataChoose';
|
||||
import { MocapVMCSetup } from './components/onboarding/pages/usage-reason/MocapVMCSetup';
|
||||
import { MocapBVHSetup } from './components/onboarding/pages/usage-reason/MocapBVHSetup';
|
||||
import { MocapSteamSetup } from './components/onboarding/pages/usage-reason/MocapSteamSetup';
|
||||
|
||||
export const GH_REPO = 'SlimeVR/SlimeVR-Server';
|
||||
export const VersionContext = createContext('');
|
||||
@@ -81,6 +92,14 @@ function Layout() {
|
||||
</MainLayout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/firmware-update"
|
||||
element={
|
||||
<MainLayout isMobile={isMobile} widgets={false}>
|
||||
<FirmwareUpdate />
|
||||
</MainLayout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/vr-mode"
|
||||
element={
|
||||
@@ -105,6 +124,7 @@ function Layout() {
|
||||
</SettingsLayout>
|
||||
}
|
||||
>
|
||||
<Route path="firmware-tool" element={<FirmwareToolSettings />} />
|
||||
<Route path="trackers" element={<GeneralSettings />} />
|
||||
<Route path="serial" element={<Serial />} />
|
||||
<Route path="osc/router" element={<OSCRouterSettings />} />
|
||||
@@ -132,6 +152,21 @@ function Layout() {
|
||||
path="assign-tutorial"
|
||||
element={<AssignmentTutorialPage />}
|
||||
/>
|
||||
|
||||
<Route path="usage">
|
||||
<Route path="choose" element={<UsageChoose />} />
|
||||
<Route path="vr/choose" element={<VRUsageChoose />} />
|
||||
<Route path="vr/standalone" element={<StandaloneUsageSetup />} />
|
||||
<Route path="mocap/data/choose" element={<MocapDataChoose />} />
|
||||
<Route
|
||||
path="mocap/head-choose"
|
||||
element={<HeadTrackingChoose />}
|
||||
/>
|
||||
<Route path="mocap/data/vmc" element={<MocapVMCSetup />} />
|
||||
<Route path="mocap/data/bvh" element={<MocapBVHSetup />} />
|
||||
<Route path="mocap/data/steamvr" element={<MocapSteamSetup />} />
|
||||
</Route>
|
||||
|
||||
<Route path="trackers-assign" element={<TrackersAssignPage />} />
|
||||
<Route path="enter-vr" element={<EnterVRPage />} />
|
||||
<Route path="mounting/choose" element={<MountingChoose />}></Route>
|
||||
@@ -272,19 +307,16 @@ export default function App() {
|
||||
<VersionContext.Provider value={updateFound}>
|
||||
<div className="h-full w-full text-standard bg-background-80 text-background-10">
|
||||
<Preload />
|
||||
<div className="flex-col h-full">
|
||||
{!websocketAPI.isConnected && (
|
||||
<>
|
||||
<TopBar></TopBar>
|
||||
<div className="flex w-full h-full justify-center items-center p-2">
|
||||
{websocketAPI.isFirstConnection
|
||||
? l10n.getString('websocket-connecting')
|
||||
: l10n.getString('websocket-connection_lost')}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{websocketAPI.isConnected && <Layout></Layout>}
|
||||
</div>
|
||||
{!websocketAPI.isConnected && (
|
||||
<EmptyLayout>
|
||||
<div className="flex w-full h-full justify-center items-center p-2">
|
||||
{websocketAPI.isFirstConnection
|
||||
? l10n.getString('websocket-connecting')
|
||||
: l10n.getString('websocket-connection_lost')}
|
||||
</div>
|
||||
</EmptyLayout>
|
||||
)}
|
||||
{websocketAPI.isConnected && <Layout></Layout>}
|
||||
</div>
|
||||
</VersionContext.Provider>
|
||||
</StatusProvider>
|
||||
|
||||
7
gui/src/components/EmptyLayout.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.empty-layout {
|
||||
display: grid;
|
||||
grid-template:
|
||||
't' var(--topbar-h)
|
||||
'c' calc(100% - var(--topbar-h))
|
||||
/ 100%;
|
||||
}
|
||||
16
gui/src/components/EmptyLayout.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { TopBar } from './TopBar';
|
||||
import './EmptyLayout.scss';
|
||||
|
||||
export function EmptyLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="empty-layout h-full">
|
||||
<div style={{ gridArea: 't' }}>
|
||||
<TopBar></TopBar>
|
||||
</div>
|
||||
<div style={{ gridArea: 'c' }} className="mt-2 relative">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -39,12 +39,6 @@ export function SerialDetectionModal() {
|
||||
|
||||
const openWifi = () => {
|
||||
setShowWifiForm(true);
|
||||
// if (!hasWifiCreds) {
|
||||
// setShowWifiForm(true);
|
||||
// } else {
|
||||
// closeModal();
|
||||
// nav('/onboarding/connect-trackers', { state: { alonePage: true } });
|
||||
// }
|
||||
};
|
||||
|
||||
const modalWifiSubmit = (form: WifiFormData) => {
|
||||
@@ -58,7 +52,11 @@ export function SerialDetectionModal() {
|
||||
({ device }: NewSerialDeviceResponseT) => {
|
||||
if (
|
||||
config?.watchNewDevices &&
|
||||
!['/settings/serial', '/onboarding/connect-trackers'].includes(pathname)
|
||||
![
|
||||
'/settings/serial',
|
||||
'/onboarding/connect-trackers',
|
||||
'/settings/firmware-tool',
|
||||
].includes(pathname)
|
||||
) {
|
||||
setOpen(device);
|
||||
}
|
||||
|
||||
@@ -290,7 +290,9 @@ export function TopBar({
|
||||
await invoke('update_tray_text');
|
||||
} else if (
|
||||
config?.connectedTrackersWarning &&
|
||||
connectedIMUTrackers.length > 0
|
||||
connectedIMUTrackers.filter(
|
||||
(t) => t.tracker.status !== TrackerStatus.TIMED_OUT
|
||||
).length > 0
|
||||
) {
|
||||
setConnectedTrackerWarning(true);
|
||||
} else {
|
||||
|
||||
@@ -25,7 +25,9 @@ export function UnknownDeviceModal() {
|
||||
RpcMessage.UnknownDeviceHandshakeNotification,
|
||||
({ macAddress }: UnknownDeviceHandshakeNotificationT) => {
|
||||
if (
|
||||
['/onboarding/connect-trackers'].includes(pathname) ||
|
||||
['/onboarding/connect-trackers', '/settings/firmware-tool'].includes(
|
||||
pathname
|
||||
) ||
|
||||
state.ignoredTrackers.has(macAddress as string) ||
|
||||
(currentTracker !== null && currentTracker !== macAddress)
|
||||
)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export function A({ href, children }: { href: string; children?: ReactNode }) {
|
||||
export function A({ href, children }: { href?: string; children?: ReactNode }) {
|
||||
return (
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
onClick={() => open(href).catch(() => window.open(href, '_blank'))}
|
||||
onClick={() =>
|
||||
href && open(href).catch(() => window.open(href, '_blank'))
|
||||
}
|
||||
className="underline"
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { UpperArmIcon } from './icon/UpperArmIcon';
|
||||
import { UpperLegIcon } from './icon/UpperLegIcon';
|
||||
import { WaistIcon } from './icon/WaistIcon';
|
||||
import { UpperChestIcon } from './icon/UpperChestIcon';
|
||||
import { FingersIcon } from './icon/FingersIcon';
|
||||
|
||||
// All body parts that are right or left, are by default left!
|
||||
export const mapPart: Record<
|
||||
@@ -86,6 +87,96 @@ export const mapPart: Record<
|
||||
<UpperLegIcon width={width} flipped></UpperLegIcon>
|
||||
),
|
||||
[BodyPart.WAIST]: ({ width }) => <WaistIcon width={width}></WaistIcon>,
|
||||
[BodyPart.LEFT_THUMB_METACARPAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_THUMB_PROXIMAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_THUMB_DISTAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_INDEX_PROXIMAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_INDEX_INTERMEDIATE]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_INDEX_DISTAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_MIDDLE_PROXIMAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_MIDDLE_INTERMEDIATE]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_MIDDLE_DISTAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_RING_PROXIMAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_RING_INTERMEDIATE]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_RING_DISTAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_LITTLE_PROXIMAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_LITTLE_INTERMEDIATE]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_LITTLE_DISTAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_THUMB_METACARPAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_THUMB_PROXIMAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_THUMB_DISTAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_INDEX_PROXIMAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_INDEX_INTERMEDIATE]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_INDEX_DISTAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_MIDDLE_PROXIMAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_MIDDLE_INTERMEDIATE]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_MIDDLE_DISTAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_RING_PROXIMAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_RING_INTERMEDIATE]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_RING_DISTAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_LITTLE_PROXIMAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_LITTLE_INTERMEDIATE]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_LITTLE_DISTAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
};
|
||||
|
||||
export function BodyPartIcon({
|
||||
|
||||
@@ -2,6 +2,7 @@ import classNames from 'classnames';
|
||||
import React, { ReactNode, useMemo } from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { LoaderIcon, SlimeState } from './icon/LoaderIcon';
|
||||
import { XOR } from 'ts-xor';
|
||||
|
||||
function ButtonContent({
|
||||
loading,
|
||||
@@ -36,6 +37,25 @@ function ButtonContent({
|
||||
);
|
||||
}
|
||||
|
||||
type ButtonBaseParams = {
|
||||
children?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
variant: 'primary' | 'secondary' | 'tertiary' | 'quaternary';
|
||||
loading?: boolean;
|
||||
rounded?: boolean;
|
||||
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'>;
|
||||
|
||||
type ButtonNavigateParams = { to: string; state?: any } & ButtonBaseParams;
|
||||
type ButtonScriptParams = {
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
} & ButtonBaseParams;
|
||||
type ButtonSubmitParams = { type: 'submit' } & ButtonBaseParams;
|
||||
type ButtonParams = XOR<
|
||||
ButtonNavigateParams,
|
||||
ButtonScriptParams,
|
||||
ButtonSubmitParams
|
||||
>;
|
||||
|
||||
export function Button({
|
||||
children,
|
||||
variant,
|
||||
@@ -46,15 +66,7 @@ export function Button({
|
||||
icon,
|
||||
rounded = false,
|
||||
...props
|
||||
}: {
|
||||
children?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
variant: 'primary' | 'secondary' | 'tertiary' | 'quaternary';
|
||||
to?: string;
|
||||
loading?: boolean;
|
||||
rounded?: boolean;
|
||||
state?: any;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
}: ButtonParams) {
|
||||
const classes = useMemo(() => {
|
||||
const variantsMap = {
|
||||
primary: classNames({
|
||||
|
||||
@@ -2,6 +2,10 @@ import classNames from 'classnames';
|
||||
import { useMemo } from 'react';
|
||||
import { Control, Controller } from 'react-hook-form';
|
||||
|
||||
export const CHECKBOX_CLASSES = classNames(
|
||||
'bg-background-50 border-background-50 rounded-md w-5 h-5 text-accent-background-30 focus:border-accent-background-40 focus:ring-transparent focus:ring-offset-transparent focus:outline-transparent'
|
||||
);
|
||||
|
||||
export function CheckBox({
|
||||
label,
|
||||
variant = 'checkbox',
|
||||
@@ -25,9 +29,7 @@ export function CheckBox({
|
||||
const classes = useMemo(() => {
|
||||
const vriantsMap = {
|
||||
checkbox: {
|
||||
checkbox: classNames(
|
||||
'bg-background-50 border-background-50 rounded-md w-5 h-5 text-accent-background-30 focus:border-accent-background-40 focus:ring-transparent focus:ring-offset-transparent focus:outline-transparent'
|
||||
),
|
||||
checkbox: CHECKBOX_CLASSES,
|
||||
toggle: '',
|
||||
pin: '',
|
||||
},
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { Localized } from '@fluent/react';
|
||||
import classNames from 'classnames';
|
||||
import { forwardRef, useMemo, useState } from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Control, Controller, UseControllerProps } from 'react-hook-form';
|
||||
import { FileIcon } from './icon/FileIcon';
|
||||
import { UploadFileIcon } from './icon/UploadFileIcon';
|
||||
import { Typography } from './Typography';
|
||||
import { CloseIcon } from './icon/CloseIcon';
|
||||
|
||||
interface InputProps {
|
||||
variant?: 'primary' | 'secondary';
|
||||
@@ -11,6 +19,80 @@ interface InputProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const FileInputContentBlank = ({
|
||||
isDragging,
|
||||
label,
|
||||
}: {
|
||||
isDragging: boolean;
|
||||
label: string;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex justify-center w-full h-32 px-4 transition border-2',
|
||||
'border-background-20 rounded-md appearance-none cursor-pointer',
|
||||
'hover:border-accent-background-20 focus:outline-none',
|
||||
'border-dashed'
|
||||
)}
|
||||
>
|
||||
<span className="flex items-center space-x-2 pointer-events-none">
|
||||
<UploadFileIcon isDragging={isDragging} />
|
||||
<div>
|
||||
<Localized
|
||||
id={label}
|
||||
elems={{
|
||||
u: <span className="underline text-background-20"></span>,
|
||||
}}
|
||||
>
|
||||
<Typography>
|
||||
Drop files to attach, or{' '}
|
||||
<span className="underline text-background-20">browse</span>
|
||||
</Typography>
|
||||
</Localized>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FileInputContentFile = ({
|
||||
importedFileName,
|
||||
onClearPicker,
|
||||
}: {
|
||||
importedFileName: string;
|
||||
onClearPicker: () => any;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col w-full transition border-2',
|
||||
'border-background-20 rounded-md appearance-none cursor-pointer',
|
||||
'hover:border-accent-background-20 focus:outline-none'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center space-x-2 px-4">
|
||||
<FileIcon />
|
||||
<span>{importedFileName}</span>
|
||||
</div>
|
||||
<span className="flex-grow"></span>
|
||||
<a
|
||||
href="#"
|
||||
className="h-12 w-12 hover:bg-accent-background-20 cursor-pointer"
|
||||
onClick={() => {
|
||||
onClearPicker();
|
||||
}}
|
||||
>
|
||||
<CloseIcon
|
||||
className="stroke-background-20 hover:stroke-background-90"
|
||||
size={48}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const FileInputInside = forwardRef<
|
||||
HTMLInputElement,
|
||||
{
|
||||
@@ -22,44 +104,36 @@ export const FileInputInside = forwardRef<
|
||||
value: FileList;
|
||||
onChange: (...event: any[]) => void;
|
||||
name: string;
|
||||
importedFileName: string | null;
|
||||
}
|
||||
>(function AppInput(
|
||||
{
|
||||
label = 'tips-file_select',
|
||||
name,
|
||||
onChange,
|
||||
variant = 'primary',
|
||||
accept,
|
||||
capture,
|
||||
multiple = false,
|
||||
importedFileName,
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const classes = useMemo(() => {
|
||||
const variantsMap = {
|
||||
primary: classNames('bg-background-60 border-background-60'),
|
||||
secondary: classNames('bg-background-50 border-background-50'),
|
||||
};
|
||||
const innerRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useImperativeHandle(ref, () => innerRef.current!);
|
||||
|
||||
return classNames(
|
||||
variantsMap[variant],
|
||||
'w-full focus:ring-transparent focus:ring-offset-transparent',
|
||||
'focus:outline-transparent rounded-md bg-background-60 border-background-60',
|
||||
'focus:border-accent-background-40 placeholder:text-background-30 text-standard',
|
||||
'relative hidden'
|
||||
);
|
||||
}, [variant]);
|
||||
const acceptList = useMemo(() => accept.split(/, ?/), [accept]);
|
||||
const [isDragging, setDragging] = useState(false);
|
||||
|
||||
const isFileImported = importedFileName !== null && !isDragging;
|
||||
|
||||
const onClearPicker = () => {
|
||||
onChange([]);
|
||||
innerRef.current!.value = '';
|
||||
};
|
||||
|
||||
return (
|
||||
<label
|
||||
className={classNames(
|
||||
'flex justify-center w-full h-32 px-4 transition border-2',
|
||||
'border-background-20 border-dashed rounded-md appearance-none cursor-pointer',
|
||||
'hover:border-accent-background-20 focus:outline-none'
|
||||
)}
|
||||
onClick={() => typeof ref !== 'function' && ref?.current?.click()}
|
||||
onDragOver={(ev) => ev.preventDefault()}
|
||||
onDrop={(ev) => {
|
||||
ev.preventDefault();
|
||||
@@ -86,30 +160,20 @@ export const FileInputInside = forwardRef<
|
||||
setDragging(false);
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center space-x-2 pointer-events-none">
|
||||
<FileIcon isDragging={isDragging} />
|
||||
<div>
|
||||
<Localized
|
||||
id={label}
|
||||
elems={{
|
||||
u: <span className="underline text-background-20"></span>,
|
||||
}}
|
||||
>
|
||||
<Typography>
|
||||
Drop files to attach, or{' '}
|
||||
<span className="underline text-background-20">browse</span>
|
||||
</Typography>
|
||||
</Localized>
|
||||
</div>
|
||||
</span>
|
||||
{isFileImported
|
||||
? FileInputContentFile({ importedFileName, onClearPicker })
|
||||
: FileInputContentBlank({ isDragging, label })}
|
||||
|
||||
<input
|
||||
type="file"
|
||||
className={classNames(classes)}
|
||||
className="hidden"
|
||||
onChange={(ev) => {
|
||||
onChange(ev.target.files);
|
||||
if (ev.target.files?.length) {
|
||||
onChange(ev.target.files);
|
||||
}
|
||||
}}
|
||||
name={name}
|
||||
ref={ref}
|
||||
ref={innerRef}
|
||||
accept={accept}
|
||||
multiple={multiple}
|
||||
capture={capture}
|
||||
@@ -127,6 +191,7 @@ export const FileInput = ({
|
||||
accept,
|
||||
multiple,
|
||||
capture,
|
||||
importedFileName,
|
||||
}: {
|
||||
rules: UseControllerProps<any>['rules'];
|
||||
control: Control<any>;
|
||||
@@ -137,6 +202,7 @@ export const FileInput = ({
|
||||
* Use a translation key!
|
||||
**/
|
||||
label?: string;
|
||||
importedFileName: string | null;
|
||||
} & InputProps &
|
||||
Partial<HTMLInputElement>) => {
|
||||
return (
|
||||
@@ -155,6 +221,7 @@ export const FileInput = ({
|
||||
accept={accept}
|
||||
capture={capture}
|
||||
multiple={multiple}
|
||||
importedFileName={importedFileName}
|
||||
></FileInputInside>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -98,7 +98,7 @@ export const InputInside = forwardRef<
|
||||
></input>
|
||||
{type === 'password' && (
|
||||
<div
|
||||
className="fill-background-10 absolute inset-y-0 right-0 pr-6 z-10 my-auto w-[16px] h-[16px]"
|
||||
className="fill-background-10 absolute inset-y-0 right-0 pr-6 z-10 my-auto w-[16px] h-[16px] cursor-pointer"
|
||||
onClick={togglePassword}
|
||||
>
|
||||
<EyeIcon width={16} closed={forceText}></EyeIcon>
|
||||
|
||||
70
gui/src/components/commons/PausableVideo.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { PlayCircleIcon } from './icon/PlayIcon';
|
||||
import { useDebouncedEffect } from '@/hooks/timeout';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export function PausableVideo({
|
||||
src,
|
||||
poster,
|
||||
restartOnPause = false,
|
||||
autoPlay = false,
|
||||
}: {
|
||||
src?: string;
|
||||
poster?: string;
|
||||
restartOnPause?: boolean;
|
||||
autoPlay?: boolean;
|
||||
}) {
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const [paused, setPaused] = useState(!autoPlay);
|
||||
const [atStart, setAtStart] = useState(true);
|
||||
|
||||
function toggleVideo() {
|
||||
if (!videoRef.current) return;
|
||||
if (videoRef.current.paused) {
|
||||
videoRef.current.play();
|
||||
} else {
|
||||
videoRef.current.pause();
|
||||
if (restartOnPause) {
|
||||
videoRef.current.currentTime = 0;
|
||||
}
|
||||
setAtStart(videoRef.current.currentTime === 0);
|
||||
}
|
||||
setPaused(videoRef.current.paused);
|
||||
}
|
||||
|
||||
useDebouncedEffect(
|
||||
() => {
|
||||
if (paused) videoRef.current?.pause();
|
||||
},
|
||||
[paused],
|
||||
250
|
||||
);
|
||||
|
||||
return (
|
||||
<button className="relative appearance-none" onClick={toggleVideo}>
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute w-[100px] h-[100px] top-0 bottom-0 left-0 right-0 m-auto',
|
||||
'fill-background-20',
|
||||
paused && !atStart && 'opacity-50'
|
||||
)}
|
||||
hidden={!paused}
|
||||
>
|
||||
<PlayCircleIcon width={100}></PlayCircleIcon>
|
||||
</div>
|
||||
|
||||
<video
|
||||
preload="auto"
|
||||
ref={videoRef}
|
||||
src={src}
|
||||
poster={poster}
|
||||
className="min-w-[12rem] w-[30rem]"
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
autoPlay={autoPlay}
|
||||
controls={false}
|
||||
></video>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -7,12 +7,14 @@ export function ProgressBar({
|
||||
height = 10,
|
||||
colorClass = 'bg-accent-background-20',
|
||||
animated = false,
|
||||
bottom = false,
|
||||
}: {
|
||||
progress: number;
|
||||
parts?: number;
|
||||
height?: number;
|
||||
colorClass?: string;
|
||||
animated?: boolean;
|
||||
bottom?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex w-full flex-row gap-2">
|
||||
@@ -25,6 +27,7 @@ export function ProgressBar({
|
||||
colorClass={colorClass}
|
||||
animated={animated}
|
||||
parts={parts}
|
||||
bottom={bottom}
|
||||
></Bar>
|
||||
))}
|
||||
</div>
|
||||
@@ -38,6 +41,7 @@ export function Bar({
|
||||
height,
|
||||
animated,
|
||||
colorClass,
|
||||
bottom,
|
||||
}: {
|
||||
index: number;
|
||||
progress: number;
|
||||
@@ -45,6 +49,7 @@ export function Bar({
|
||||
height: number;
|
||||
colorClass: string;
|
||||
animated: boolean;
|
||||
bottom: boolean;
|
||||
}) {
|
||||
const value = useMemo(
|
||||
() => Math.min(Math.max((progress * parts) / 1 - index, 0), 1),
|
||||
@@ -52,12 +57,16 @@ export function Bar({
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className="flex relative flex-grow bg-background-50 rounded-lg overflow-hidden"
|
||||
className={classNames(
|
||||
'flex relative flex-grow bg-background-50 rounded-lg overflow-hidden',
|
||||
bottom && 'rounded-t-none'
|
||||
)}
|
||||
style={{ height: `${height}px` }}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-lg overflow-hidden absolute top-0',
|
||||
'overflow-hidden absolute top-0',
|
||||
bottom ? 'rounded-none' : 'rounded-lg',
|
||||
animated && 'transition-[width,background-color]',
|
||||
colorClass
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import { Control, Controller } from 'react-hook-form';
|
||||
import { Typography } from './Typography';
|
||||
import { ReactNode } from 'react';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
|
||||
export function Radio({
|
||||
control,
|
||||
@@ -12,6 +12,7 @@ export function Radio({
|
||||
children,
|
||||
// input props
|
||||
disabled,
|
||||
variant = 'secondary',
|
||||
...props
|
||||
}: {
|
||||
control: Control<any>;
|
||||
@@ -20,19 +21,36 @@ export function Radio({
|
||||
value: string;
|
||||
description?: string | null;
|
||||
children?: ReactNode;
|
||||
variant?: 'secondary' | 'none';
|
||||
} & React.HTMLProps<HTMLInputElement>) {
|
||||
const variantClasses = useMemo(() => {
|
||||
const variantsMap = {
|
||||
secondary: classNames({
|
||||
'bg-background-60 hover:bg-background-50': !disabled,
|
||||
'bg-background-80': disabled,
|
||||
}),
|
||||
none: '',
|
||||
};
|
||||
return variantsMap[variant];
|
||||
}, [variant, disabled]);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field: { onChange, ref, name, value: checked } }) => (
|
||||
<label
|
||||
className={classNames('w-full p-3 rounded-md flex gap-3 border-2', {
|
||||
'border-accent-background-30': value == checked,
|
||||
'border-transparent': value != checked,
|
||||
'bg-background-60 cursor-pointer hover:bg-background-50': !disabled,
|
||||
'bg-background-80 cursor-not-allowed': disabled,
|
||||
})}
|
||||
className={classNames(
|
||||
'w-full rounded-md flex gap-3 border-2 group/radio',
|
||||
variantClasses,
|
||||
{
|
||||
'border-accent-background-30': value == checked,
|
||||
'border-transparent': value != checked,
|
||||
'cursor-pointer': !disabled,
|
||||
'cursor-not-allowed': disabled,
|
||||
'p-3': variant !== 'none',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
@@ -48,7 +66,7 @@ export function Radio({
|
||||
checked={value == checked}
|
||||
{...props}
|
||||
/>
|
||||
<div className="flex flex-col gap-2 pointer-events-none">
|
||||
<div className="flex flex-col gap-2 pointer-events-none w-full">
|
||||
{children ? children : <Typography bold>{label}</Typography>}
|
||||
{description && (
|
||||
<Typography variant="standard" color="secondary">
|
||||
|
||||
@@ -64,7 +64,7 @@ export function WarningBox({
|
||||
>
|
||||
<WarningIcon></WarningIcon>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col justify-center">
|
||||
<Typography
|
||||
color="text-background-60"
|
||||
whitespace={whitespace ? 'whitespace-pre-line' : undefined}
|
||||
|
||||
@@ -54,7 +54,7 @@ export function Typography({
|
||||
tag,
|
||||
{
|
||||
className: classNames([
|
||||
'transition-colors',
|
||||
'transition-colors hyphens-auto',
|
||||
variant === 'mobile-title' &&
|
||||
'xs:text-main-title mobile:text-section-title',
|
||||
variant === 'main-title' && 'text-main-title',
|
||||
|
||||
138
gui/src/components/commons/VerticalStepper.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import classNames from 'classnames';
|
||||
import { CheckIcon } from './icon/CheckIcon';
|
||||
import { Typography } from './Typography';
|
||||
import {
|
||||
FC,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useElemSize } from '@/hooks/layout';
|
||||
import { useDebouncedEffect } from '@/hooks/timeout';
|
||||
|
||||
export function VerticalStep({
|
||||
active,
|
||||
index,
|
||||
children,
|
||||
title,
|
||||
}: {
|
||||
active: number;
|
||||
index: number;
|
||||
children: ReactNode;
|
||||
title: string;
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const refTop = useRef<HTMLLIElement | null>(null);
|
||||
const [shouldAnimate, setShouldAnimate] = useState(false);
|
||||
const { height } = useElemSize(ref);
|
||||
|
||||
const isSelected = active === index;
|
||||
const isPrevious = active > index;
|
||||
|
||||
useEffect(() => {
|
||||
if (!refTop.current) return;
|
||||
if (isSelected)
|
||||
setTimeout(() => {
|
||||
if (!refTop.current) return;
|
||||
refTop.current.scrollIntoView({ behavior: 'smooth' });
|
||||
}, 500);
|
||||
}, [isSelected]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setShouldAnimate(true);
|
||||
}, [active]);
|
||||
|
||||
// Make it so it wont try to animate the size
|
||||
// if we are not changing active step
|
||||
useDebouncedEffect(
|
||||
() => {
|
||||
setShouldAnimate(false);
|
||||
},
|
||||
[active],
|
||||
1000
|
||||
);
|
||||
|
||||
return (
|
||||
<li className="mb-10 scroll-m-4" ref={refTop}>
|
||||
<span
|
||||
className={classNames(
|
||||
'absolute flex items-center justify-center w-8 h-8 rounded-full -left-4 transition-colors fill-background-10',
|
||||
{
|
||||
'bg-accent-background-20': isSelected || isPrevious,
|
||||
'bg-background-40': !isSelected && !isPrevious,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{isPrevious ? (
|
||||
<CheckIcon></CheckIcon>
|
||||
) : (
|
||||
<Typography variant="section-title">{index + 1}</Typography>
|
||||
)}
|
||||
</span>
|
||||
<div className="ml-7 pt-1.5">
|
||||
<div className="px-1">
|
||||
<Typography variant="section-title">{title}</Typography>
|
||||
</div>
|
||||
<div
|
||||
style={{ height: !isSelected ? 0 : height }}
|
||||
className={classNames('overflow-clip px-1', {
|
||||
'duration-500 transition-[height]': shouldAnimate,
|
||||
})}
|
||||
>
|
||||
<div ref={ref}>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
type VerticalStepComponentType = FC<{
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
goTo: (id: string) => void;
|
||||
isActive: boolean;
|
||||
}>;
|
||||
|
||||
export type VerticalStep = {
|
||||
title: string;
|
||||
id?: string;
|
||||
component: VerticalStepComponentType;
|
||||
};
|
||||
|
||||
export default function VerticalStepper({ steps }: { steps: VerticalStep[] }) {
|
||||
const [currStep, setStep] = useState(0);
|
||||
|
||||
const nextStep = () => {
|
||||
if (currStep + 1 === steps.length) return;
|
||||
setStep(currStep + 1);
|
||||
};
|
||||
|
||||
const prevStep = () => {
|
||||
if (currStep - 1 < 0) return;
|
||||
setStep(currStep - 1);
|
||||
};
|
||||
|
||||
const goTo = (id: string) => {
|
||||
const step = steps.findIndex(({ id: stepId }) => stepId === id);
|
||||
if (step === -1) throw new Error('step not found');
|
||||
|
||||
setStep(step);
|
||||
};
|
||||
|
||||
return (
|
||||
<ol className="relative border-l border-gray-700 text-gray-400">
|
||||
{steps.map(({ title, component: StepComponent }, index) => (
|
||||
<VerticalStep active={currStep} index={index} title={title} key={index}>
|
||||
<StepComponent
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
goTo={goTo}
|
||||
isActive={currStep === index}
|
||||
></StepComponent>
|
||||
</VerticalStep>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
export function CloseIcon({
|
||||
className,
|
||||
className = 'stroke-window-icon',
|
||||
size = 35,
|
||||
}: {
|
||||
className?: string;
|
||||
@@ -11,7 +9,7 @@ export function CloseIcon({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
className={classNames('stroke-window-icon', className)}
|
||||
className={className}
|
||||
viewBox="0 0 31 29"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
export function FileIcon({
|
||||
width = 24,
|
||||
isDragging = false,
|
||||
}: {
|
||||
width?: number;
|
||||
isDragging?: boolean;
|
||||
}) {
|
||||
export function FileIcon({ width = 24 }: { width?: number }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -14,11 +6,10 @@ export function FileIcon({
|
||||
width={width}
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
className={classNames('transition-transform', isDragging && 'scale-150')}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.625 1.5H9a3.75 3.75 0 013.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 013.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 01-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875zm6.905 9.97a.75.75 0 00-1.06 0l-3 3a.75.75 0 101.06 1.06l1.72-1.72V18a.75.75 0 001.5 0v-4.19l1.72 1.72a.75.75 0 101.06-1.06l-3-3z"
|
||||
d="M 5.625 1.5 L 9 1.5 C 11.071 1.5 12.75 3.179 12.75 5.25 L 12.75 7.125 C 12.75 8.161 13.59 9 14.625 9 L 16.5 9 C 18.571 9 20.25 10.679 20.25 12.75 L 20.25 20.625 C 20.25 21.66 19.41 22.5 18.375 22.5 L 5.625 22.5 C 4.589 22.5 3.75 21.661 3.75 20.625 L 3.75 3.375 C 3.75 2.339 4.59 1.5 5.625 1.5 Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<path d="M14.25 5.25a5.23 5.23 0 00-1.279-3.434 9.768 9.768 0 016.963 6.963A5.23 5.23 0 0016.5 7.5h-1.875a.375.375 0 01-.375-.375V5.25z" />
|
||||
|
||||
15
gui/src/components/commons/icon/FingersIcon.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
export function FingersIcon({ width = 28 }: { width?: number }) {
|
||||
return (
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
viewBox="0 0 93.49 130"
|
||||
>
|
||||
<g>
|
||||
<path d="M2.34,62.52l-0.26,0.27l-2.08-0.83V31.27c0-1.42,0.42-2.76,1.14-3.89l0,0c0.14-0.22,0.29-0.44,0.46-0.64 c0.17-0.22,0.35-0.42,0.53-0.6l0.02-0.02c0.54-0.54,1.18-1.01,1.89-1.36l0.03-0.01l0.35-0.17l0.04-0.02 c0.86-0.37,1.82-0.58,2.81-0.58l0,0h0.04v0c2.01,0,3.84,0.82,5.16,2.14c0.54,0.54,1.01,1.18,1.36,1.88l0.02,0.04l0.16,0.35 l0.01,0.03c0.37,0.86,0.58,1.82,0.58,2.81l0,0.01v0.04v24.96v1.13l-1.13,0.07c-3.08,0.19-5.92,1.18-8.32,2.77 c-0.48,0.32-0.94,0.66-1.38,1.02c-0.41,0.34-0.84,0.72-1.26,1.15L2.34,62.52L2.34,62.52L2.34,62.52z M65.62,83.35l1.23,0.46 l0.53,0.39c0.09,0.12,0.2,0.22,0.33,0.31l0,0l0.16,0.09l0,0.01c0.17,0.08,0.35,0.12,0.54,0.12v0h0.03c0.18,0,0.34-0.03,0.49-0.09 l0.12-0.06l0.12-0.07l0.04-0.02l0.04-0.02c0.54-0.31,1.26-0.85,2.05-1.5c0.8-0.67,1.71-1.49,2.61-2.33 c1.76-1.66,3.76-3.66,4.56-4.45l0.04-0.04c2.53-2.53,5.11-3.7,7.38-3.85c0.46-0.03,0.92-0.02,1.35,0.03 c0.44,0.05,0.87,0.14,1.28,0.27h0.01l0.05,0.02l0.01,0c0.81,0.26,1.56,0.67,2.22,1.2l0.03,0.03l0.31,0.27l0.06,0.05l0.29,0.29 l0.05,0.06l0.01,0.01l0,0l0.01,0.02l0,0c0.56,0.62,1.01,1.35,1.34,2.16l0.02,0.03l0.15,0.42l0.02,0.09l0.12,0.43l0.01,0.05 l0.01,0.06h0c0.57,2.38,0.1,5.27-1.88,8.17c-0.37,0.55-0.81,1.11-1.29,1.65c-0.48,0.54-1.02,1.09-1.62,1.62l0,0l-0.08,0.07 l-0.1,0.09l-0.07,0.07l-0.04,0.04L63.64,114.3l-0.85,0.93l-0.06-0.06c-1.35,1.23-2.67,2.29-4.01,3.2c-1.6,1.08-3.22,1.95-4.9,2.61 c-1.69,0.67-3.46,1.15-5.33,1.46c-1.87,0.3-3.84,0.45-5.94,0.45h-15.9c-5.3,0-10.23-1.56-14.36-4.23l0,0 c-0.79-0.51-1.57-1.08-2.32-1.69c-0.76-0.62-1.47-1.26-2.12-1.92l-0.02-0.02l0,0c-2.01-2.04-3.71-4.42-5-7.03 c-0.25-0.52-0.49-1.04-0.71-1.56C0.76,103.2,0.01,99.65,0,95.93h0V95.9V74.93c0-1.93,0.36-3.79,1-5.49l0-0.01 c0.12-0.31,0.26-0.64,0.41-0.97h0c0.15-0.32,0.31-0.64,0.48-0.95l0.01-0.02l0.03-0.05l0.02-0.04c0.62-0.97,1.36-1.88,2.19-2.69 l0.02-0.02l0.46-0.43l0.04-0.03l0.48-0.41l0.04-0.04l0.02-0.02l0,0c1.06-0.85,2.24-1.57,3.51-2.11h0c0.29-0.12,0.57-0.24,0.76-0.3 v0c1.56-0.57,3.25-0.88,5.01-0.88v0h0.04h0.64l0.29,0.04l0.27,0.07l0.21,0.02v0h17.27v0l0.11,0h0.08l0.11,0v0h17.27v0l0.05,0h0.07 l0.05,0v0h1.28c2.54,0,4.94,0.65,7.05,1.79l0,0c0.42,0.23,0.82,0.47,1.19,0.72v0l0.01,0c0.36,0.24,0.74,0.52,1.11,0.82l0.01,0.01 l0.02,0.02l0,0c1.82,1.49,3.3,3.41,4.25,5.6c0.2,0.45,0.37,0.89,0.5,1.31v0c0.15,0.45,0.27,0.91,0.38,1.37v0.01l0.01,0.07 l0.02,0.11c0.01,0.08,0.02,0.16,0.04,0.22h0l0.01,0.03h0l0.04,0.11h0l0.02,0.06L67,73.21l0.06,0.65l0,0.04l0.02,0.26v0.04 l0.02,0.46v0.03l0,0.25l0,0.01v4.43v1.66l-1.58-0.52c-2.46-0.81-4.81-1.36-7.03-1.66h0c-0.5-0.07-0.98-0.12-1.42-0.17 c-0.45-0.04-0.92-0.08-1.39-0.1l-1.02-0.03c-2.85-0.04-5.48,0.37-7.81,1.17c-0.51,0.18-0.99,0.36-1.42,0.55 c-0.45,0.2-0.9,0.41-1.32,0.64l-0.71,0.41c-2.23,1.34-4.08,3.14-5.49,5.34c-0.29,0.46-0.56,0.9-0.78,1.33 c-0.24,0.45-0.46,0.94-0.68,1.44v0l-0.01,0.03h0c-0.68,1.62-1.17,3.4-1.45,5.33c-0.06,0.44-0.12,0.87-0.15,1.28 c-0.03,0.34-0.07,0.7-0.08,1.06l2.66,0.03c0.08-1.35,0.28-2.64,0.57-3.84h0c0.09-0.37,0.18-0.72,0.27-1.03h0 c0.09-0.3,0.2-0.64,0.33-0.98v0l0.32-0.82l0,0c0.89-2.13,2.18-3.94,3.8-5.38c0.32-0.28,0.66-0.55,0.99-0.8 c0.37-0.27,0.72-0.51,1.06-0.71l0.02-0.01l0.03-0.02v0c1.7-1.02,3.68-1.73,5.9-2.09c0.45-0.07,0.94-0.14,1.44-0.18 c0.49-0.05,1-0.07,1.49-0.09h0.03l0.98,0h0.02c2.3,0.03,4.79,0.39,7.44,1.07v0c0.61,0.15,1.18,0.32,1.72,0.49 c0.62,0.19,1.21,0.39,1.77,0.58L65.62,83.35L65.62,83.35z M15.74,60.59L15.74,60.59L15.74,60.59L15.74,60.59L15.74,60.59z M48.24,57.4H36.05h-1.2v-1.2V7.3h0c0-2.01,0.82-3.84,2.14-5.16c0.54-0.54,1.18-1.01,1.88-1.36l0.03-0.01l0.35-0.17l0.04-0.02 c0.86-0.37,1.81-0.58,2.81-0.58l0-0.01h0.04v0.01c2.01,0,3.84,0.82,5.16,2.14c0.54,0.54,1,1.18,1.36,1.88l0.02,0.03l0.16,0.35 l0.02,0.04c0.37,0.86,0.58,1.81,0.58,2.81l0,0.01V7.3v48.89v1.2H48.24L48.24,57.4z M53.63,57.45l-0.22-0.02l-1.12-0.09v-1.11V19.01 h0c0-2.01,0.82-3.84,2.14-5.16c0.54-0.54,1.18-1,1.89-1.36l0.04-0.02l0.35-0.16l0.03-0.02c0.86-0.37,1.81-0.58,2.81-0.58l0,0h0.04 c1.42,0,2.76,0.42,3.89,1.14l0,0l0.01,0.01c0.22,0.13,0.43,0.29,0.63,0.45l0,0l0.01,0.01c0.21,0.16,0.41,0.34,0.59,0.52l0.02,0.02 c0.54,0.54,1.01,1.18,1.36,1.88l0.01,0.03l0.17,0.35l0.02,0.04c0.37,0.86,0.58,1.82,0.58,2.81l0,0v0.04v42.9l-2.07,0.84l-0.2-0.2 c-2.06-2.06-4.63-3.62-7.49-4.45c-0.57-0.17-1.16-0.31-1.73-0.41C54.84,57.58,54.24,57.5,53.63,57.45L53.63,57.45z M30.68,57.4 H18.49h-1.21v-1.2V31.27h0V18.89h0c0-1.42,0.42-2.77,1.14-3.9h0c0.14-0.23,0.3-0.45,0.46-0.65c0.17-0.22,0.35-0.42,0.52-0.59 l0.02-0.02c0.54-0.54,1.18-1,1.89-1.36l0.03-0.01l0.35-0.16l0.04-0.02c0.86-0.37,1.81-0.58,2.81-0.58l0,0h0.04v0 c2.01,0,3.84,0.82,5.16,2.14c0.54,0.54,1,1.18,1.36,1.88l0.01,0.03L31.28,16l0.02,0.04c0.37,0.86,0.58,1.82,0.58,2.81l0,0v0.04 v37.3v1.2H30.68L30.68,57.4z" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
27
gui/src/components/commons/icon/UploadFileIcon.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
export function UploadFileIcon({
|
||||
width = 24,
|
||||
isDragging = false,
|
||||
}: {
|
||||
width?: number;
|
||||
isDragging?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width={width}
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
className={classNames('transition-transform', isDragging && 'scale-150')}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.625 1.5H9a3.75 3.75 0 013.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 013.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 01-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875zm6.905 9.97a.75.75 0 00-1.06 0l-3 3a.75.75 0 101.06 1.06l1.72-1.72V18a.75.75 0 001.5 0v-4.19l1.72 1.72a.75.75 0 101.06-1.06l-3-3z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<path d="M14.25 5.25a5.23 5.23 0 00-1.279-3.434 9.768 9.768 0 016.963 6.963A5.23 5.23 0 0016.5 7.5h-1.875a.375.375 0 01-.375-.375V5.25z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
308
gui/src/components/firmware-tool/AddImusStep.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
|
||||
import { useFirmwareTool } from '@/hooks/firmware-tool';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { Control, useForm } from 'react-hook-form';
|
||||
import {
|
||||
CreateImuConfigDTO,
|
||||
Imudto,
|
||||
} from '@/firmware-tool-api/firmwareToolSchemas';
|
||||
import { Dropdown } from '@/components/commons/Dropdown';
|
||||
import { TrashIcon } from '@/components/commons/icon/TrashIcon';
|
||||
import { Input } from '@/components/commons/Input';
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
} from '@/components/commons/icon/ArrowIcons';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useElemSize } from '@/hooks/layout';
|
||||
import { useGetFirmwaresImus } from '@/firmware-tool-api/firmwareToolComponents';
|
||||
import { CheckBox } from '@/components/commons/Checkbox';
|
||||
|
||||
function IMUCard({
|
||||
control,
|
||||
imuTypes,
|
||||
hasIntPin,
|
||||
index,
|
||||
onDelete,
|
||||
}: {
|
||||
imuTypes: Imudto[];
|
||||
hasIntPin: boolean;
|
||||
control: Control<{ imus: CreateImuConfigDTO[] }, any>;
|
||||
index: number;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const { l10n } = useLocalization();
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const { height } = useElemSize(ref);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg flex flex-col text-background-10">
|
||||
<div className="flex gap-3 p-4 shadow-md bg-background-50 rounded-md">
|
||||
<div className="bg-accent-background-40 rounded-full h-8 w-9 mt-[28px] flex flex-col items-center justify-center">
|
||||
<Typography variant="section-title" bold>
|
||||
{index + 1}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={'w-full flex flex-col gap-2'}>
|
||||
<div className="grid xs-settings:grid-cols-2 mobile-settings:grid-cols-1 gap-3 fill-background-10">
|
||||
<label className="flex flex-col justify-end gap-1">
|
||||
<Localized id="firmware_tool-add_imus_step-imu_type-label"></Localized>
|
||||
<Dropdown
|
||||
control={control}
|
||||
name={`imus[${index}].type`}
|
||||
items={imuTypes.map(({ type }) => ({
|
||||
label: type.split('_').slice(1).join(' '),
|
||||
value: type,
|
||||
}))}
|
||||
variant="secondary"
|
||||
maxHeight="25vh"
|
||||
placeholder={l10n.getString(
|
||||
'firmware_tool-add_imus_step-imu_type-placeholder'
|
||||
)}
|
||||
direction="down"
|
||||
display="block"
|
||||
></Dropdown>
|
||||
</label>
|
||||
<Localized
|
||||
id="firmware_tool-add_imus_step-imu_rotation"
|
||||
attrs={{ label: true, placeholder: true }}
|
||||
>
|
||||
<Input
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
}}
|
||||
type="number"
|
||||
name={`imus[${index}].rotation`}
|
||||
variant="primary"
|
||||
label="Rotation Degree"
|
||||
placeholder="Rotation Degree"
|
||||
autocomplete="off"
|
||||
></Input>
|
||||
</Localized>
|
||||
</div>
|
||||
<div
|
||||
className="duration-500 transition-[height] overflow-hidden"
|
||||
style={{ height: open ? height : 0 }}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
className="grid xs-settings:grid-cols-2 mobile-settings:grid-cols-1 gap-2"
|
||||
>
|
||||
<Localized
|
||||
id="firmware_tool-add_imus_step-scl_pin"
|
||||
attrs={{ label: true, placeholder: true }}
|
||||
>
|
||||
<Input
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
type="text"
|
||||
name={`imus[${index}].sclPin`}
|
||||
variant="primary"
|
||||
autocomplete="off"
|
||||
></Input>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="firmware_tool-add_imus_step-sda_pin"
|
||||
attrs={{ label: true, placeholder: true }}
|
||||
>
|
||||
<Input
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
type="text"
|
||||
name={`imus[${index}].sdaPin`}
|
||||
variant="primary"
|
||||
label="SDA Pin"
|
||||
placeholder="SDA Pin"
|
||||
autocomplete="off"
|
||||
></Input>
|
||||
</Localized>
|
||||
|
||||
{hasIntPin && (
|
||||
<Localized
|
||||
id="firmware_tool-add_imus_step-int_pin"
|
||||
attrs={{ label: true, placeholder: true }}
|
||||
>
|
||||
<Input
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
type="text"
|
||||
name={`imus[${index}].intPin`}
|
||||
variant="primary"
|
||||
autocomplete="off"
|
||||
></Input>
|
||||
</Localized>
|
||||
)}
|
||||
<label className="flex flex-col justify-end gap-1 md:pt-3 sm:pt-3">
|
||||
<Localized
|
||||
id="firmware_tool-add_imus_step-optional_tracker"
|
||||
attrs={{ label: true }}
|
||||
>
|
||||
<CheckBox
|
||||
control={control}
|
||||
name={`imus[${index}].optional`}
|
||||
variant="toggle"
|
||||
color="tertiary"
|
||||
label=""
|
||||
></CheckBox>
|
||||
</Localized>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center mt-[25px] fill-background-10">
|
||||
<Button variant="quaternary" rounded onClick={onDelete}>
|
||||
<TrashIcon size={15}></TrashIcon>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="items-center flex justify-center hover:bg-background-60 bg-background-80 -mt-0.5 transition-colors duration-300 fill-background-10 rounded-b-lg pt-1 pb-0.5"
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
<Typography>
|
||||
{l10n.getString(
|
||||
open
|
||||
? 'firmware_tool-add_imus_step-show_less'
|
||||
: 'firmware_tool-add_imus_step-show_more'
|
||||
)}
|
||||
</Typography>
|
||||
{!open && <ArrowDownIcon></ArrowDownIcon>}
|
||||
{open && <ArrowUpIcon></ArrowUpIcon>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AddImusStep({
|
||||
nextStep,
|
||||
prevStep,
|
||||
isActive,
|
||||
}: {
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
goTo: (id: string) => void;
|
||||
isActive: boolean;
|
||||
}) {
|
||||
const { l10n } = useLocalization();
|
||||
const {
|
||||
isStepLoading: isLoading,
|
||||
newConfig,
|
||||
defaultConfig,
|
||||
updateImus,
|
||||
} = useFirmwareTool();
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { isValid: isValidState },
|
||||
reset,
|
||||
watch,
|
||||
} = useForm<{ imus: CreateImuConfigDTO[] }>({
|
||||
defaultValues: {
|
||||
imus: [],
|
||||
},
|
||||
reValidateMode: 'onChange',
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset({
|
||||
imus: newConfig?.imusConfig || [],
|
||||
});
|
||||
}, [isActive]);
|
||||
|
||||
const { isFetching, data: imuTypes } = useGetFirmwaresImus({});
|
||||
|
||||
const isAckchuallyLoading = isFetching || isLoading;
|
||||
const form = watch();
|
||||
|
||||
const addImu = () => {
|
||||
if (!newConfig || !defaultConfig) throw new Error('unreachable');
|
||||
|
||||
const imuPinToAdd =
|
||||
defaultConfig.imuDefaults[form.imus.length ?? 0] ??
|
||||
defaultConfig.imuDefaults[0];
|
||||
const imuTypeToAdd: CreateImuConfigDTO['type'] =
|
||||
form.imus[0]?.type ?? 'IMU_BNO085';
|
||||
reset({
|
||||
imus: [...form.imus, { ...imuPinToAdd, type: imuTypeToAdd }],
|
||||
});
|
||||
};
|
||||
const deleteImu = (index: number) => {
|
||||
reset({ imus: form.imus.filter((_, i) => i !== index) });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('firmware_tool-board_pins_step-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="my-4 flex flex-col gap-4">
|
||||
{!isAckchuallyLoading && imuTypes && newConfig && (
|
||||
<>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div
|
||||
className={classNames(
|
||||
'grid gap-2 px-2',
|
||||
form.imus.length > 1
|
||||
? 'md:grid-cols-2 mobile-settings:grid-cols-1'
|
||||
: 'grid-cols-1'
|
||||
)}
|
||||
>
|
||||
{form.imus.map((imu, index) => (
|
||||
<IMUCard
|
||||
control={control}
|
||||
imuTypes={imuTypes}
|
||||
key={`${index}:${imu.type}`}
|
||||
hasIntPin={
|
||||
imuTypes?.find(({ type: t }) => t == imu.type)
|
||||
?.hasIntPin ?? false
|
||||
}
|
||||
index={index}
|
||||
onDelete={() => deleteImu(index)}
|
||||
></IMUCard>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Localized id="firmware_tool-add_imus_step-add_more">
|
||||
<Button variant="primary" onClick={addImu}></Button>
|
||||
</Localized>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<Localized id="firmware_tool-previous_step">
|
||||
<Button variant="tertiary" onClick={prevStep}></Button>
|
||||
</Localized>
|
||||
<Localized id="firmware_tool-next_step">
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={!isValidState || form.imus.length === 0}
|
||||
onClick={() => {
|
||||
updateImus(form.imus);
|
||||
nextStep();
|
||||
}}
|
||||
></Button>
|
||||
</Localized>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isAckchuallyLoading && (
|
||||
<div className="flex justify-center flex-col items-center gap-3 h-44">
|
||||
<LoaderIcon slimeState={SlimeState.JUMPY}></LoaderIcon>
|
||||
<Localized id="firmware_tool-loading">
|
||||
<Typography color="secondary"></Typography>
|
||||
</Localized>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
198
gui/src/components/firmware-tool/BoardPinsStep.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
|
||||
import { useFirmwareTool } from '@/hooks/firmware-tool';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Input } from '@/components/commons/Input';
|
||||
import { useEffect } from 'react';
|
||||
import { CheckBox } from '@/components/commons/Checkbox';
|
||||
import { CreateBoardConfigDTO } from '@/firmware-tool-api/firmwareToolSchemas';
|
||||
import { Dropdown } from '@/components/commons/Dropdown';
|
||||
import classNames from 'classnames';
|
||||
import { useGetFirmwaresBatteries } from '@/firmware-tool-api/firmwareToolComponents';
|
||||
|
||||
export type BoardPinsForm = Omit<CreateBoardConfigDTO, 'type'>;
|
||||
|
||||
export function BoardPinsStep({
|
||||
nextStep,
|
||||
prevStep,
|
||||
}: {
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
}) {
|
||||
const { l10n } = useLocalization();
|
||||
const {
|
||||
isStepLoading: isLoading,
|
||||
defaultConfig,
|
||||
updatePins,
|
||||
} = useFirmwareTool();
|
||||
const { isFetching, data: batteryTypes } = useGetFirmwaresBatteries({});
|
||||
|
||||
const { reset, control, watch, formState } = useForm<BoardPinsForm>({
|
||||
reValidateMode: 'onChange',
|
||||
defaultValues: {
|
||||
batteryResistances: [0, 0, 0],
|
||||
},
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
const formValue = watch();
|
||||
const ledEnabled = watch('enableLed');
|
||||
const batteryType = watch('batteryType');
|
||||
|
||||
useEffect(() => {
|
||||
if (!defaultConfig) return;
|
||||
const { type, ...resetConfig } = defaultConfig.boardConfig;
|
||||
reset({
|
||||
...resetConfig,
|
||||
});
|
||||
}, [defaultConfig]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col w-full justify-between text-background-10">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('firmware_tool-board_pins_step-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="my-4 p-2">
|
||||
{!isLoading && !isFetching && batteryTypes && (
|
||||
<form className="flex flex-col gap-2">
|
||||
<div className="grid xs-settings:grid-cols-2 mobile-settings:grid-cols-1 gap-2">
|
||||
<label className="flex flex-col justify-end">
|
||||
{/* Allows to have the right spacing at the top of the checkbox */}
|
||||
<CheckBox
|
||||
control={control}
|
||||
color="tertiary"
|
||||
name="enableLed"
|
||||
variant="toggle"
|
||||
outlined
|
||||
label={l10n.getString(
|
||||
'firmware_tool-board_pins_step-enable_led'
|
||||
)}
|
||||
></CheckBox>
|
||||
</label>
|
||||
<Localized
|
||||
id="firmware_tool-board_pins_step-led_pin"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
type="text"
|
||||
name="ledPin"
|
||||
variant="secondary"
|
||||
disabled={!ledEnabled}
|
||||
></Input>
|
||||
</Localized>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
batteryType === 'BAT_EXTERNAL' &&
|
||||
'bg-background-80 p-2 rounded-md',
|
||||
'transition-all duration-500 flex-col flex gap-2'
|
||||
)}
|
||||
>
|
||||
<Dropdown
|
||||
control={control}
|
||||
name="batteryType"
|
||||
variant="primary"
|
||||
placeholder={l10n.getString(
|
||||
'firmware_tool-board_pins_step-battery_type'
|
||||
)}
|
||||
direction="up"
|
||||
display="block"
|
||||
items={batteryTypes.map((battery) => ({
|
||||
label: l10n.getString(
|
||||
'firmware_tool-board_pins_step-battery_type-' + battery
|
||||
),
|
||||
value: battery,
|
||||
}))}
|
||||
></Dropdown>
|
||||
{batteryType === 'BAT_EXTERNAL' && (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Localized
|
||||
id="firmware_tool-board_pins_step-battery_sensor_pin"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
type="text"
|
||||
name="batteryPin"
|
||||
variant="secondary"
|
||||
></Input>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="firmware_tool-board_pins_step-battery_resistor"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
control={control}
|
||||
rules={{ required: true, min: 0 }}
|
||||
type="number"
|
||||
name="batteryResistances[0]"
|
||||
variant="secondary"
|
||||
label="Battery Resistor"
|
||||
placeholder="Battery Resistor"
|
||||
></Input>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="firmware_tool-board_pins_step-battery_shield_resistor-0"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
control={control}
|
||||
rules={{ required: true, min: 0 }}
|
||||
type="number"
|
||||
name="batteryResistances[1]"
|
||||
variant="secondary"
|
||||
></Input>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="firmware_tool-board_pins_step-battery_shield_resistor-1"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
control={control}
|
||||
rules={{ required: true, min: 0 }}
|
||||
type="number"
|
||||
name="batteryResistances[2]"
|
||||
variant="secondary"
|
||||
></Input>
|
||||
</Localized>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
{(isLoading || isFetching) && (
|
||||
<div className="flex justify-center flex-col items-center gap-3 h-44">
|
||||
<LoaderIcon slimeState={SlimeState.JUMPY}></LoaderIcon>
|
||||
<Localized id="firmware_tool-loading">
|
||||
<Typography color="secondary"></Typography>
|
||||
</Localized>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<Localized id="firmware_tool-previous_step">
|
||||
<Button variant="tertiary" onClick={prevStep}></Button>
|
||||
</Localized>
|
||||
<Localized id="firmware_tool-ok">
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={Object.keys(formState.errors).length !== 0}
|
||||
onClick={() => {
|
||||
updatePins(formValue);
|
||||
nextStep();
|
||||
}}
|
||||
></Button>
|
||||
</Localized>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
111
gui/src/components/firmware-tool/BuildStep.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { fetchPostFirmwaresBuild } from '@/firmware-tool-api/firmwareToolComponents';
|
||||
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
|
||||
import { useFirmwareTool } from '@/hooks/firmware-tool';
|
||||
import {
|
||||
BuildResponseDTO,
|
||||
CreateBuildFirmwareDTO,
|
||||
} from '@/firmware-tool-api/firmwareToolSchemas';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { firmwareToolBaseUrl } from '@/firmware-tool-api/firmwareToolFetcher';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
|
||||
export function BuildStep({
|
||||
isActive,
|
||||
goTo,
|
||||
nextStep,
|
||||
}: {
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
goTo: (id: string) => void;
|
||||
isActive: boolean;
|
||||
}) {
|
||||
const { l10n } = useLocalization();
|
||||
const { isGlobalLoading, newConfig, setBuildStatus, buildStatus } =
|
||||
useFirmwareTool();
|
||||
|
||||
const startBuild = async () => {
|
||||
try {
|
||||
const res = await fetchPostFirmwaresBuild({
|
||||
body: newConfig as CreateBuildFirmwareDTO,
|
||||
});
|
||||
|
||||
setBuildStatus(res);
|
||||
if (res.status !== 'DONE') {
|
||||
const events = new EventSource(
|
||||
`${firmwareToolBaseUrl}/firmwares/build-status/${res.id}`
|
||||
);
|
||||
events.onmessage = ({ data }) => {
|
||||
const buildEvent: BuildResponseDTO = JSON.parse(data);
|
||||
setBuildStatus(buildEvent);
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setBuildStatus({ id: '', status: 'ERROR' });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) return;
|
||||
startBuild();
|
||||
}, [isActive]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) return;
|
||||
if (buildStatus.status === 'DONE') {
|
||||
nextStep();
|
||||
}
|
||||
}, [buildStatus]);
|
||||
|
||||
const hasPendingBuild = useMemo(
|
||||
() => !['DONE', 'ERROR'].includes(buildStatus.status),
|
||||
[buildStatus.status]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex flex-grow flex-col gap-4">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('firmware_tool-build_step-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="my-4">
|
||||
{!isGlobalLoading && (
|
||||
<div className="flex justify-center flex-col items-center gap-3 h-44">
|
||||
<LoaderIcon
|
||||
slimeState={
|
||||
buildStatus.status !== 'ERROR'
|
||||
? SlimeState.JUMPY
|
||||
: SlimeState.SAD
|
||||
}
|
||||
></LoaderIcon>
|
||||
<Typography variant="section-title" color="secondary">
|
||||
{l10n.getString('firmware_tool-build-' + buildStatus.status)}
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
{isGlobalLoading && (
|
||||
<div className="flex justify-center flex-col items-center gap-3 h-44">
|
||||
<LoaderIcon slimeState={SlimeState.JUMPY}></LoaderIcon>
|
||||
<Localized id="firmware_tool-loading">
|
||||
<Typography color="secondary"></Typography>
|
||||
</Localized>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Localized id="firmware_tool-retry">
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={hasPendingBuild}
|
||||
onClick={() => goTo('FlashingMethod')}
|
||||
></Button>
|
||||
</Localized>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
132
gui/src/components/firmware-tool/DeviceCard.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { CHECKBOX_CLASSES } from '@/components/commons/Checkbox';
|
||||
import { ProgressBar } from '@/components/commons/ProgressBar';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { firmwareUpdateErrorStatus } from '@/hooks/firmware-tool';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import classNames from 'classnames';
|
||||
import { Control, Controller } from 'react-hook-form';
|
||||
import {
|
||||
FirmwareUpdateStatus,
|
||||
TrackerStatus as TrackerStatusEnum,
|
||||
} from 'solarxr-protocol';
|
||||
import { TrackerStatus } from '@/components/tracker/TrackerStatus';
|
||||
|
||||
interface DeviceCardProps {
|
||||
deviceNames: string[];
|
||||
status?: FirmwareUpdateStatus;
|
||||
online?: boolean | null;
|
||||
}
|
||||
|
||||
interface DeviceCardControlProps {
|
||||
control?: Control<any>;
|
||||
name?: string;
|
||||
progress?: number;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function DeviceCardContent({ deviceNames, status }: DeviceCardProps) {
|
||||
const { l10n } = useLocalization();
|
||||
|
||||
return (
|
||||
<div className="p-2 flex h-full gap-2 justify-between flex-col">
|
||||
<div className="flex flex-row flex-wrap gap-2 items-center h-full">
|
||||
{deviceNames.map((name) => (
|
||||
<span
|
||||
key={name}
|
||||
className="p-1 px-3 rounded-l-full rounded-r-full bg-background-40"
|
||||
>
|
||||
<Typography>{name}</Typography>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{status !== undefined ? (
|
||||
<Typography color="secondary">
|
||||
{l10n.getString(
|
||||
'firmware_update-status-' + FirmwareUpdateStatus[status]
|
||||
)}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography> </Typography> // placeholder so the size of the component does not change if there is no status
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DeviceCardControl({
|
||||
control,
|
||||
name,
|
||||
progress,
|
||||
disabled = false,
|
||||
online = null,
|
||||
...props
|
||||
}: DeviceCardControlProps & DeviceCardProps) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-md bg-background-60 h-[86px] pt-2 flex flex-col justify-between border-2 relative',
|
||||
props.status &&
|
||||
firmwareUpdateErrorStatus.includes(props.status) &&
|
||||
'border-status-critical',
|
||||
props.status === FirmwareUpdateStatus.DONE && 'border-status-success',
|
||||
(!props.status ||
|
||||
(props.status !== FirmwareUpdateStatus.DONE &&
|
||||
!firmwareUpdateErrorStatus.includes(props.status))) &&
|
||||
'border-transparent'
|
||||
)}
|
||||
>
|
||||
{control && name ? (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field: { onChange, value, ref } }) => (
|
||||
<label className="flex flex-row gap-2 px-4 h-full">
|
||||
<div className="flex justify-center flex-col">
|
||||
<input
|
||||
ref={ref}
|
||||
onChange={onChange}
|
||||
className={CHECKBOX_CLASSES}
|
||||
checked={value || false}
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
></input>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<DeviceCardContent {...props}></DeviceCardContent>
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
></Controller>
|
||||
) : (
|
||||
<div className="px-2 h-full">
|
||||
<DeviceCardContent {...props}></DeviceCardContent>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
'align-bottom',
|
||||
props.status != FirmwareUpdateStatus.UPLOADING ||
|
||||
progress === undefined
|
||||
? 'opacity-0'
|
||||
: 'opacity-100'
|
||||
)}
|
||||
>
|
||||
<ProgressBar
|
||||
progress={progress || 0}
|
||||
bottom
|
||||
height={6}
|
||||
colorClass="bg-accent-background-20"
|
||||
></ProgressBar>
|
||||
</div>
|
||||
{online !== null && (
|
||||
<div className="absolute top-2 right-2">
|
||||
<TrackerStatus
|
||||
status={
|
||||
online ? TrackerStatusEnum.OK : TrackerStatusEnum.DISCONNECTED
|
||||
}
|
||||
></TrackerStatus>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
140
gui/src/components/firmware-tool/FirmwareTool.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import {
|
||||
FirmwareToolContextC,
|
||||
useFirmwareToolContext,
|
||||
} from '@/hooks/firmware-tool';
|
||||
import { AddImusStep } from './AddImusStep';
|
||||
import { SelectBoardStep } from './SelectBoardStep';
|
||||
import { BoardPinsStep } from './BoardPinsStep';
|
||||
import VerticalStepper from '@/components/commons/VerticalStepper';
|
||||
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { SelectFirmwareStep } from './SelectFirmwareStep';
|
||||
import { BuildStep } from './BuildStep';
|
||||
import { FlashingMethodStep } from './FlashingMethodStep';
|
||||
import { FlashingStep } from './FlashingStep';
|
||||
import { FlashBtnStep } from './FlashBtnStep';
|
||||
import { FirmwareUpdateMethod } from 'solarxr-protocol';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
function FirmwareToolContent() {
|
||||
const { l10n } = useLocalization();
|
||||
const context = useFirmwareToolContext();
|
||||
const { isError, isGlobalLoading: isLoading, retry, isCompatible } = context;
|
||||
|
||||
const steps = useMemo(() => {
|
||||
const steps = [
|
||||
{
|
||||
id: 'SelectBoard',
|
||||
component: SelectBoardStep,
|
||||
title: l10n.getString('firmware_tool-board_step'),
|
||||
},
|
||||
{
|
||||
component: BoardPinsStep,
|
||||
title: l10n.getString('firmware_tool-board_pins_step'),
|
||||
},
|
||||
{
|
||||
component: AddImusStep,
|
||||
title: l10n.getString('firmware_tool-add_imus_step'),
|
||||
},
|
||||
{
|
||||
id: 'SelectFirmware',
|
||||
component: SelectFirmwareStep,
|
||||
title: l10n.getString('firmware_tool-select_firmware_step'),
|
||||
},
|
||||
{
|
||||
component: FlashingMethodStep,
|
||||
id: 'FlashingMethod',
|
||||
title: l10n.getString('firmware_tool-flash_method_step'),
|
||||
},
|
||||
{
|
||||
component: BuildStep,
|
||||
title: l10n.getString('firmware_tool-build_step'),
|
||||
},
|
||||
{
|
||||
component: FlashingStep,
|
||||
title: l10n.getString('firmware_tool-flashing_step'),
|
||||
},
|
||||
];
|
||||
|
||||
if (
|
||||
context.defaultConfig?.needBootPress &&
|
||||
context.selectedDevices?.find(
|
||||
({ type }) => type === FirmwareUpdateMethod.SerialFirmwareUpdate
|
||||
)
|
||||
) {
|
||||
steps.splice(5, 0, {
|
||||
component: FlashBtnStep,
|
||||
title: l10n.getString('firmware_tool-flashbtn_step'),
|
||||
});
|
||||
}
|
||||
return steps;
|
||||
}, [context.defaultConfig?.needBootPress, context.selectedDevices, l10n]);
|
||||
|
||||
return (
|
||||
<FirmwareToolContextC.Provider value={context}>
|
||||
<div className="flex flex-col bg-background-70 p-4 rounded-md">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('firmware_tool')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pt-2 pb-4">
|
||||
<>
|
||||
{l10n
|
||||
.getString('firmware_tool-description')
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
</Typography>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
<div className="m-4 h-full">
|
||||
{isError && (
|
||||
<div className="w-full flex flex-col justify-center items-center gap-3 h-full">
|
||||
<LoaderIcon slimeState={SlimeState.SAD}></LoaderIcon>
|
||||
{!isCompatible ? (
|
||||
<Localized id="firmware_tool-not_compatible">
|
||||
<Typography variant="section-title"></Typography>
|
||||
</Localized>
|
||||
) : (
|
||||
<Localized id="firmware_tool-not_available">
|
||||
<Typography variant="section-title"></Typography>
|
||||
</Localized>
|
||||
)}
|
||||
<Localized id="firmware_tool-retry">
|
||||
<Button variant="primary" onClick={retry}></Button>
|
||||
</Localized>
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div className="w-full flex flex-col justify-center items-center gap-3 h-full">
|
||||
<LoaderIcon slimeState={SlimeState.JUMPY}></LoaderIcon>
|
||||
<Localized id="firmware_tool-loading">
|
||||
<Typography variant="section-title"></Typography>
|
||||
</Localized>
|
||||
</div>
|
||||
)}
|
||||
{!isError && !isLoading && <VerticalStepper steps={steps} />}
|
||||
</div>
|
||||
</div>
|
||||
</FirmwareToolContextC.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function FirmwareToolSettings() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false, // default: true
|
||||
},
|
||||
},
|
||||
});
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<FirmwareToolContent />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
86
gui/src/components/firmware-tool/FlashBtnStep.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import {
|
||||
boardTypeToFirmwareToolBoardType,
|
||||
useFirmwareTool,
|
||||
} from '@/hooks/firmware-tool';
|
||||
import { BoardType } from 'solarxr-protocol';
|
||||
|
||||
export function FlashBtnStep({
|
||||
nextStep,
|
||||
}: {
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
goTo: (id: string) => void;
|
||||
isActive: boolean;
|
||||
}) {
|
||||
const { l10n } = useLocalization();
|
||||
const { defaultConfig } = useFirmwareTool();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex flex-grow flex-col gap-4">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('firmware_tool-flashbtn_step-description')}
|
||||
</Typography>
|
||||
{defaultConfig?.boardConfig.type ===
|
||||
boardTypeToFirmwareToolBoardType[BoardType.SLIMEVR] ? (
|
||||
<>
|
||||
<Typography variant="standard" whitespace="whitespace-pre">
|
||||
{l10n.getString('firmware_tool-flashbtn_step-board_SLIMEVR')}
|
||||
</Typography>
|
||||
<div className="gap-2 grid lg:grid-cols-3 md:grid-cols-2 mobile:grid-cols-1">
|
||||
<div className="bg-background-80 p-2 rounded-lg gap-2 flex flex-col justify-between">
|
||||
<Typography variant="main-title">R11</Typography>
|
||||
<Typography variant="standard">
|
||||
{l10n.getString(
|
||||
'firmware_tool-flashbtn_step-board_SLIMEVR-r11'
|
||||
)}
|
||||
</Typography>
|
||||
<img src="/images/R11_board_reset.webp"></img>
|
||||
</div>
|
||||
<div className="bg-background-80 p-2 rounded-lg gap-2 flex flex-col justify-between">
|
||||
<Typography variant="main-title">R12</Typography>
|
||||
<Typography variant="standard">
|
||||
{l10n.getString(
|
||||
'firmware_tool-flashbtn_step-board_SLIMEVR-r12'
|
||||
)}
|
||||
</Typography>
|
||||
<img src="/images/R12_board_reset.webp"></img>
|
||||
</div>
|
||||
|
||||
<div className="bg-background-80 p-2 rounded-lg gap-2 flex flex-col justify-between">
|
||||
<Typography variant="main-title">R14</Typography>
|
||||
<Typography variant="standard">
|
||||
{l10n.getString(
|
||||
'firmware_tool-flashbtn_step-board_SLIMEVR-r14'
|
||||
)}
|
||||
</Typography>
|
||||
<img src="/images/R14_board_reset_sw.webp"></img>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography variant="standard" whitespace="whitespace-pre">
|
||||
{l10n.getString('firmware_tool-flashbtn_step-board_OTHER')}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
<div className="flex justify-end">
|
||||
<Localized id="firmware_tool-next_step">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
nextStep();
|
||||
}}
|
||||
></Button>
|
||||
</Localized>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
421
gui/src/components/firmware-tool/FlashingMethodStep.tsx
Normal file
@@ -0,0 +1,421 @@
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
|
||||
import {
|
||||
boardTypeToFirmwareToolBoardType,
|
||||
useFirmwareTool,
|
||||
} from '@/hooks/firmware-tool';
|
||||
import { Control, UseFormReset, UseFormWatch, useForm } from 'react-hook-form';
|
||||
import { Radio } from '@/components/commons/Radio';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
|
||||
import {
|
||||
BoardType,
|
||||
DeviceDataT,
|
||||
FirmwareUpdateMethod,
|
||||
NewSerialDeviceResponseT,
|
||||
RpcMessage,
|
||||
SerialDeviceT,
|
||||
SerialDevicesRequestT,
|
||||
SerialDevicesResponseT,
|
||||
TrackerStatus,
|
||||
} from 'solarxr-protocol';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { useAppContext } from '@/hooks/app';
|
||||
import { Input } from '@/components/commons/Input';
|
||||
import { Dropdown } from '@/components/commons/Dropdown';
|
||||
import { useOnboarding } from '@/hooks/onboarding';
|
||||
import { DeviceCardControl } from './DeviceCard';
|
||||
import { getTrackerName } from '@/hooks/tracker';
|
||||
import { ObjectSchema, object, string } from 'yup';
|
||||
|
||||
interface FlashingMethodForm {
|
||||
flashingMethod?: string;
|
||||
serial?: {
|
||||
selectedDevicePort: string;
|
||||
ssid: string;
|
||||
password?: string;
|
||||
};
|
||||
ota?: {
|
||||
selectedDevices: { [key: string]: boolean };
|
||||
};
|
||||
}
|
||||
|
||||
function SerialDevicesList({
|
||||
control,
|
||||
watch,
|
||||
reset,
|
||||
}: {
|
||||
control: Control<FlashingMethodForm>;
|
||||
watch: UseFormWatch<FlashingMethodForm>;
|
||||
reset: UseFormReset<FlashingMethodForm>;
|
||||
}) {
|
||||
const { l10n } = useLocalization();
|
||||
const { selectDevices } = useFirmwareTool();
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
const [devices, setDevices] = useState<Record<string, SerialDeviceT>>({});
|
||||
const { state, setWifiCredentials } = useOnboarding();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
sendRPCPacket(RpcMessage.SerialDevicesRequest, new SerialDevicesRequestT());
|
||||
selectDevices(null);
|
||||
reset({
|
||||
flashingMethod: FirmwareUpdateMethod.SerialFirmwareUpdate.toString(),
|
||||
serial: {
|
||||
...state.wifi,
|
||||
selectedDevicePort: undefined,
|
||||
},
|
||||
ota: undefined,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useRPCPacket(
|
||||
RpcMessage.SerialDevicesResponse,
|
||||
(res: SerialDevicesResponseT) => {
|
||||
setDevices((old) =>
|
||||
res.devices.reduce(
|
||||
(curr, device) => ({
|
||||
...curr,
|
||||
[device?.port?.toString() ?? 'unknown']: device,
|
||||
}),
|
||||
old
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
useRPCPacket(
|
||||
RpcMessage.NewSerialDeviceResponse,
|
||||
({ device }: NewSerialDeviceResponseT) => {
|
||||
if (device?.port)
|
||||
setDevices((old) => ({
|
||||
...old,
|
||||
[device?.port?.toString() ?? 'unknown']: device,
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
const serialValues = watch('serial');
|
||||
|
||||
useEffect(() => {
|
||||
if (!serialValues) {
|
||||
selectDevices(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setWifiCredentials(serialValues.ssid, serialValues.password);
|
||||
if (
|
||||
serialValues.selectedDevicePort &&
|
||||
devices[serialValues.selectedDevicePort]
|
||||
) {
|
||||
selectDevices([
|
||||
{
|
||||
type: FirmwareUpdateMethod.SerialFirmwareUpdate,
|
||||
deviceId: serialValues.selectedDevicePort,
|
||||
deviceNames: [
|
||||
devices[serialValues.selectedDevicePort].name?.toString() ??
|
||||
'unknown',
|
||||
],
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
selectDevices(null);
|
||||
}
|
||||
}, [JSON.stringify(serialValues), devices]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Localized id="firmware_tool-flash_method_serial-wifi">
|
||||
<Typography variant="section-title"></Typography>
|
||||
</Localized>
|
||||
<div className="grid xs-settings:grid-cols-2 mobile-settings:grid-cols-1 gap-3 text-background-10">
|
||||
<Localized
|
||||
id="onboarding-wifi_creds-ssid"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
control={control}
|
||||
name="serial.ssid"
|
||||
label="SSID"
|
||||
variant="secondary"
|
||||
/>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="onboarding-wifi_creds-password"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
control={control}
|
||||
name="serial.password"
|
||||
type="password"
|
||||
variant="secondary"
|
||||
/>
|
||||
</Localized>
|
||||
</div>
|
||||
<Localized id="firmware_tool-flash_method_serial-devices-label">
|
||||
<Typography variant="section-title"></Typography>
|
||||
</Localized>
|
||||
{Object.keys(devices).length === 0 ? (
|
||||
<Localized id="firmware_tool-flash_method_serial-no_devices">
|
||||
<Typography variant="standard" color="secondary"></Typography>
|
||||
</Localized>
|
||||
) : (
|
||||
<Dropdown
|
||||
control={control}
|
||||
name="serial.selectedDevicePort"
|
||||
items={Object.keys(devices).map((port) => ({
|
||||
label: devices[port].name?.toString() ?? 'unknown',
|
||||
value: port,
|
||||
}))}
|
||||
placeholder={l10n.getString(
|
||||
'firmware_tool-flash_method_serial-devices-placeholder'
|
||||
)}
|
||||
display="block"
|
||||
direction="down"
|
||||
></Dropdown>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function OTADevicesList({
|
||||
control,
|
||||
watch,
|
||||
reset,
|
||||
}: {
|
||||
control: Control<FlashingMethodForm>;
|
||||
watch: UseFormWatch<FlashingMethodForm>;
|
||||
reset: UseFormReset<FlashingMethodForm>;
|
||||
}) {
|
||||
const { l10n } = useLocalization();
|
||||
const { selectDevices, newConfig } = useFirmwareTool();
|
||||
const { state } = useAppContext();
|
||||
|
||||
const devices =
|
||||
state.datafeed?.devices.filter(({ trackers, hardwareInfo }) => {
|
||||
// We make sure the device is not one of these types
|
||||
if (
|
||||
hardwareInfo?.officialBoardType === BoardType.SLIMEVR_LEGACY ||
|
||||
hardwareInfo?.officialBoardType === BoardType.SLIMEVR_DEV ||
|
||||
hardwareInfo?.officialBoardType === BoardType.CUSTOM
|
||||
)
|
||||
return false;
|
||||
|
||||
// if the device has no trackers it is prob misconfigured so we skip for safety
|
||||
if (trackers.length <= 0) return false;
|
||||
|
||||
// We make sure that the tracker is in working condition before doing ota as an error (that could be hardware)
|
||||
// could cause an error during the update
|
||||
if (!trackers.every(({ status }) => status === TrackerStatus.OK))
|
||||
return false;
|
||||
|
||||
const boardType = hardwareInfo?.officialBoardType ?? BoardType.UNKNOWN;
|
||||
return (
|
||||
boardTypeToFirmwareToolBoardType[boardType] ===
|
||||
newConfig?.boardConfig?.type
|
||||
);
|
||||
}) || [];
|
||||
|
||||
const deviceNames = ({ trackers }: DeviceDataT) =>
|
||||
trackers
|
||||
.map(({ info }) => getTrackerName(l10n, info))
|
||||
.filter((i): i is string => !!i);
|
||||
|
||||
const selectedDevices = watch('ota.selectedDevices');
|
||||
|
||||
useLayoutEffect(() => {
|
||||
reset({
|
||||
flashingMethod: FirmwareUpdateMethod.OTAFirmwareUpdate.toString(),
|
||||
ota: {
|
||||
selectedDevices: devices.reduce(
|
||||
(curr, { id }) => ({ ...curr, [id?.id ?? 0]: false }),
|
||||
{}
|
||||
),
|
||||
},
|
||||
serial: undefined,
|
||||
});
|
||||
selectDevices(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDevices) {
|
||||
selectDevices(
|
||||
Object.keys(selectedDevices)
|
||||
.filter((d) => selectedDevices[d])
|
||||
.map((id) => id.substring('id-'.length))
|
||||
.map((id) => {
|
||||
const device = devices.find(
|
||||
({ id: dId }) => id === dId?.id.toString()
|
||||
);
|
||||
|
||||
if (!device) throw new Error('no device found');
|
||||
return {
|
||||
type: FirmwareUpdateMethod.OTAFirmwareUpdate,
|
||||
deviceId: id,
|
||||
deviceNames: deviceNames(device),
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [JSON.stringify(selectedDevices)]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Localized id="firmware_tool-flash_method_ota-devices">
|
||||
<Typography variant="section-title"></Typography>
|
||||
</Localized>
|
||||
{devices.length === 0 && (
|
||||
<Localized id="firmware_tool-flash_method_ota-no_devices">
|
||||
<Typography color="secondary"></Typography>
|
||||
</Localized>
|
||||
)}
|
||||
<div className="grid xs-settings:grid-cols-2 mobile-settings:grid-cols-1 gap-2">
|
||||
{devices.map((device) => (
|
||||
<DeviceCardControl
|
||||
control={control}
|
||||
key={device.id?.id ?? 0}
|
||||
name={`ota.selectedDevices.id-${device.id?.id ?? 0}`}
|
||||
deviceNames={deviceNames(device)}
|
||||
></DeviceCardControl>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function FlashingMethodStep({
|
||||
nextStep,
|
||||
prevStep,
|
||||
}: {
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
isActive: boolean;
|
||||
}) {
|
||||
const { l10n } = useLocalization();
|
||||
const { isGlobalLoading, selectedDevices } = useFirmwareTool();
|
||||
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
reset,
|
||||
formState: { isValid },
|
||||
} = useForm<FlashingMethodForm>({
|
||||
reValidateMode: 'onChange',
|
||||
mode: 'onChange',
|
||||
resolver: yupResolver(
|
||||
object({
|
||||
flashingMethod: string().optional(),
|
||||
serial: object().when('flashingMethod', {
|
||||
is: FirmwareUpdateMethod.SerialFirmwareUpdate.toString(),
|
||||
then: (s) =>
|
||||
s
|
||||
.shape({
|
||||
selectedDevicePort: string().required(),
|
||||
ssid: string().required(
|
||||
l10n.getString('onboarding-wifi_creds-ssid-required')
|
||||
),
|
||||
password: string(),
|
||||
})
|
||||
.required(),
|
||||
otherwise: (s) => s.optional(),
|
||||
}),
|
||||
ota: object().when('flashingMethod', {
|
||||
is: FirmwareUpdateMethod.OTAFirmwareUpdate.toString(),
|
||||
then: (s) =>
|
||||
s
|
||||
.shape({
|
||||
selectedDevices: object(),
|
||||
})
|
||||
.required(),
|
||||
otherwise: (s) => s.optional(),
|
||||
}),
|
||||
}) as ObjectSchema<FlashingMethodForm>
|
||||
),
|
||||
});
|
||||
|
||||
const flashingMethod = watch('flashingMethod');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex flex-grow flex-col gap-4">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('firmware_tool-flash_method_step-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="my-4">
|
||||
{!isGlobalLoading && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="grid xs-settings:grid-cols-2 mobile-settings:grid-cols-1 gap-3">
|
||||
<Localized
|
||||
id="firmware_tool-flash_method_step-ota"
|
||||
attrs={{ label: true, description: true }}
|
||||
>
|
||||
<Radio
|
||||
control={control}
|
||||
name="flashingMethod"
|
||||
value={FirmwareUpdateMethod.OTAFirmwareUpdate.toString()}
|
||||
label=""
|
||||
></Radio>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="firmware_tool-flash_method_step-serial"
|
||||
attrs={{ label: true, description: true }}
|
||||
>
|
||||
<Radio
|
||||
control={control}
|
||||
name="flashingMethod"
|
||||
value={FirmwareUpdateMethod.SerialFirmwareUpdate.toString()}
|
||||
label=""
|
||||
></Radio>
|
||||
</Localized>
|
||||
</div>
|
||||
{flashingMethod ===
|
||||
FirmwareUpdateMethod.SerialFirmwareUpdate.toString() && (
|
||||
<SerialDevicesList
|
||||
control={control}
|
||||
watch={watch}
|
||||
reset={reset}
|
||||
></SerialDevicesList>
|
||||
)}
|
||||
{flashingMethod ===
|
||||
FirmwareUpdateMethod.OTAFirmwareUpdate.toString() && (
|
||||
<OTADevicesList
|
||||
control={control}
|
||||
watch={watch}
|
||||
reset={reset}
|
||||
></OTADevicesList>
|
||||
)}
|
||||
<div className="flex justify-between">
|
||||
<Localized id="firmware_tool-previous_step">
|
||||
<Button variant="secondary" onClick={prevStep}></Button>
|
||||
</Localized>
|
||||
<Localized id="firmware_tool-next_step">
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={
|
||||
!isValid ||
|
||||
selectedDevices === null ||
|
||||
selectedDevices.length === 0
|
||||
}
|
||||
onClick={nextStep}
|
||||
></Button>
|
||||
</Localized>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isGlobalLoading && (
|
||||
<div className="flex justify-center flex-col items-center gap-3 h-44">
|
||||
<LoaderIcon slimeState={SlimeState.JUMPY}></LoaderIcon>
|
||||
<Localized id="firmware_tool-loading">
|
||||
<Typography color="secondary"></Typography>
|
||||
</Localized>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
220
gui/src/components/firmware-tool/FlashingStep.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import {
|
||||
SelectedDevice,
|
||||
firmwareUpdateErrorStatus,
|
||||
getFlashingRequests,
|
||||
useFirmwareTool,
|
||||
} from '@/hooks/firmware-tool';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import {
|
||||
DeviceIdTableT,
|
||||
FirmwareUpdateMethod,
|
||||
FirmwareUpdateStatus,
|
||||
FirmwareUpdateStatusResponseT,
|
||||
FirmwareUpdateStopQueuesRequestT,
|
||||
RpcMessage,
|
||||
} from 'solarxr-protocol';
|
||||
import { useOnboarding } from '@/hooks/onboarding';
|
||||
import { DeviceCardControl } from './DeviceCard';
|
||||
import { WarningBox } from '@/components/commons/TipBox';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { firmwareToolS3BaseUrl } from '@/firmware-tool-api/firmwareToolFetcher';
|
||||
|
||||
export function FlashingStep({
|
||||
goTo,
|
||||
isActive,
|
||||
}: {
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
goTo: (id: string) => void;
|
||||
isActive: boolean;
|
||||
}) {
|
||||
const nav = useNavigate();
|
||||
const { l10n } = useLocalization();
|
||||
const { selectedDevices, buildStatus, selectDevices, defaultConfig } =
|
||||
useFirmwareTool();
|
||||
const { state: onboardingState } = useOnboarding();
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
const [status, setStatus] = useState<{
|
||||
[key: string]: {
|
||||
status: FirmwareUpdateStatus;
|
||||
type: FirmwareUpdateMethod;
|
||||
progress: number;
|
||||
deviceNames: string[];
|
||||
};
|
||||
}>({});
|
||||
|
||||
const clear = () => {
|
||||
setStatus({});
|
||||
sendRPCPacket(
|
||||
RpcMessage.FirmwareUpdateStopQueuesRequest,
|
||||
new FirmwareUpdateStopQueuesRequestT()
|
||||
);
|
||||
};
|
||||
|
||||
const queueFlashing = (selectedDevices: SelectedDevice[]) => {
|
||||
clear();
|
||||
if (!buildStatus.firmwareFiles)
|
||||
throw new Error('invalid state - no firmware files');
|
||||
const requests = getFlashingRequests(
|
||||
selectedDevices,
|
||||
buildStatus.firmwareFiles.map(({ url, ...fields }) => ({
|
||||
url: `${firmwareToolS3BaseUrl}/${url}`,
|
||||
...fields,
|
||||
})),
|
||||
onboardingState,
|
||||
defaultConfig
|
||||
);
|
||||
|
||||
requests.forEach((req) => {
|
||||
sendRPCPacket(RpcMessage.FirmwareUpdateRequest, req);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) return;
|
||||
if (!selectedDevices)
|
||||
throw new Error('invalid state - no selected devices');
|
||||
queueFlashing(selectedDevices);
|
||||
return () => clear();
|
||||
}, [isActive]);
|
||||
|
||||
useRPCPacket(
|
||||
RpcMessage.FirmwareUpdateStatusResponse,
|
||||
(data: FirmwareUpdateStatusResponseT) => {
|
||||
if (!data.deviceId) throw new Error('no device id');
|
||||
const id =
|
||||
data.deviceId instanceof DeviceIdTableT
|
||||
? data.deviceId.id?.id
|
||||
: data.deviceId.port;
|
||||
if (!id) throw new Error('invalid device id');
|
||||
|
||||
const selectedDevice = selectedDevices?.find(
|
||||
({ deviceId }) => deviceId == id.toString()
|
||||
);
|
||||
|
||||
// We skip the status as it can be old trackers still sending status
|
||||
if (!selectedDevice) return;
|
||||
|
||||
setStatus((last) => ({
|
||||
...last,
|
||||
[id.toString()]: {
|
||||
progress: data.progress / 100,
|
||||
status: data.status,
|
||||
type: selectedDevice.type,
|
||||
deviceNames: selectedDevice.deviceNames,
|
||||
},
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
const trackerWithErrors = useMemo(
|
||||
() =>
|
||||
Object.keys(status).filter((id) =>
|
||||
firmwareUpdateErrorStatus.includes(status[id].status)
|
||||
),
|
||||
[status, firmwareUpdateErrorStatus]
|
||||
);
|
||||
|
||||
const retryError = () => {
|
||||
const devices = trackerWithErrors.map((id) => {
|
||||
const device = status[id];
|
||||
return {
|
||||
type: device.type,
|
||||
deviceId: id,
|
||||
deviceNames: device.deviceNames,
|
||||
};
|
||||
});
|
||||
|
||||
selectDevices(devices);
|
||||
queueFlashing(devices);
|
||||
};
|
||||
|
||||
const hasPendingTrackers = useMemo(
|
||||
() =>
|
||||
Object.keys(status).filter((id) =>
|
||||
[
|
||||
FirmwareUpdateStatus.NEED_MANUAL_REBOOT,
|
||||
FirmwareUpdateStatus.DOWNLOADING,
|
||||
FirmwareUpdateStatus.AUTHENTICATING,
|
||||
FirmwareUpdateStatus.REBOOTING,
|
||||
FirmwareUpdateStatus.SYNCING_WITH_MCU,
|
||||
FirmwareUpdateStatus.UPLOADING,
|
||||
FirmwareUpdateStatus.PROVISIONING,
|
||||
].includes(status[id].status)
|
||||
).length > 0,
|
||||
[status]
|
||||
);
|
||||
|
||||
const shouldShowRebootWarning = useMemo(
|
||||
() =>
|
||||
Object.keys(status).find((id) =>
|
||||
[
|
||||
FirmwareUpdateStatus.REBOOTING,
|
||||
FirmwareUpdateStatus.UPLOADING,
|
||||
].includes(status[id].status)
|
||||
),
|
||||
[status]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex flex-grow flex-col gap-4">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('firmware_tool-flashing_step-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<div className="my-4 flex gap-2 flex-col">
|
||||
{shouldShowRebootWarning && (
|
||||
<Localized id="firmware_tool-flashing_step-warning">
|
||||
<WarningBox>Warning</WarningBox>
|
||||
</Localized>
|
||||
)}
|
||||
|
||||
{Object.keys(status).map((id) => {
|
||||
const val = status[id];
|
||||
|
||||
return (
|
||||
<DeviceCardControl
|
||||
status={val.status}
|
||||
progress={val.progress}
|
||||
key={id}
|
||||
deviceNames={val.deviceNames}
|
||||
></DeviceCardControl>
|
||||
);
|
||||
})}
|
||||
<div className="flex gap-2 self-end">
|
||||
<Localized id="firmware_tool-retry">
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={trackerWithErrors.length === 0}
|
||||
onClick={retryError}
|
||||
></Button>
|
||||
</Localized>
|
||||
<Localized id="firmware_tool-flashing_step-flash_more">
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={hasPendingTrackers}
|
||||
onClick={() => goTo('FlashingMethod')}
|
||||
></Button>
|
||||
</Localized>
|
||||
<Localized id="firmware_tool-flashing_step-exit">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
clear();
|
||||
nav('/');
|
||||
}}
|
||||
></Button>
|
||||
</Localized>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
95
gui/src/components/firmware-tool/SelectBoardStep.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
|
||||
import {
|
||||
firmwareToolToBoardType,
|
||||
useFirmwareTool,
|
||||
} from '@/hooks/firmware-tool';
|
||||
import { CreateBoardConfigDTO } from '@/firmware-tool-api/firmwareToolSchemas';
|
||||
import classNames from 'classnames';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { useGetFirmwaresBoards } from '@/firmware-tool-api/firmwareToolComponents';
|
||||
import { BoardType } from 'solarxr-protocol';
|
||||
|
||||
export function SelectBoardStep({
|
||||
nextStep,
|
||||
goTo,
|
||||
}: {
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
goTo: (id: string) => void;
|
||||
}) {
|
||||
const { l10n } = useLocalization();
|
||||
const { selectBoard, newConfig, defaultConfig } = useFirmwareTool();
|
||||
const { isFetching, data: boards } = useGetFirmwaresBoards({});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex flex-grow flex-col gap-4">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('firmware_tool-board_step-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="my-4">
|
||||
{!isFetching && (
|
||||
<div className="gap-2 flex flex-col">
|
||||
<div className="grid sm:grid-cols-2 mobile-settings:grid-cols-1 gap-2">
|
||||
{boards?.map((board) => (
|
||||
<div
|
||||
key={board}
|
||||
className={classNames(
|
||||
'p-3 rounded-md hover:bg-background-50',
|
||||
{
|
||||
'bg-background-50 text-background-10':
|
||||
newConfig?.boardConfig?.type === board,
|
||||
'bg-background-60':
|
||||
newConfig?.boardConfig?.type !== board,
|
||||
}
|
||||
)}
|
||||
onClick={() => {
|
||||
selectBoard(board as CreateBoardConfigDTO['type']);
|
||||
}}
|
||||
>
|
||||
{l10n.getString(
|
||||
`board_type-${
|
||||
BoardType[
|
||||
firmwareToolToBoardType[
|
||||
board as CreateBoardConfigDTO['type']
|
||||
] ?? BoardType.UNKNOWN
|
||||
]
|
||||
}`
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Localized id="firmware_tool-next_step">
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={!newConfig?.boardConfig?.type}
|
||||
onClick={() => {
|
||||
if (defaultConfig?.shouldOnlyUseDefaults) {
|
||||
goTo('SelectFirmware');
|
||||
} else {
|
||||
nextStep();
|
||||
}
|
||||
}}
|
||||
></Button>
|
||||
</Localized>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isFetching && (
|
||||
<div className="flex justify-center flex-col items-center gap-3 h-44">
|
||||
<LoaderIcon slimeState={SlimeState.JUMPY}></LoaderIcon>
|
||||
<Localized id="firmware_tool-loading">
|
||||
<Typography color="secondary"></Typography>
|
||||
</Localized>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
120
gui/src/components/firmware-tool/SelectFirmwareStep.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { useGetFirmwaresVersions } from '@/firmware-tool-api/firmwareToolComponents';
|
||||
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
|
||||
import { useFirmwareTool } from '@/hooks/firmware-tool';
|
||||
import classNames from 'classnames';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { useMemo } from 'react';
|
||||
import { CheckBox } from '@/components/commons/Checkbox';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
export function SelectFirmwareStep({
|
||||
nextStep,
|
||||
prevStep,
|
||||
goTo,
|
||||
}: {
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
goTo: (id: string) => void;
|
||||
}) {
|
||||
const { l10n } = useLocalization();
|
||||
const { selectVersion, newConfig, defaultConfig } = useFirmwareTool();
|
||||
const { isFetching, data: firmwares } = useGetFirmwaresVersions({});
|
||||
|
||||
const { control, watch } = useForm<{ thirdParty: boolean }>({});
|
||||
|
||||
const showThirdParty = watch('thirdParty');
|
||||
|
||||
const getName = (name: string) => {
|
||||
return showThirdParty ? name : name.substring(name.indexOf('/') + 1);
|
||||
};
|
||||
|
||||
const filteredFirmwares = useMemo(() => {
|
||||
return firmwares?.filter(
|
||||
({ name }) => name.split('/')[0] === 'SlimeVR' || showThirdParty
|
||||
);
|
||||
}, [firmwares, showThirdParty]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex justify-between items-center mobile:flex-col gap-4">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('firmware_tool-select_firmware_step-description')}
|
||||
</Typography>
|
||||
<div>
|
||||
<Localized
|
||||
id="firmware_tool-select_firmware_step-show-third-party"
|
||||
attrs={{ label: true }}
|
||||
>
|
||||
<CheckBox
|
||||
control={control}
|
||||
name="thirdParty"
|
||||
label="Show third party firmwares"
|
||||
></CheckBox>
|
||||
</Localized>
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-4">
|
||||
{!isFetching && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="xs-settings:max-h-96 xs-settings:overflow-y-auto xs-settings:px-2">
|
||||
<div className="grid sm:grid-cols-2 mobile-settings:grid-cols-1 gap-2">
|
||||
{filteredFirmwares?.map((firmware) => (
|
||||
<div
|
||||
key={firmware.id}
|
||||
className={classNames(
|
||||
'p-3 rounded-md hover:bg-background-50',
|
||||
{
|
||||
'bg-background-50 text-background-10':
|
||||
newConfig?.version === firmware.name,
|
||||
'bg-background-60':
|
||||
newConfig?.version !== firmware.name,
|
||||
}
|
||||
)}
|
||||
onClick={() => {
|
||||
selectVersion(firmware.name);
|
||||
}}
|
||||
>
|
||||
{getName(firmware.name)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<Localized id="firmware_tool-previous_step">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={() => {
|
||||
if (defaultConfig?.shouldOnlyUseDefaults) {
|
||||
goTo('SelectBoard');
|
||||
} else {
|
||||
prevStep();
|
||||
}
|
||||
}}
|
||||
></Button>
|
||||
</Localized>
|
||||
<Localized id="firmware_tool-next_step">
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={!newConfig?.version}
|
||||
onClick={nextStep}
|
||||
></Button>
|
||||
</Localized>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isFetching && (
|
||||
<div className="flex justify-center flex-col items-center gap-3 h-44">
|
||||
<LoaderIcon slimeState={SlimeState.JUMPY}></LoaderIcon>
|
||||
<Localized id="firmware_tool-loading">
|
||||
<Typography color="secondary"></Typography>
|
||||
</Localized>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
402
gui/src/components/firmware-update/FirmwareUpdate.tsx
Normal file
@@ -0,0 +1,402 @@
|
||||
import { Localized, ReactLocalization, useLocalization } from '@fluent/react';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { getTrackerName } from '@/hooks/tracker';
|
||||
import { ComponentProps, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
BoardType,
|
||||
DeviceDataT,
|
||||
DeviceIdTableT,
|
||||
FirmwareUpdateMethod,
|
||||
FirmwareUpdateStatus,
|
||||
FirmwareUpdateStatusResponseT,
|
||||
FirmwareUpdateStopQueuesRequestT,
|
||||
HardwareInfoT,
|
||||
RpcMessage,
|
||||
TrackerStatus,
|
||||
} from 'solarxr-protocol';
|
||||
import semver from 'semver';
|
||||
import classNames from 'classnames';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import Markdown from 'react-markdown';
|
||||
import remark from 'remark-gfm';
|
||||
import { WarningBox } from '@/components/commons/TipBox';
|
||||
import { FirmwareRelease, useAppContext } from '@/hooks/app';
|
||||
import { DeviceCardControl } from '@/components/firmware-tool/DeviceCard';
|
||||
import { Control, useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import {
|
||||
firmwareUpdateErrorStatus,
|
||||
getFlashingRequests,
|
||||
SelectedDevice,
|
||||
} from '@/hooks/firmware-tool';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { object } from 'yup';
|
||||
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
|
||||
import { A } from '@/components/commons/A';
|
||||
|
||||
export function checkForUpdate(
|
||||
currentFirmwareRelease: FirmwareRelease,
|
||||
hardwareInfo: HardwareInfoT
|
||||
) {
|
||||
return (
|
||||
// TODO: This is temporary, end goal is to support all board types
|
||||
hardwareInfo.officialBoardType === BoardType.SLIMEVR &&
|
||||
semver.valid(currentFirmwareRelease.version) &&
|
||||
semver.valid(hardwareInfo.firmwareVersion?.toString() ?? 'none') &&
|
||||
semver.lt(
|
||||
hardwareInfo.firmwareVersion?.toString() ?? 'none',
|
||||
currentFirmwareRelease.version
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface FirmwareUpdateForm {
|
||||
selectedDevices: { [key: string]: boolean };
|
||||
}
|
||||
|
||||
interface UpdateStatus {
|
||||
status: FirmwareUpdateStatus;
|
||||
type: FirmwareUpdateMethod;
|
||||
progress: number;
|
||||
deviceNames: string[];
|
||||
}
|
||||
|
||||
const deviceNames = ({ trackers }: DeviceDataT, l10n: ReactLocalization) =>
|
||||
trackers
|
||||
.map(({ info }) => getTrackerName(l10n, info))
|
||||
.filter((i): i is string => !!i);
|
||||
|
||||
const DeviceList = ({
|
||||
control,
|
||||
devices,
|
||||
}: {
|
||||
control: Control<any>;
|
||||
devices: DeviceDataT[];
|
||||
}) => {
|
||||
const { l10n } = useLocalization();
|
||||
|
||||
return devices.map((device, index) => (
|
||||
<DeviceCardControl
|
||||
key={index}
|
||||
control={control}
|
||||
name={`selectedDevices.${device.id?.id ?? 0}`}
|
||||
deviceNames={deviceNames(device, l10n)}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
const StatusList = ({ status }: { status: Record<string, UpdateStatus> }) => {
|
||||
const statusKeys = Object.keys(status);
|
||||
|
||||
return statusKeys.map((id, index) => {
|
||||
const val = status[id];
|
||||
|
||||
if (!val) throw new Error('there should always be a val');
|
||||
const { state } = useAppContext();
|
||||
const device = state.datafeed?.devices.find(
|
||||
({ id: dId }) => id === dId?.id.toString()
|
||||
);
|
||||
|
||||
return (
|
||||
<DeviceCardControl
|
||||
status={val.status}
|
||||
progress={val.progress}
|
||||
key={index}
|
||||
deviceNames={val.deviceNames}
|
||||
online={device?.trackers.some(
|
||||
({ status }) => status === TrackerStatus.OK
|
||||
)}
|
||||
></DeviceCardControl>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const MarkdownLink = (props: ComponentProps<'a'>) => (
|
||||
<A href={props.href}>{props.children}</A>
|
||||
);
|
||||
|
||||
export function FirmwareUpdate() {
|
||||
const navigate = useNavigate();
|
||||
const { l10n } = useLocalization();
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
const [selectedDevices, setSelectedDevices] = useState<SelectedDevice[]>([]);
|
||||
const { state, currentFirmwareRelease } = useAppContext();
|
||||
const [status, setStatus] = useState<Record<string, UpdateStatus>>({});
|
||||
|
||||
const devices =
|
||||
state.datafeed?.devices.filter(
|
||||
({ trackers, hardwareInfo }) =>
|
||||
trackers.length > 0 &&
|
||||
currentFirmwareRelease &&
|
||||
hardwareInfo &&
|
||||
checkForUpdate(currentFirmwareRelease, hardwareInfo) &&
|
||||
trackers.every(({ status }) => status === TrackerStatus.OK)
|
||||
) || [];
|
||||
|
||||
useRPCPacket(
|
||||
RpcMessage.FirmwareUpdateStatusResponse,
|
||||
(data: FirmwareUpdateStatusResponseT) => {
|
||||
if (!data.deviceId) throw new Error('no device id');
|
||||
const id =
|
||||
data.deviceId instanceof DeviceIdTableT
|
||||
? data.deviceId.id?.id
|
||||
: data.deviceId.port;
|
||||
if (!id) throw new Error('invalid device id');
|
||||
|
||||
const selectedDevice = selectedDevices?.find(
|
||||
({ deviceId }) => deviceId === id.toString()
|
||||
);
|
||||
|
||||
// We skip the status as it can be old trackers still sending status
|
||||
if (!selectedDevice) return;
|
||||
|
||||
setStatus((last) => ({
|
||||
...last,
|
||||
[id.toString()]: {
|
||||
progress: data.progress / 100,
|
||||
status: data.status,
|
||||
type: selectedDevice.type,
|
||||
deviceNames: selectedDevice.deviceNames,
|
||||
},
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
reset,
|
||||
formState: { isValid },
|
||||
} = useForm<FirmwareUpdateForm>({
|
||||
reValidateMode: 'onChange',
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
selectedDevices: devices.reduce(
|
||||
(curr, { id }) => ({ ...curr, [id?.id ?? 0]: false }),
|
||||
{}
|
||||
),
|
||||
},
|
||||
resolver: yupResolver(
|
||||
object({
|
||||
selectedDevices: object().test(
|
||||
'at-least-one-true',
|
||||
'At least one field must be true',
|
||||
(value) => {
|
||||
if (typeof value !== 'object' || value === null) return false;
|
||||
return Object.values(value).some((val) => val === true);
|
||||
}
|
||||
),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
const selectedDevicesForm = watch('selectedDevices');
|
||||
|
||||
const clear = () => {
|
||||
setStatus({});
|
||||
sendRPCPacket(
|
||||
RpcMessage.FirmwareUpdateStopQueuesRequest,
|
||||
new FirmwareUpdateStopQueuesRequestT()
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentFirmwareRelease) {
|
||||
navigate('/');
|
||||
return;
|
||||
}
|
||||
return () => {
|
||||
clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const queueFlashing = (selectedDevices: SelectedDevice[]) => {
|
||||
clear();
|
||||
const firmwareFile = currentFirmwareRelease?.firmwareFile;
|
||||
if (!firmwareFile) throw new Error('invalid state - no firmware file');
|
||||
const requests = getFlashingRequests(
|
||||
selectedDevices,
|
||||
[{ isFirmware: true, firmwareId: '', url: firmwareFile, offset: 0 }],
|
||||
{ wifi: undefined, alonePage: false, progress: 0 }, // we do not use serial
|
||||
null // we do not use serial
|
||||
);
|
||||
|
||||
requests.forEach((req) => {
|
||||
sendRPCPacket(RpcMessage.FirmwareUpdateRequest, req);
|
||||
});
|
||||
};
|
||||
|
||||
const trackerWithErrors = useMemo(
|
||||
() =>
|
||||
Object.keys(status).filter((id) =>
|
||||
firmwareUpdateErrorStatus.includes(status[id].status)
|
||||
),
|
||||
[status]
|
||||
);
|
||||
|
||||
const hasPendingTrackers = useMemo(
|
||||
() =>
|
||||
Object.keys(status).filter((id) =>
|
||||
[
|
||||
FirmwareUpdateStatus.NEED_MANUAL_REBOOT,
|
||||
FirmwareUpdateStatus.DOWNLOADING,
|
||||
FirmwareUpdateStatus.AUTHENTICATING,
|
||||
FirmwareUpdateStatus.REBOOTING,
|
||||
FirmwareUpdateStatus.SYNCING_WITH_MCU,
|
||||
FirmwareUpdateStatus.UPLOADING,
|
||||
FirmwareUpdateStatus.PROVISIONING,
|
||||
].includes(status[id].status)
|
||||
).length > 0,
|
||||
[status]
|
||||
);
|
||||
|
||||
const shouldShowRebootWarning = useMemo(
|
||||
() =>
|
||||
Object.keys(status).find((id) =>
|
||||
[
|
||||
FirmwareUpdateStatus.REBOOTING,
|
||||
FirmwareUpdateStatus.UPLOADING,
|
||||
].includes(status[id].status)
|
||||
),
|
||||
[status]
|
||||
);
|
||||
|
||||
const retryError = () => {
|
||||
const devices = trackerWithErrors.map((id) => {
|
||||
const device = status[id];
|
||||
return {
|
||||
type: device.type,
|
||||
deviceId: id,
|
||||
deviceNames: device.deviceNames,
|
||||
};
|
||||
});
|
||||
|
||||
reset({
|
||||
selectedDevices: devices.reduce(
|
||||
(curr, { deviceId }) => ({ ...curr, [deviceId]: true }),
|
||||
{}
|
||||
),
|
||||
});
|
||||
queueFlashing(devices);
|
||||
};
|
||||
|
||||
const startUpdate = () => {
|
||||
const selectedDevices = Object.keys(selectedDevicesForm)
|
||||
.filter((d) => selectedDevicesForm[d])
|
||||
.map((id) => {
|
||||
const device = devices.find(({ id: dId }) => id === dId?.id.toString());
|
||||
|
||||
if (!device) throw new Error('no device found');
|
||||
return {
|
||||
type: FirmwareUpdateMethod.OTAFirmwareUpdate,
|
||||
deviceId: id,
|
||||
deviceNames: deviceNames(device, l10n),
|
||||
};
|
||||
});
|
||||
if (!selectedDevices)
|
||||
throw new Error('invalid state - no selected devices');
|
||||
setSelectedDevices(selectedDevices);
|
||||
queueFlashing(selectedDevices);
|
||||
};
|
||||
|
||||
const canStartUpdate =
|
||||
isValid &&
|
||||
devices.length !== 0 &&
|
||||
!hasPendingTrackers &&
|
||||
trackerWithErrors.length === 0;
|
||||
const canRetry =
|
||||
isValid && devices.length !== 0 && trackerWithErrors.length !== 0;
|
||||
|
||||
const statusKeys = Object.keys(status);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col p-4 w-full items-center justify-center">
|
||||
<div className="mobile:w-full w-10/12 h-full flex flex-col gap-2">
|
||||
<Localized id="firmware_update-title">
|
||||
<Typography variant="main-title"></Typography>
|
||||
</Localized>
|
||||
<div className="grid md:grid-cols-2 xs:grid-cols-1 gap-5">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Localized id="firmware_update-devices">
|
||||
<Typography variant="section-title"></Typography>
|
||||
</Localized>
|
||||
<Localized id="firmware_update-devices-description">
|
||||
<Typography variant="standard" color="secondary"></Typography>
|
||||
</Localized>
|
||||
<div className="flex flex-col gap-4 overflow-y-auto xs:max-h-[530px]">
|
||||
{devices.length === 0 &&
|
||||
!hasPendingTrackers &&
|
||||
statusKeys.length == 0 && (
|
||||
<Localized id="firmware_update-no_devices">
|
||||
<WarningBox>Warning</WarningBox>
|
||||
</Localized>
|
||||
)}
|
||||
{shouldShowRebootWarning && (
|
||||
<Localized id="firmware_tool-flashing_step-warning">
|
||||
<WarningBox>Warning</WarningBox>
|
||||
</Localized>
|
||||
)}
|
||||
<div className="flex flex-col gap-4 h-full">
|
||||
{statusKeys.length > 0 ? (
|
||||
<StatusList status={status}></StatusList>
|
||||
) : (
|
||||
<DeviceList control={control} devices={devices}></DeviceList>
|
||||
)}
|
||||
{devices.length === 0 && statusKeys.length === 0 && (
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-xl bg-background-60 justify-center flex-col items-center flex pb-10 py-5 gap-5'
|
||||
)}
|
||||
>
|
||||
<LoaderIcon slimeState={SlimeState.JUMPY}></LoaderIcon>
|
||||
<Localized id="firmware_update-looking_for_devices">
|
||||
<Typography></Typography>
|
||||
</Localized>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-fit w-full flex flex-col gap-2">
|
||||
<Localized
|
||||
id="firmware_update-changelog-title"
|
||||
vars={{ version: currentFirmwareRelease?.name ?? 'unknown' }}
|
||||
>
|
||||
<Typography variant="main-title"></Typography>
|
||||
</Localized>
|
||||
<div className="overflow-y-scroll max-h-[430px] md:h-[430px] bg-background-60 rounded-lg p-4">
|
||||
<Markdown
|
||||
remarkPlugins={[remark]}
|
||||
components={{ a: MarkdownLink }}
|
||||
className={classNames(
|
||||
'w-full text-sm prose-xl prose text-background-10 prose-h1:text-background-10',
|
||||
'prose-h2:text-background-10 prose-a:text-background-20 prose-strong:text-background-10',
|
||||
'prose-code:text-background-20'
|
||||
)}
|
||||
>
|
||||
{currentFirmwareRelease?.changelog}
|
||||
</Markdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end pb-2 gap-2 mobile:flex-col">
|
||||
<Localized id="firmware_update-retry">
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={!canRetry}
|
||||
onClick={retryError}
|
||||
></Button>
|
||||
</Localized>
|
||||
<Localized id="firmware_update-update">
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={!canStartUpdate}
|
||||
onClick={startUpdate}
|
||||
></Button>
|
||||
</Localized>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -51,7 +51,7 @@ export function Home() {
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div
|
||||
className={classNames(
|
||||
'px-2 pt-2 gap-3 w-full grid md:grid-cols-2 mobile:grid-cols-1',
|
||||
'px-3 pt-3 gap-3 w-full grid md:grid-cols-2 mobile:grid-cols-1',
|
||||
filteredStatuses.filter(([, status]) => status.prioritized)
|
||||
.length === 0 && 'hidden'
|
||||
)}
|
||||
@@ -70,7 +70,7 @@ export function Home() {
|
||||
</Localized>
|
||||
))}
|
||||
</div>
|
||||
<div className="overflow-y-auto flex flex-col gap-2">
|
||||
<div className="overflow-y-auto flex flex-col gap-3">
|
||||
{trackers.length === 0 && (
|
||||
<div className="flex px-5 pt-5 justify-center">
|
||||
<Typography variant="standard">
|
||||
@@ -80,7 +80,7 @@ export function Home() {
|
||||
)}
|
||||
|
||||
{!config?.debug && trackers.length > 0 && (
|
||||
<div className="grid sm:grid-cols-1 md:grid-cols-2 gap-3 px-2 my-2">
|
||||
<div className="grid sm:grid-cols-1 md:grid-cols-2 gap-4 px-5 my-5">
|
||||
{trackers.map(({ tracker, device }, index) => (
|
||||
<TrackerCard
|
||||
key={index}
|
||||
@@ -88,6 +88,7 @@ export function Home() {
|
||||
device={device}
|
||||
onClick={() => sendToSettings(tracker)}
|
||||
smol
|
||||
showUpdates
|
||||
interactable
|
||||
warning={Object.values(statuses).some((status) =>
|
||||
trackerStatusRelated(tracker, status)
|
||||
|
||||
@@ -3,11 +3,9 @@ import classNames from 'classnames';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
AddUnknownDeviceRequestT,
|
||||
RpcMessage,
|
||||
StartWifiProvisioningRequestT,
|
||||
StopWifiProvisioningRequestT,
|
||||
UnknownDeviceHandshakeNotificationT,
|
||||
WifiProvisioningStatus,
|
||||
WifiProvisioningStatusResponseT,
|
||||
} from 'solarxr-protocol';
|
||||
@@ -97,15 +95,6 @@ export function ConnectTrackersPage() {
|
||||
}
|
||||
);
|
||||
|
||||
useRPCPacket(
|
||||
RpcMessage.UnknownDeviceHandshakeNotification,
|
||||
({ macAddress }: UnknownDeviceHandshakeNotificationT) =>
|
||||
sendRPCPacket(
|
||||
RpcMessage.AddUnknownDeviceRequest,
|
||||
new AddUnknownDeviceRequestT(macAddress)
|
||||
)
|
||||
);
|
||||
|
||||
const isError =
|
||||
provisioningStatus === WifiProvisioningStatus.CONNECTION_ERROR ||
|
||||
provisioningStatus === WifiProvisioningStatus.COULD_NOT_FIND_SERVER;
|
||||
|
||||
@@ -33,7 +33,7 @@ export function EnterVRPage() {
|
||||
</div>
|
||||
<div className="w-full py-4 flex flex-row">
|
||||
<div className="flex flex-grow">
|
||||
<Button variant="secondary" to="/" onClick={skipSetup}>
|
||||
<Button variant="secondary" onClick={skipSetup}>
|
||||
{l10n.getString('onboarding-skip')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ export function HomePage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center px-4">
|
||||
<div className="flex relative flex-col gap-5 h-full items-center w-full justify-center px-4 overflow-clip">
|
||||
<div className="flex flex-col gap-5 items-center z-10 scale-150 mb-20">
|
||||
<SlimeVRIcon></SlimeVRIcon>
|
||||
<Typography variant="mobile-title">
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { useOnboarding } from '@/hooks/onboarding';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { useState } from 'react';
|
||||
import { SkipSetupWarningModal } from '@/components/onboarding/SkipSetupWarningModal';
|
||||
import classNames from 'classnames';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
|
||||
export function MountingChoose() {
|
||||
const { l10n } = useLocalization();
|
||||
const { applyProgress, skipSetup, state } = useOnboarding();
|
||||
const { applyProgress, state } = useOnboarding();
|
||||
const [animated, setAnimated] = useState(false);
|
||||
const [showWarning, setShowWarning] = useState(false);
|
||||
|
||||
applyProgress(0.65);
|
||||
|
||||
@@ -137,11 +135,6 @@ export function MountingChoose() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SkipSetupWarningModal
|
||||
accept={skipSetup}
|
||||
onClose={() => setShowWarning(false)}
|
||||
isOpen={showWarning}
|
||||
></SkipSetupWarningModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { SlimeUpIcon } from '@/components/commons/icon/SlimeUpIcon';
|
||||
import { BodyPart } from 'solarxr-protocol';
|
||||
import { PawIcon } from '@/components/commons/icon/PawIcon';
|
||||
import { useLocaleConfig } from '@/i18n/config';
|
||||
import { FingersIcon } from '@/components/commons/icon/FingersIcon';
|
||||
|
||||
// All body parts that are right or left, are by default left!
|
||||
export const mapPart: Record<
|
||||
@@ -97,6 +98,96 @@ export const mapPart: Record<
|
||||
<FootIcon width={width} flipped></FootIcon>
|
||||
),
|
||||
[BodyPart.WAIST]: ({ width }) => <FootIcon width={width}></FootIcon>,
|
||||
[BodyPart.LEFT_THUMB_METACARPAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_THUMB_PROXIMAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_THUMB_DISTAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_INDEX_PROXIMAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_INDEX_INTERMEDIATE]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_INDEX_DISTAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_MIDDLE_PROXIMAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_MIDDLE_INTERMEDIATE]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_MIDDLE_DISTAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_RING_PROXIMAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_RING_INTERMEDIATE]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_RING_DISTAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_LITTLE_PROXIMAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_LITTLE_INTERMEDIATE]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.LEFT_LITTLE_DISTAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_THUMB_METACARPAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_THUMB_PROXIMAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_THUMB_DISTAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_INDEX_PROXIMAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_INDEX_INTERMEDIATE]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_INDEX_DISTAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_MIDDLE_PROXIMAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_MIDDLE_INTERMEDIATE]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_MIDDLE_DISTAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_RING_PROXIMAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_RING_INTERMEDIATE]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_RING_DISTAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_LITTLE_PROXIMAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_LITTLE_INTERMEDIATE]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
[BodyPart.RIGHT_LITTLE_DISTAL]: ({ width }) => (
|
||||
<FingersIcon width={width}></FingersIcon>
|
||||
),
|
||||
};
|
||||
|
||||
export function MountingBodyPartIcon({
|
||||
|
||||
@@ -247,7 +247,7 @@ export function TrackersAssignPage() {
|
||||
}
|
||||
);
|
||||
|
||||
applyProgress(0.5);
|
||||
applyProgress(0.55);
|
||||
|
||||
const { closeChokerWarning, tryOpenChokerWarning, shouldShowChokerWarn } =
|
||||
useChokerWarning({
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import { DOCS_SITE } from '@/App';
|
||||
import { A } from '@/components/commons/A';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { WarningBox } from '@/components/commons/TipBox';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { useOnboarding } from '@/hooks/onboarding';
|
||||
import { useStatusContext } from '@/hooks/status-system';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
ChangeSettingsRequestT,
|
||||
ModelSettingsT,
|
||||
ModelTogglesT,
|
||||
RpcMessage,
|
||||
SettingsRequestT,
|
||||
SettingsResponseT,
|
||||
StatusData,
|
||||
StatusSteamVRDisconnectedT,
|
||||
} from 'solarxr-protocol';
|
||||
|
||||
export function HeadTrackingChoose() {
|
||||
const { l10n } = useLocalization();
|
||||
const { applyProgress, state } = useOnboarding();
|
||||
const { statuses } = useStatusContext();
|
||||
const [animated, setAnimated] = useState(false);
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
const fetchedSettings = useRef<ModelTogglesT | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT());
|
||||
}, []);
|
||||
|
||||
const toggleMocap = (bool: boolean) => {
|
||||
const settings = new ChangeSettingsRequestT();
|
||||
const modelSettings = new ModelSettingsT();
|
||||
|
||||
modelSettings.toggles = fetchedSettings.current ?? new ModelTogglesT();
|
||||
modelSettings.toggles.selfLocalization = bool;
|
||||
|
||||
settings.modelSettings = modelSettings;
|
||||
sendRPCPacket(RpcMessage.ChangeSettingsRequest, settings);
|
||||
};
|
||||
|
||||
useRPCPacket(
|
||||
RpcMessage.SettingsResponse,
|
||||
(oldSettings: SettingsResponseT) => {
|
||||
fetchedSettings.current = oldSettings.modelSettings?.toggles ?? null;
|
||||
|
||||
toggleMocap(false);
|
||||
}
|
||||
);
|
||||
|
||||
const missingSteamVr = useMemo(
|
||||
() =>
|
||||
Object.values(statuses).some(
|
||||
(x) =>
|
||||
x.dataType === StatusData.StatusSteamVRDisconnected &&
|
||||
(x.data as StatusSteamVRDisconnectedT).bridgeSettingsName ===
|
||||
'steamvr'
|
||||
),
|
||||
[statuses]
|
||||
);
|
||||
|
||||
applyProgress(0.55);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center relative overflow-y-auto px-4 pb-4">
|
||||
<div className="flex flex-col gap-4 justify-center">
|
||||
<div className="xs:w-10/12 xs:max-w-[666px]">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-usage-mocap-head_choose')}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="standard"
|
||||
color="secondary"
|
||||
whitespace="whitespace-pre-line"
|
||||
>
|
||||
{l10n.getString('onboarding-usage-mocap-head_choose-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'grid xs:grid-cols-2 w-full xs:flex-row mobile:flex-col gap-4 [&>div]:grow'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-lg p-4 flex',
|
||||
!state.alonePage && 'bg-background-70',
|
||||
state.alonePage && 'bg-background-60'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<div>
|
||||
<Typography variant="main-title" bold>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-mocap-head_choose-standalone'
|
||||
)}
|
||||
</Typography>
|
||||
<Typography variant="vr-accessible" italic>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-mocap-head_choose-standalone-label'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography
|
||||
color="secondary"
|
||||
whitespace="whitespace-pre-line"
|
||||
>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-mocap-head_choose-standalone-description'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant={!state.alonePage ? 'secondary' : 'tertiary'}
|
||||
className="self-start mt-auto"
|
||||
onClick={() => {
|
||||
toggleMocap(true);
|
||||
|
||||
navigate('/onboarding/usage/mocap/data/choose', {
|
||||
state: { alonePage: state.alonePage },
|
||||
});
|
||||
}}
|
||||
>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-mocap-head_choose-standalone-button'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-lg p-4 flex flex-row relative',
|
||||
!state.alonePage && 'bg-background-70',
|
||||
state.alonePage && 'bg-background-60'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<div>
|
||||
<img
|
||||
onMouseEnter={() => setAnimated(() => true)}
|
||||
onAnimationEnd={() => setAnimated(() => false)}
|
||||
src="/images/nighty-vr-sitting.webp"
|
||||
className={classNames(
|
||||
'absolute w-[150px] -right-8 -top-36',
|
||||
animated && 'animate-[bounce_1s_1]'
|
||||
)}
|
||||
></img>
|
||||
<Typography variant="main-title" bold>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-mocap-head_choose-steamvr'
|
||||
)}
|
||||
</Typography>
|
||||
<Typography variant="vr-accessible" italic>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-mocap-head_choose-steamvr-label'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Typography
|
||||
color="secondary"
|
||||
whitespace="whitespace-pre-line"
|
||||
>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-mocap-head_choose-steamvr-description'
|
||||
)}
|
||||
</Typography>
|
||||
{
|
||||
// TODO: Add a button to open SteamVR via tauri's open()
|
||||
missingSteamVr && (
|
||||
<Localized
|
||||
id="onboarding-usage-vr-choose-steamvr-warning"
|
||||
elems={{
|
||||
docs: (
|
||||
<A
|
||||
href={`${DOCS_SITE}/common-issues.html#the-trackers-are-connected-to-the-slimevr-server-but-arent-turning-up-on-steam`}
|
||||
></A>
|
||||
),
|
||||
b: <b></b>,
|
||||
}}
|
||||
>
|
||||
<WarningBox>SteamVR driver not connected</WarningBox>
|
||||
</Localized>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant={'primary'}
|
||||
to="/onboarding/usage/mocap/data/choose"
|
||||
className="self-start mt-auto"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
disabled={missingSteamVr}
|
||||
>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-mocap-head_choose-steamvr-button'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="self-start"
|
||||
to="/onboarding/usage/choose"
|
||||
>
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function MocapBVHSetup() {
|
||||
return <div className="bg-background-70 w-[512px] rounded-md"></div>;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { useOnboarding } from '@/hooks/onboarding';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Radio } from '@/components/commons/Radio';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export enum MocapDataType {
|
||||
BVH = 'BVH',
|
||||
STEAMVR = 'STEAMVR',
|
||||
VMC = 'VMC',
|
||||
}
|
||||
|
||||
const TYPE_TO_NAV = {
|
||||
[MocapDataType.BVH]: '/onboarding/usage/mocap/data/bvh',
|
||||
[MocapDataType.STEAMVR]: '/onboarding/usage/mocap/data/steamvr',
|
||||
[MocapDataType.VMC]: '/onboarding/usage/mocap/data/vmc',
|
||||
};
|
||||
|
||||
export function MocapDataChoose() {
|
||||
const { l10n } = useLocalization();
|
||||
const { applyProgress } = useOnboarding();
|
||||
const navigate = useNavigate();
|
||||
const { control, watch } = useForm<{
|
||||
usageReason?: MocapDataType;
|
||||
}>();
|
||||
|
||||
const usageReason = watch('usageReason');
|
||||
|
||||
const ItemContent = ({ mode }: { mode: MocapDataType }) => (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex bg-background-60 py-2 px-4 group-hover/radio:bg-background-50 rounded-t-md'
|
||||
)}
|
||||
>
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-usage-mocap-data_choose-option-title', {
|
||||
mode: MocapDataType[mode],
|
||||
})}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex flex-col bg-background-70 group-hover/radio:bg-background-60 rounded-b-md py-2 px-4">
|
||||
<Typography>
|
||||
{l10n.getString('onboarding-usage-mocap-data_choose-option-label', {
|
||||
mode: MocapDataType[mode],
|
||||
})}
|
||||
</Typography>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const usages = useMemo(
|
||||
() =>
|
||||
Object.values(MocapDataType).map((mode) => (
|
||||
<Radio
|
||||
key={mode}
|
||||
name="usageReason"
|
||||
control={control}
|
||||
value={mode.toString()}
|
||||
variant="none"
|
||||
className="hidden"
|
||||
>
|
||||
<div>
|
||||
<ItemContent mode={mode}></ItemContent>
|
||||
</div>
|
||||
</Radio>
|
||||
)),
|
||||
[control, l10n]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => usageReason && navigate(TYPE_TO_NAV[usageReason]),
|
||||
[usageReason]
|
||||
);
|
||||
|
||||
applyProgress(0.6);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center">
|
||||
<div className="flex flex-col w-full overflow-y-auto px-4 py-4 xs:items-center">
|
||||
<div className="flex mobile:flex-col xs:gap-8 mobile:gap-4 mobile:pb-4 w-full min-h-0 justify-center">
|
||||
<div className="flex flex-col xs:max-w-sm gap-3 justify-center">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-usage-mocap-data_choose')}
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('onboarding-usage-mocap-data_choose-description')}
|
||||
</Typography>
|
||||
{usages}
|
||||
<div className="flex flex-row">
|
||||
<Button
|
||||
variant="secondary"
|
||||
to="/onboarding/usage/mocap/head-choose"
|
||||
>
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function MocapSteamSetup() {
|
||||
return <div className="bg-background-70 w-[512px] rounded-md"></div>;
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import { CheckBox } from '@/components/commons/Checkbox';
|
||||
import { FileInput } from '@/components/commons/FileInput';
|
||||
import { VMCIcon } from '@/components/commons/icon/VMCIcon';
|
||||
import { Input } from '@/components/commons/Input';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import {
|
||||
DEFAULT_VMC_VALUES,
|
||||
parseVRMFile,
|
||||
VMCSettingsForm,
|
||||
} from '@/components/settings/pages/VMCSettings';
|
||||
import { SettingsPagePaneLayout } from '@/components/settings/SettingsPageLayout';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
ChangeSettingsRequestT,
|
||||
OSCSettingsT,
|
||||
RpcMessage,
|
||||
SettingsRequestT,
|
||||
SettingsResponseT,
|
||||
VMCOSCSettingsT,
|
||||
} from 'solarxr-protocol';
|
||||
|
||||
export function MocapVMCSetup() {
|
||||
const { l10n } = useLocalization();
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
const [modelName, setModelName] = useState<string | null>(null);
|
||||
|
||||
const { reset, control, watch, handleSubmit } = useForm<VMCSettingsForm>({
|
||||
defaultValues: {
|
||||
...DEFAULT_VMC_VALUES,
|
||||
vmc: { oscSettings: { enabled: true }, anchorHip: false },
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (values: VMCSettingsForm) => {
|
||||
const settings = new ChangeSettingsRequestT();
|
||||
|
||||
if (values.vmc) {
|
||||
const vmcOsc = new VMCOSCSettingsT();
|
||||
|
||||
vmcOsc.oscSettings = Object.assign(new OSCSettingsT(), {
|
||||
...values.vmc.oscSettings,
|
||||
vmc: { oscSettings: { enabled: true }, anchorHip: false },
|
||||
});
|
||||
|
||||
if (values.vmc.vrmJson !== undefined) {
|
||||
if (values.vmc.vrmJson.length > 0) {
|
||||
vmcOsc.vrmJson = await parseVRMFile(values.vmc.vrmJson[0]);
|
||||
if (vmcOsc.vrmJson) {
|
||||
setModelName(
|
||||
JSON.parse(vmcOsc.vrmJson)?.extensions?.VRM?.meta?.title || ''
|
||||
);
|
||||
}
|
||||
} else {
|
||||
vmcOsc.vrmJson = '';
|
||||
setModelName(null);
|
||||
}
|
||||
}
|
||||
vmcOsc.anchorHip = values.vmc.anchorHip;
|
||||
vmcOsc.mirrorTracking = values.vmc.mirrorTracking;
|
||||
|
||||
settings.vmcOsc = vmcOsc;
|
||||
}
|
||||
sendRPCPacket(RpcMessage.ChangeSettingsRequest, settings);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch(() => handleSubmit(onSubmit)());
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT());
|
||||
}, []);
|
||||
|
||||
useRPCPacket(RpcMessage.SettingsResponse, (settings: SettingsResponseT) => {
|
||||
const formData: VMCSettingsForm = DEFAULT_VMC_VALUES;
|
||||
if (settings.vmcOsc) {
|
||||
if (settings.vmcOsc.oscSettings) {
|
||||
formData.vmc.oscSettings.enabled = settings.vmcOsc.oscSettings.enabled;
|
||||
if (settings.vmcOsc.oscSettings.portIn)
|
||||
formData.vmc.oscSettings.portIn = settings.vmcOsc.oscSettings.portIn;
|
||||
if (settings.vmcOsc.oscSettings.portOut)
|
||||
formData.vmc.oscSettings.portOut =
|
||||
settings.vmcOsc.oscSettings.portOut;
|
||||
if (settings.vmcOsc.oscSettings.address)
|
||||
formData.vmc.oscSettings.address =
|
||||
settings.vmcOsc.oscSettings.address.toString();
|
||||
}
|
||||
const vrmJson = settings.vmcOsc.vrmJson?.toString();
|
||||
if (vrmJson) {
|
||||
setModelName(JSON.parse(vrmJson)?.extensions?.VRM?.meta?.title || '');
|
||||
}
|
||||
|
||||
formData.vmc.anchorHip = settings.vmcOsc.anchorHip;
|
||||
formData.vmc.mirrorTracking = settings.vmcOsc.mirrorTracking;
|
||||
}
|
||||
|
||||
reset(formData);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="bg-background-70 sm:w-[512px] rounded-md overflow-scroll">
|
||||
<SettingsPagePaneLayout icon={<VMCIcon></VMCIcon>} id="vmc">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('settings-osc-vmc')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pt-2 pb-4">
|
||||
<>
|
||||
{l10n
|
||||
.getString('settings-osc-vmc-description')
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
</Typography>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vmc-network')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<>
|
||||
{l10n
|
||||
.getString('settings-osc-vmc-network-description')
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
</Typography>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pb-5">
|
||||
<Localized
|
||||
id="settings-osc-vmc-network-port_in"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
control={control}
|
||||
name="vmc.oscSettings.portIn"
|
||||
rules={{ required: true }}
|
||||
placeholder="9002"
|
||||
label=""
|
||||
></Input>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="settings-osc-vmc-network-port_out"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
control={control}
|
||||
name="vmc.oscSettings.portOut"
|
||||
rules={{ required: true }}
|
||||
placeholder="9000"
|
||||
label=""
|
||||
></Input>
|
||||
</Localized>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vmc-network-address')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('settings-osc-vmc-network-address-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid gap-3 pb-5">
|
||||
<Input
|
||||
type="text"
|
||||
control={control}
|
||||
name="vmc.oscSettings.address"
|
||||
rules={{
|
||||
required: true,
|
||||
pattern: /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/i,
|
||||
}}
|
||||
placeholder={l10n.getString(
|
||||
'settings-osc-vmc-network-address-placeholder'
|
||||
)}
|
||||
label=""
|
||||
></Input>
|
||||
</div>
|
||||
<Typography bold>{l10n.getString('settings-osc-vmc-vrm')}</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('settings-osc-vmc-vrm-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid gap-3 pb-5">
|
||||
<FileInput
|
||||
control={control}
|
||||
name="vmc.vrmJson"
|
||||
rules={{
|
||||
required: false,
|
||||
}}
|
||||
value="help"
|
||||
importedFileName={
|
||||
// if modelname is an empty string, it's an untitled model
|
||||
modelName === ''
|
||||
? l10n.getString('settings-osc-vmc-vrm-untitled_model')
|
||||
: modelName
|
||||
}
|
||||
label="settings-osc-vmc-vrm-file_select"
|
||||
accept="model/gltf-binary, model/gltf+json, model/vrml, .vrm, .glb, .gltf"
|
||||
></FileInput>
|
||||
{/* For some reason, linux (GNOME) is detecting the VRM file is a VRML */}
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vmc-mirror_tracking')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('settings-osc-vmc-mirror_tracking-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pb-5">
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="vmc.mirrorTracking"
|
||||
label={l10n.getString('settings-osc-vmc-mirror_tracking-label')}
|
||||
/>
|
||||
</div>
|
||||
</SettingsPagePaneLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { PausableVideo } from '@/components/commons/PausableVideo';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { useOnboarding } from '@/hooks/onboarding';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import { VRCHAT_OSC_VIDEO } from '@/utils/tauri';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
ChangeSettingsRequestT,
|
||||
OSCSettingsT,
|
||||
RpcMessage,
|
||||
SettingsRequestT,
|
||||
SettingsResponseT,
|
||||
VRCOSCSettingsT,
|
||||
} from 'solarxr-protocol';
|
||||
|
||||
export function StandaloneUsageSetup() {
|
||||
const { applyProgress, state } = useOnboarding();
|
||||
const { l10n } = useLocalization();
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
const fetchedSettings = useRef<OSCSettingsT | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT());
|
||||
}, []);
|
||||
|
||||
const toggleVrc = (bool: boolean) => {
|
||||
const oldOscSettings = fetchedSettings.current;
|
||||
|
||||
const settings = new ChangeSettingsRequestT();
|
||||
const vrcOsc = new VRCOSCSettingsT();
|
||||
vrcOsc.oscSettings = new OSCSettingsT(
|
||||
bool,
|
||||
oldOscSettings?.portIn,
|
||||
oldOscSettings?.portOut,
|
||||
oldOscSettings?.address
|
||||
);
|
||||
|
||||
settings.vrcOsc = vrcOsc;
|
||||
sendRPCPacket(RpcMessage.ChangeSettingsRequest, settings);
|
||||
};
|
||||
|
||||
useRPCPacket(
|
||||
RpcMessage.SettingsResponse,
|
||||
(oldSettings: SettingsResponseT) => {
|
||||
fetchedSettings.current = oldSettings.vrcOsc?.oscSettings ?? null;
|
||||
|
||||
toggleVrc(true);
|
||||
}
|
||||
);
|
||||
|
||||
applyProgress(0.6);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full w-full mobile:pt-10">
|
||||
<div className="flex mobile:flex-col items-center xs:justify-center gap-5">
|
||||
<div className="mb-auto w-[512px] flex flex-col gap-2">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-usage-vr-standalone-title')}
|
||||
</Typography>
|
||||
<Typography color="secondary" whitespace="whitespace-pre-line">
|
||||
{l10n.getString('settings-osc-vrchat-description-guide')}
|
||||
</Typography>
|
||||
<div className="flex pt-2">
|
||||
<Button
|
||||
variant={!state.alonePage ? 'secondary' : 'tertiary'}
|
||||
className="self-start mt-auto"
|
||||
onClick={() => {
|
||||
toggleVrc(false);
|
||||
navigate('/onboarding/usage/vr/choose', {
|
||||
state: { alonePage: state.alonePage },
|
||||
});
|
||||
}}
|
||||
>
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
to=""
|
||||
className="ml-auto"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
>
|
||||
{l10n.getString('onboarding-usage-vr-standalone-next')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex gap-5 w-10/12 max-w-[600px] items-center'
|
||||
)}
|
||||
>
|
||||
<div className="rounded-lg overflow-hidden aspect-square">
|
||||
<PausableVideo src={VRCHAT_OSC_VIDEO} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
140
gui/src/components/onboarding/pages/usage-reason/UsageChoose.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { useOnboarding } from '@/hooks/onboarding';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Radio } from '@/components/commons/Radio';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import classNames from 'classnames';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
enum UsageReason {
|
||||
VR,
|
||||
VTUBING,
|
||||
MOCAP,
|
||||
}
|
||||
|
||||
interface UsageInfo {
|
||||
path: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
const REASON_TO_PATH: Record<UsageReason, UsageInfo> = {
|
||||
[UsageReason.MOCAP]: {
|
||||
path: '/onboarding/usage/mocap/head-choose',
|
||||
image: '/images/usage-mocap.webp',
|
||||
},
|
||||
[UsageReason.VR]: {
|
||||
path: '/onboarding/usage/vr/choose',
|
||||
image: '/images/usage-vr.webp',
|
||||
},
|
||||
[UsageReason.VTUBING]: {
|
||||
path: '/onboarding/usage/vtubing/choose',
|
||||
image: '/images/usage-vtuber.webp',
|
||||
},
|
||||
};
|
||||
|
||||
export function UsageChoose() {
|
||||
const { l10n } = useLocalization();
|
||||
const { applyProgress } = useOnboarding();
|
||||
const { control, watch } = useForm<{
|
||||
usageReason: UsageReason;
|
||||
}>({
|
||||
defaultValues: {
|
||||
usageReason: UsageReason.VR,
|
||||
},
|
||||
});
|
||||
|
||||
const usageReason = watch('usageReason');
|
||||
|
||||
const ItemContent = ({ mode }: { mode: UsageReason }) => (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex bg-background-60 py-2 px-4 group-hover/radio:bg-background-50 rounded-t-md'
|
||||
)}
|
||||
>
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-usage-choose-option-title', {
|
||||
mode: UsageReason[mode],
|
||||
})}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex flex-col bg-background-70 group-hover/radio:bg-background-60 rounded-b-md py-2 px-4">
|
||||
<Typography>
|
||||
{l10n.getString('onboarding-usage-choose-option-label', {
|
||||
mode: UsageReason[mode],
|
||||
})}
|
||||
</Typography>
|
||||
<Typography variant="standard" color="secondary">
|
||||
{l10n.getString('onboarding-usage-choose-option-description', {
|
||||
mode: UsageReason[mode],
|
||||
})}
|
||||
</Typography>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const usages = useMemo(
|
||||
() =>
|
||||
Object.values(UsageReason)
|
||||
.filter(checkIfUsageReason)
|
||||
.map((mode) => (
|
||||
<Radio
|
||||
key={mode}
|
||||
name="usageReason"
|
||||
control={control}
|
||||
value={mode.toString()}
|
||||
variant="none"
|
||||
className="hidden"
|
||||
>
|
||||
<div>
|
||||
<ItemContent mode={mode}></ItemContent>
|
||||
</div>
|
||||
</Radio>
|
||||
)),
|
||||
[control, l10n]
|
||||
);
|
||||
|
||||
applyProgress(0.5);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center">
|
||||
<div className="flex flex-col w-full overflow-y-auto px-4 xs:items-center">
|
||||
<div className="flex mobile:flex-col xs:gap-8 mobile:gap-4 mobile:pb-4 w-full justify-center">
|
||||
<div className="flex flex-col xs:max-w-sm gap-3 justify-center">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-usage-choose')}
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('onboarding-usage-choose-description')}
|
||||
</Typography>
|
||||
{usages}
|
||||
<div className="flex flex-row">
|
||||
<Button variant="secondary" to="/onboarding/assign-tutorial">
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
to={REASON_TO_PATH[usageReason].path}
|
||||
className="ml-auto"
|
||||
>
|
||||
{l10n.getString('onboarding-enter_vr-ready')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center">
|
||||
<img
|
||||
className="mobile:hidden rounded-3xl"
|
||||
src={REASON_TO_PATH[usageReason].image}
|
||||
width="496"
|
||||
></img>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function checkIfUsageReason(val: any): val is UsageReason {
|
||||
return typeof val === 'number';
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import { DOCS_SITE } from '@/App';
|
||||
import { A } from '@/components/commons/A';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { WarningBox } from '@/components/commons/TipBox';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { useOnboarding } from '@/hooks/onboarding';
|
||||
import { useStatusContext } from '@/hooks/status-system';
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import classNames from 'classnames';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { StatusData, StatusSteamVRDisconnectedT } from 'solarxr-protocol';
|
||||
|
||||
export function VRUsageChoose() {
|
||||
const { l10n } = useLocalization();
|
||||
const { applyProgress, state } = useOnboarding();
|
||||
const { statuses } = useStatusContext();
|
||||
const [animated, setAnimated] = useState(false);
|
||||
|
||||
const missingSteamVr = useMemo(
|
||||
() =>
|
||||
Object.values(statuses).some(
|
||||
(x) =>
|
||||
x.dataType === StatusData.StatusSteamVRDisconnected &&
|
||||
(x.data as StatusSteamVRDisconnectedT).bridgeSettingsName ===
|
||||
'steamvr'
|
||||
),
|
||||
[statuses]
|
||||
);
|
||||
|
||||
applyProgress(0.55);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center relative overflow-y-auto px-4 pb-4">
|
||||
<div className="flex flex-col gap-4 justify-center">
|
||||
<div className="xs:w-10/12 xs:max-w-[666px]">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-usage-vr-choose')}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="standard"
|
||||
color="secondary"
|
||||
whitespace="whitespace-pre-line"
|
||||
>
|
||||
{l10n.getString('onboarding-usage-vr-choose-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'grid xs:grid-cols-2 w-full xs:flex-row mobile:flex-col gap-4 [&>div]:grow'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-lg p-4 flex',
|
||||
!state.alonePage && 'bg-background-70',
|
||||
state.alonePage && 'bg-background-60'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<div>
|
||||
<Typography variant="main-title" bold>
|
||||
{l10n.getString('onboarding-usage-vr-choose-standalone')}
|
||||
</Typography>
|
||||
<Typography variant="vr-accessible" italic>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-vr-choose-standalone-label'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography
|
||||
color="secondary"
|
||||
whitespace="whitespace-pre-line"
|
||||
>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-vr-choose-standalone-description'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant={!state.alonePage ? 'secondary' : 'tertiary'}
|
||||
to={'/onboarding/usage/vr/standalone'}
|
||||
className="self-start mt-auto"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
>
|
||||
{l10n.getString('onboarding-usage-vr-choose-standalone')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-lg p-4 flex flex-row relative',
|
||||
!state.alonePage && 'bg-background-70',
|
||||
state.alonePage && 'bg-background-60'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<div>
|
||||
<img
|
||||
onMouseEnter={() => setAnimated(() => true)}
|
||||
onAnimationEnd={() => setAnimated(() => false)}
|
||||
src="/images/vrslimes.webp"
|
||||
className={classNames(
|
||||
'absolute w-[150px] -right-8 -top-16',
|
||||
animated && 'animate-[bounce_1s_1]'
|
||||
)}
|
||||
></img>
|
||||
<Typography variant="main-title" bold>
|
||||
{l10n.getString('onboarding-usage-vr-choose-steamvr')}
|
||||
</Typography>
|
||||
<Typography variant="vr-accessible" italic>
|
||||
{l10n.getString('onboarding-usage-vr-choose-steamvr-label')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Typography
|
||||
color="secondary"
|
||||
whitespace="whitespace-pre-line"
|
||||
>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-vr-choose-steamvr-description'
|
||||
)}
|
||||
</Typography>
|
||||
{
|
||||
// TODO: Add a button to open SteamVR via tauri's open()
|
||||
missingSteamVr && (
|
||||
<Localized
|
||||
id="onboarding-usage-vr-choose-steamvr-warning"
|
||||
elems={{
|
||||
docs: (
|
||||
<A
|
||||
href={`${DOCS_SITE}/common-issues.html#the-trackers-are-connected-to-the-slimevr-server-but-arent-turning-up-on-steam`}
|
||||
></A>
|
||||
),
|
||||
b: <b></b>,
|
||||
}}
|
||||
>
|
||||
<WarningBox>SteamVR driver not connected</WarningBox>
|
||||
</Localized>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant={'primary'}
|
||||
to="/onboarding/mounting/manual"
|
||||
className="self-start mt-auto"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
disabled={missingSteamVr}
|
||||
>
|
||||
{l10n.getString('onboarding-usage-vr-choose-steamvr')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="self-start"
|
||||
to="/onboarding/usage/choose"
|
||||
>
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -40,6 +40,10 @@ export function SettingSelectorMobile() {
|
||||
label: l10n.getString('settings-sidebar-serial'),
|
||||
value: { url: '/settings/serial' },
|
||||
},
|
||||
{
|
||||
label: l10n.getString('settings-sidebar-firmware-tool'),
|
||||
value: { url: '/settings/firmware-tool' },
|
||||
},
|
||||
{
|
||||
label: l10n.getString('settings-sidebar-advanced'),
|
||||
value: { url: '/settings/advanced' },
|
||||
@@ -99,7 +103,7 @@ export function SettingsLayout({ children }: { children: ReactNode }) {
|
||||
<div style={{ gridArea: 'n' }}>
|
||||
<Navbar></Navbar>
|
||||
</div>
|
||||
<div style={{ gridArea: 's' }} className="my-2">
|
||||
<div style={{ gridArea: 's' }} className="my-2 mobile:hidden">
|
||||
<SettingsSidebar></SettingsSidebar>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -100,6 +100,9 @@ export function SettingsSidebar() {
|
||||
<SettingsLink to="/settings/serial">
|
||||
{l10n.getString('settings-sidebar-serial')}
|
||||
</SettingsLink>
|
||||
<SettingsLink to="/settings/firmware-tool">
|
||||
{l10n.getString('settings-sidebar-firmware-tool')}
|
||||
</SettingsLink>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<SettingsLink to="/settings/advanced">
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from '@/components/settings/SettingsPageLayout';
|
||||
import { error } from '@/utils/logging';
|
||||
|
||||
interface VMCSettingsForm {
|
||||
export interface VMCSettingsForm {
|
||||
vmc: {
|
||||
oscSettings: {
|
||||
enabled: boolean;
|
||||
@@ -36,7 +36,7 @@ interface VMCSettingsForm {
|
||||
};
|
||||
}
|
||||
|
||||
const defaultValues = {
|
||||
export const DEFAULT_VMC_VALUES = {
|
||||
vmc: {
|
||||
oscSettings: {
|
||||
enabled: false,
|
||||
@@ -53,15 +53,9 @@ export function VMCSettings() {
|
||||
const { l10n } = useLocalization();
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
const [modelName, setModelName] = useState<string | null>(null);
|
||||
const [flashLoaded, setFlashLoaded] = useState(false);
|
||||
|
||||
const toggleFlash = (bool: boolean) => {
|
||||
setFlashLoaded(bool);
|
||||
setTimeout(() => setFlashLoaded(!bool), 1000);
|
||||
};
|
||||
|
||||
const { reset, control, watch, handleSubmit } = useForm<VMCSettingsForm>({
|
||||
defaultValues,
|
||||
defaultValues: DEFAULT_VMC_VALUES,
|
||||
});
|
||||
|
||||
const onSubmit = async (values: VMCSettingsForm) => {
|
||||
@@ -74,11 +68,17 @@ export function VMCSettings() {
|
||||
new OSCSettingsT(),
|
||||
values.vmc.oscSettings
|
||||
);
|
||||
if (values.vmc.vrmJson?.length) {
|
||||
vmcOsc.vrmJson = await parseVRMFile(values.vmc.vrmJson[0]);
|
||||
if (vmcOsc.vrmJson) {
|
||||
toggleFlash(true);
|
||||
setModelName(JSON.parse(vmcOsc.vrmJson).extensions.VRM.meta.title);
|
||||
if (values.vmc.vrmJson !== undefined) {
|
||||
if (values.vmc.vrmJson.length > 0) {
|
||||
vmcOsc.vrmJson = await parseVRMFile(values.vmc.vrmJson[0]);
|
||||
if (vmcOsc.vrmJson) {
|
||||
setModelName(
|
||||
JSON.parse(vmcOsc.vrmJson)?.extensions?.VRM?.meta?.title || ''
|
||||
);
|
||||
}
|
||||
} else {
|
||||
vmcOsc.vrmJson = '';
|
||||
setModelName(null);
|
||||
}
|
||||
}
|
||||
vmcOsc.anchorHip = values.vmc.anchorHip;
|
||||
@@ -99,7 +99,7 @@ export function VMCSettings() {
|
||||
}, []);
|
||||
|
||||
useRPCPacket(RpcMessage.SettingsResponse, (settings: SettingsResponseT) => {
|
||||
const formData: VMCSettingsForm = defaultValues;
|
||||
const formData: VMCSettingsForm = DEFAULT_VMC_VALUES;
|
||||
if (settings.vmcOsc) {
|
||||
if (settings.vmcOsc.oscSettings) {
|
||||
formData.vmc.oscSettings.enabled = settings.vmcOsc.oscSettings.enabled;
|
||||
@@ -114,7 +114,7 @@ export function VMCSettings() {
|
||||
}
|
||||
const vrmJson = settings.vmcOsc.vrmJson?.toString();
|
||||
if (vrmJson) {
|
||||
setModelName(JSON.parse(vrmJson).extensions.VRM.meta.title);
|
||||
setModelName(JSON.parse(vrmJson)?.extensions?.VRM?.meta?.title || '');
|
||||
}
|
||||
|
||||
formData.vmc.anchorHip = settings.vmcOsc.anchorHip;
|
||||
@@ -236,16 +236,6 @@ export function VMCSettings() {
|
||||
{l10n.getString('settings-osc-vmc-vrm-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color={flashLoaded ? 'primary' : 'secondary'}>
|
||||
{modelName === null
|
||||
? l10n.getString('settings-osc-vmc-vrm-model_unloaded')
|
||||
: l10n.getString('settings-osc-vmc-vrm-model_loaded', {
|
||||
name: modelName,
|
||||
titled: (!!modelName).toString(),
|
||||
})}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid gap-3 pb-5">
|
||||
<FileInput
|
||||
control={control}
|
||||
@@ -254,6 +244,12 @@ export function VMCSettings() {
|
||||
required: false,
|
||||
}}
|
||||
value="help"
|
||||
importedFileName={
|
||||
// if modelname is an empty string, it's an untitled model
|
||||
modelName === ''
|
||||
? l10n.getString('settings-osc-vmc-vrm-untitled_model')
|
||||
: modelName
|
||||
}
|
||||
label="settings-osc-vmc-vrm-file_select"
|
||||
accept="model/gltf-binary, model/gltf+json, model/vrml, .vrm, .glb, .gltf"
|
||||
></FileInput>
|
||||
@@ -303,7 +299,7 @@ export function VMCSettings() {
|
||||
const gltfHeaderStart = 0;
|
||||
const gltfHeaderEnd = 20;
|
||||
|
||||
async function parseVRMFile(vrm: File): Promise<string | null> {
|
||||
export async function parseVRMFile(vrm: File): Promise<string | null> {
|
||||
const headerView = new DataView(
|
||||
await vrm.slice(gltfHeaderStart, gltfHeaderEnd).arrayBuffer()
|
||||
);
|
||||
|
||||
@@ -131,8 +131,13 @@ export function VRCOSCSettings() {
|
||||
<div className="flex flex-col pt-2 pb-4">
|
||||
<>
|
||||
{l10n
|
||||
.getString('settings-osc-vrchat-description-v1')
|
||||
.getString('settings-osc-vrchat-description-v2')
|
||||
.split('\n')
|
||||
.concat(
|
||||
l10n
|
||||
.getString('settings-osc-vrchat-description-guide')
|
||||
.split('\n')
|
||||
)
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
|
||||
@@ -11,6 +11,10 @@ import { TrackerStatus } from './TrackerStatus';
|
||||
import classNames from 'classnames';
|
||||
import { useTracker } from '@/hooks/tracker';
|
||||
import { BodyPartIcon } from '@/components/commons/BodyPartIcon';
|
||||
import { DownloadIcon } from '@/components/commons/icon/DownloadIcon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAppContext } from '@/hooks/app';
|
||||
import { checkForUpdate } from '@/components/firmware-update/FirmwareUpdate';
|
||||
|
||||
function TrackerBig({
|
||||
device,
|
||||
@@ -41,6 +45,7 @@ function TrackerBig({
|
||||
<>
|
||||
{device.hardwareStatus.batteryPctEstimate && (
|
||||
<TrackerBattery
|
||||
voltage={device.hardwareStatus.batteryVoltage}
|
||||
value={device.hardwareStatus.batteryPctEstimate / 100}
|
||||
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
|
||||
/>
|
||||
@@ -89,6 +94,7 @@ function TrackerSmol({
|
||||
<div className="flex flex-col justify-center items-center">
|
||||
{device.hardwareStatus.batteryPctEstimate && (
|
||||
<TrackerBattery
|
||||
voltage={device.hardwareStatus.batteryVoltage}
|
||||
value={device.hardwareStatus.batteryPctEstimate / 100}
|
||||
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
|
||||
/>
|
||||
@@ -120,6 +126,7 @@ export function TrackerCard({
|
||||
bg = 'bg-background-60',
|
||||
shakeHighlight = true,
|
||||
warning = false,
|
||||
showUpdates = false,
|
||||
}: {
|
||||
tracker: TrackerDataT;
|
||||
device?: DeviceDataT;
|
||||
@@ -130,33 +137,51 @@ export function TrackerCard({
|
||||
shakeHighlight?: boolean;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
warning?: boolean;
|
||||
showUpdates?: boolean;
|
||||
}) {
|
||||
const { currentFirmwareRelease } = useAppContext();
|
||||
const { useVelocity } = useTracker(tracker);
|
||||
|
||||
const velocity = useVelocity();
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
'rounded-lg overflow-hidden',
|
||||
interactable && 'hover:bg-background-50 cursor-pointer',
|
||||
outlined && 'outline outline-2 outline-accent-background-40',
|
||||
warning && 'border-status-warning border-solid border-2',
|
||||
bg
|
||||
)}
|
||||
style={
|
||||
shakeHighlight
|
||||
? {
|
||||
boxShadow: `0px 0px ${Math.floor(velocity * 8)}px ${Math.floor(
|
||||
velocity * 8
|
||||
)}px rgb(var(--accent-background-30))`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{smol && <TrackerSmol tracker={tracker} device={device}></TrackerSmol>}
|
||||
{!smol && <TrackerBig tracker={tracker} device={device}></TrackerBig>}
|
||||
<div className="relative">
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
'rounded-lg overflow-hidden',
|
||||
interactable && 'hover:bg-background-50 cursor-pointer',
|
||||
outlined && 'outline outline-2 outline-accent-background-40',
|
||||
warning && 'border-status-warning border-solid border-2',
|
||||
bg
|
||||
)}
|
||||
style={
|
||||
shakeHighlight
|
||||
? {
|
||||
boxShadow: `0px 0px ${Math.floor(velocity * 8)}px ${Math.floor(
|
||||
velocity * 8
|
||||
)}px rgb(var(--accent-background-30))`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{smol && <TrackerSmol tracker={tracker} device={device}></TrackerSmol>}
|
||||
{!smol && <TrackerBig tracker={tracker} device={device}></TrackerBig>}
|
||||
</div>
|
||||
{showUpdates &&
|
||||
tracker.status !== TrackerStatusEnum.DISCONNECTED &&
|
||||
currentFirmwareRelease &&
|
||||
device?.hardwareInfo &&
|
||||
checkForUpdate(currentFirmwareRelease, device.hardwareInfo) && (
|
||||
<Link to="/firmware-update" className="absolute right-5 -top-2.5">
|
||||
<div className="relative">
|
||||
<div className="absolute rounded-full h-6 w-6 left-1 top-1 bg-accent-background-10 animate-[ping_2s_linear_infinite]"></div>
|
||||
<div className="absolute rounded-full h-8 w-8 hover:bg-background-40 hover:cursor-pointer bg-background-50 justify-center flex items-center">
|
||||
<DownloadIcon width={15}></DownloadIcon>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import classNames from 'classnames';
|
||||
import { IPv4 } from 'ip-num/IPNumber';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
@@ -6,11 +6,13 @@ import { useForm } from 'react-hook-form';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
AssignTrackerRequestT,
|
||||
BoardType,
|
||||
BodyPart,
|
||||
ForgetDeviceRequestT,
|
||||
ImuType,
|
||||
MagnetometerStatus,
|
||||
RpcMessage,
|
||||
TrackerDataType,
|
||||
} from 'solarxr-protocol';
|
||||
import { useDebouncedEffect } from '@/hooks/timeout';
|
||||
import { useTrackerFromId } from '@/hooks/tracker';
|
||||
@@ -35,6 +37,8 @@ import { TrackerCard } from './TrackerCard';
|
||||
import { Quaternion } from 'three';
|
||||
import { useAppContext } from '@/hooks/app';
|
||||
import { MagnetometerToggleSetting } from '@/components/settings/pages/MagnetometerToggleSetting';
|
||||
import semver from 'semver';
|
||||
import { checkForUpdate } from '@/components/firmware-update/FirmwareUpdate';
|
||||
|
||||
const rotationsLabels: [Quaternion, string][] = [
|
||||
[rotationToQuatMap.BACK, 'tracker-rotation-back'],
|
||||
@@ -148,6 +152,26 @@ export function TrackerSettingsPage() {
|
||||
}
|
||||
}, [firstLoad]);
|
||||
|
||||
const boardType = useMemo(() => {
|
||||
if (tracker?.device?.hardwareInfo?.officialBoardType) {
|
||||
return l10n.getString(
|
||||
'board_type-' +
|
||||
BoardType[
|
||||
tracker?.device?.hardwareInfo?.officialBoardType ??
|
||||
BoardType.UNKNOWN
|
||||
]
|
||||
);
|
||||
} else if (tracker?.device?.hardwareInfo?.boardType) {
|
||||
return tracker?.device?.hardwareInfo?.boardType;
|
||||
} else {
|
||||
return '--';
|
||||
}
|
||||
}, [
|
||||
tracker?.device?.hardwareInfo?.officialBoardType,
|
||||
tracker?.device?.hardwareInfo?.boardType,
|
||||
l10n,
|
||||
]);
|
||||
|
||||
const macAddress = useMemo(() => {
|
||||
if (
|
||||
/(?:[a-zA-Z\d]{2}:){5}[a-zA-Z\d]{2}/.test(
|
||||
@@ -160,6 +184,18 @@ export function TrackerSettingsPage() {
|
||||
return null;
|
||||
}, [tracker?.device?.hardwareInfo?.hardwareIdentifier]);
|
||||
|
||||
const { currentFirmwareRelease } = useAppContext();
|
||||
|
||||
const needUpdate =
|
||||
currentFirmwareRelease &&
|
||||
tracker?.device?.hardwareInfo &&
|
||||
checkForUpdate(currentFirmwareRelease, tracker?.device?.hardwareInfo);
|
||||
const updateUnavailable =
|
||||
tracker?.device?.hardwareInfo?.officialBoardType !== BoardType.SLIMEVR ||
|
||||
!semver.valid(
|
||||
tracker?.device?.hardwareInfo?.firmwareVersion?.toString() ?? 'none'
|
||||
);
|
||||
|
||||
return (
|
||||
<form
|
||||
className="h-full overflow-y-auto"
|
||||
@@ -187,21 +223,55 @@ export function TrackerSettingsPage() {
|
||||
shakeHighlight={false}
|
||||
></TrackerCard>
|
||||
)}
|
||||
{/* <div className="flex flex-col bg-background-70 p-3 rounded-lg gap-2">
|
||||
<Typography bold>Firmware version</Typography>
|
||||
<div className="flex gap-2">
|
||||
<Typography color="secondary">
|
||||
{tracker?.device?.hardwareInfo?.firmwareVersion}
|
||||
</Typography>
|
||||
<Typography color="secondary">-</Typography>
|
||||
<Typography color="text-accent-background-10">
|
||||
Up to date
|
||||
</Typography>
|
||||
{
|
||||
<div className="flex flex-col bg-background-70 p-3 rounded-lg gap-2">
|
||||
<Localized id="tracker-settings-update-title">
|
||||
<Typography variant="section-title">
|
||||
Firmware version
|
||||
</Typography>
|
||||
</Localized>
|
||||
<div className="flex gap-2">
|
||||
<Typography color="secondary">
|
||||
v{tracker?.device?.hardwareInfo?.firmwareVersion}
|
||||
</Typography>
|
||||
<Typography color="secondary">-</Typography>
|
||||
{updateUnavailable && (
|
||||
<Localized id="tracker-settings-update-unavailable">
|
||||
<Typography>Cannot be updated (DIY)</Typography>
|
||||
</Localized>
|
||||
)}
|
||||
{!updateUnavailable && (
|
||||
<>
|
||||
{!needUpdate && (
|
||||
<Localized id="tracker-settings-update-up_to_date">
|
||||
<Typography>Up to date</Typography>
|
||||
</Localized>
|
||||
)}
|
||||
{needUpdate && (
|
||||
<Localized
|
||||
id="tracker-settings-update-available"
|
||||
vars={{ versionName: currentFirmwareRelease?.name }}
|
||||
>
|
||||
<Typography color="text-accent-background-10">
|
||||
New version available
|
||||
</Typography>
|
||||
</Localized>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Localized id="tracker-settings-update">
|
||||
<Button
|
||||
variant={needUpdate ? 'primary' : 'secondary'}
|
||||
disabled={!needUpdate}
|
||||
to="/firmware-update"
|
||||
>
|
||||
Update now
|
||||
</Button>
|
||||
</Localized>
|
||||
</div>
|
||||
<Button variant="primary" disabled>
|
||||
Update now
|
||||
</Button>
|
||||
</div> */}
|
||||
}
|
||||
|
||||
<div className="flex flex-col bg-background-70 p-3 rounded-lg gap-2 overflow-x-auto">
|
||||
<div className="flex justify-between">
|
||||
<Typography color="secondary">
|
||||
@@ -236,22 +306,6 @@ export function TrackerSettingsPage() {
|
||||
).toString()}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('tracker-infos-version')}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{tracker?.device?.hardwareInfo?.firmwareVersion || '--'}
|
||||
</Typography>
|
||||
</div>
|
||||
{/* <div className="flex justify-between">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('tracker-infos-hardware_rev')}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{tracker?.device?.hardwareInfo?.hardwareRevision || '--'}
|
||||
</Typography>
|
||||
</div> */}
|
||||
<div className="flex justify-between">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('tracker-infos-hardware_identifier')}
|
||||
@@ -260,6 +314,16 @@ export function TrackerSettingsPage() {
|
||||
{tracker?.device?.hardwareInfo?.hardwareIdentifier || '--'}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('tracker-infos-data_support')}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{tracker?.tracker.info?.dataSupport
|
||||
? TrackerDataType[tracker?.tracker.info?.dataSupport]
|
||||
: '--'}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('tracker-infos-imu')}
|
||||
@@ -274,9 +338,7 @@ export function TrackerSettingsPage() {
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('tracker-infos-board_type')}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{tracker?.device?.hardwareInfo?.boardType || '--'}
|
||||
</Typography>
|
||||
<Typography>{boardType}</Typography>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<Typography color="secondary">
|
||||
|
||||
@@ -45,7 +45,8 @@ const displayColumns: { [k: string]: boolean } = {
|
||||
};
|
||||
|
||||
const isSlime = ({ device }: FlatDeviceTracker) =>
|
||||
device?.hardwareInfo?.manufacturer === 'SlimeVR';
|
||||
device?.hardwareInfo?.manufacturer === 'SlimeVR' ||
|
||||
device?.hardwareInfo?.manufacturer === 'HID Device';
|
||||
|
||||
const getDeviceName = ({ device }: FlatDeviceTracker) =>
|
||||
device?.customName?.toString() || '';
|
||||
|
||||
659
gui/src/firmware-tool-api/firmwareToolComponents.ts
Normal file
@@ -0,0 +1,659 @@
|
||||
/**
|
||||
* Generated by @openapi-codegen
|
||||
*
|
||||
* @version 0.0.1
|
||||
*/
|
||||
import * as reactQuery from '@tanstack/react-query';
|
||||
import { useFirmwareToolContext, FirmwareToolContext } from './firmwareToolContext';
|
||||
import type * as Fetcher from './firmwareToolFetcher';
|
||||
import { firmwareToolFetch } from './firmwareToolFetcher';
|
||||
import type * as Schemas from './firmwareToolSchemas';
|
||||
|
||||
export type GetIsCompatibleVersionPathParams = {
|
||||
version: string;
|
||||
};
|
||||
|
||||
export type GetIsCompatibleVersionError = Fetcher.ErrorWrapper<undefined>;
|
||||
|
||||
export type GetIsCompatibleVersionVariables = {
|
||||
pathParams: GetIsCompatibleVersionPathParams;
|
||||
} & FirmwareToolContext['fetcherOptions'];
|
||||
|
||||
/**
|
||||
* Is this api compatible with the server version given
|
||||
*/
|
||||
export const fetchGetIsCompatibleVersion = (
|
||||
variables: GetIsCompatibleVersionVariables,
|
||||
signal?: AbortSignal
|
||||
) =>
|
||||
firmwareToolFetch<
|
||||
Schemas.VerionCheckResponse,
|
||||
GetIsCompatibleVersionError,
|
||||
undefined,
|
||||
{},
|
||||
{},
|
||||
GetIsCompatibleVersionPathParams
|
||||
>({ url: '/is-compatible/{version}', method: 'get', ...variables, signal });
|
||||
|
||||
/**
|
||||
* Is this api compatible with the server version given
|
||||
*/
|
||||
export const useGetIsCompatibleVersion = <TData = Schemas.VerionCheckResponse>(
|
||||
variables: GetIsCompatibleVersionVariables,
|
||||
options?: Omit<
|
||||
reactQuery.UseQueryOptions<
|
||||
Schemas.VerionCheckResponse,
|
||||
GetIsCompatibleVersionError,
|
||||
TData
|
||||
>,
|
||||
'queryKey' | 'queryFn' | 'initialData'
|
||||
>
|
||||
) => {
|
||||
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
|
||||
return reactQuery.useQuery<
|
||||
Schemas.VerionCheckResponse,
|
||||
GetIsCompatibleVersionError,
|
||||
TData
|
||||
>({
|
||||
queryKey: queryKeyFn({
|
||||
path: '/is-compatible/{version}',
|
||||
operationId: 'getIsCompatibleVersion',
|
||||
variables,
|
||||
}),
|
||||
queryFn: ({ signal }) =>
|
||||
fetchGetIsCompatibleVersion({ ...fetcherOptions, ...variables }, signal),
|
||||
...options,
|
||||
...queryOptions,
|
||||
});
|
||||
};
|
||||
|
||||
export type GetFirmwaresError = Fetcher.ErrorWrapper<undefined>;
|
||||
|
||||
export type GetFirmwaresResponse = Schemas.FirmwareDTO[];
|
||||
|
||||
export type GetFirmwaresVariables = FirmwareToolContext['fetcherOptions'];
|
||||
|
||||
/**
|
||||
* List all the built firmwares
|
||||
*/
|
||||
export const fetchGetFirmwares = (
|
||||
variables: GetFirmwaresVariables,
|
||||
signal?: AbortSignal
|
||||
) =>
|
||||
firmwareToolFetch<GetFirmwaresResponse, GetFirmwaresError, undefined, {}, {}, {}>({
|
||||
url: '/firmwares',
|
||||
method: 'get',
|
||||
...variables,
|
||||
signal,
|
||||
});
|
||||
|
||||
/**
|
||||
* List all the built firmwares
|
||||
*/
|
||||
export const useGetFirmwares = <TData = GetFirmwaresResponse>(
|
||||
variables: GetFirmwaresVariables,
|
||||
options?: Omit<
|
||||
reactQuery.UseQueryOptions<GetFirmwaresResponse, GetFirmwaresError, TData>,
|
||||
'queryKey' | 'queryFn' | 'initialData'
|
||||
>
|
||||
) => {
|
||||
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
|
||||
return reactQuery.useQuery<GetFirmwaresResponse, GetFirmwaresError, TData>({
|
||||
queryKey: queryKeyFn({
|
||||
path: '/firmwares',
|
||||
operationId: 'getFirmwares',
|
||||
variables,
|
||||
}),
|
||||
queryFn: ({ signal }) =>
|
||||
fetchGetFirmwares({ ...fetcherOptions, ...variables }, signal),
|
||||
...options,
|
||||
...queryOptions,
|
||||
});
|
||||
};
|
||||
|
||||
export type PostFirmwaresBuildError = Fetcher.ErrorWrapper<{
|
||||
status: 400;
|
||||
payload: Schemas.VersionNotFoundExeption;
|
||||
}>;
|
||||
|
||||
export type PostFirmwaresBuildVariables = {
|
||||
body: Schemas.CreateBuildFirmwareDTO;
|
||||
} & FirmwareToolContext['fetcherOptions'];
|
||||
|
||||
/**
|
||||
* Build a firmware from the requested configuration
|
||||
*/
|
||||
export const fetchPostFirmwaresBuild = (
|
||||
variables: PostFirmwaresBuildVariables,
|
||||
signal?: AbortSignal
|
||||
) =>
|
||||
firmwareToolFetch<
|
||||
Schemas.BuildResponseDTO,
|
||||
PostFirmwaresBuildError,
|
||||
Schemas.CreateBuildFirmwareDTO,
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
>({ url: '/firmwares/build', method: 'post', ...variables, signal });
|
||||
|
||||
/**
|
||||
* Build a firmware from the requested configuration
|
||||
*/
|
||||
export const usePostFirmwaresBuild = (
|
||||
options?: Omit<
|
||||
reactQuery.UseMutationOptions<
|
||||
Schemas.BuildResponseDTO,
|
||||
PostFirmwaresBuildError,
|
||||
PostFirmwaresBuildVariables
|
||||
>,
|
||||
'mutationFn'
|
||||
>
|
||||
) => {
|
||||
const { fetcherOptions } = useFirmwareToolContext();
|
||||
return reactQuery.useMutation<
|
||||
Schemas.BuildResponseDTO,
|
||||
PostFirmwaresBuildError,
|
||||
PostFirmwaresBuildVariables
|
||||
>({
|
||||
mutationFn: (variables: PostFirmwaresBuildVariables) =>
|
||||
fetchPostFirmwaresBuild({ ...fetcherOptions, ...variables }),
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export type GetFirmwaresBuildStatusIdPathParams = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type GetFirmwaresBuildStatusIdError = Fetcher.ErrorWrapper<undefined>;
|
||||
|
||||
export type GetFirmwaresBuildStatusIdVariables = {
|
||||
pathParams: GetFirmwaresBuildStatusIdPathParams;
|
||||
} & FirmwareToolContext['fetcherOptions'];
|
||||
|
||||
/**
|
||||
* Get the build status of a firmware
|
||||
* This is a SSE (Server Sent Event)
|
||||
* you can use the web browser api to check for the build status and update the ui in real time
|
||||
*/
|
||||
export const fetchGetFirmwaresBuildStatusId = (
|
||||
variables: GetFirmwaresBuildStatusIdVariables,
|
||||
signal?: AbortSignal
|
||||
) =>
|
||||
firmwareToolFetch<
|
||||
Schemas.ObservableType,
|
||||
GetFirmwaresBuildStatusIdError,
|
||||
undefined,
|
||||
{},
|
||||
{},
|
||||
GetFirmwaresBuildStatusIdPathParams
|
||||
>({
|
||||
url: '/firmwares/build-status/{id}',
|
||||
method: 'get',
|
||||
...variables,
|
||||
signal,
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the build status of a firmware
|
||||
* This is a SSE (Server Sent Event)
|
||||
* you can use the web browser api to check for the build status and update the ui in real time
|
||||
*/
|
||||
export const useGetFirmwaresBuildStatusId = <TData = Schemas.ObservableType>(
|
||||
variables: GetFirmwaresBuildStatusIdVariables,
|
||||
options?: Omit<
|
||||
reactQuery.UseQueryOptions<
|
||||
Schemas.ObservableType,
|
||||
GetFirmwaresBuildStatusIdError,
|
||||
TData
|
||||
>,
|
||||
'queryKey' | 'queryFn' | 'initialData'
|
||||
>
|
||||
) => {
|
||||
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
|
||||
return reactQuery.useQuery<
|
||||
Schemas.ObservableType,
|
||||
GetFirmwaresBuildStatusIdError,
|
||||
TData
|
||||
>({
|
||||
queryKey: queryKeyFn({
|
||||
path: '/firmwares/build-status/{id}',
|
||||
operationId: 'getFirmwaresBuildStatusId',
|
||||
variables,
|
||||
}),
|
||||
queryFn: ({ signal }) =>
|
||||
fetchGetFirmwaresBuildStatusId({ ...fetcherOptions, ...variables }, signal),
|
||||
...options,
|
||||
...queryOptions,
|
||||
});
|
||||
};
|
||||
|
||||
export type GetFirmwaresBoardsError = Fetcher.ErrorWrapper<undefined>;
|
||||
|
||||
export type GetFirmwaresBoardsResponse = string[];
|
||||
|
||||
export type GetFirmwaresBoardsVariables = FirmwareToolContext['fetcherOptions'];
|
||||
|
||||
/**
|
||||
* List all the possible board types
|
||||
*/
|
||||
export const fetchGetFirmwaresBoards = (
|
||||
variables: GetFirmwaresBoardsVariables,
|
||||
signal?: AbortSignal
|
||||
) =>
|
||||
firmwareToolFetch<
|
||||
GetFirmwaresBoardsResponse,
|
||||
GetFirmwaresBoardsError,
|
||||
undefined,
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
>({ url: '/firmwares/boards', method: 'get', ...variables, signal });
|
||||
|
||||
/**
|
||||
* List all the possible board types
|
||||
*/
|
||||
export const useGetFirmwaresBoards = <TData = GetFirmwaresBoardsResponse>(
|
||||
variables: GetFirmwaresBoardsVariables,
|
||||
options?: Omit<
|
||||
reactQuery.UseQueryOptions<
|
||||
GetFirmwaresBoardsResponse,
|
||||
GetFirmwaresBoardsError,
|
||||
TData
|
||||
>,
|
||||
'queryKey' | 'queryFn' | 'initialData'
|
||||
>
|
||||
) => {
|
||||
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
|
||||
return reactQuery.useQuery<
|
||||
GetFirmwaresBoardsResponse,
|
||||
GetFirmwaresBoardsError,
|
||||
TData
|
||||
>({
|
||||
queryKey: queryKeyFn({
|
||||
path: '/firmwares/boards',
|
||||
operationId: 'getFirmwaresBoards',
|
||||
variables,
|
||||
}),
|
||||
queryFn: ({ signal }) =>
|
||||
fetchGetFirmwaresBoards({ ...fetcherOptions, ...variables }, signal),
|
||||
...options,
|
||||
...queryOptions,
|
||||
});
|
||||
};
|
||||
|
||||
export type GetFirmwaresVersionsError = Fetcher.ErrorWrapper<undefined>;
|
||||
|
||||
export type GetFirmwaresVersionsResponse = Schemas.ReleaseDTO[];
|
||||
|
||||
export type GetFirmwaresVersionsVariables = FirmwareToolContext['fetcherOptions'];
|
||||
|
||||
/**
|
||||
* List all the possible versions to build a firmware from
|
||||
*/
|
||||
export const fetchGetFirmwaresVersions = (
|
||||
variables: GetFirmwaresVersionsVariables,
|
||||
signal?: AbortSignal
|
||||
) =>
|
||||
firmwareToolFetch<
|
||||
GetFirmwaresVersionsResponse,
|
||||
GetFirmwaresVersionsError,
|
||||
undefined,
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
>({ url: '/firmwares/versions', method: 'get', ...variables, signal });
|
||||
|
||||
/**
|
||||
* List all the possible versions to build a firmware from
|
||||
*/
|
||||
export const useGetFirmwaresVersions = <TData = GetFirmwaresVersionsResponse>(
|
||||
variables: GetFirmwaresVersionsVariables,
|
||||
options?: Omit<
|
||||
reactQuery.UseQueryOptions<
|
||||
GetFirmwaresVersionsResponse,
|
||||
GetFirmwaresVersionsError,
|
||||
TData
|
||||
>,
|
||||
'queryKey' | 'queryFn' | 'initialData'
|
||||
>
|
||||
) => {
|
||||
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
|
||||
return reactQuery.useQuery<
|
||||
GetFirmwaresVersionsResponse,
|
||||
GetFirmwaresVersionsError,
|
||||
TData
|
||||
>({
|
||||
queryKey: queryKeyFn({
|
||||
path: '/firmwares/versions',
|
||||
operationId: 'getFirmwaresVersions',
|
||||
variables,
|
||||
}),
|
||||
queryFn: ({ signal }) =>
|
||||
fetchGetFirmwaresVersions({ ...fetcherOptions, ...variables }, signal),
|
||||
...options,
|
||||
...queryOptions,
|
||||
});
|
||||
};
|
||||
|
||||
export type GetFirmwaresImusError = Fetcher.ErrorWrapper<undefined>;
|
||||
|
||||
export type GetFirmwaresImusResponse = Schemas.Imudto[];
|
||||
|
||||
export type GetFirmwaresImusVariables = FirmwareToolContext['fetcherOptions'];
|
||||
|
||||
/**
|
||||
* List all the possible imus to use
|
||||
*/
|
||||
export const fetchGetFirmwaresImus = (
|
||||
variables: GetFirmwaresImusVariables,
|
||||
signal?: AbortSignal
|
||||
) =>
|
||||
firmwareToolFetch<
|
||||
GetFirmwaresImusResponse,
|
||||
GetFirmwaresImusError,
|
||||
undefined,
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
>({ url: '/firmwares/imus', method: 'get', ...variables, signal });
|
||||
|
||||
/**
|
||||
* List all the possible imus to use
|
||||
*/
|
||||
export const useGetFirmwaresImus = <TData = GetFirmwaresImusResponse>(
|
||||
variables: GetFirmwaresImusVariables,
|
||||
options?: Omit<
|
||||
reactQuery.UseQueryOptions<GetFirmwaresImusResponse, GetFirmwaresImusError, TData>,
|
||||
'queryKey' | 'queryFn' | 'initialData'
|
||||
>
|
||||
) => {
|
||||
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
|
||||
return reactQuery.useQuery<GetFirmwaresImusResponse, GetFirmwaresImusError, TData>({
|
||||
queryKey: queryKeyFn({
|
||||
path: '/firmwares/imus',
|
||||
operationId: 'getFirmwaresImus',
|
||||
variables,
|
||||
}),
|
||||
queryFn: ({ signal }) =>
|
||||
fetchGetFirmwaresImus({ ...fetcherOptions, ...variables }, signal),
|
||||
...options,
|
||||
...queryOptions,
|
||||
});
|
||||
};
|
||||
|
||||
export type GetFirmwaresBatteriesError = Fetcher.ErrorWrapper<undefined>;
|
||||
|
||||
export type GetFirmwaresBatteriesResponse = string[];
|
||||
|
||||
export type GetFirmwaresBatteriesVariables = FirmwareToolContext['fetcherOptions'];
|
||||
|
||||
/**
|
||||
* List all the battery types
|
||||
*/
|
||||
export const fetchGetFirmwaresBatteries = (
|
||||
variables: GetFirmwaresBatteriesVariables,
|
||||
signal?: AbortSignal
|
||||
) =>
|
||||
firmwareToolFetch<
|
||||
GetFirmwaresBatteriesResponse,
|
||||
GetFirmwaresBatteriesError,
|
||||
undefined,
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
>({ url: '/firmwares/batteries', method: 'get', ...variables, signal });
|
||||
|
||||
/**
|
||||
* List all the battery types
|
||||
*/
|
||||
export const useGetFirmwaresBatteries = <TData = GetFirmwaresBatteriesResponse>(
|
||||
variables: GetFirmwaresBatteriesVariables,
|
||||
options?: Omit<
|
||||
reactQuery.UseQueryOptions<
|
||||
GetFirmwaresBatteriesResponse,
|
||||
GetFirmwaresBatteriesError,
|
||||
TData
|
||||
>,
|
||||
'queryKey' | 'queryFn' | 'initialData'
|
||||
>
|
||||
) => {
|
||||
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
|
||||
return reactQuery.useQuery<
|
||||
GetFirmwaresBatteriesResponse,
|
||||
GetFirmwaresBatteriesError,
|
||||
TData
|
||||
>({
|
||||
queryKey: queryKeyFn({
|
||||
path: '/firmwares/batteries',
|
||||
operationId: 'getFirmwaresBatteries',
|
||||
variables,
|
||||
}),
|
||||
queryFn: ({ signal }) =>
|
||||
fetchGetFirmwaresBatteries({ ...fetcherOptions, ...variables }, signal),
|
||||
...options,
|
||||
...queryOptions,
|
||||
});
|
||||
};
|
||||
|
||||
export type GetFirmwaresDefaultConfigBoardPathParams = {
|
||||
board:
|
||||
| 'BOARD_SLIMEVR'
|
||||
| 'BOARD_NODEMCU'
|
||||
| 'BOARD_WROOM32'
|
||||
| 'BOARD_WEMOSD1MINI'
|
||||
| 'BOARD_TTGO_TBASE'
|
||||
| 'BOARD_ESP01'
|
||||
| 'BOARD_LOLIN_C3_MINI'
|
||||
| 'BOARD_BEETLE32C3'
|
||||
| 'BOARD_ES32C3DEVKITM1';
|
||||
};
|
||||
|
||||
export type GetFirmwaresDefaultConfigBoardError = Fetcher.ErrorWrapper<undefined>;
|
||||
|
||||
export type GetFirmwaresDefaultConfigBoardVariables = {
|
||||
pathParams: GetFirmwaresDefaultConfigBoardPathParams;
|
||||
} & FirmwareToolContext['fetcherOptions'];
|
||||
|
||||
/**
|
||||
* Gives the default pins / configuration of a given board
|
||||
*/
|
||||
export const fetchGetFirmwaresDefaultConfigBoard = (
|
||||
variables: GetFirmwaresDefaultConfigBoardVariables,
|
||||
signal?: AbortSignal
|
||||
) =>
|
||||
firmwareToolFetch<
|
||||
Schemas.DefaultBuildConfigDTO,
|
||||
GetFirmwaresDefaultConfigBoardError,
|
||||
undefined,
|
||||
{},
|
||||
{},
|
||||
GetFirmwaresDefaultConfigBoardPathParams
|
||||
>({
|
||||
url: '/firmwares/default-config/{board}',
|
||||
method: 'get',
|
||||
...variables,
|
||||
signal,
|
||||
});
|
||||
|
||||
/**
|
||||
* Gives the default pins / configuration of a given board
|
||||
*/
|
||||
export const useGetFirmwaresDefaultConfigBoard = <
|
||||
TData = Schemas.DefaultBuildConfigDTO,
|
||||
>(
|
||||
variables: GetFirmwaresDefaultConfigBoardVariables,
|
||||
options?: Omit<
|
||||
reactQuery.UseQueryOptions<
|
||||
Schemas.DefaultBuildConfigDTO,
|
||||
GetFirmwaresDefaultConfigBoardError,
|
||||
TData
|
||||
>,
|
||||
'queryKey' | 'queryFn' | 'initialData'
|
||||
>
|
||||
) => {
|
||||
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
|
||||
return reactQuery.useQuery<
|
||||
Schemas.DefaultBuildConfigDTO,
|
||||
GetFirmwaresDefaultConfigBoardError,
|
||||
TData
|
||||
>({
|
||||
queryKey: queryKeyFn({
|
||||
path: '/firmwares/default-config/{board}',
|
||||
operationId: 'getFirmwaresDefaultConfigBoard',
|
||||
variables,
|
||||
}),
|
||||
queryFn: ({ signal }) =>
|
||||
fetchGetFirmwaresDefaultConfigBoard({ ...fetcherOptions, ...variables }, signal),
|
||||
...options,
|
||||
...queryOptions,
|
||||
});
|
||||
};
|
||||
|
||||
export type GetFirmwaresIdPathParams = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type GetFirmwaresIdError = Fetcher.ErrorWrapper<{
|
||||
status: 404;
|
||||
payload: Schemas.HttpException;
|
||||
}>;
|
||||
|
||||
export type GetFirmwaresIdVariables = {
|
||||
pathParams: GetFirmwaresIdPathParams;
|
||||
} & FirmwareToolContext['fetcherOptions'];
|
||||
|
||||
/**
|
||||
* Get the inforamtions about a firmware from its id
|
||||
* also provide more informations than the simple list, like pins and imus and files
|
||||
*/
|
||||
export const fetchGetFirmwaresId = (
|
||||
variables: GetFirmwaresIdVariables,
|
||||
signal?: AbortSignal
|
||||
) =>
|
||||
firmwareToolFetch<
|
||||
Schemas.FirmwareDetailDTO,
|
||||
GetFirmwaresIdError,
|
||||
undefined,
|
||||
{},
|
||||
{},
|
||||
GetFirmwaresIdPathParams
|
||||
>({ url: '/firmwares/{id}', method: 'get', ...variables, signal });
|
||||
|
||||
/**
|
||||
* Get the inforamtions about a firmware from its id
|
||||
* also provide more informations than the simple list, like pins and imus and files
|
||||
*/
|
||||
export const useGetFirmwaresId = <TData = Schemas.FirmwareDetailDTO>(
|
||||
variables: GetFirmwaresIdVariables,
|
||||
options?: Omit<
|
||||
reactQuery.UseQueryOptions<Schemas.FirmwareDetailDTO, GetFirmwaresIdError, TData>,
|
||||
'queryKey' | 'queryFn' | 'initialData'
|
||||
>
|
||||
) => {
|
||||
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
|
||||
return reactQuery.useQuery<Schemas.FirmwareDetailDTO, GetFirmwaresIdError, TData>({
|
||||
queryKey: queryKeyFn({
|
||||
path: '/firmwares/{id}',
|
||||
operationId: 'getFirmwaresId',
|
||||
variables,
|
||||
}),
|
||||
queryFn: ({ signal }) =>
|
||||
fetchGetFirmwaresId({ ...fetcherOptions, ...variables }, signal),
|
||||
...options,
|
||||
...queryOptions,
|
||||
});
|
||||
};
|
||||
|
||||
export type GetHealthError = Fetcher.ErrorWrapper<undefined>;
|
||||
|
||||
export type GetHealthVariables = FirmwareToolContext['fetcherOptions'];
|
||||
|
||||
/**
|
||||
* Gives the status of the api
|
||||
* this endpoint will always return true
|
||||
*/
|
||||
export const fetchGetHealth = (variables: GetHealthVariables, signal?: AbortSignal) =>
|
||||
firmwareToolFetch<boolean, GetHealthError, undefined, {}, {}, {}>({
|
||||
url: '/health',
|
||||
method: 'get',
|
||||
...variables,
|
||||
signal,
|
||||
});
|
||||
|
||||
/**
|
||||
* Gives the status of the api
|
||||
* this endpoint will always return true
|
||||
*/
|
||||
export const useGetHealth = <TData = boolean>(
|
||||
variables: GetHealthVariables,
|
||||
options?: Omit<
|
||||
reactQuery.UseQueryOptions<boolean, GetHealthError, TData>,
|
||||
'queryKey' | 'queryFn' | 'initialData'
|
||||
>
|
||||
) => {
|
||||
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
|
||||
return reactQuery.useQuery<boolean, GetHealthError, TData>({
|
||||
queryKey: queryKeyFn({
|
||||
path: '/health',
|
||||
operationId: 'getHealth',
|
||||
variables,
|
||||
}),
|
||||
queryFn: ({ signal }) =>
|
||||
fetchGetHealth({ ...fetcherOptions, ...variables }, signal),
|
||||
...options,
|
||||
...queryOptions,
|
||||
});
|
||||
};
|
||||
|
||||
export type QueryOperation =
|
||||
| {
|
||||
path: '/is-compatible/{version}';
|
||||
operationId: 'getIsCompatibleVersion';
|
||||
variables: GetIsCompatibleVersionVariables;
|
||||
}
|
||||
| {
|
||||
path: '/firmwares';
|
||||
operationId: 'getFirmwares';
|
||||
variables: GetFirmwaresVariables;
|
||||
}
|
||||
| {
|
||||
path: '/firmwares/build-status/{id}';
|
||||
operationId: 'getFirmwaresBuildStatusId';
|
||||
variables: GetFirmwaresBuildStatusIdVariables;
|
||||
}
|
||||
| {
|
||||
path: '/firmwares/boards';
|
||||
operationId: 'getFirmwaresBoards';
|
||||
variables: GetFirmwaresBoardsVariables;
|
||||
}
|
||||
| {
|
||||
path: '/firmwares/versions';
|
||||
operationId: 'getFirmwaresVersions';
|
||||
variables: GetFirmwaresVersionsVariables;
|
||||
}
|
||||
| {
|
||||
path: '/firmwares/imus';
|
||||
operationId: 'getFirmwaresImus';
|
||||
variables: GetFirmwaresImusVariables;
|
||||
}
|
||||
| {
|
||||
path: '/firmwares/batteries';
|
||||
operationId: 'getFirmwaresBatteries';
|
||||
variables: GetFirmwaresBatteriesVariables;
|
||||
}
|
||||
| {
|
||||
path: '/firmwares/default-config/{board}';
|
||||
operationId: 'getFirmwaresDefaultConfigBoard';
|
||||
variables: GetFirmwaresDefaultConfigBoardVariables;
|
||||
}
|
||||
| {
|
||||
path: '/firmwares/{id}';
|
||||
operationId: 'getFirmwaresId';
|
||||
variables: GetFirmwaresIdVariables;
|
||||
}
|
||||
| {
|
||||
path: '/health';
|
||||
operationId: 'getHealth';
|
||||
variables: GetHealthVariables;
|
||||
};
|
||||
99
gui/src/firmware-tool-api/firmwareToolContext.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import type { QueryKey, UseQueryOptions } from '@tanstack/react-query';
|
||||
import { QueryOperation } from './firmwareToolComponents';
|
||||
|
||||
export type FirmwareToolContext = {
|
||||
fetcherOptions: {
|
||||
/**
|
||||
* Headers to inject in the fetcher
|
||||
*/
|
||||
headers?: {};
|
||||
/**
|
||||
* Query params to inject in the fetcher
|
||||
*/
|
||||
queryParams?: {};
|
||||
};
|
||||
queryOptions: {
|
||||
/**
|
||||
* Set this to `false` to disable automatic refetching when the query mounts or changes query keys.
|
||||
* Defaults to `true`.
|
||||
*/
|
||||
enabled?: boolean;
|
||||
};
|
||||
/**
|
||||
* Query key manager.
|
||||
*/
|
||||
queryKeyFn: (operation: QueryOperation) => QueryKey;
|
||||
};
|
||||
|
||||
/**
|
||||
* Context injected into every react-query hook wrappers
|
||||
*
|
||||
* @param queryOptions options from the useQuery wrapper
|
||||
*/
|
||||
export function useFirmwareToolContext<
|
||||
TQueryFnData = unknown,
|
||||
TError = unknown,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
_queryOptions?: Omit<
|
||||
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
'queryKey' | 'queryFn'
|
||||
>
|
||||
): FirmwareToolContext {
|
||||
return {
|
||||
fetcherOptions: {},
|
||||
queryOptions: {},
|
||||
queryKeyFn,
|
||||
};
|
||||
}
|
||||
|
||||
export const queryKeyFn = (operation: QueryOperation) => {
|
||||
const queryKey: unknown[] = hasPathParams(operation)
|
||||
? operation.path
|
||||
.split('/')
|
||||
.filter(Boolean)
|
||||
.map((i) => resolvePathParam(i, operation.variables.pathParams))
|
||||
: operation.path.split('/').filter(Boolean);
|
||||
|
||||
if (hasQueryParams(operation)) {
|
||||
queryKey.push(operation.variables.queryParams);
|
||||
}
|
||||
|
||||
if (hasBody(operation)) {
|
||||
queryKey.push(operation.variables.body);
|
||||
}
|
||||
|
||||
return queryKey;
|
||||
};
|
||||
// Helpers
|
||||
const resolvePathParam = (key: string, pathParams: Record<string, string>) => {
|
||||
if (key.startsWith('{') && key.endsWith('}')) {
|
||||
return pathParams[key.slice(1, -1)];
|
||||
}
|
||||
return key;
|
||||
};
|
||||
|
||||
const hasPathParams = (
|
||||
operation: QueryOperation
|
||||
): operation is QueryOperation & {
|
||||
variables: { pathParams: Record<string, string> };
|
||||
} => {
|
||||
return Boolean((operation.variables as any).pathParams);
|
||||
};
|
||||
|
||||
const hasBody = (
|
||||
operation: QueryOperation
|
||||
): operation is QueryOperation & {
|
||||
variables: { body: Record<string, unknown> };
|
||||
} => {
|
||||
return Boolean((operation.variables as any).body);
|
||||
};
|
||||
|
||||
const hasQueryParams = (
|
||||
operation: QueryOperation
|
||||
): operation is QueryOperation & {
|
||||
variables: { queryParams: Record<string, unknown> };
|
||||
} => {
|
||||
return Boolean((operation.variables as any).queryParams);
|
||||
};
|
||||
109
gui/src/firmware-tool-api/firmwareToolFetcher.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { FirmwareToolContext } from './firmwareToolContext';
|
||||
|
||||
export const firmwareToolBaseUrl =
|
||||
import.meta.env.VITE_FIRMWARE_TOOL_URL ?? 'http://localhost:3000';
|
||||
export const firmwareToolS3BaseUrl =
|
||||
import.meta.env.VITE_FIRMWARE_TOOL_S3_URL ?? 'http://localhost:9099';
|
||||
|
||||
export type ErrorWrapper<TError> = TError | { status: 'unknown'; payload: string };
|
||||
|
||||
export type FirmwareToolFetcherOptions<TBody, THeaders, TQueryParams, TPathParams> = {
|
||||
url: string;
|
||||
method: string;
|
||||
body?: TBody;
|
||||
headers?: THeaders;
|
||||
queryParams?: TQueryParams;
|
||||
pathParams?: TPathParams;
|
||||
signal?: AbortSignal;
|
||||
} & FirmwareToolContext['fetcherOptions'];
|
||||
|
||||
export async function firmwareToolFetch<
|
||||
TData,
|
||||
TError,
|
||||
TBody extends {} | FormData | undefined | null,
|
||||
THeaders extends {},
|
||||
TQueryParams extends {},
|
||||
TPathParams extends {},
|
||||
>({
|
||||
url,
|
||||
method,
|
||||
body,
|
||||
headers,
|
||||
pathParams,
|
||||
queryParams,
|
||||
signal,
|
||||
}: FirmwareToolFetcherOptions<
|
||||
TBody,
|
||||
THeaders,
|
||||
TQueryParams,
|
||||
TPathParams
|
||||
>): Promise<TData> {
|
||||
try {
|
||||
const requestHeaders: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
};
|
||||
|
||||
/**
|
||||
* As the fetch API is being used, when multipart/form-data is specified
|
||||
* the Content-Type header must be deleted so that the browser can set
|
||||
* the correct boundary.
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects#sending_files_using_a_formdata_object
|
||||
*/
|
||||
if (requestHeaders['Content-Type'].toLowerCase().includes('multipart/form-data')) {
|
||||
delete requestHeaders['Content-Type'];
|
||||
}
|
||||
|
||||
const response = await window.fetch(
|
||||
`${firmwareToolBaseUrl}${resolveUrl(url, queryParams, pathParams)}`,
|
||||
{
|
||||
signal,
|
||||
method: method.toUpperCase(),
|
||||
body: body
|
||||
? body instanceof FormData
|
||||
? body
|
||||
: JSON.stringify(body)
|
||||
: undefined,
|
||||
headers: requestHeaders,
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
let error: ErrorWrapper<TError>;
|
||||
try {
|
||||
error = await response.json();
|
||||
} catch (e) {
|
||||
error = {
|
||||
status: 'unknown' as const,
|
||||
payload:
|
||||
e instanceof Error ? `Unexpected error (${e.message})` : 'Unexpected error',
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (response.headers.get('content-type')?.includes('json')) {
|
||||
return await response.json();
|
||||
} else {
|
||||
// if it is not a json response, assume it is a blob and cast it to TData
|
||||
return (await response.blob()) as unknown as TData;
|
||||
}
|
||||
} catch (e) {
|
||||
let errorObject: Error = {
|
||||
name: 'unknown' as const,
|
||||
message: e instanceof Error ? `Network error (${e.message})` : 'Network error',
|
||||
stack: e as string,
|
||||
};
|
||||
throw errorObject;
|
||||
}
|
||||
}
|
||||
|
||||
const resolveUrl = (
|
||||
url: string,
|
||||
queryParams: Record<string, string> = {},
|
||||
pathParams: Record<string, string> = {}
|
||||
) => {
|
||||
let query = new URLSearchParams(queryParams).toString();
|
||||
if (query) query = `?${query}`;
|
||||
return url.replace(/\{\w*\}/g, (key) => pathParams[key.slice(1, -1)]) + query;
|
||||
};
|
||||
608
gui/src/firmware-tool-api/firmwareToolSchemas.ts
Normal file
@@ -0,0 +1,608 @@
|
||||
/**
|
||||
* Generated by @openapi-codegen
|
||||
*
|
||||
* @version 0.0.1
|
||||
*/
|
||||
export type VerionCheckResponse = {
|
||||
success: boolean;
|
||||
reason?: {
|
||||
message: string;
|
||||
versions: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Root object declaring a built firmware
|
||||
* this object contains:
|
||||
* - the status of the build
|
||||
* - the the repository and commit used as source
|
||||
*/
|
||||
export type FirmwareDTO = {
|
||||
/**
|
||||
* UUID of the firmware
|
||||
*
|
||||
* @format uuid
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Id of the firmware version used.
|
||||
* Usually the commit id of the source
|
||||
* used to build the firmware
|
||||
*/
|
||||
releaseId: string;
|
||||
/**
|
||||
* Current status of the build
|
||||
* this value will change during the build
|
||||
* process
|
||||
*
|
||||
* BUILDING -> DONE \\ the firmwrare is build and ready
|
||||
* -> FAILED \\ the build failled and will be garbage collected
|
||||
*/
|
||||
buildStatus:
|
||||
| 'CREATING_BUILD_FOLDER'
|
||||
| 'DOWNLOADING_FIRMWARE'
|
||||
| 'EXTRACTING_FIRMWARE'
|
||||
| 'SETTING_UP_DEFINES'
|
||||
| 'BUILDING'
|
||||
| 'SAVING'
|
||||
| 'DONE'
|
||||
| 'ERROR';
|
||||
/**
|
||||
* The repository and branch used as source of the firmware
|
||||
*/
|
||||
buildVersion: string;
|
||||
/**
|
||||
* The date of creation of this firmware build
|
||||
*
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type BuildResponseDTO = {
|
||||
/**
|
||||
* Id of the firmware
|
||||
*
|
||||
* @format uuid
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Build status of the firmware
|
||||
*/
|
||||
status:
|
||||
| 'CREATING_BUILD_FOLDER'
|
||||
| 'DOWNLOADING_FIRMWARE'
|
||||
| 'EXTRACTING_FIRMWARE'
|
||||
| 'SETTING_UP_DEFINES'
|
||||
| 'BUILDING'
|
||||
| 'SAVING'
|
||||
| 'DONE'
|
||||
| 'ERROR';
|
||||
/**
|
||||
* List of built firmware files, only set if the build succeeded
|
||||
*/
|
||||
firmwareFiles?: FirmwareFileDTO[];
|
||||
};
|
||||
|
||||
export type FirmwareFileDTO = {
|
||||
/**
|
||||
* Url to the file
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* Address of the partition
|
||||
*/
|
||||
offset: number;
|
||||
/**
|
||||
* Is this file the main firmware
|
||||
*/
|
||||
isFirmware: boolean;
|
||||
/**
|
||||
* Id of the linked firmware
|
||||
*
|
||||
* @format uuid
|
||||
*/
|
||||
firmwareId: string;
|
||||
};
|
||||
|
||||
export type CreateBuildFirmwareDTO = {
|
||||
/**
|
||||
* Repository of the firmware used
|
||||
*/
|
||||
version: string;
|
||||
/**
|
||||
* Board config, used to declare the pins used by the board
|
||||
*/
|
||||
boardConfig: CreateBoardConfigDTO;
|
||||
/**
|
||||
* Imu config, list of all the imus used and their pins
|
||||
*
|
||||
* @minItems 1
|
||||
*/
|
||||
imusConfig: CreateImuConfigDTO[];
|
||||
};
|
||||
|
||||
export type CreateBoardConfigDTO = {
|
||||
/**
|
||||
* Type of the board
|
||||
*/
|
||||
type:
|
||||
| 'BOARD_SLIMEVR'
|
||||
| 'BOARD_NODEMCU'
|
||||
| 'BOARD_WROOM32'
|
||||
| 'BOARD_WEMOSD1MINI'
|
||||
| 'BOARD_TTGO_TBASE'
|
||||
| 'BOARD_ESP01'
|
||||
| 'BOARD_LOLIN_C3_MINI'
|
||||
| 'BOARD_BEETLE32C3'
|
||||
| 'BOARD_ES32C3DEVKITM1';
|
||||
/**
|
||||
* Pin address of the indicator LED
|
||||
*/
|
||||
ledPin: string;
|
||||
/**
|
||||
* Is the indicator LED enabled
|
||||
*/
|
||||
enableLed: boolean;
|
||||
/**
|
||||
* Is the led inverted
|
||||
*/
|
||||
ledInverted: boolean;
|
||||
/**
|
||||
* Pin address of the battery indicator
|
||||
*/
|
||||
batteryPin: string;
|
||||
/**
|
||||
* Type of battery
|
||||
*/
|
||||
batteryType: 'BAT_EXTERNAL' | 'BAT_INTERNAL' | 'BAT_MCP3021' | 'BAT_INTERNAL_MCP3021';
|
||||
/**
|
||||
* Array of the different battery resistors, [indicator, SHIELD_R1, SHIELD_R2]
|
||||
*
|
||||
* @minItems 3
|
||||
* @maxItems 3
|
||||
*/
|
||||
batteryResistances: number[];
|
||||
};
|
||||
|
||||
export type CreateImuConfigDTO = {
|
||||
/**
|
||||
* Type of the imu
|
||||
*/
|
||||
type:
|
||||
| 'IMU_BNO085'
|
||||
| 'IMU_MPU9250'
|
||||
| 'IMU_MPU6500'
|
||||
| 'IMU_BNO080'
|
||||
| 'IMU_BNO055'
|
||||
| 'IMU_BNO086'
|
||||
| 'IMU_MPU6050'
|
||||
| 'IMU_BMI160'
|
||||
| 'IMU_ICM20948'
|
||||
| 'IMU_BMI270';
|
||||
/**
|
||||
* Pin address of the imu int pin
|
||||
* not all imus use it
|
||||
*/
|
||||
intPin: string | null;
|
||||
/**
|
||||
* Rotation of the imu in degrees
|
||||
*/
|
||||
rotation: number;
|
||||
/**
|
||||
* Pin address of the scl pin
|
||||
*/
|
||||
sclPin: string;
|
||||
/**
|
||||
* Pin address of the sda pin
|
||||
*/
|
||||
sdaPin: string;
|
||||
/**
|
||||
* Is this imu optionnal
|
||||
* Allows for extensions to be unplugged
|
||||
*/
|
||||
optional: boolean;
|
||||
};
|
||||
|
||||
export type VersionNotFoundExeption = {
|
||||
cause: void;
|
||||
name: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A representation of any set of values over any amount of time. This is the most basic building block
|
||||
* of RxJS.
|
||||
*/
|
||||
export type ObservableType = {
|
||||
/**
|
||||
* @deprecated true
|
||||
*/
|
||||
source?: Observableany;
|
||||
/**
|
||||
* @deprecated true
|
||||
*/
|
||||
operator?: OperatoranyType;
|
||||
};
|
||||
|
||||
/**
|
||||
* A representation of any set of values over any amount of time. This is the most basic building block
|
||||
* of RxJS.
|
||||
*/
|
||||
export type Observableany = {
|
||||
/**
|
||||
* @deprecated true
|
||||
*/
|
||||
source?: Observableany;
|
||||
/**
|
||||
* @deprecated true
|
||||
*/
|
||||
operator?: Operatoranyany;
|
||||
};
|
||||
|
||||
/**
|
||||
* *
|
||||
*/
|
||||
export type Operatoranyany = {};
|
||||
|
||||
/**
|
||||
* *
|
||||
*/
|
||||
export type OperatoranyType = {};
|
||||
|
||||
export type ReleaseDTO = {
|
||||
/**
|
||||
* id of the release, usually the commit id
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* url of the release
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* name of the release
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* url of the source archive
|
||||
*/
|
||||
zipball_url: string;
|
||||
/**
|
||||
* Is this release a pre release
|
||||
*/
|
||||
prerelease: boolean;
|
||||
/**
|
||||
* Is this release a draft
|
||||
*/
|
||||
draft: boolean;
|
||||
};
|
||||
|
||||
export type Imudto = {
|
||||
/**
|
||||
* Type of the imu
|
||||
*/
|
||||
type:
|
||||
| 'IMU_BNO085'
|
||||
| 'IMU_MPU9250'
|
||||
| 'IMU_MPU6500'
|
||||
| 'IMU_BNO080'
|
||||
| 'IMU_BNO055'
|
||||
| 'IMU_BNO086'
|
||||
| 'IMU_MPU6050'
|
||||
| 'IMU_BMI160'
|
||||
| 'IMU_ICM20948'
|
||||
| 'IMU_BMI270';
|
||||
/**
|
||||
* Does that imu type require a int pin
|
||||
*/
|
||||
hasIntPin: boolean;
|
||||
/**
|
||||
* First address of the imu
|
||||
*/
|
||||
imuStartAddress: number;
|
||||
/**
|
||||
* Increment of the address for each new imus
|
||||
*/
|
||||
addressIncrement: number;
|
||||
};
|
||||
|
||||
export type DefaultBuildConfigDTO = {
|
||||
/**
|
||||
* Default config of the selected board
|
||||
* contains all the default pins information about the selected board
|
||||
*/
|
||||
boardConfig: CreateBoardConfigDTO;
|
||||
/**
|
||||
* Inform the flashing utility that the user need to press the boot (or Flash) button
|
||||
* on the tracker
|
||||
*/
|
||||
needBootPress?: boolean;
|
||||
/**
|
||||
* Inform the flashing utility that the board will need a reboot after
|
||||
* being flashed
|
||||
*/
|
||||
needManualReboot?: boolean;
|
||||
/**
|
||||
* Will use the default values and skip the customisation options
|
||||
*/
|
||||
shouldOnlyUseDefaults?: boolean;
|
||||
/**
|
||||
* List of the possible imus pins, usually only two items will be sent
|
||||
*
|
||||
* @minItems 1
|
||||
*/
|
||||
imuDefaults: IMUDefaultDTO[];
|
||||
/**
|
||||
* Gives the offset of the firmare file in the eeprom. Used for flashing
|
||||
*/
|
||||
application_offset: number;
|
||||
};
|
||||
|
||||
export type IMUDefaultDTO = {
|
||||
/**
|
||||
* Type of the imu
|
||||
*/
|
||||
type?:
|
||||
| 'IMU_BNO085'
|
||||
| 'IMU_MPU9250'
|
||||
| 'IMU_MPU6500'
|
||||
| 'IMU_BNO080'
|
||||
| 'IMU_BNO055'
|
||||
| 'IMU_BNO086'
|
||||
| 'IMU_MPU6050'
|
||||
| 'IMU_BMI160'
|
||||
| 'IMU_ICM20948'
|
||||
| 'IMU_BMI270';
|
||||
/**
|
||||
* Pin address of the imu int pin
|
||||
* not all imus use it
|
||||
*/
|
||||
intPin: string | null;
|
||||
/**
|
||||
* Rotation of the imu in degrees
|
||||
*/
|
||||
rotation?: number;
|
||||
/**
|
||||
* Pin address of the scl pin
|
||||
*/
|
||||
sclPin: string;
|
||||
/**
|
||||
* Pin address of the sda pin
|
||||
*/
|
||||
sdaPin: string;
|
||||
/**
|
||||
* Is this imu optionnal
|
||||
* Allows for extensions to be unplugged
|
||||
*/
|
||||
optional: boolean;
|
||||
};
|
||||
|
||||
export type BoardConfigDTONullable = {
|
||||
/**
|
||||
* Unique id of the board config, used for relations
|
||||
*
|
||||
* @format uuid
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Type of the board
|
||||
*/
|
||||
type:
|
||||
| 'BOARD_SLIMEVR'
|
||||
| 'BOARD_NODEMCU'
|
||||
| 'BOARD_WROOM32'
|
||||
| 'BOARD_WEMOSD1MINI'
|
||||
| 'BOARD_TTGO_TBASE'
|
||||
| 'BOARD_ESP01'
|
||||
| 'BOARD_LOLIN_C3_MINI'
|
||||
| 'BOARD_BEETLE32C3'
|
||||
| 'BOARD_ES32C3DEVKITM1';
|
||||
/**
|
||||
* Pin address of the indicator LED
|
||||
*/
|
||||
ledPin: string;
|
||||
/**
|
||||
* Is the indicator LED enabled
|
||||
*/
|
||||
enableLed: boolean;
|
||||
/**
|
||||
* Is the led inverted
|
||||
*/
|
||||
ledInverted: boolean;
|
||||
/**
|
||||
* Pin address of the battery indicator
|
||||
*/
|
||||
batteryPin: string;
|
||||
/**
|
||||
* Type of battery
|
||||
*/
|
||||
batteryType: 'BAT_EXTERNAL' | 'BAT_INTERNAL' | 'BAT_MCP3021' | 'BAT_INTERNAL_MCP3021';
|
||||
/**
|
||||
* Array of the different battery resistors, [indicator, SHIELD_R1, SHIELD_R2]
|
||||
*
|
||||
* @minItems 3
|
||||
* @maxItems 3
|
||||
*/
|
||||
batteryResistances: number[];
|
||||
/**
|
||||
* Id of the linked firmware, used for relations
|
||||
*
|
||||
* @format uuid
|
||||
*/
|
||||
firmwareId: string;
|
||||
};
|
||||
|
||||
export type FirmwareDetailDTO = {
|
||||
/**
|
||||
* Pins informations about the board
|
||||
*/
|
||||
boardConfig: BoardConfigDTONullable;
|
||||
/**
|
||||
* List of the declared imus, and their pin configuration
|
||||
*
|
||||
* @minItems 1
|
||||
*/
|
||||
imusConfig: ImuConfigDTO[];
|
||||
/**
|
||||
* List of the built files / partitions with their url and offsets
|
||||
*/
|
||||
firmwareFiles: FirmwareFileDTO[];
|
||||
/**
|
||||
* UUID of the firmware
|
||||
*
|
||||
* @format uuid
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Id of the firmware version used.
|
||||
* Usually the commit id of the source
|
||||
* used to build the firmware
|
||||
*/
|
||||
releaseId: string;
|
||||
/**
|
||||
* Current status of the build
|
||||
* this value will change during the build
|
||||
* process
|
||||
*
|
||||
* BUILDING -> DONE \\ the firmwrare is build and ready
|
||||
* -> FAILED \\ the build failled and will be garbage collected
|
||||
*/
|
||||
buildStatus:
|
||||
| 'CREATING_BUILD_FOLDER'
|
||||
| 'DOWNLOADING_FIRMWARE'
|
||||
| 'EXTRACTING_FIRMWARE'
|
||||
| 'SETTING_UP_DEFINES'
|
||||
| 'BUILDING'
|
||||
| 'SAVING'
|
||||
| 'DONE'
|
||||
| 'ERROR';
|
||||
/**
|
||||
* The repository and branch used as source of the firmware
|
||||
*/
|
||||
buildVersion: string;
|
||||
/**
|
||||
* The date of creation of this firmware build
|
||||
*
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type BoardConfigDTO = {
|
||||
/**
|
||||
* Unique id of the board config, used for relations
|
||||
*
|
||||
* @format uuid
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Type of the board
|
||||
*/
|
||||
type:
|
||||
| 'BOARD_SLIMEVR'
|
||||
| 'BOARD_NODEMCU'
|
||||
| 'BOARD_WROOM32'
|
||||
| 'BOARD_WEMOSD1MINI'
|
||||
| 'BOARD_TTGO_TBASE'
|
||||
| 'BOARD_ESP01'
|
||||
| 'BOARD_LOLIN_C3_MINI'
|
||||
| 'BOARD_BEETLE32C3'
|
||||
| 'BOARD_ES32C3DEVKITM1';
|
||||
/**
|
||||
* Pin address of the indicator LED
|
||||
*/
|
||||
ledPin: string;
|
||||
/**
|
||||
* Is the indicator LED enabled
|
||||
*/
|
||||
enableLed: boolean;
|
||||
/**
|
||||
* Is the led inverted
|
||||
*/
|
||||
ledInverted: boolean;
|
||||
/**
|
||||
* Pin address of the battery indicator
|
||||
*/
|
||||
batteryPin: string;
|
||||
/**
|
||||
* Type of battery
|
||||
*/
|
||||
batteryType: 'BAT_EXTERNAL' | 'BAT_INTERNAL' | 'BAT_MCP3021' | 'BAT_INTERNAL_MCP3021';
|
||||
/**
|
||||
* Array of the different battery resistors, [indicator, SHIELD_R1, SHIELD_R2]
|
||||
*
|
||||
* @minItems 3
|
||||
* @maxItems 3
|
||||
*/
|
||||
batteryResistances: number[];
|
||||
/**
|
||||
* Id of the linked firmware, used for relations
|
||||
*
|
||||
* @format uuid
|
||||
*/
|
||||
firmwareId: string;
|
||||
};
|
||||
|
||||
export type ImuConfigDTO = {
|
||||
/**
|
||||
* Unique id of the config
|
||||
* this probably will never be shown to the user as it is moslty use for relations
|
||||
*
|
||||
* @format uuid
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Type of the imu
|
||||
*/
|
||||
type:
|
||||
| 'IMU_BNO085'
|
||||
| 'IMU_MPU9250'
|
||||
| 'IMU_MPU6500'
|
||||
| 'IMU_BNO080'
|
||||
| 'IMU_BNO055'
|
||||
| 'IMU_BNO086'
|
||||
| 'IMU_MPU6050'
|
||||
| 'IMU_BMI160'
|
||||
| 'IMU_ICM20948'
|
||||
| 'IMU_BMI270';
|
||||
/**
|
||||
* Rotation of the imu in degrees
|
||||
*/
|
||||
rotation: number;
|
||||
/**
|
||||
* Pin address of the imu int pin
|
||||
* not all imus use it
|
||||
*/
|
||||
intPin: string | null;
|
||||
/**
|
||||
* Pin address of the scl pin
|
||||
*/
|
||||
sclPin: string;
|
||||
/**
|
||||
* Pin address of the sda pin
|
||||
*/
|
||||
sdaPin: string;
|
||||
/**
|
||||
* Is this imu optionnal
|
||||
* Allows for extensions to be unplugged
|
||||
*/
|
||||
optional: boolean;
|
||||
/**
|
||||
* id of the linked firmware, used for relations
|
||||
*
|
||||
* @format uuid
|
||||
*/
|
||||
firmwareId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines the base Nest HTTP exception, which is handled by the default
|
||||
* Exceptions Handler.
|
||||
*/
|
||||
export type HttpException = {
|
||||
cause: void;
|
||||
name: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
};
|
||||
15
gui/src/firmware-tool-api/firmwareToolUtils.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
type ComputeRange<
|
||||
N extends number,
|
||||
Result extends Array<unknown> = [],
|
||||
> = Result['length'] extends N
|
||||
? Result
|
||||
: ComputeRange<N, [...Result, Result['length']]>;
|
||||
|
||||
export type ClientErrorStatus = Exclude<
|
||||
ComputeRange<500>[number],
|
||||
ComputeRange<400>[number]
|
||||
>;
|
||||
export type ServerErrorStatus = Exclude<
|
||||
ComputeRange<600>[number],
|
||||
ComputeRange<500>[number]
|
||||
>;
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
BoneT,
|
||||
@@ -23,6 +24,14 @@ import { useConfig } from './config';
|
||||
import { useDataFeedConfig } from './datafeed-config';
|
||||
import { useWebsocketAPI } from './websocket-api';
|
||||
import { error } from '@/utils/logging';
|
||||
import { cacheWrap } from './cache';
|
||||
|
||||
export interface FirmwareRelease {
|
||||
name: string;
|
||||
version: string;
|
||||
changelog: string;
|
||||
firmwareFile: string;
|
||||
}
|
||||
|
||||
export interface FlatDeviceTracker {
|
||||
device?: DeviceDataT;
|
||||
@@ -39,6 +48,7 @@ export interface AppState {
|
||||
}
|
||||
|
||||
export interface AppContext {
|
||||
currentFirmwareRelease: FirmwareRelease | null;
|
||||
state: AppState;
|
||||
trackers: FlatDeviceTracker[];
|
||||
dispatch: Dispatch<AppStateAction>;
|
||||
@@ -69,6 +79,8 @@ export function useProvideAppContext(): AppContext {
|
||||
datafeed: new DataFeedUpdateT(),
|
||||
ignoredTrackers: new Set(),
|
||||
});
|
||||
const [currentFirmwareRelease, setCurrentFirmwareRelease] =
|
||||
useState<FirmwareRelease | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected) {
|
||||
@@ -115,7 +127,55 @@ export function useProvideAppContext(): AppContext {
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCurrentFirmwareRelease = async () => {
|
||||
const releases: any[] | null = JSON.parse(
|
||||
await cacheWrap(
|
||||
'firmware-releases',
|
||||
() =>
|
||||
fetch('https://api.github.com/repos/SlimeVR/SlimeVR-Tracker-ESP/releases')
|
||||
.then((res) => res.text())
|
||||
.catch(() => 'null'),
|
||||
1000 * 60 * 60
|
||||
)
|
||||
);
|
||||
if (!releases) return null;
|
||||
|
||||
const firstRelease = releases.find(
|
||||
(release) =>
|
||||
release.prerelease === false &&
|
||||
release.assets &&
|
||||
release.assets.find(
|
||||
(asset: any) =>
|
||||
asset.name === 'BOARD_SLIMEVR-firmware.bin' && asset.browser_download_url
|
||||
)
|
||||
);
|
||||
|
||||
let version = firstRelease.tag_name;
|
||||
if (version.charAt(0) === 'v') {
|
||||
version = version.substring(1);
|
||||
}
|
||||
|
||||
if (firstRelease) {
|
||||
return {
|
||||
name: firstRelease.name,
|
||||
version,
|
||||
changelog: firstRelease.body,
|
||||
firmwareFile: firstRelease.assets.find(
|
||||
(asset: any) =>
|
||||
asset.name === 'BOARD_SLIMEVR-firmware.bin' && asset.browser_download_url
|
||||
).browser_download_url,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
fetchCurrentFirmwareRelease().then((res) => setCurrentFirmwareRelease(res));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
currentFirmwareRelease,
|
||||
state,
|
||||
trackers,
|
||||
dispatch,
|
||||
|
||||
@@ -3,9 +3,8 @@ import { useMediaQuery } from 'react-responsive';
|
||||
import tailwindConfig from '../../tailwind.config';
|
||||
|
||||
const fullConfig = resolveConfig(tailwindConfig as any);
|
||||
const breakpoints = tailwindConfig.theme.screens;
|
||||
|
||||
type BreakpointKey = keyof typeof breakpoints;
|
||||
type BreakpointKey = keyof typeof tailwindConfig.theme.screens;
|
||||
|
||||
export function useBreakpoint<K extends BreakpointKey>(breakpointKey: K) {
|
||||
// FIXME There is a flickering issue caused by this, because isMobile is not resolved fast enough
|
||||
|
||||
68
gui/src/hooks/cache.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { isTauri } from '@tauri-apps/api/core';
|
||||
import { createStore } from '@tauri-apps/plugin-store';
|
||||
|
||||
interface CrossStorage {
|
||||
set(key: string, value: string): Promise<void>;
|
||||
get(key: string): Promise<string | null>;
|
||||
delete(key: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
const localStore: CrossStorage = {
|
||||
get: async (key) => localStorage.getItem(`slimevr-cache/${key}`),
|
||||
set: async (key, value) => localStorage.setItem(`slimevr-cache/${key}`, value),
|
||||
delete: async (key) => {
|
||||
localStorage.removeItem(`slimevr-cache/${key}`);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
const store: CrossStorage = isTauri()
|
||||
? await createStore('gui-cache.dat', { autoSave: 100 as never })
|
||||
: localStore;
|
||||
|
||||
export async function cacheGet(key: string): Promise<string | null> {
|
||||
const itemStr = await store.get(key);
|
||||
|
||||
if (!itemStr) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const item = JSON.parse(itemStr);
|
||||
const now = new Date();
|
||||
|
||||
if (now.getTime() > item.expiry) {
|
||||
await store.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return item.value;
|
||||
}
|
||||
|
||||
export async function cacheSet(key: string, value: unknown, ttl: number | undefined) {
|
||||
const now = new Date();
|
||||
const item = {
|
||||
value,
|
||||
expiry: ttl ? now.getTime() + ttl : 0,
|
||||
};
|
||||
|
||||
await store.set(key, JSON.stringify(item));
|
||||
}
|
||||
|
||||
export async function cacheWrap(
|
||||
key: string,
|
||||
orDefault: () => Promise<string>,
|
||||
ttl: number | undefined
|
||||
) {
|
||||
const realItem = await store.get(key);
|
||||
if (!realItem) {
|
||||
const defaultItem = await orDefault();
|
||||
await cacheSet(key, defaultItem, ttl);
|
||||
return defaultItem;
|
||||
} else {
|
||||
return (await cacheGet(key))!;
|
||||
}
|
||||
}
|
||||
|
||||
export async function cacheDelete(key: string) {
|
||||
await store.delete(key);
|
||||
}
|
||||
252
gui/src/hooks/firmware-tool.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
import {
|
||||
fetchGetFirmwaresDefaultConfigBoard,
|
||||
useGetHealth,
|
||||
useGetIsCompatibleVersion,
|
||||
} from '@/firmware-tool-api/firmwareToolComponents';
|
||||
import {
|
||||
BuildResponseDTO,
|
||||
CreateBoardConfigDTO,
|
||||
CreateBuildFirmwareDTO,
|
||||
DefaultBuildConfigDTO,
|
||||
FirmwareFileDTO,
|
||||
} from '@/firmware-tool-api/firmwareToolSchemas';
|
||||
import { BoardPinsForm } from '@/components/firmware-tool/BoardPinsStep';
|
||||
import { DeepPartial } from 'react-hook-form';
|
||||
import {
|
||||
BoardType,
|
||||
DeviceIdT,
|
||||
FirmwarePartT,
|
||||
FirmwareUpdateMethod,
|
||||
FirmwareUpdateRequestT,
|
||||
FirmwareUpdateStatus,
|
||||
OTAFirmwareUpdateT,
|
||||
SerialDevicePortT,
|
||||
SerialFirmwareUpdateT,
|
||||
} from 'solarxr-protocol';
|
||||
import { OnboardingContext } from './onboarding';
|
||||
|
||||
export type PartialBuildFirmware = DeepPartial<CreateBuildFirmwareDTO>;
|
||||
export type FirmwareBuildStatus = BuildResponseDTO;
|
||||
export type SelectedDevice = {
|
||||
type: FirmwareUpdateMethod;
|
||||
deviceId: string | number;
|
||||
deviceNames: string[];
|
||||
};
|
||||
|
||||
export const boardTypeToFirmwareToolBoardType: Record<
|
||||
Exclude<
|
||||
BoardType,
|
||||
// This boards will not be handled by the firmware tool.
|
||||
// These are either impossible to compile automatically or deprecated
|
||||
BoardType.CUSTOM | BoardType.SLIMEVR_DEV | BoardType.SLIMEVR_LEGACY
|
||||
>,
|
||||
CreateBoardConfigDTO['type'] | null
|
||||
> = {
|
||||
[BoardType.UNKNOWN]: null,
|
||||
[BoardType.NODEMCU]: 'BOARD_NODEMCU',
|
||||
[BoardType.WROOM32]: 'BOARD_WROOM32',
|
||||
[BoardType.WEMOSD1MINI]: 'BOARD_WEMOSD1MINI',
|
||||
[BoardType.TTGO_TBASE]: 'BOARD_TTGO_TBASE',
|
||||
[BoardType.ESP01]: 'BOARD_ESP01',
|
||||
[BoardType.SLIMEVR]: 'BOARD_SLIMEVR',
|
||||
[BoardType.LOLIN_C3_MINI]: 'BOARD_LOLIN_C3_MINI',
|
||||
[BoardType.BEETLE32C3]: 'BOARD_BEETLE32C3',
|
||||
[BoardType.ES32C3DEVKITM1]: 'BOARD_ES32C3DEVKITM1',
|
||||
};
|
||||
|
||||
export const firmwareToolToBoardType: Record<CreateBoardConfigDTO['type'], BoardType> =
|
||||
Object.fromEntries(
|
||||
Object.entries(boardTypeToFirmwareToolBoardType).map((a) => a.reverse())
|
||||
);
|
||||
|
||||
export const firmwareUpdateErrorStatus = [
|
||||
FirmwareUpdateStatus.ERROR_AUTHENTICATION_FAILED,
|
||||
FirmwareUpdateStatus.ERROR_DEVICE_NOT_FOUND,
|
||||
FirmwareUpdateStatus.ERROR_DOWNLOAD_FAILED,
|
||||
FirmwareUpdateStatus.ERROR_PROVISIONING_FAILED,
|
||||
FirmwareUpdateStatus.ERROR_TIMEOUT,
|
||||
FirmwareUpdateStatus.ERROR_UNKNOWN,
|
||||
FirmwareUpdateStatus.ERROR_UNSUPPORTED_METHOD,
|
||||
FirmwareUpdateStatus.ERROR_UPLOAD_FAILED,
|
||||
];
|
||||
|
||||
export interface FirmwareToolContext {
|
||||
selectBoard: (boardType: CreateBoardConfigDTO['type']) => Promise<void>;
|
||||
selectVersion: (version: CreateBuildFirmwareDTO['version']) => void;
|
||||
updatePins: (form: BoardPinsForm) => void;
|
||||
updateImus: (imus: CreateBuildFirmwareDTO['imusConfig']) => void;
|
||||
setBuildStatus: (buildStatus: FirmwareBuildStatus) => void;
|
||||
selectDevices: (device: SelectedDevice[] | null) => void;
|
||||
retry: () => void;
|
||||
buildStatus: FirmwareBuildStatus;
|
||||
defaultConfig: DefaultBuildConfigDTO | null;
|
||||
newConfig: PartialBuildFirmware | null;
|
||||
selectedDevices: SelectedDevice[] | null;
|
||||
isStepLoading: boolean;
|
||||
isGlobalLoading: boolean;
|
||||
isCompatible: boolean;
|
||||
isError: boolean;
|
||||
}
|
||||
|
||||
export const FirmwareToolContextC = createContext<FirmwareToolContext>(
|
||||
undefined as any
|
||||
);
|
||||
|
||||
export function useFirmwareTool() {
|
||||
const context = useContext<FirmwareToolContext>(FirmwareToolContextC);
|
||||
if (!context) {
|
||||
throw new Error('useFirmwareTool must be within a FirmwareToolContext Provider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useFirmwareToolContext(): FirmwareToolContext {
|
||||
const [defaultConfig, setDefaultConfig] = useState<DefaultBuildConfigDTO | null>(
|
||||
null
|
||||
);
|
||||
const [selectedDevices, selectDevices] = useState<SelectedDevice[] | null>(null);
|
||||
const [newConfig, setNewConfig] = useState<PartialBuildFirmware>({});
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const { isError, isLoading: isInitialLoading, refetch } = useGetHealth({});
|
||||
const compatibilityCheckEnabled = !!__VERSION_TAG__;
|
||||
const { isLoading: isCompatibilityLoading, data: compatibilityData } =
|
||||
useGetIsCompatibleVersion(
|
||||
{ pathParams: { version: __VERSION_TAG__ } },
|
||||
{ enabled: compatibilityCheckEnabled }
|
||||
);
|
||||
const [buildStatus, setBuildStatus] = useState<FirmwareBuildStatus>({
|
||||
status: 'CREATING_BUILD_FOLDER',
|
||||
id: '',
|
||||
});
|
||||
|
||||
return {
|
||||
selectBoard: async (boardType: CreateBoardConfigDTO['type']) => {
|
||||
setLoading(true);
|
||||
const boardDefaults = await fetchGetFirmwaresDefaultConfigBoard({
|
||||
pathParams: { board: boardType },
|
||||
});
|
||||
setDefaultConfig(boardDefaults);
|
||||
if (boardDefaults.shouldOnlyUseDefaults) {
|
||||
setNewConfig((currConfig) => ({
|
||||
...currConfig,
|
||||
...boardDefaults,
|
||||
imusConfig: boardDefaults.imuDefaults,
|
||||
}));
|
||||
} else {
|
||||
setNewConfig((currConfig) => ({
|
||||
...currConfig,
|
||||
boardConfig: { ...currConfig.boardConfig, type: boardType },
|
||||
imusConfig: [],
|
||||
}));
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
updatePins: (form: BoardPinsForm) => {
|
||||
setNewConfig((currConfig) => {
|
||||
return {
|
||||
...currConfig,
|
||||
imusConfig: [...(currConfig?.imusConfig || [])],
|
||||
boardConfig: {
|
||||
...currConfig.boardConfig,
|
||||
...form,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
updateImus: (imus: CreateBuildFirmwareDTO['imusConfig']) => {
|
||||
setNewConfig((currConfig) => {
|
||||
return {
|
||||
...currConfig,
|
||||
imusConfig: imus.map(({ rotation, ...fields }) => ({
|
||||
...fields,
|
||||
rotation: Number(rotation),
|
||||
})), // Make sure that the rotation is handled as number
|
||||
};
|
||||
});
|
||||
},
|
||||
retry: async () => {
|
||||
setLoading(true);
|
||||
await refetch();
|
||||
setLoading(false);
|
||||
},
|
||||
selectVersion: (version: CreateBuildFirmwareDTO['version']) => {
|
||||
setNewConfig((currConfig) => ({ ...currConfig, version }));
|
||||
},
|
||||
setBuildStatus,
|
||||
selectDevices,
|
||||
selectedDevices,
|
||||
buildStatus,
|
||||
defaultConfig,
|
||||
newConfig,
|
||||
isStepLoading: isLoading,
|
||||
isGlobalLoading: isInitialLoading || isCompatibilityLoading,
|
||||
isCompatible: !compatibilityCheckEnabled || (compatibilityData?.success ?? false),
|
||||
isError: isError || (!compatibilityData?.success && compatibilityCheckEnabled),
|
||||
};
|
||||
}
|
||||
|
||||
export const getFlashingRequests = (
|
||||
devices: SelectedDevice[],
|
||||
firmwareFiles: FirmwareFileDTO[],
|
||||
onboardingState: OnboardingContext['state'],
|
||||
defaultConfig: DefaultBuildConfigDTO | null
|
||||
) => {
|
||||
const firmware = firmwareFiles.find(({ isFirmware }) => isFirmware);
|
||||
if (!firmware) throw new Error('invalid state - no firmware to find');
|
||||
|
||||
const requests = [];
|
||||
|
||||
for (const device of devices) {
|
||||
switch (device.type) {
|
||||
case FirmwareUpdateMethod.OTAFirmwareUpdate: {
|
||||
const dId = new DeviceIdT();
|
||||
dId.id = +device.deviceId;
|
||||
|
||||
const part = new FirmwarePartT();
|
||||
part.offset = 0;
|
||||
part.url = firmware.url;
|
||||
|
||||
const method = new OTAFirmwareUpdateT();
|
||||
method.deviceId = dId;
|
||||
method.firmwarePart = part;
|
||||
|
||||
const req = new FirmwareUpdateRequestT();
|
||||
req.method = method;
|
||||
req.methodType = FirmwareUpdateMethod.OTAFirmwareUpdate;
|
||||
requests.push(req);
|
||||
break;
|
||||
}
|
||||
case FirmwareUpdateMethod.SerialFirmwareUpdate: {
|
||||
const id = new SerialDevicePortT();
|
||||
id.port = device.deviceId.toString();
|
||||
|
||||
if (!onboardingState.wifi?.ssid || !onboardingState.wifi?.password)
|
||||
throw new Error('invalid state, wifi should be set');
|
||||
|
||||
const method = new SerialFirmwareUpdateT();
|
||||
method.deviceId = id;
|
||||
method.ssid = onboardingState.wifi.ssid;
|
||||
method.password = onboardingState.wifi.password;
|
||||
method.needManualReboot = defaultConfig?.needManualReboot ?? false;
|
||||
|
||||
method.firmwarePart = firmwareFiles.map(({ offset, url }) => {
|
||||
const part = new FirmwarePartT();
|
||||
part.offset = offset;
|
||||
part.url = url;
|
||||
return part;
|
||||
});
|
||||
|
||||
const req = new FirmwareUpdateRequestT();
|
||||
req.method = method;
|
||||
req.methodType = FirmwareUpdateMethod.SerialFirmwareUpdate;
|
||||
requests.push(req);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error('unsupported flashing method');
|
||||
}
|
||||
}
|
||||
}
|
||||
return requests;
|
||||
};
|
||||
@@ -16,7 +16,7 @@ interface OnboardingState {
|
||||
export interface OnboardingContext {
|
||||
state: OnboardingState;
|
||||
applyProgress: (value: number) => void;
|
||||
setWifiCredentials: (ssid: string, password: string) => void;
|
||||
setWifiCredentials: (ssid: string, password?: string) => void;
|
||||
skipSetup: () => void;
|
||||
}
|
||||
|
||||
@@ -68,8 +68,8 @@ export function useProvideOnboarding(): OnboardingContext {
|
||||
dispatch({ type: 'progress', value });
|
||||
}, []);
|
||||
},
|
||||
setWifiCredentials: (ssid: string, password: string) => {
|
||||
dispatch({ type: 'wifi-creds', ssid, password });
|
||||
setWifiCredentials: (ssid: string, password?: string) => {
|
||||
dispatch({ type: 'wifi-creds', ssid, password: password ?? '' });
|
||||
},
|
||||
skipSetup: () => {
|
||||
setConfig({ doneOnboarding: true });
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { BodyPart, TrackerDataT, TrackerStatus } from 'solarxr-protocol';
|
||||
import { BodyPart, TrackerDataT, TrackerInfoT, TrackerStatus } from 'solarxr-protocol';
|
||||
import { QuaternionFromQuatT, QuaternionToEulerDegrees } from '@/maths/quaternion';
|
||||
import { useAppContext } from './app';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { ReactLocalization, useLocalization } from '@fluent/react';
|
||||
import { useDataFeedConfig } from './datafeed-config';
|
||||
import { Quaternion, Vector3 } from 'three';
|
||||
import { Vector3FromVec3fT } from '@/maths/vector3';
|
||||
@@ -36,18 +36,19 @@ export function useTrackers() {
|
||||
};
|
||||
}
|
||||
|
||||
export function getTrackerName(l10n: ReactLocalization, info: TrackerInfoT | null) {
|
||||
if (info?.customName) return info?.customName;
|
||||
if (info?.bodyPart) return l10n.getString('body_part-' + BodyPart[info?.bodyPart]);
|
||||
return info?.displayName || 'NONE';
|
||||
}
|
||||
|
||||
export function useTracker(tracker: TrackerDataT) {
|
||||
const { l10n } = useLocalization();
|
||||
const { feedMaxTps } = useDataFeedConfig();
|
||||
|
||||
return {
|
||||
useName: () =>
|
||||
useMemo(() => {
|
||||
if (tracker.info?.customName) return tracker.info?.customName;
|
||||
if (tracker.info?.bodyPart)
|
||||
return l10n.getString('body_part-' + BodyPart[tracker.info?.bodyPart]);
|
||||
return tracker.info?.displayName || 'NONE';
|
||||
}, [tracker.info]),
|
||||
useMemo(() => getTrackerName(l10n, tracker.info), [tracker.info, l10n]),
|
||||
useRawRotationEulerDegrees: () =>
|
||||
useMemo(() => QuaternionToEulerDegrees(tracker?.rotation), [tracker.rotation]),
|
||||
useRefAdjRotationEulerDegrees: () =>
|
||||
|
||||
@@ -17,6 +17,12 @@ body {
|
||||
}
|
||||
|
||||
@media (-webkit-animation) {
|
||||
img.uncrisp {
|
||||
// Webkit moment https://stackoverflow.com/questions/7908168/image-resize-gives-slight-brief-pixelation-in-webkit-browsers
|
||||
// doesn't happen in newer versions, weird.
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-name), 'Noto Sans CJK', sans-serif, 'Twemoji Webkit',
|
||||
emoji;
|
||||
@@ -84,7 +90,7 @@ body {
|
||||
}
|
||||
|
||||
:root {
|
||||
overflow: hidden;
|
||||
// overflow: hidden; -- NEVER EVER BRING THIS BACK <3
|
||||
background: theme('colors.background.20');
|
||||
|
||||
--navbar-w: 101px;
|
||||
@@ -382,6 +388,14 @@ body {
|
||||
background: theme('colors.background.60');
|
||||
}
|
||||
|
||||
.bg-background-60::-webkit-scrollbar-thumb:hover {
|
||||
background: theme('colors.background.40');
|
||||
}
|
||||
|
||||
.bg-background-60 {
|
||||
scrollbar-color: theme('colors.background.50') transparent;
|
||||
}
|
||||
|
||||
.dropdown-scroll {
|
||||
scrollbar-color: theme('colors.background.40') theme('colors.background.50');
|
||||
}
|
||||
|
||||
@@ -223,6 +223,37 @@ export class BoneKind extends Bone {
|
||||
case BodyPart.LEFT_HIP:
|
||||
case BodyPart.RIGHT_HIP:
|
||||
return new Color('pink');
|
||||
case BodyPart.LEFT_THUMB_METACARPAL:
|
||||
case BodyPart.LEFT_THUMB_PROXIMAL:
|
||||
case BodyPart.LEFT_THUMB_DISTAL:
|
||||
case BodyPart.LEFT_INDEX_PROXIMAL:
|
||||
case BodyPart.LEFT_INDEX_INTERMEDIATE:
|
||||
case BodyPart.LEFT_INDEX_DISTAL:
|
||||
case BodyPart.LEFT_MIDDLE_PROXIMAL:
|
||||
case BodyPart.LEFT_MIDDLE_INTERMEDIATE:
|
||||
case BodyPart.LEFT_MIDDLE_DISTAL:
|
||||
case BodyPart.LEFT_RING_PROXIMAL:
|
||||
case BodyPart.LEFT_RING_INTERMEDIATE:
|
||||
case BodyPart.LEFT_RING_DISTAL:
|
||||
case BodyPart.LEFT_LITTLE_PROXIMAL:
|
||||
case BodyPart.LEFT_LITTLE_INTERMEDIATE:
|
||||
case BodyPart.LEFT_LITTLE_DISTAL:
|
||||
case BodyPart.RIGHT_THUMB_METACARPAL:
|
||||
case BodyPart.RIGHT_THUMB_PROXIMAL:
|
||||
case BodyPart.RIGHT_THUMB_DISTAL:
|
||||
case BodyPart.RIGHT_INDEX_PROXIMAL:
|
||||
case BodyPart.RIGHT_INDEX_INTERMEDIATE:
|
||||
case BodyPart.RIGHT_INDEX_DISTAL:
|
||||
case BodyPart.RIGHT_MIDDLE_PROXIMAL:
|
||||
case BodyPart.RIGHT_MIDDLE_INTERMEDIATE:
|
||||
case BodyPart.RIGHT_MIDDLE_DISTAL:
|
||||
case BodyPart.RIGHT_RING_PROXIMAL:
|
||||
case BodyPart.RIGHT_RING_INTERMEDIATE:
|
||||
case BodyPart.RIGHT_RING_DISTAL:
|
||||
case BodyPart.RIGHT_LITTLE_PROXIMAL:
|
||||
case BodyPart.RIGHT_LITTLE_INTERMEDIATE:
|
||||
case BodyPart.RIGHT_LITTLE_DISTAL:
|
||||
return new Color('pink');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,8 +306,81 @@ export class BoneKind extends Bone {
|
||||
case BodyPart.RIGHT_LOWER_ARM:
|
||||
return [BodyPart.RIGHT_HAND];
|
||||
case BodyPart.LEFT_HAND:
|
||||
return [];
|
||||
return [
|
||||
BodyPart.LEFT_THUMB_METACARPAL,
|
||||
BodyPart.LEFT_INDEX_PROXIMAL,
|
||||
BodyPart.LEFT_MIDDLE_PROXIMAL,
|
||||
BodyPart.LEFT_RING_PROXIMAL,
|
||||
BodyPart.LEFT_LITTLE_PROXIMAL,
|
||||
];
|
||||
case BodyPart.RIGHT_HAND:
|
||||
return [
|
||||
BodyPart.RIGHT_THUMB_METACARPAL,
|
||||
BodyPart.RIGHT_INDEX_PROXIMAL,
|
||||
BodyPart.RIGHT_MIDDLE_PROXIMAL,
|
||||
BodyPart.RIGHT_RING_PROXIMAL,
|
||||
BodyPart.RIGHT_LITTLE_PROXIMAL,
|
||||
];
|
||||
|
||||
case BodyPart.LEFT_THUMB_METACARPAL:
|
||||
return [BodyPart.LEFT_THUMB_PROXIMAL];
|
||||
case BodyPart.LEFT_THUMB_PROXIMAL:
|
||||
return [BodyPart.LEFT_THUMB_DISTAL];
|
||||
case BodyPart.LEFT_THUMB_DISTAL:
|
||||
return [];
|
||||
case BodyPart.LEFT_INDEX_PROXIMAL:
|
||||
return [BodyPart.LEFT_INDEX_INTERMEDIATE];
|
||||
case BodyPart.LEFT_INDEX_INTERMEDIATE:
|
||||
return [BodyPart.LEFT_INDEX_DISTAL];
|
||||
case BodyPart.LEFT_INDEX_DISTAL:
|
||||
return [];
|
||||
case BodyPart.LEFT_MIDDLE_PROXIMAL:
|
||||
return [BodyPart.LEFT_MIDDLE_INTERMEDIATE];
|
||||
case BodyPart.LEFT_MIDDLE_INTERMEDIATE:
|
||||
return [BodyPart.LEFT_MIDDLE_DISTAL];
|
||||
case BodyPart.LEFT_MIDDLE_DISTAL:
|
||||
return [];
|
||||
case BodyPart.LEFT_RING_PROXIMAL:
|
||||
return [BodyPart.LEFT_RING_INTERMEDIATE];
|
||||
case BodyPart.LEFT_RING_INTERMEDIATE:
|
||||
return [BodyPart.LEFT_RING_DISTAL];
|
||||
case BodyPart.LEFT_RING_DISTAL:
|
||||
return [];
|
||||
case BodyPart.LEFT_LITTLE_PROXIMAL:
|
||||
return [BodyPart.LEFT_LITTLE_INTERMEDIATE];
|
||||
case BodyPart.LEFT_LITTLE_INTERMEDIATE:
|
||||
return [BodyPart.LEFT_LITTLE_DISTAL];
|
||||
case BodyPart.LEFT_LITTLE_DISTAL:
|
||||
return [];
|
||||
case BodyPart.RIGHT_THUMB_METACARPAL:
|
||||
return [BodyPart.RIGHT_THUMB_PROXIMAL];
|
||||
case BodyPart.RIGHT_THUMB_PROXIMAL:
|
||||
return [BodyPart.RIGHT_THUMB_DISTAL];
|
||||
case BodyPart.RIGHT_THUMB_DISTAL:
|
||||
return [];
|
||||
case BodyPart.RIGHT_INDEX_PROXIMAL:
|
||||
return [BodyPart.RIGHT_INDEX_INTERMEDIATE];
|
||||
case BodyPart.RIGHT_INDEX_INTERMEDIATE:
|
||||
return [BodyPart.RIGHT_INDEX_DISTAL];
|
||||
case BodyPart.RIGHT_INDEX_DISTAL:
|
||||
return [];
|
||||
case BodyPart.RIGHT_MIDDLE_PROXIMAL:
|
||||
return [BodyPart.RIGHT_MIDDLE_INTERMEDIATE];
|
||||
case BodyPart.RIGHT_MIDDLE_INTERMEDIATE:
|
||||
return [BodyPart.RIGHT_MIDDLE_DISTAL];
|
||||
case BodyPart.RIGHT_MIDDLE_DISTAL:
|
||||
return [];
|
||||
case BodyPart.RIGHT_RING_PROXIMAL:
|
||||
return [BodyPart.RIGHT_RING_INTERMEDIATE];
|
||||
case BodyPart.RIGHT_RING_INTERMEDIATE:
|
||||
return [BodyPart.RIGHT_RING_DISTAL];
|
||||
case BodyPart.RIGHT_RING_DISTAL:
|
||||
return [];
|
||||
case BodyPart.RIGHT_LITTLE_PROXIMAL:
|
||||
return [BodyPart.RIGHT_LITTLE_INTERMEDIATE];
|
||||
case BodyPart.RIGHT_LITTLE_INTERMEDIATE:
|
||||
return [BodyPart.RIGHT_LITTLE_DISTAL];
|
||||
case BodyPart.RIGHT_LITTLE_DISTAL:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -329,6 +433,67 @@ export class BoneKind extends Bone {
|
||||
return BodyPart.LEFT_LOWER_ARM;
|
||||
case BodyPart.RIGHT_HAND:
|
||||
return BodyPart.RIGHT_LOWER_ARM;
|
||||
|
||||
case BodyPart.LEFT_THUMB_METACARPAL:
|
||||
return BodyPart.LEFT_HAND;
|
||||
case BodyPart.LEFT_THUMB_PROXIMAL:
|
||||
return BodyPart.LEFT_THUMB_METACARPAL;
|
||||
case BodyPart.LEFT_THUMB_DISTAL:
|
||||
return BodyPart.LEFT_THUMB_PROXIMAL;
|
||||
case BodyPart.LEFT_INDEX_PROXIMAL:
|
||||
return BodyPart.LEFT_HAND;
|
||||
case BodyPart.LEFT_INDEX_INTERMEDIATE:
|
||||
return BodyPart.LEFT_INDEX_PROXIMAL;
|
||||
case BodyPart.LEFT_INDEX_DISTAL:
|
||||
return BodyPart.LEFT_INDEX_INTERMEDIATE;
|
||||
case BodyPart.LEFT_MIDDLE_PROXIMAL:
|
||||
return BodyPart.LEFT_HAND;
|
||||
case BodyPart.LEFT_MIDDLE_INTERMEDIATE:
|
||||
return BodyPart.LEFT_MIDDLE_PROXIMAL;
|
||||
case BodyPart.LEFT_MIDDLE_DISTAL:
|
||||
return BodyPart.LEFT_MIDDLE_INTERMEDIATE;
|
||||
case BodyPart.LEFT_RING_PROXIMAL:
|
||||
return BodyPart.LEFT_HAND;
|
||||
case BodyPart.LEFT_RING_INTERMEDIATE:
|
||||
return BodyPart.LEFT_RING_PROXIMAL;
|
||||
case BodyPart.LEFT_RING_DISTAL:
|
||||
return BodyPart.LEFT_RING_INTERMEDIATE;
|
||||
case BodyPart.LEFT_LITTLE_PROXIMAL:
|
||||
return BodyPart.LEFT_HAND;
|
||||
case BodyPart.LEFT_LITTLE_INTERMEDIATE:
|
||||
return BodyPart.LEFT_LITTLE_PROXIMAL;
|
||||
case BodyPart.LEFT_LITTLE_DISTAL:
|
||||
return BodyPart.LEFT_LITTLE_INTERMEDIATE;
|
||||
case BodyPart.RIGHT_THUMB_METACARPAL:
|
||||
return BodyPart.RIGHT_HAND;
|
||||
case BodyPart.RIGHT_THUMB_PROXIMAL:
|
||||
return BodyPart.RIGHT_THUMB_METACARPAL;
|
||||
case BodyPart.RIGHT_THUMB_DISTAL:
|
||||
return BodyPart.RIGHT_THUMB_PROXIMAL;
|
||||
case BodyPart.RIGHT_INDEX_PROXIMAL:
|
||||
return BodyPart.RIGHT_HAND;
|
||||
case BodyPart.RIGHT_INDEX_INTERMEDIATE:
|
||||
return BodyPart.RIGHT_INDEX_PROXIMAL;
|
||||
case BodyPart.RIGHT_INDEX_DISTAL:
|
||||
return BodyPart.RIGHT_INDEX_INTERMEDIATE;
|
||||
case BodyPart.RIGHT_MIDDLE_PROXIMAL:
|
||||
return BodyPart.RIGHT_HAND;
|
||||
case BodyPart.RIGHT_MIDDLE_INTERMEDIATE:
|
||||
return BodyPart.RIGHT_MIDDLE_PROXIMAL;
|
||||
case BodyPart.RIGHT_MIDDLE_DISTAL:
|
||||
return BodyPart.RIGHT_MIDDLE_INTERMEDIATE;
|
||||
case BodyPart.RIGHT_RING_PROXIMAL:
|
||||
return BodyPart.RIGHT_HAND;
|
||||
case BodyPart.RIGHT_RING_INTERMEDIATE:
|
||||
return BodyPart.RIGHT_RING_PROXIMAL;
|
||||
case BodyPart.RIGHT_RING_DISTAL:
|
||||
return BodyPart.RIGHT_RING_INTERMEDIATE;
|
||||
case BodyPart.RIGHT_LITTLE_PROXIMAL:
|
||||
return BodyPart.RIGHT_HAND;
|
||||
case BodyPart.RIGHT_LITTLE_INTERMEDIATE:
|
||||
return BodyPart.RIGHT_LITTLE_PROXIMAL;
|
||||
case BodyPart.RIGHT_LITTLE_DISTAL:
|
||||
return BodyPart.RIGHT_LITTLE_INTERMEDIATE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export async function fetchResourceUrl(url: string) {
|
||||
// FIXME: For some fucking reason, you can't top-level await on a react component file
|
||||
// on Chromium on developments builds specifically -Uriel
|
||||
export const AUTOBONE_VIDEO = await fetchResourceUrl('/videos/autobone.webm');
|
||||
export const VRCHAT_OSC_VIDEO = await fetchResourceUrl('/videos/vrchatosc.webm');
|
||||
|
||||
export const isTrayAvailable =
|
||||
isTauri() && (await invoke<boolean>('is_tray_available'));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import plugin from 'tailwindcss/plugin';
|
||||
import forms from '@tailwindcss/forms';
|
||||
import typography from '@tailwindcss/typography';
|
||||
import gradient from 'tailwind-gradient-mask-image';
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
@@ -150,7 +151,7 @@ const colors = {
|
||||
700: '#b3b3b3',
|
||||
900: '#d8d8d8',
|
||||
},
|
||||
'asexual': {
|
||||
asexual: {
|
||||
100: '#000000',
|
||||
200: '#A3A3A3',
|
||||
300: '#FFFFFF',
|
||||
@@ -162,9 +163,11 @@ const config = {
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||
theme: {
|
||||
screens: {
|
||||
'mobile-settings': { raw: 'not (min-width: 900px)' },
|
||||
nsmol: { raw: 'not (min-width: 525px)' },
|
||||
smol: '525px',
|
||||
mobile: { raw: 'not (min-width: 800px)' },
|
||||
'xs-settings': '900px',
|
||||
xs: '800px',
|
||||
nsm: { raw: 'not (min-width: 900px)' },
|
||||
sm: '900px',
|
||||
@@ -245,6 +248,7 @@ const config = {
|
||||
plugins: [
|
||||
forms,
|
||||
gradient,
|
||||
typography,
|
||||
plugin(function ({ addUtilities }) {
|
||||
const textConfig = (fontSize: any, fontWeight: any) => ({
|
||||
fontSize,
|
||||
|
||||