mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
New look (#10)
* WIP * Current progress for erimel * basic components + Typography * Current progress + formating * Settings ok * Small Tracker cards + progress on onboarding * git attributes * Fix lf * change some settings naming stuff (#8) * change some settings naming stuff * rename legs label to feet * Body proportions begin * Serial console + Tracker settings * interactions on cards * Body proportions adjust * Standalone body proportions + some responsive changes * Dev mode + uniform ui * Serial layout fix * Remove old code / cleaning * make dev table interactable * Autobone Co-authored-by: Erimel <marioluigivideo@gmail.com>
This commit is contained in:
40
.eslintrc.json
Normal file
40
.eslintrc.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"jest": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["react", "react-hooks", "@typescript-eslint", "prettier"],
|
||||
"rules": {
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/prop-types": "off",
|
||||
"spaced-comment": "error",
|
||||
"quotes": ["error", "single"],
|
||||
"no-duplicate-imports": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"react/no-unescaped-entities": "off",
|
||||
"prettier/prettier": "warn"
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"typescript": {}
|
||||
},
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
4
.husky/pre-commit
Normal file
4
.husky/pre-commit
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx pretty-quick --staged
|
||||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true
|
||||
}
|
||||
258
Cargo.lock
generated
258
Cargo.lock
generated
@@ -153,9 +153,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.10.0"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c53dfa917ec274df8ed3c572698f381a24eef2efba9492d797301b72b6db408a"
|
||||
checksum = "a5377c8865e74a160d21f29c2d40669f53286db6eab59b88540cbb12ffc8b835"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
@@ -165,9 +165,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
|
||||
checksum = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e"
|
||||
|
||||
[[package]]
|
||||
name = "cairo-rs"
|
||||
@@ -252,9 +252,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.2.8"
|
||||
version = "3.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83"
|
||||
checksum = "44bbe24bbd31a185bc2c4f7c2abe80bea13a20d57ee4e55be70ac512bdc76417"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
@@ -279,9 +279,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "3.2.7"
|
||||
version = "3.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902"
|
||||
checksum = "9ba52acd3b0a5c33aeada5cdaa3267cdc7c594a98731d4268cdc1532f4264cb4"
|
||||
dependencies = [
|
||||
"heck 0.4.0",
|
||||
"proc-macro-error",
|
||||
@@ -413,9 +413,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.5"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c"
|
||||
checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
@@ -423,9 +423,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.10"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83"
|
||||
checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -433,9 +433,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.4"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5999502d32b9c48d492abe66392408144895020ec4709e549e840799f3bb74c0"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
@@ -470,9 +470,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.1.22"
|
||||
version = "0.1.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c"
|
||||
checksum = "cdffe87e1d521a10f9696f833fe502293ea446d7f256c06128293a4119bdf4cb"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
@@ -624,9 +624,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
|
||||
checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
|
||||
dependencies = [
|
||||
"instant",
|
||||
]
|
||||
@@ -891,15 +891,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1d9279ca822891c1a4dae06d185612cf8fc6acfe5dff37781b41297811b12ee"
|
||||
checksum = "cc184cace1cea8335047a471cc1da80f18acf8a76f3bab2028d499e328948ec7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"log",
|
||||
"rustversion",
|
||||
"winapi",
|
||||
"windows 0.32.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1096,9 +1096,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.1"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
@@ -1211,14 +1211,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.24.2"
|
||||
version = "0.24.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28edd9d7bc256be2502e325ac0628bde30b7001b9b52e0abe31a1a9dc2701212"
|
||||
checksum = "7e30ca2ecf7666107ff827a8e481de6a132a9b687ed3bb20bb1c144a36c00964"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"color_quant",
|
||||
"num-iter",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
]
|
||||
@@ -1373,6 +1372,15 @@ dependencies = [
|
||||
"safemem",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "line-wrap"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9"
|
||||
dependencies = [
|
||||
"safemem",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.7"
|
||||
@@ -1626,9 +1634,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.12.0"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
|
||||
checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
@@ -1642,9 +1650,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.1.0"
|
||||
version = "6.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa"
|
||||
checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4"
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
@@ -1677,17 +1685,6 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
|
||||
dependencies = [
|
||||
"instant",
|
||||
"lock_api",
|
||||
"parking_lot_core 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.1"
|
||||
@@ -1695,21 +1692,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core 0.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"instant",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"winapi",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1964,9 +1947,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.40"
|
||||
version = "1.0.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7"
|
||||
checksum = "c278e965f1d8cf32d6e0e96de3d3e79712178ae67986d9cf9151f51e95aac89b"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -2078,9 +2061,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.13"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
|
||||
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
@@ -2098,9 +2081,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.5.6"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
|
||||
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -2118,9 +2101,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.26"
|
||||
version = "0.6.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
|
||||
checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
|
||||
|
||||
[[package]]
|
||||
name = "remove_dir_all"
|
||||
@@ -2151,9 +2134,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.7"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0a5f7c728f5d284929a1cccb5bc19884422bfe6ef4d6c409da2c41838983fcf"
|
||||
checksum = "24c8ad4f0c00e1eb5bc7614d236a7f1300e3dbd76b68cac8e06fb00b015ad8d8"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
@@ -2237,18 +2220,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.138"
|
||||
version = "1.0.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1578c6245786b9d168c5447eeacfb96856573ca56c9d68fdcf394be134882a47"
|
||||
checksum = "fc855a42c7967b7c369eb5860f7164ef1f6f81c20c7cc1141f2a604e18723b03"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.138"
|
||||
version = "1.0.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "023e9b1467aef8a10fb88f25611870ada9800ef7e22afce356bb0d2387b6f27c"
|
||||
checksum = "6f2122636b9fe3b81f1cb25099fcf2d3f542cdb1d45940d56c713158884a05da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2369,9 +2352,12 @@ checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.6"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
|
||||
checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slimevr_ui"
|
||||
@@ -2444,7 +2430,7 @@ checksum = "213494b7a2b503146286049378ce02b482200519accc31872ee8be91fa820a08"
|
||||
dependencies = [
|
||||
"new_debug_unreachable",
|
||||
"once_cell",
|
||||
"parking_lot 0.12.1",
|
||||
"parking_lot",
|
||||
"phf_shared 0.10.0",
|
||||
"precomputed-hash",
|
||||
"serde",
|
||||
@@ -2507,9 +2493,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tao"
|
||||
version = "0.12.1"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a71c32c2fa7bba46b01becf9cf470f6a781573af7e376c5e317a313ecce27545"
|
||||
checksum = "f6fd7725dc1e593e9ecabd9fe49c112a204c8c8694db4182e78b2a5af490b1ae"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cairo-rs",
|
||||
@@ -2538,15 +2524,15 @@ dependencies = [
|
||||
"ndk-sys",
|
||||
"objc",
|
||||
"once_cell",
|
||||
"parking_lot 0.11.2",
|
||||
"parking_lot",
|
||||
"paste",
|
||||
"png 0.17.5",
|
||||
"raw-window-handle",
|
||||
"scopeguard",
|
||||
"serde",
|
||||
"unicode-segmentation",
|
||||
"uuid 0.8.2",
|
||||
"windows",
|
||||
"uuid 1.1.2",
|
||||
"windows 0.37.0",
|
||||
"windows-implement",
|
||||
"x11-dl",
|
||||
]
|
||||
@@ -2564,9 +2550,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "1.0.2"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "421641ec549d34935530886151a42ce5ecbbb57beb30e5eec1b22f8e08e10ee9"
|
||||
checksum = "e1a56a8b125069c2682bd31610109b4436c050c74447bee1078217a0325c1add"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2608,18 +2594,19 @@ dependencies = [
|
||||
"uuid 1.1.2",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.37.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "1.0.2"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "598bd36884ee15ac73dfca9921066fd87d13d9beea60384b99a66c3a5d800d70"
|
||||
checksum = "acafb1c515c5d14234a294461bd43c723639a84891a45f6a250fd3441ad2e8ed"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
"heck 0.4.0",
|
||||
"json-patch",
|
||||
"semver 1.0.12",
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
@@ -2628,13 +2615,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "1.0.2"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "048a7b404b92c86e7dc32458fd0963f042a76d520681e6f598d73a97c2feeeef"
|
||||
checksum = "16d62a3c8790d6cba686cea6e3f7f569d12c662c3274c2d165a4fd33e3871b72"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"brotli",
|
||||
"ico",
|
||||
"json-patch",
|
||||
"plist",
|
||||
"png 0.17.5",
|
||||
"proc-macro2",
|
||||
@@ -2653,9 +2641,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "1.0.2"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aaf70098bfab21efde9b2c089008b319ba333f4ee6e55c38bdea188dea86497f"
|
||||
checksum = "7296fa17996629f43081e1c66d554703900187ed900c5bf46f97f0bcfb069278"
|
||||
dependencies = [
|
||||
"heck 0.4.0",
|
||||
"proc-macro2",
|
||||
@@ -2667,9 +2655,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "0.10.1"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82d34f58c61a6790ba3de5753daea61b5beb6926b2384d1ad03b9dfe622c72be"
|
||||
checksum = "4e4cff3b4d9469727fa2107c4b3d2eda110df1ba45103fb420178e536362fae4"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
@@ -2682,14 +2670,14 @@ dependencies = [
|
||||
"thiserror",
|
||||
"uuid 1.1.2",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.37.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "0.10.1"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd9a56e25146ff1f13f37bdb010ed0d692e7e81c824b9f977ae439f446f37ab4"
|
||||
checksum = "3fa8c4edaf01d8b556e7172c844b1b4dd3399adcd1a606bd520fc3e65f698546"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"gtk",
|
||||
@@ -2701,15 +2689,15 @@ dependencies = [
|
||||
"uuid 1.1.2",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.37.0",
|
||||
"wry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "616f178da1e0466ca45963ed108a1567d4b8803662addaca313169d0dcd97715"
|
||||
checksum = "12ff4b68d9faeb57c9c727bf58c9c9768d2b67d8e84e62ce6146e7859a2e9c6b"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"ctor",
|
||||
@@ -2729,7 +2717,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"url",
|
||||
"walkdir",
|
||||
"windows",
|
||||
"windows 0.37.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2835,10 +2823,11 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.19.2"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439"
|
||||
checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bytes",
|
||||
"memchr",
|
||||
"num_cpus",
|
||||
@@ -2901,9 +2890,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.14"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a713421342a5a666b7577783721d3117f1b69a393df803ee17bb73b1e122a59"
|
||||
checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"matchers",
|
||||
@@ -2934,9 +2923,9 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
|
||||
|
||||
[[package]]
|
||||
name = "ucd-trie"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
|
||||
checksum = "89570599c4fe5585de2b388aab47e99f7fa4e9238a1399f707a02e356058141c"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
@@ -2946,9 +2935,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
|
||||
checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
@@ -2989,6 +2978,12 @@ name = "uuid"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f"
|
||||
dependencies = [
|
||||
"getrandom 0.2.7",
|
||||
]
|
||||
@@ -3002,12 +2997,6 @@ dependencies = [
|
||||
"getrandom 0.2.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.0.11"
|
||||
@@ -3110,7 +3099,7 @@ checksum = "a489a9420acabb3c2ed0434b6f71f6b56b9485ec32665a28dec1ee186d716e0f"
|
||||
dependencies = [
|
||||
"webview2-com-macros",
|
||||
"webview2-com-sys",
|
||||
"windows",
|
||||
"windows 0.37.0",
|
||||
"windows-implement",
|
||||
]
|
||||
|
||||
@@ -3135,7 +3124,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"windows",
|
||||
"windows 0.37.0",
|
||||
"windows-bindgen",
|
||||
]
|
||||
|
||||
@@ -3180,6 +3169,19 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbedf6db9096bc2364adce0ae0aa636dcd89f3c3f2cd67947062aaf0ca2a10ec"
|
||||
dependencies = [
|
||||
"windows_aarch64_msvc 0.32.0",
|
||||
"windows_i686_gnu 0.32.0",
|
||||
"windows_i686_msvc 0.32.0",
|
||||
"windows_x86_64_gnu 0.32.0",
|
||||
"windows_x86_64_msvc 0.32.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.37.0"
|
||||
@@ -3239,6 +3241,12 @@ version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3263d25f1170419995b78ff10c06b949e8a986c35c208dc24333c64753a87169"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.36.1"
|
||||
@@ -3251,6 +3259,12 @@ version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.36.1"
|
||||
@@ -3263,6 +3277,12 @@ version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.36.1"
|
||||
@@ -3275,6 +3295,12 @@ version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.36.1"
|
||||
@@ -3287,6 +3313,12 @@ version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.36.1"
|
||||
@@ -3336,7 +3368,7 @@ dependencies = [
|
||||
"webkit2gtk",
|
||||
"webkit2gtk-sys",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.37.0",
|
||||
"windows-implement",
|
||||
]
|
||||
|
||||
|
||||
19
README.md
19
README.md
@@ -1,36 +1,39 @@
|
||||
# SlimeVR UI
|
||||
|
||||
|
||||
This is the GUI of SlimeVR, it uses the SolarXR protocol to communicate with the server and is completely isolated from the server logic.
|
||||
|
||||
This project is written in Typescript + React for the frontend and uses Tauri + Rust as a small backend. This makes the application more lightweight than electron.
|
||||
|
||||
|
||||
|
||||
## Compiling
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 16 (We recommend the use of `nvm` instead of installing Node.js directly)
|
||||
- Windows Webview
|
||||
- SlimeVR server installed
|
||||
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
Build for production
|
||||
|
||||
Build for production
|
||||
|
||||
```
|
||||
npm run tauri build
|
||||
```
|
||||
|
||||
Launch in dev mode
|
||||
|
||||
```
|
||||
npm run tauri dev
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
By contributing to this project you are placing all your code under MIT or less restricting licenses, and you certify that the code you have used is compatible with those licenses or is authored by you. If you're doing so on your work time, you certify that your employer is okay with this.
|
||||
|
||||
## License
|
||||
|
||||
All code in this repository is dual-licensed under either:
|
||||
|
||||
- MIT License ([LICENSE-MIT](docs/LICENSE-MIT))
|
||||
@@ -41,8 +44,10 @@ at your option. This means you can select the license you prefer!
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
|
||||
|
||||
### Complying with the license
|
||||
|
||||
Please note that these licenses are very permissive, but if you wish to distribute software based on this code, you need to be aware of the following limits of these licenses:
|
||||
* When distributing any software that uses or is based on SlimeVR, you have to provide to the end-user the original, unmodified `LICENSE-MIT` or `LICENSE-APACHE` file (or both) from SlimeVR. This includes the `Copyright (c) 2022 SlimeVR Contributors` part of the license. It is not sufficient to use a generic MIT/Apache License, it must be the original license file.
|
||||
* This applies even if you distribute software without the source code. In this case, one way to provide it to the end-user is to have a menu in your application that lists all the open source licenses used, including SlimeVR's.
|
||||
|
||||
- When distributing any software that uses or is based on SlimeVR, you have to provide to the end-user the original, unmodified `LICENSE-MIT` or `LICENSE-APACHE` file (or both) from SlimeVR. This includes the `Copyright (c) 2022 SlimeVR Contributors` part of the license. It is not sufficient to use a generic MIT/Apache License, it must be the original license file.
|
||||
- This applies even if you distribute software without the source code. In this case, one way to provide it to the end-user is to have a menu in your application that lists all the open source licenses used, including SlimeVR's.
|
||||
|
||||
Please refer to the original license files if you are at any point uncertain what the exact the requirements are.
|
||||
|
||||
9771
package-lock.json
generated
9771
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@@ -1,20 +1,13 @@
|
||||
{
|
||||
"name": "slimevr-ui",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.16.0",
|
||||
"@fontsource/work-sans": "^4.5.7",
|
||||
"@fontsource/poppins": "^4.5.8",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
|
||||
"@svgr/webpack": "^6.2.1",
|
||||
"@tauri-apps/api": "^1.0.0-rc.3",
|
||||
"@testing-library/jest-dom": "^5.16.3",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/node": "^16.11.26",
|
||||
"@types/react": "^18.0.14",
|
||||
"@types/react-dom": "^18.0.5",
|
||||
"@tauri-apps/api": "^1.0.2",
|
||||
"add": "^2.0.6",
|
||||
"babel-jest": "^27.4.2",
|
||||
"babel-loader": "^8.2.3",
|
||||
@@ -29,7 +22,6 @@
|
||||
"css-minimizer-webpack-plugin": "^3.2.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"dotenv-expand": "^5.1.0",
|
||||
"eslint": "^8.3.0",
|
||||
"eslint-config-react-app": "^7.0.0",
|
||||
"eslint-webpack-plugin": "^3.1.1",
|
||||
"file-loader": "^6.2.0",
|
||||
@@ -61,7 +53,7 @@
|
||||
"resolve-url-loader": "^4.0.0",
|
||||
"sass-loader": "^12.3.0",
|
||||
"semver": "^7.3.5",
|
||||
"solarxr-protocol": "github:SlimeVR/SolarXR-Protocol",
|
||||
"solarxr-protocol": "file:../SlimeVR-Server/solarxr-protocol",
|
||||
"source-map-loader": "^3.0.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"tailwindcss": "^3.0.2",
|
||||
@@ -77,7 +69,11 @@
|
||||
"start": "cross-env BROWSER=none node scripts/start.js",
|
||||
"build": "node scripts/build.js",
|
||||
"test": "node scripts/test.js",
|
||||
"tauri": "tauri"
|
||||
"tauri": "tauri",
|
||||
"lint": "eslint src/**/*.{js,jsx,ts,tsx,json}",
|
||||
"lint:fix": "eslint --fix src/**/*.{js,jsx,ts,tsx,json}",
|
||||
"format": "prettier --write src/**/*.{js,jsx,ts,tsx,css,md,json} --config ./.prettierrc",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
@@ -99,14 +95,35 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.0",
|
||||
"@tauri-apps/cli": "^1.0.0-rc.8",
|
||||
"@tauri-apps/cli": "^1.0.2",
|
||||
"@types/math3d": "^0.2.3",
|
||||
"@types/react-dom": "^18.0.5",
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.0",
|
||||
"@typescript-eslint/parser": "^5.30.0",
|
||||
"autoprefixer": "^10.4.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.18.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.1.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.0",
|
||||
"eslint-plugin-prettier": "^4.1.0",
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"husky": "^8.0.0",
|
||||
"node-polyfill-webpack-plugin": "^1.1.4",
|
||||
"postcss": "^8.4.12",
|
||||
"tailwindcss": "^3.0.23"
|
||||
"prettier": "^2.7.1",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"tailwindcss": "^3.0.23",
|
||||
"@testing-library/jest-dom": "^5.16.3",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/node": "^16.11.26",
|
||||
"@types/react": "^17.0.43"
|
||||
},
|
||||
"jest": {
|
||||
"roots": [
|
||||
|
||||
BIN
public/images/slime-girl.png
Normal file
BIN
public/images/slime-girl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 948 KiB |
BIN
public/images/slimes.png
Normal file
BIN
public/images/slimes.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 570 KiB |
@@ -19,12 +19,12 @@ default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.0.0-rc.5", features = [] }
|
||||
tauri-build = { version = "1.0.4", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tauri = { version = "1.0.0-rc.6", features = ["cli", "devtools", "fs-all", "path-all", "shell-execute", "window-close", "window-maximize", "window-minimize", "window-set-resizable", "window-set-title", "window-start-dragging", "window-unmaximize", "window-unminimize"] }
|
||||
tauri = { version = "1.0.5", features = ["cli", "devtools", "fs-all", "path-all", "shell-execute", "window-close", "window-maximize", "window-minimize", "window-set-resizable", "window-set-title", "window-start-dragging", "window-unmaximize", "window-unminimize"] }
|
||||
pretty_env_logger = "0.4"
|
||||
log = "0.4"
|
||||
clap-verbosity-flag = "1"
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
"width": 1289,
|
||||
"height": 709,
|
||||
"minWidth": 880,
|
||||
"minHeight": 709,
|
||||
"minHeight": 740,
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"decorations": false,
|
||||
|
||||
228
src/App.tsx
228
src/App.tsx
@@ -1,139 +1,169 @@
|
||||
import { useProvideWebsocketApi, useWebsocketAPI, WebSocketApiContext } from './hooks/websocket-api';
|
||||
import {
|
||||
useProvideWebsocketApi,
|
||||
WebSocketApiContext,
|
||||
} from './hooks/websocket-api';
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
Routes,
|
||||
Route,
|
||||
Outlet,
|
||||
} from "react-router-dom";
|
||||
import { Overview } from './components/Overview';
|
||||
import { BodyProportions } from './components/proportions/BodyProportions';
|
||||
} from 'react-router-dom';
|
||||
import { Home } from './components/home/Home';
|
||||
import { AppContextProvider } from './components/providers/AppContext';
|
||||
import { useEffect } from 'react';
|
||||
import { DataFeedConfigT, DataFeedMessage, DeviceDataMaskT, StartDataFeedT, TrackerDataMaskT } from 'solarxr-protocol';
|
||||
import { MainLayoutRoute } from './components/MainLayout';
|
||||
import { SettingsLayoutRoute } from './components/settings/SettingsLayout';
|
||||
import { TrackersSettings } from './components/settings/pages/TrackersSettings';
|
||||
import { Navbar } from './components/Navbar';
|
||||
import { GeneralSettings } from './components/settings/pages/GeneralSettings';
|
||||
import { Serial } from './components/settings/pages/Serial';
|
||||
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import type { Event } from '@tauri-apps/api/event'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import { Event, listen } from '@tauri-apps/api/event';
|
||||
import { TopBar } from './components/TopBar';
|
||||
import { ConfigContextProvider } from './components/providers/ConfigContext';
|
||||
import { OnboardingLayout } from './components/onboarding/OnboardingLayout';
|
||||
import { HomePage } from './components/onboarding/pages/Home';
|
||||
import { WifiCredsPage } from './components/onboarding/pages/WifiCreds';
|
||||
import { ConnectTrackersPage } from './components/onboarding/pages/ConnectTracker';
|
||||
import { OnboardingContextProvider } from './components/onboarding/OnboardingContextProvicer';
|
||||
import { TrackersAssignPage } from './components/onboarding/pages/trackers-assign/TrackerAssignment';
|
||||
import { EnterVRPage } from './components/onboarding/pages/EnterVR';
|
||||
import { AutomaticMountingPage } from './components/onboarding/pages/mounting/AutomaticMounting';
|
||||
import { ManualMountingPage } from './components/onboarding/pages/mounting/ManualMounting';
|
||||
import { ResetTutorialPage } from './components/onboarding/pages/ResetTutorial';
|
||||
import { AutomaticProportionsPage } from './components/onboarding/pages/body-proportions/AutomaticProportions';
|
||||
import { ManualProportionsPage } from './components/onboarding/pages/body-proportions/ManualProportions';
|
||||
import { TrackerSettingsPage } from './components/tracker/TrackerSettings';
|
||||
import { DonePage } from './components/onboarding/pages/Done';
|
||||
|
||||
function Layout() {
|
||||
const { sendDataFeedPacket } = useWebsocketAPI();
|
||||
|
||||
useEffect(() => {
|
||||
const trackerData = new TrackerDataMaskT();
|
||||
trackerData.position = true;
|
||||
trackerData.rotation = true;
|
||||
trackerData.info = true;
|
||||
trackerData.status = true;
|
||||
trackerData.temp = true;
|
||||
|
||||
const dataMask = new DeviceDataMaskT();
|
||||
dataMask.deviceData = true;
|
||||
dataMask.trackerData = trackerData;
|
||||
|
||||
const config = new DataFeedConfigT();
|
||||
config.dataMask = dataMask;
|
||||
config.minimumTimeSinceLast = 100;
|
||||
config.syntheticTrackersMask = trackerData
|
||||
|
||||
const startDataFeed = new StartDataFeedT()
|
||||
startDataFeed.dataFeeds = [config]
|
||||
sendDataFeedPacket(DataFeedMessage.StartDataFeed, startDataFeed);
|
||||
}, [])
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Routes>
|
||||
<Route path="/" element={
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<MainLayoutRoute>
|
||||
<Overview/>
|
||||
<Home />
|
||||
</MainLayoutRoute>
|
||||
}/>
|
||||
<Route path="/proportions" element={
|
||||
<MainLayoutRoute>
|
||||
<BodyProportions/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/tracker/:trackernum/:deviceid"
|
||||
element={
|
||||
<MainLayoutRoute background={false}>
|
||||
<TrackerSettingsPage />
|
||||
</MainLayoutRoute>
|
||||
}/>
|
||||
<Route path="/settings" element={
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings"
|
||||
element={
|
||||
<SettingsLayoutRoute>
|
||||
<Outlet></Outlet>
|
||||
</SettingsLayoutRoute>
|
||||
}>
|
||||
<Route path="trackers" element={<TrackersSettings />} />
|
||||
}
|
||||
>
|
||||
<Route path="trackers" element={<GeneralSettings />} />
|
||||
<Route path="serial" element={<Serial />} />
|
||||
</Route>
|
||||
<Route path="*" element={<Navbar></Navbar>}></Route>
|
||||
<Route
|
||||
path="/onboarding"
|
||||
element={
|
||||
<OnboardingContextProvider>
|
||||
<OnboardingLayout>
|
||||
<Outlet></Outlet>
|
||||
</OnboardingLayout>
|
||||
</OnboardingContextProvider>
|
||||
}
|
||||
>
|
||||
<Route path="home" element={<HomePage />} />
|
||||
<Route path="wifi-creds" element={<WifiCredsPage />} />
|
||||
<Route path="connect-trackers" element={<ConnectTrackersPage />} />
|
||||
<Route path="trackers-assign" element={<TrackersAssignPage />} />
|
||||
<Route path="enter-vr" element={<EnterVRPage />} />
|
||||
<Route path="mounting/auto" element={<AutomaticMountingPage />} />
|
||||
<Route path="mounting/manual" element={<ManualMountingPage />} />
|
||||
<Route path="reset-tutorial" element={<ResetTutorialPage />} />
|
||||
<Route
|
||||
path="body-proportions/auto"
|
||||
element={<AutomaticProportionsPage />}
|
||||
/>
|
||||
<Route
|
||||
path="body-proportions/manual"
|
||||
element={<ManualProportionsPage />}
|
||||
/>
|
||||
<Route path="done" element={<DonePage />} />
|
||||
</Route>
|
||||
<Route path="*" element={<TopBar></TopBar>}></Route>
|
||||
</Routes>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function App() {
|
||||
const websocketAPI = useProvideWebsocketApi();
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = listen("server-status", (event: Event<[string, string]>) => {
|
||||
let [event_type, s] = event.payload;
|
||||
if ("stderr" === event_type) {
|
||||
// This strange invocation is what lets us lose the line information in the console
|
||||
// See more here: https://stackoverflow.com/a/48994308
|
||||
setTimeout(console.log.bind(console, `%c[SERVER] %c${s}`, "color:cyan", "color:red"));
|
||||
} else if (event_type === "stdout") {
|
||||
setTimeout(console.log.bind(console, `%c[SERVER] %c${s}`, "color:cyan", "color:green"));
|
||||
} else if (event_type === "error") {
|
||||
console.error("Error: %s", s)
|
||||
} else if (event_type === "terminated") {
|
||||
console.error("Server Process Terminated: %s", s);
|
||||
} else if (event_type === "other") {
|
||||
console.log("Other process event: %s", s);
|
||||
const unlisten = listen(
|
||||
'server-status',
|
||||
(event: Event<[string, string]>) => {
|
||||
const [event_type, s] = event.payload;
|
||||
if ('stderr' === event_type) {
|
||||
// This strange invocation is what lets us lose the line information in the console
|
||||
// See more here: https://stackoverflow.com/a/48994308
|
||||
setTimeout(
|
||||
console.log.bind(
|
||||
console,
|
||||
`%c[SERVER] %c${s}`,
|
||||
'color:cyan',
|
||||
'color:red'
|
||||
)
|
||||
);
|
||||
} else if (event_type === 'stdout') {
|
||||
setTimeout(
|
||||
console.log.bind(
|
||||
console,
|
||||
`%c[SERVER] %c${s}`,
|
||||
'color:cyan',
|
||||
'color:green'
|
||||
)
|
||||
);
|
||||
} else if (event_type === 'error') {
|
||||
console.error('Error: %s', s);
|
||||
} else if (event_type === 'terminated') {
|
||||
console.error('Server Process Terminated: %s', s);
|
||||
} else if (event_type === 'other') {
|
||||
console.log('Other process event: %s', s);
|
||||
}
|
||||
}
|
||||
return async () => {
|
||||
await unlisten
|
||||
}
|
||||
});
|
||||
}, [])
|
||||
|
||||
const updateCorners = () => {
|
||||
// Check if the window is maximized to remove rounded corners
|
||||
const body = document.getElementsByTagName('body');
|
||||
if (!body) return;
|
||||
appWindow.isMaximized().then((maximized) => {
|
||||
body[0].style.borderRadius = maximized ? '0' : `15px`;
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', updateCorners);
|
||||
);
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateCorners)
|
||||
}
|
||||
}, [])
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
unlisten.then(() => {});
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<WebSocketApiContext.Provider value={websocketAPI}>
|
||||
<AppContextProvider>
|
||||
<Router>
|
||||
<div className='h-full w-full text-default bg-purple-gray-900 '>
|
||||
<div className='flex-col h-full'>
|
||||
{!websocketAPI.isConnected && (
|
||||
<>
|
||||
<Navbar></Navbar>
|
||||
<div className='flex w-full h-full justify-center items-center p-2'>Connection lost to server</div>
|
||||
</>
|
||||
)}
|
||||
{websocketAPI.isConnected && <Layout></Layout>}
|
||||
<Router>
|
||||
<ConfigContextProvider>
|
||||
<WebSocketApiContext.Provider value={websocketAPI}>
|
||||
<AppContextProvider>
|
||||
<div className="h-full w-full text-standard bg-background-80 text-background-10">
|
||||
<div className="flex-col h-full">
|
||||
{!websocketAPI.isConnected && (
|
||||
<>
|
||||
<TopBar></TopBar>
|
||||
<div className="flex w-full h-full justify-center items-center p-2">
|
||||
Connection lost to server
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{websocketAPI.isConnected && <Layout></Layout>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Router>
|
||||
</AppContextProvider>
|
||||
</WebSocketApiContext.Provider>
|
||||
</AppContextProvider>
|
||||
</WebSocketApiContext.Provider>
|
||||
</ConfigContextProvider>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
import { useState } from "react";
|
||||
import { RpcMessage } from "solarxr-protocol";
|
||||
import { RecordBVHRequestT } from "solarxr-protocol/protocol/typescript/dist/solarxr-protocol/rpc/record-bvhrequest";
|
||||
import { RecordBVHStatusT } from "solarxr-protocol/protocol/typescript/dist/solarxr-protocol/rpc/record-bvhstatus";
|
||||
import { useWebsocketAPI } from "../hooks/websocket-api";
|
||||
import { BigButton } from "./commons/BigButton";
|
||||
import { RecordIcon } from "./commons/icon/RecordIcon";
|
||||
import { useState } from 'react';
|
||||
import { RpcMessage } from 'solarxr-protocol';
|
||||
import { RecordBVHRequestT } from 'solarxr-protocol/protocol/typescript/dist/solarxr-protocol/rpc/record-bvhrequest';
|
||||
import { RecordBVHStatusT } from 'solarxr-protocol/protocol/typescript/dist/solarxr-protocol/rpc/record-bvhstatus';
|
||||
import { useWebsocketAPI } from '../hooks/websocket-api';
|
||||
import { BigButton } from './commons/BigButton';
|
||||
import { RecordIcon } from './commons/icon/RecordIcon';
|
||||
|
||||
export function BVHButton() {
|
||||
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
|
||||
const [recording, setRecording] = useState(false);
|
||||
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
|
||||
const [recording, setRecording] = useState(false);
|
||||
|
||||
const toggleBVH = () => {
|
||||
const record = new RecordBVHRequestT();
|
||||
record.stop = recording;
|
||||
sendRPCPacket(RpcMessage.RecordBVHRequest, record)
|
||||
}
|
||||
const toggleBVH = () => {
|
||||
const record = new RecordBVHRequestT();
|
||||
record.stop = recording;
|
||||
sendRPCPacket(RpcMessage.RecordBVHRequest, record);
|
||||
};
|
||||
|
||||
useRPCPacket(RpcMessage.RecordBVHStatus, (data: RecordBVHStatusT) => {
|
||||
setRecording(data.recording);
|
||||
})
|
||||
useRPCPacket(RpcMessage.RecordBVHStatus, (data: RecordBVHStatusT) => {
|
||||
setRecording(data.recording);
|
||||
});
|
||||
|
||||
return (
|
||||
<BigButton text={recording ? 'Recording...' : 'Record BVH'} className="fill-purple-100" icon={<RecordIcon />} onClick={toggleBVH}></BigButton>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<BigButton
|
||||
text={recording ? 'Recording...' : 'Record BVH'}
|
||||
icon={<RecordIcon />}
|
||||
onClick={toggleBVH}
|
||||
></BigButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,52 +1,59 @@
|
||||
import { ReactChild } from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { ResetType } from "solarxr-protocol";
|
||||
import { useLayout } from "../hooks/layout";
|
||||
import { BVHButton } from "./BVHButton";
|
||||
import { Button } from "./commons/Button";
|
||||
import { CubeIcon } from "./commons/icon/CubeIcon";
|
||||
import { GearIcon } from "./commons/icon/GearIcon";
|
||||
import { Navbar, NavButton } from "./Navbar";
|
||||
import { ResetButton } from "./ResetButton";
|
||||
import { ReactNode } from 'react';
|
||||
import { ResetType } from 'solarxr-protocol';
|
||||
import { useLayout } from '../hooks/layout';
|
||||
import { BVHButton } from './BVHButton';
|
||||
import { Navbar } from './Navbar';
|
||||
import { ResetButton } from './home/ResetButton';
|
||||
import { TopBar } from './TopBar';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export function MainLayoutRoute({
|
||||
children,
|
||||
background = true,
|
||||
widgets = true,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
background?: boolean;
|
||||
widgets?: boolean;
|
||||
}) {
|
||||
const { layoutHeight, ref } = useLayout<HTMLDivElement>();
|
||||
const { layoutWidth, ref: refw } = useLayout<HTMLDivElement>();
|
||||
|
||||
export function MainLayoutRoute({ children }: { children: ReactChild }) {
|
||||
|
||||
const { layoutHeight, ref } = useLayout<HTMLDivElement>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar>
|
||||
<>
|
||||
<NavButton to="/" icon={<CubeIcon></CubeIcon>}>Overview</NavButton>
|
||||
<NavButton to="/proportions" icon={<GearIcon></GearIcon>}>Body proportions</NavButton>
|
||||
</>
|
||||
</Navbar>
|
||||
<div ref={ref} className='flex-grow' style={{ height: layoutHeight }}>
|
||||
<div className="flex h-full ">
|
||||
<div className="flex flex-grow gap-10 flex-col rounded-tr-3xl bg-purple-gray-800">
|
||||
return (
|
||||
<>
|
||||
<TopBar></TopBar>
|
||||
<div ref={ref} className="flex-grow" style={{ height: layoutHeight }}>
|
||||
<div className="flex h-full pb-3">
|
||||
<Navbar></Navbar>
|
||||
<div
|
||||
className="flex gap-2 pr-3 w-full"
|
||||
ref={refw}
|
||||
style={{ minWidth: layoutWidth }}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col rounded-xl w-full overflow-hidden',
|
||||
background && 'bg-background-70'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<div className="flex flex-col px-8 w-60 gap-8 pb-5 overflow-y-auto">
|
||||
<div className='flex'>
|
||||
<ResetButton type={ResetType.Quick}></ResetButton>
|
||||
{widgets && (
|
||||
<div className="flex flex-col px-4 min-w-[274px] w-[274px] gap-2 pt-4 rounded-xl overflow-y-auto bg-background-70">
|
||||
<div className="flex">
|
||||
<ResetButton type={ResetType.Quick}></ResetButton>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<ResetButton type={ResetType.Full}></ResetButton>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<BVHButton></BVHButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<ResetButton type={ResetType.Full}></ResetButton>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<BVHButton></BVHButton>
|
||||
</div>
|
||||
<div className='flex flex-grow flex-col justify-end'>
|
||||
{/* <Button variant='primary' className='w-full'>Debug</Button> */}
|
||||
<NavLink to="/settings/trackers" className="flex gap-5 group cursor-pointer">
|
||||
<div className="flex rounded-full p-2 fill-purple-gray-300 bg-purple-gray-700 group-hover:fill-white"><GearIcon></GearIcon></div>
|
||||
<div className="flex flex-col justify-around text-section-indicator group-hover:text-purple-gray-300">Settings</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { ReactChild } from "react";
|
||||
import ReactModal from "react-modal";
|
||||
import { IconButton } from "./commons/ButtonIcon";
|
||||
import { CrossIcon } from "./commons/icon/CrossIcon";
|
||||
|
||||
export function AppModal({ children, name, ...props }: { children?: ReactChild, name: ReactChild } & ReactModal.Props) {
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
{...props}
|
||||
overlayClassName="fixed top-0 right-0 left-0 bottom-0 flex bg-purple-gray-900 bg-opacity-60 justify-center items-center overflow-y-auto border-none rounded-[15px]"
|
||||
className="items-center w-full max-w-2xl h-full md:h-auto bg-purple-gray-700 relative rounded-lg shadow-lg border-none "
|
||||
>
|
||||
<div className="flex justify-between items-start p-5 rounded-t border-b-2 border-primary-1">
|
||||
<h3 className="text-extra-emphasised">
|
||||
{name}
|
||||
</h3>
|
||||
<div className="flex">
|
||||
<IconButton icon={<CrossIcon></CrossIcon>} className="fill-purple-gray-200" onClick={props.onRequestClose}></IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{children}
|
||||
</div>
|
||||
</ReactModal>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,57 +1,101 @@
|
||||
import classnames from 'classnames';
|
||||
import { ReactChild } from 'react';
|
||||
import {
|
||||
useMatch,
|
||||
NavLink,
|
||||
} from "react-router-dom";
|
||||
import { SlimeVRIcon } from './commons/icon/SimevrIcon';
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import { MinimiseIcon } from './commons/icon/MinimiseIcon';
|
||||
import { MaximiseIcon } from './commons/icon/MaximiseIcon';
|
||||
import { CloseIcon } from './commons/icon/CloseIcon';
|
||||
import { useMatch, NavLink } from 'react-router-dom';
|
||||
import { CubeIcon } from './commons/icon/CubeIcon';
|
||||
import { GearIcon } from './commons/icon/GearIcon';
|
||||
|
||||
export function NavButton({ to, children, match, icon }: { to: string, children: ReactChild, match?: string, icon: ReactChild }) {
|
||||
export function NavButton({
|
||||
to,
|
||||
children,
|
||||
match,
|
||||
state = {},
|
||||
icon,
|
||||
}: {
|
||||
to: string;
|
||||
children: ReactChild;
|
||||
match?: string;
|
||||
state?: any;
|
||||
icon: ReactChild;
|
||||
}) {
|
||||
const doesMatch = useMatch({
|
||||
path: match || to,
|
||||
});
|
||||
|
||||
const doesMatch = useMatch({
|
||||
path: match || to,
|
||||
});
|
||||
|
||||
return (
|
||||
<NavLink to={to} className={classnames("flex flex-grow flex-row gap-3 py-3 px-8 rounded-t-md group select-text text-emphasised", { 'bg-purple-gray-800 ': doesMatch, 'hover:bg-purple-gray-600': !doesMatch })}>
|
||||
<div className="flex align-middle justify-center justify-items-center flex-col">
|
||||
<div className={classnames("group-hover:fill-accent-lighter ", { 'fill-accent-lighter': doesMatch, 'fill-purple-gray-600': !doesMatch })}>{icon}</div>
|
||||
</div>
|
||||
<div className={classnames("flex", { 'text-purple-gray-100': doesMatch, 'text-purple-gray-300': !doesMatch })}>{children}</div>
|
||||
</NavLink>
|
||||
)
|
||||
return (
|
||||
<NavLink
|
||||
to={to}
|
||||
state={state}
|
||||
className={classnames(
|
||||
'flex flex-col justify-center gap-4 w-[85px] h-[85px] rounded-md group select-text',
|
||||
{
|
||||
'bg-accent-background-50 fill-accent-background-20': doesMatch,
|
||||
'hover:bg-background-70': !doesMatch,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-around">
|
||||
<div
|
||||
className={classnames('scale-150', {
|
||||
'fill-accent-lighter': doesMatch,
|
||||
'fill-background-50': !doesMatch,
|
||||
})}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classnames('text-center', {
|
||||
'text-accent-background-10': doesMatch,
|
||||
'text-background-10': !doesMatch,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function Navbar({ children }: { children?: ReactChild }) {
|
||||
return (
|
||||
<div data-tauri-drag-region className='flex gap-2 min-h-[56px]'>
|
||||
<div className="flex px-8 py-2 pt-3 justify-around" data-tauri-drag-region>
|
||||
<div className="flex flex-row gap-3" data-tauri-drag-region>
|
||||
<NavLink to="/" className="flex justify-around flex-col select-all" data-tauri-drag-region>
|
||||
<SlimeVRIcon></SlimeVRIcon>
|
||||
</NavLink>
|
||||
<div className="flex justify-around flex-col text-extra-emphasised" data-tauri-drag-region>SlimeVR</div>
|
||||
</div>
|
||||
</div>
|
||||
{children && <div className="flex px-5 gap-2 pt-2">
|
||||
{children}
|
||||
</div>}
|
||||
<div className="flex flex-grow justify-end px-2 gap-2" data-tauri-drag-region>
|
||||
<div className='flex flex-col justify-around ' onClick={() => appWindow.minimize()}>
|
||||
<MinimiseIcon className="rounded-full hover:bg-purple-gray-700"></MinimiseIcon>
|
||||
</div>
|
||||
<div className='flex flex-col justify-around ' onClick={() => appWindow.toggleMaximize()}>
|
||||
<MaximiseIcon className="rounded-full hover:bg-purple-gray-700"></MaximiseIcon>
|
||||
</div>
|
||||
<div className='flex flex-col justify-around ' onClick={() => appWindow.close()}>
|
||||
<CloseIcon className="rounded-full hover:bg-purple-gray-700"></CloseIcon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export function Navbar() {
|
||||
return (
|
||||
<div data-tauri-drag-region className="flex flex-col px-2 pt-2">
|
||||
<div className="flex flex-col flex-grow gap-2">
|
||||
<NavButton to="/" icon={<CubeIcon></CubeIcon>}>
|
||||
Home
|
||||
</NavButton>
|
||||
<NavButton
|
||||
to="/onboarding/body-proportions/auto"
|
||||
match="/onboarding/body-proportions/*"
|
||||
state={{ alonePage: true }}
|
||||
icon={<GearIcon></GearIcon>}
|
||||
>
|
||||
Body proportions
|
||||
</NavButton>
|
||||
<NavButton
|
||||
to="/onboarding/trackers-assign"
|
||||
state={{ alonePage: true }}
|
||||
icon={<GearIcon></GearIcon>}
|
||||
>
|
||||
Tracker assignment
|
||||
</NavButton>
|
||||
<NavButton
|
||||
to="/onboarding/mounting/auto"
|
||||
match="/onboarding/mounting/*"
|
||||
state={{ alonePage: true }}
|
||||
icon={<GearIcon></GearIcon>}
|
||||
>
|
||||
Mounting Calibration
|
||||
</NavButton>
|
||||
<NavButton to="/onboarding/home" icon={<GearIcon></GearIcon>}>
|
||||
Setup Wizard
|
||||
</NavButton>
|
||||
</div>
|
||||
<NavButton
|
||||
to="/settings/trackers"
|
||||
state={{ scrollTo: 'steamvr' }}
|
||||
icon={<GearIcon></GearIcon>}
|
||||
>
|
||||
Settings
|
||||
</NavButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { BodyPart, DataFeedMessage, DataFeedUpdateT, DeviceDataT, TrackerDataT } from "solarxr-protocol";
|
||||
import { useWebsocketAPI } from "../hooks/websocket-api";
|
||||
import { TrackerCard } from "./tracker/TrackerCard";
|
||||
// import { TrackerCard } from "./tracker/TrackerCard";
|
||||
|
||||
interface FlatDeviceTracker {
|
||||
device?: DeviceDataT;
|
||||
tracker: TrackerDataT;
|
||||
}
|
||||
|
||||
|
||||
export function Overview() {
|
||||
|
||||
const { useDataFeedPacket } = useWebsocketAPI();
|
||||
const [list, setDevicesList] = useState<DeviceDataT[]>([]);
|
||||
const [syntheticlist, setSyntheticTrackersList] = useState<TrackerDataT[]>([]);
|
||||
|
||||
useDataFeedPacket(DataFeedMessage.DataFeedUpdate, (packet: DataFeedUpdateT) => {
|
||||
setDevicesList(packet.devices)
|
||||
setSyntheticTrackersList(packet.syntheticTrackers)
|
||||
})
|
||||
|
||||
const trackers = useMemo(() => list.reduce<FlatDeviceTracker[]>((curr, device) => ([...curr, ...device.trackers.map((tracker) => ({ tracker, device }))]), []), [list]);
|
||||
|
||||
const asignedTrackers = useMemo(() =>
|
||||
trackers.filter(({ tracker: { info } }) => {
|
||||
return info && info.bodyPart !== BodyPart.NONE
|
||||
})
|
||||
, [trackers]);
|
||||
|
||||
const unasignedTrackers = useMemo(() =>
|
||||
trackers.filter(({ tracker: { info } }) => {
|
||||
return info && info.bodyPart === BodyPart.NONE
|
||||
})
|
||||
, [trackers]);
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto flex flex-col gap-8">
|
||||
<div className="flex px-8 pt-8 text-secondary-heading">
|
||||
Assigned Trackers
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-5 sm:grid-cols-1 px-8">
|
||||
{asignedTrackers.map(({ tracker, device }, index) => <TrackerCard key={index} tracker={tracker} device={device}/>)}
|
||||
</div>
|
||||
{syntheticlist.length > 0 &&
|
||||
<>
|
||||
<div className="flex px-8 pt-8 text-secondary-heading">
|
||||
External Trackers
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-5 sm:grid-cols-1 px-8">
|
||||
{syntheticlist.map((tracker, index) => <TrackerCard key={index} tracker={tracker} />)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
}
|
||||
{unasignedTrackers.length > 0 &&
|
||||
<>
|
||||
<div className="flex px-8 pt-8 text-secondary-heading">
|
||||
Unassigned Trackers
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-5 sm:grid-cols-1 px-8">
|
||||
{unasignedTrackers.map(({tracker, device}, index) => <TrackerCard key={index} tracker={tracker} device={device}/>)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
}
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { ResetRequestT, ResetType, RpcMessage } from "solarxr-protocol";
|
||||
import { useWebsocketAPI } from "../hooks/websocket-api";
|
||||
import { BigButton } from "./commons/BigButton";
|
||||
import { QuickResetIcon, ResetIcon } from "./commons/icon/ResetIcon";
|
||||
|
||||
|
||||
|
||||
export function ResetButton({ type }: { type: ResetType }) {
|
||||
|
||||
const timerid = useRef<NodeJS.Timer | null>(null);
|
||||
const [reseting, setReseting] = useState(false);
|
||||
const [timer, setTimer] = useState(0);
|
||||
const { sendRPCPacket } = useWebsocketAPI();
|
||||
|
||||
const reset = () => {
|
||||
const req = new ResetRequestT();
|
||||
req.resetType = type;
|
||||
setReseting(true);
|
||||
if (type !== ResetType.Quick) {
|
||||
if (timerid.current)
|
||||
clearInterval(timerid.current);
|
||||
timerid.current = setInterval(() => {
|
||||
setTimer((timer) => {
|
||||
if (timer + 1 === 3) {
|
||||
if (timerid.current)
|
||||
clearInterval(timerid.current);
|
||||
sendRPCPacket(RpcMessage.ResetRequest, req)
|
||||
setTimer(0);
|
||||
setReseting(false)
|
||||
}
|
||||
return timer + 1;
|
||||
});
|
||||
}, 1000);
|
||||
} else {
|
||||
sendRPCPacket(RpcMessage.ResetRequest, req)
|
||||
setReseting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BigButton
|
||||
text={!reseting ? type === ResetType.Quick ? "Quick Reset" : "Reset" : `${3 - timer}`} icon={type === ResetType.Quick ? <QuickResetIcon /> : <ResetIcon/>}
|
||||
onClick={reset}
|
||||
disabled={reseting}>
|
||||
</BigButton>
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
78
src/components/TopBar.tsx
Normal file
78
src/components/TopBar.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ReactChild } from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { CloseIcon } from './commons/icon/CloseIcon';
|
||||
import { MaximiseIcon } from './commons/icon/MaximiseIcon';
|
||||
import { MinimiseIcon } from './commons/icon/MinimiseIcon';
|
||||
import { SlimeVRIcon } from './commons/icon/SimevrIcon';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { ProgressBar } from './commons/ProgressBar';
|
||||
import { Typography } from './commons/Typography';
|
||||
import packagejson from '../../package.json';
|
||||
|
||||
export function TopBar({
|
||||
progress,
|
||||
}: {
|
||||
children?: ReactChild;
|
||||
progress?: number;
|
||||
}) {
|
||||
return (
|
||||
<div data-tauri-drag-region className="flex gap-2 h-[38px] z-50">
|
||||
<div
|
||||
className="flex px-2 pb-1 mt-3 justify-around z-50"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div className="flex gap-1" data-tauri-drag-region>
|
||||
<NavLink
|
||||
to="/"
|
||||
className="flex justify-around flex-col select-all"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<SlimeVRIcon></SlimeVRIcon>
|
||||
</NavLink>
|
||||
<div className="flex justify-around flex-col" data-tauri-drag-region>
|
||||
<Typography>SlimeVR</Typography>
|
||||
</div>
|
||||
<div
|
||||
className="mx-2 flex justify-around flex-col text-standard-bold text-status-success bg-status-success bg-opacity-20 rounded-lg px-3"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
v{packagejson.version}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-grow items-center h-full justify-center z-50"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div
|
||||
className="flex max-w-xl h-full items-center w-full"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
{progress !== undefined && (
|
||||
<ProgressBar progress={progress} height={3} parts={3}></ProgressBar>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end px-2 gap-2 z-50" data-tauri-drag-region>
|
||||
<div
|
||||
className="flex flex-col justify-around"
|
||||
onClick={() => appWindow.minimize()}
|
||||
>
|
||||
<MinimiseIcon className="rounded-full"></MinimiseIcon>
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-col justify-around"
|
||||
onClick={() => appWindow.toggleMaximize()}
|
||||
>
|
||||
<MaximiseIcon className="rounded-full"></MaximiseIcon>
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-col justify-around"
|
||||
onClick={() => appWindow.close()}
|
||||
>
|
||||
<CloseIcon className="rounded-full"></CloseIcon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
src/components/commons/ArrowLink.tsx
Normal file
45
src/components/commons/ArrowLink.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import classNames from 'classnames';
|
||||
import { ReactChild, useMemo } from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from './icon/ArrowIcons';
|
||||
|
||||
export function ArrowLink({
|
||||
to,
|
||||
children,
|
||||
direction = 'left',
|
||||
variant = 'flat',
|
||||
}: {
|
||||
to: string;
|
||||
children: ReactChild;
|
||||
direction?: 'left' | 'right';
|
||||
variant?: 'flat' | 'boxed';
|
||||
}) {
|
||||
const classes = useMemo(() => {
|
||||
const variantsMap = {
|
||||
flat: classNames('justify-start'),
|
||||
boxed: classNames(
|
||||
'justify-between bg-background-70 rounded-md hover:bg-background-60 p-3'
|
||||
),
|
||||
};
|
||||
return classNames(
|
||||
variantsMap[variant],
|
||||
'flex gap-2 hover:fill-background-10 hover:text-background-10 fill-background-30 text-background-30'
|
||||
);
|
||||
}, [variant]);
|
||||
|
||||
return (
|
||||
<NavLink to={to} className={classes}>
|
||||
{direction === 'left' && (
|
||||
<div className="flex flex-col justify-center">
|
||||
<ArrowLeftIcon></ArrowLeftIcon>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
{direction === 'right' && (
|
||||
<div className="flex flex-col justify-center">
|
||||
<ArrowRightIcon></ArrowRightIcon>
|
||||
</div>
|
||||
)}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,36 @@
|
||||
import classNames from "classnames";
|
||||
import React, { ReactChild } from "react";
|
||||
import classNames from 'classnames';
|
||||
import React, { ReactChild } from 'react';
|
||||
|
||||
export function BigButton({ text, icon, disabled, onClick, ...props }: { text: string, icon: ReactChild } & React.AllHTMLAttributes<HTMLButtonElement>) {
|
||||
return (
|
||||
<button disabled={disabled} onClick={onClick} {...props} type="button" className={classNames("flex w-full flex-col rounded-md hover:bg-primary-5 py-10 gap-5 cursor-pointer items-center bg-purple-gray-700 hover:bg-purple-gray-600", { ' hover:bg-purple-gray-300 bg-purple-gray-300 cursor-not-allowed': disabled}, props.className)}>
|
||||
<div className="flex justify-around">{icon}</div>
|
||||
<div className="flex justify-around text-field-title">{text}</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
export function BigButton({
|
||||
text,
|
||||
icon,
|
||||
disabled,
|
||||
onClick,
|
||||
...props
|
||||
}: {
|
||||
text: string;
|
||||
disabled?: boolean;
|
||||
icon: ReactChild;
|
||||
} & React.HTMLAttributes<HTMLButtonElement>) {
|
||||
return (
|
||||
<button
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
{...props}
|
||||
type="button"
|
||||
className={classNames(
|
||||
'flex w-full justify-center rounded-md py-5 gap-5 cursor-pointer items-center ',
|
||||
{
|
||||
'bg-background-60 hover:bg-background-60 cursor-not-allowed text-background-40 fill-background-40':
|
||||
disabled,
|
||||
'bg-background-60 hover:bg-background-50 text-standard fill-background-10':
|
||||
!disabled,
|
||||
},
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-around">{icon}</div>
|
||||
<div className="flex justify-around text-default">{text}</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
187
src/components/commons/BodyInteractions.tsx
Normal file
187
src/components/commons/BodyInteractions.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
ReactChild,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { BodyPart } from 'solarxr-protocol';
|
||||
import { PersonFrontIcon } from './PersonFrontIcon';
|
||||
|
||||
export function BodyInteractions({
|
||||
leftControls,
|
||||
rightControls,
|
||||
assignedRoles,
|
||||
}: {
|
||||
leftControls?: ReactChild;
|
||||
rightControls?: ReactChild;
|
||||
assignedRoles: BodyPart[];
|
||||
}) {
|
||||
const personRef = useRef<HTMLDivElement | null>(null);
|
||||
const leftContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const rightContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const canvasRefRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const [slotsButtonsPos, setSlotsButtonPos] = useState<
|
||||
{
|
||||
id: string;
|
||||
left: number;
|
||||
top: number;
|
||||
height: number;
|
||||
width: number;
|
||||
hidden: boolean;
|
||||
buttonOffset: {
|
||||
left: number;
|
||||
top: number;
|
||||
};
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
const getSlotsPos = () => {
|
||||
return (
|
||||
(personRef.current && [
|
||||
...(personRef.current.querySelectorAll('.body-part-circle') as any),
|
||||
]) ||
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
const getControlsPos = () => {
|
||||
const pos = (container: HTMLDivElement) =>
|
||||
[...(container.querySelectorAll('.control') as any)].filter(
|
||||
({ id }) => !!id
|
||||
);
|
||||
|
||||
const left =
|
||||
(leftContainerRef.current && pos(leftContainerRef.current)) || [];
|
||||
const right =
|
||||
(rightContainerRef.current && pos(rightContainerRef.current)) || [];
|
||||
return [...left, ...right];
|
||||
};
|
||||
|
||||
const getOffset = (el: HTMLDivElement, offset = { left: 0, top: 0 }) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
return {
|
||||
left: rect.left - (offset.left || 0),
|
||||
top: rect.top - (offset.top || 0),
|
||||
width: rect.width || el.offsetWidth,
|
||||
height: rect.height || el.offsetHeight,
|
||||
};
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (
|
||||
!(
|
||||
personRef.current &&
|
||||
canvasRefRef.current &&
|
||||
rightContainerRef.current &&
|
||||
leftContainerRef.current
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
const ctx = canvasRefRef.current.getContext('2d');
|
||||
if (!ctx) return;
|
||||
const slotsPos = getSlotsPos();
|
||||
const controlsPos = getControlsPos();
|
||||
|
||||
canvasRefRef.current.width = canvasRefRef.current.offsetWidth;
|
||||
canvasRefRef.current.height = canvasRefRef.current.offsetHeight;
|
||||
|
||||
ctx.strokeStyle = '#608AAB';
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
const canvasBox = canvasRefRef.current.getBoundingClientRect();
|
||||
const personBox = personRef.current.getBoundingClientRect();
|
||||
|
||||
const controlsPosIds = controlsPos.map(({ id: cid }) => cid);
|
||||
const slots = slotsPos.map((slot: HTMLDivElement) => {
|
||||
const slotPosition = getOffset(slot, canvasBox);
|
||||
return {
|
||||
...slotPosition,
|
||||
id: slot.id,
|
||||
hidden: !controlsPosIds.includes(slot.id),
|
||||
buttonOffset: {
|
||||
left: canvasBox.left - personBox.left,
|
||||
top: canvasBox.top - personBox.top,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
slots.forEach((slot) => {
|
||||
const controls = controlsPos.filter(({ id }) => id === slot.id);
|
||||
controls.forEach((control) => {
|
||||
const controlPosition = getOffset(control, canvasBox);
|
||||
|
||||
const offsetX =
|
||||
controlPosition.left < slot.left ? controlPosition.width : 0;
|
||||
|
||||
const constolLeft = controlPosition.left + offsetX;
|
||||
const LINE_BREAK_WIDTH = 40;
|
||||
const leftOffsetX =
|
||||
LINE_BREAK_WIDTH * (controlPosition.left < slot.left ? -1 : 1);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(
|
||||
constolLeft,
|
||||
controlPosition.top + controlPosition.height / 2
|
||||
);
|
||||
ctx.lineTo(
|
||||
constolLeft - leftOffsetX,
|
||||
controlPosition.top + controlPosition.height / 2
|
||||
);
|
||||
ctx.lineTo(slot.left + slot.width / 2, slot.top + slot.height / 2);
|
||||
ctx.stroke();
|
||||
});
|
||||
});
|
||||
setSlotsButtonPos(slots);
|
||||
}, [leftControls, rightControls]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<canvas
|
||||
ref={canvasRefRef}
|
||||
className="absolute w-full h-full top-0 z-10"
|
||||
width="100%"
|
||||
height="100%"
|
||||
></canvas>
|
||||
<div className="flex">
|
||||
<div ref={leftContainerRef} className="z-10">
|
||||
{leftControls}
|
||||
</div>
|
||||
<div
|
||||
ref={personRef}
|
||||
className="relative w-full flex justify-center mx-10"
|
||||
>
|
||||
<PersonFrontIcon width={238}></PersonFrontIcon>
|
||||
{slotsButtonsPos.map(
|
||||
({ top, left, height, width, id, hidden, buttonOffset }) => (
|
||||
<div
|
||||
key={id}
|
||||
className="absolute z-10"
|
||||
style={{
|
||||
top: top + height / 2 - 20 / 2 + buttonOffset.top,
|
||||
left: left + width / 2 - 20 / 2 + buttonOffset.left,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'w-5 h-5 rounded-full outline outline-2 outline-background-20 transition-opacity',
|
||||
(assignedRoles.includes((BodyPart as any)[id]) &&
|
||||
'bg-background-70') ||
|
||||
'bg-background-10',
|
||||
(hidden && 'opacity-0') || 'opacity-100'
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div ref={rightContainerRef} className="z-10">
|
||||
{rightControls}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,103 @@
|
||||
import classNames from "classnames";
|
||||
import { ReactChild, useMemo } from "react";
|
||||
import classNames from 'classnames';
|
||||
import React, { ReactChild, useMemo } from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { LoaderIcon } from './icon/LoaderIcon';
|
||||
|
||||
function ButtonContent({
|
||||
loading,
|
||||
icon,
|
||||
children,
|
||||
}: {
|
||||
loading: boolean;
|
||||
icon?: ReactChild;
|
||||
children: ReactChild;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
{ 'opacity-0': loading },
|
||||
'flex flex-row gap-2 justify-center'
|
||||
)}
|
||||
>
|
||||
{icon && (
|
||||
<div className="flex justify-center items-center fill-background-10 w-5 h-5">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
{loading && (
|
||||
<div className="absolute top-0 left-0 w-full h-full flex justify-center items-center fill-background-10">
|
||||
<LoaderIcon youSpinMeRightRoundBabyRightRound></LoaderIcon>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function Button({
|
||||
children,
|
||||
variant,
|
||||
disabled,
|
||||
to,
|
||||
loading = false,
|
||||
state = {},
|
||||
icon,
|
||||
rounded = false,
|
||||
...props
|
||||
}: {
|
||||
children: ReactChild;
|
||||
icon?: ReactChild;
|
||||
variant: 'primary' | 'secondary' | 'tierciary';
|
||||
to?: string;
|
||||
loading?: boolean;
|
||||
rounded?: boolean;
|
||||
state?: any;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
const classes = useMemo(() => {
|
||||
const variantsMap = {
|
||||
primary: classNames({
|
||||
'bg-accent-background-30 hover:bg-accent-background-20 text-standard text-background-10':
|
||||
!disabled,
|
||||
'bg-accent-background-40 hover:bg-accent-background-40 cursor-not-allowed text-accent-background-10':
|
||||
disabled,
|
||||
}),
|
||||
secondary: classNames({
|
||||
'bg-background-60 hover:bg-background-50 text-standard text-background-10':
|
||||
!disabled,
|
||||
'bg-background-60 hover:bg-background-60 cursor-not-allowed text-background-40':
|
||||
disabled,
|
||||
}),
|
||||
tierciary: classNames({
|
||||
'bg-background-50 hover:bg-background-40 text-standard text-background-10':
|
||||
!disabled,
|
||||
'bg-background-50 hover:bg-background-50 cursor-not-allowed text-background-40':
|
||||
disabled,
|
||||
}),
|
||||
};
|
||||
return classNames(
|
||||
variantsMap[variant],
|
||||
'focus:ring-4 text-center relative',
|
||||
{
|
||||
'rounded-full p-2 text-center min-h-[35px] min-w-[35px]': rounded,
|
||||
'rounded-md px-5 py-2.5': !rounded,
|
||||
},
|
||||
props.className
|
||||
);
|
||||
}, [variant, disabled, rounded]);
|
||||
|
||||
export function Button({ children, variant, disabled, ...props }: { children: ReactChild, variant: 'primary' } & React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
|
||||
const classes = useMemo(() => {
|
||||
const variantsMap = {
|
||||
primary: classNames('text-field-title focus:ring-4 focus:outline-none focus:ring-primary-2', { 'bg-purple-gray-600 hover:bg-purple-gray-500': !disabled, 'bg-purple-gray-900 hover:bg-purple-gray-900 text-section-indicator': disabled }),
|
||||
}
|
||||
return classNames(variantsMap[variant], 'focus:ring-4 rounded-lg px-5 py-2.5 text-center font-medium');
|
||||
|
||||
}, [variant, disabled])
|
||||
return <button type="button" {...props} className={classes} disabled={disabled}>{children}</button>
|
||||
}
|
||||
return to ? (
|
||||
<NavLink to={to} className={classes} state={state}>
|
||||
<ButtonContent icon={icon} loading={loading}>
|
||||
{children}
|
||||
</ButtonContent>
|
||||
</NavLink>
|
||||
) : (
|
||||
<button type="button" {...props} className={classes} disabled={disabled}>
|
||||
<ButtonContent icon={icon} loading={loading}>
|
||||
{children}
|
||||
</ButtonContent>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import classNames from "classnames";
|
||||
import React, { ReactChild } from "react";
|
||||
|
||||
|
||||
export function IconButton({ icon, className, ...props }: { icon: ReactChild, className?: string } & React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div {...props} className={classNames("px-2 rounded-full h-8 w-8 flex justify-center items-center hover:bg-purple-gray-600", className)}>
|
||||
{icon}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,86 @@
|
||||
import classNames from 'classnames';
|
||||
import { forwardRef, useId } from 'react'
|
||||
import { ReactChild, useMemo } from 'react';
|
||||
import { Control, Controller } from 'react-hook-form';
|
||||
|
||||
export const CheckBox = forwardRef<HTMLInputElement, { label: string, outlined?: boolean }>(({ label, outlined, ...props }, ref) => {
|
||||
const id = useId();
|
||||
export function CheckBox({
|
||||
label,
|
||||
variant = 'checkbox',
|
||||
control,
|
||||
outlined,
|
||||
name,
|
||||
...props
|
||||
}: {
|
||||
label: string | ReactChild;
|
||||
control: Control<any>;
|
||||
name: string;
|
||||
variant?: 'checkbox' | 'toggle';
|
||||
outlined?: boolean;
|
||||
}) {
|
||||
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'
|
||||
),
|
||||
toggle: '',
|
||||
pin: '',
|
||||
},
|
||||
toggle: {
|
||||
checkbox: classNames('hidden'),
|
||||
toggle: classNames('w-10 h-4 rounded-full relative transition-colors'),
|
||||
pin: classNames('h-2 w-2 bg-background-10 rounded-full absolute m-1'),
|
||||
},
|
||||
};
|
||||
return vriantsMap[variant];
|
||||
}, [variant]);
|
||||
|
||||
return (
|
||||
<div className={classNames('flex items-center gap-3', { 'rounded-md bg-purple-gray-700 pl-4 text-emphasised': outlined })}>
|
||||
<input ref={ref} id={id} {...props} className="flex flex-col rounded-sm text-accent-lighter focus:ring-purple-gray-700" type="checkbox" />
|
||||
<label htmlFor={id} className="w-full py-3">{label}</label>
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field: { onChange, value, ref } }) => (
|
||||
<div
|
||||
className={classNames(
|
||||
{
|
||||
'bg-background-60 rounded-lg text-white': outlined,
|
||||
'text-background-30': !outlined,
|
||||
},
|
||||
'flex items-center gap-2 w-full'
|
||||
)}
|
||||
>
|
||||
<label
|
||||
className={classNames(
|
||||
'w-full py-3 flex gap-2 items-center text-standard-bold',
|
||||
{ 'px-3': outlined }
|
||||
)}
|
||||
>
|
||||
<input
|
||||
ref={ref}
|
||||
onChange={onChange}
|
||||
checked={value}
|
||||
className={classes.checkbox}
|
||||
type="checkbox"
|
||||
{...props}
|
||||
/>
|
||||
{variant === 'toggle' && (
|
||||
<div
|
||||
className={classNames(classes.toggle, {
|
||||
'bg-accent-background-30': value,
|
||||
'bg-background-50': !value,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classNames(classes.pin, {
|
||||
'left-0': !value,
|
||||
'right-0': value,
|
||||
})}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,54 @@
|
||||
import { forwardRef, HTMLInputTypeAttribute } from "react";
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
forwardRef,
|
||||
HTMLInputTypeAttribute,
|
||||
MouseEvent,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { EyeIcon } from './icon/EyeIcon';
|
||||
|
||||
export interface InputProps {
|
||||
type: HTMLInputTypeAttribute,
|
||||
placeholder?: string
|
||||
type: HTMLInputTypeAttribute;
|
||||
placeholder?: string;
|
||||
label?: string;
|
||||
autocomplete?: boolean;
|
||||
}
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(({type, placeholder, ...props}, ref) => {
|
||||
return <input
|
||||
type={type}
|
||||
ref={ref}
|
||||
className="rounded-lg bg-purple-gray-500 border-purple-gray-500 focus:border-purple-gray-500 placeholder:text-purple-gray-300 text-field-title"
|
||||
placeholder={placeholder}
|
||||
{...props}
|
||||
/>;
|
||||
});
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(function AppInput(
|
||||
{ type, placeholder, label, autocomplete, ...props },
|
||||
ref
|
||||
) {
|
||||
const [forceText, setForceText] = useState(false);
|
||||
|
||||
const togglePassword = (e: MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setForceText(!forceText);
|
||||
};
|
||||
|
||||
return (
|
||||
<label className="flex flex-col gap-1">
|
||||
{label}
|
||||
<div className="relative w-full">
|
||||
<input
|
||||
type={forceText ? 'text' : type}
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
'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',
|
||||
{ 'pr-10': type === 'password' }
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
autoComplete={autocomplete ? 'off' : 'on'}
|
||||
{...props}
|
||||
></input>
|
||||
{type === 'password' && (
|
||||
<div
|
||||
className="fill-background-10 absolute top-0 h-full flex flex-col justify-center right-0 p-4"
|
||||
onClick={togglePassword}
|
||||
>
|
||||
<EyeIcon></EyeIcon>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
});
|
||||
|
||||
25
src/components/commons/Modal.tsx
Normal file
25
src/components/commons/Modal.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import classNames from 'classnames';
|
||||
import { ReactChild } from 'react';
|
||||
import ReactModal from 'react-modal';
|
||||
|
||||
export function EmptyModal({
|
||||
children,
|
||||
...props
|
||||
}: { children?: ReactChild } & ReactModal.Props) {
|
||||
return (
|
||||
<ReactModal
|
||||
{...props}
|
||||
shouldCloseOnOverlayClick
|
||||
shouldCloseOnEsc
|
||||
overlayClassName={classNames(
|
||||
'fixed top-0 right-0 left-0 bottom-0 flex flex-col justify-center items-center w-full h-full bg-background-90 bg-opacity-60 z-20'
|
||||
)}
|
||||
className={classNames(
|
||||
props.className,
|
||||
'items-center focus:ring-transparent focus:ring-offset-transparent focus:outline-transparent outline-none'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</ReactModal>
|
||||
);
|
||||
}
|
||||
@@ -1,50 +1,63 @@
|
||||
import classNames from "classnames";
|
||||
import { useMemo } from "react";
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
import { Button } from "./Button";
|
||||
import { Control, Controller } from 'react-hook-form';
|
||||
import { Button } from './Button';
|
||||
import { Typography } from './Typography';
|
||||
|
||||
export function NumberSelector({
|
||||
label,
|
||||
valueLabelFormat,
|
||||
control,
|
||||
name,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
}: {
|
||||
label: string;
|
||||
valueLabelFormat?: (value: number) => string;
|
||||
control: Control<any>;
|
||||
name: string;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number | ((value: number, add: boolean) => number);
|
||||
}) {
|
||||
const stepFn =
|
||||
typeof step === 'function'
|
||||
? step
|
||||
: (value: number, add: boolean) => (add ? value + step : value - step);
|
||||
|
||||
|
||||
export function NumberSelector({ label, valueLabelFormat, control, name, min, max, step, variant }: { label: string, valueLabelFormat?: (value: number) => string, control: Control<any>, name: string, min: number, max: number, step: number | ((value: number, add: boolean) => number), variant: 'smol' | 'big' }) {
|
||||
|
||||
const variantClass = useMemo(() => {
|
||||
const variantsMap = {
|
||||
smol: {
|
||||
container: 'flex flex-col gap-1',
|
||||
label: 'flex text-field-title',
|
||||
value: 'flex justify-center items-center w-10 text-field-title'
|
||||
},
|
||||
big: {
|
||||
container: 'flex flex-row gap-5',
|
||||
label: 'flex flex-grow justify-start items-center text-field-title',
|
||||
value: 'flex justify-center items-center w-16 text-field-title'
|
||||
}
|
||||
};
|
||||
return variantsMap[variant];
|
||||
}, [variant])
|
||||
|
||||
|
||||
const stepFn = typeof step === 'function' ? step : (value: number, add: boolean) => add ? value + step : value - step;
|
||||
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<div className={classNames(variantClass.container)}>
|
||||
<div className={classNames(variantClass.label)}>{label}</div>
|
||||
<div className="flex gap-3">
|
||||
<div className="flex">
|
||||
<Button variant="primary" onClick={() => onChange(stepFn(value, false))} disabled={stepFn(value, false) <= min}>-</Button>
|
||||
</div>
|
||||
<div className={classNames(variantClass.value)}>{valueLabelFormat ? valueLabelFormat(value) : value}</div>
|
||||
<div className="flex">
|
||||
<Button variant="primary" onClick={() => onChange(stepFn(value, true))} disabled={stepFn(value, true) >= max}>+</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<Typography bold>{label}</Typography>
|
||||
<div className="flex gap-2 bg-background-60 p-2 rounded-lg">
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="tierciary"
|
||||
rounded
|
||||
onClick={() => onChange(stepFn(value, false))}
|
||||
disabled={stepFn(value, false) < min}
|
||||
>
|
||||
-
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-grow justify-center items-center w-10">
|
||||
{valueLabelFormat ? valueLabelFormat(value) : value}
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="tierciary"
|
||||
rounded
|
||||
onClick={() => onChange(stepFn(value, true))}
|
||||
disabled={stepFn(value, true) > max}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
151
src/components/commons/PersonFrontIcon.tsx
Normal file
151
src/components/commons/PersonFrontIcon.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { BodyPart } from 'solarxr-protocol';
|
||||
|
||||
export function PersonFrontIcon({ width }: { width?: number }) {
|
||||
const CIRCLE_RADIUS = 0.0001;
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={width || 240}
|
||||
viewBox="0 0 165 392"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M84.53 224.074C83.953 230.874 88.569 266.874 90.951 280.984C92.085 287.671 95.195 298.565 94.076 304.349C92.476 312.411 92.017 322.843 92.896 328.918C93.451 332.607 95.196 349.618 92.696 355.845C91.389 359.108 88.996 375.832 88.996 375.832C82.756 391.587 86.278 390.812 86.278 390.812C88.21 393.183 91.519 390.998 91.519 390.998C92.1549 391.464 92.9388 391.682 93.7241 391.612C94.5094 391.542 95.2421 391.188 95.785 390.616C97.949 392.407 100.471 390.396 100.471 390.396C103.189 391.807 105.71 389.205 105.71 389.205C107.271 389.991 107.653 388.998 107.653 388.998C112.337 388.698 105.039 373.706 105.039 373.706C103.291 360.242 106.773 352.748 106.773 352.748C118.178 318.926 118.758 309.948 114.199 297.204C112.915 293.524 112.59 292.067 113.181 290.47C114.547 286.783 113.551 271.953 115.217 266.064C118.431 254.706 121.602 225.903 123.254 212.464C125.475 194.364 115.388 170.088 115.388 170.088C113.179 160.21 116.418 125.016 116.418 125.016C120.941 132.054 120.768 144.477 120.768 144.477C120.05 157.506 131.294 177.42 131.294 177.42C136.694 185.649 138.742 193.456 138.742 194.036C138.742 196.407 138.223 202.145 138.223 202.145L138.43 207.145C138.803 209.721 139.034 212.316 139.123 214.918C138.28 227.953 140.35 225.501 140.35 225.501C142.098 225.501 144.018 215.011 144.018 215.011C144.018 217.711 143.357 225.811 144.818 228.869C146.564 232.512 147.848 228.244 147.871 227.387C148.333 210.787 149.33 215.138 149.33 215.138C150.301 228.602 151.494 231.644 153.63 230.591C155.25 229.818 153.769 214.433 153.769 214.433C156.544 223.572 158.649 225.027 158.649 225.027C163.229 228.243 160.397 219.361 159.76 217.602C156.371 208.256 156.267 205.017 156.267 205.017C160.501 213.417 163.692 213.104 163.692 213.104C167.822 211.786 160.083 199.894 155.548 194.197C153.234 191.297 150.248 187.408 149.384 185.097C147.973 181.188 146.907 168.62 146.907 168.62C146.48 153.79 142.813 147.348 142.813 147.348C136.544 137.314 135.365 118.598 135.365 118.598L135.09 87C132.89 65.445 117.01 65.29 117.01 65.29C100.957 62.9 98.723 57.714 98.723 57.714C95.323 52.821 97.266 43.44 97.266 43.44C100.087 41.145 101.175 35.053 101.175 35.053C105.859 31.461 105.63 26.205 103.466 26.262C101.73 26.308 102.123 24.87 102.123 24.87C105.052 1.208 84.046 0 84.046 0H80.836C80.836 0 59.821 1.208 62.746 24.864C62.746 24.864 63.139 26.304 61.388 26.256C59.23 26.199 59.029 31.456 63.696 35.047C63.696 35.047 64.783 41.137 67.605 43.434C67.605 43.434 69.548 52.814 66.148 57.708C66.148 57.708 63.922 62.894 47.861 65.284C47.861 65.284 31.952 65.44 29.788 86.994L29.488 118.594C29.488 118.594 28.331 137.311 22.038 147.344C22.038 147.344 18.389 153.787 17.967 168.616C17.967 168.616 16.898 181.184 15.492 185.093C14.635 187.393 11.653 191.276 9.32001 194.193C4.74601 199.878 -2.94199 211.745 1.17101 213.1C1.17101 213.1 4.37901 213.412 8.59601 205.013C8.59601 205.013 8.50901 208.229 5.12501 217.598C4.46001 219.334 1.63201 228.217 6.21301 225.024C6.21301 225.024 8.33501 223.567 11.093 214.43C11.093 214.43 9.61301 229.815 11.26 230.588C13.412 231.642 14.586 228.599 15.56 215.135C15.56 215.135 16.56 210.787 17.017 227.384C17.04 228.241 18.295 232.509 20.049 228.866C21.529 225.811 20.864 217.727 20.864 215.008C20.864 215.008 22.764 225.498 24.536 225.498C24.536 225.498 26.624 227.95 25.767 214.915C25.628 212.786 26.375 208.415 26.467 207.142L26.667 202.142C26.667 202.142 26.146 196.417 26.146 194.033C26.146 193.442 28.194 185.646 33.594 177.417C33.594 177.417 44.826 157.494 44.103 144.474C44.103 144.474 43.947 132.051 48.47 125.013C48.47 125.013 51.68 160.205 49.505 170.085C49.505 170.085 39.405 194.358 41.629 212.461C43.27 225.937 46.435 254.702 49.657 266.061C51.34 271.938 50.345 286.761 51.693 290.467C52.301 292.076 51.982 293.558 50.675 297.201C46.141 309.947 46.718 318.925 58.123 352.745C58.123 352.745 61.633 360.239 59.859 373.703C59.859 373.703 52.572 388.695 57.239 388.995C57.239 388.995 57.604 389.988 59.182 389.202C59.182 389.202 61.703 391.802 64.427 390.393C64.427 390.393 66.95 392.407 69.106 390.613C69.6451 391.185 70.3751 391.54 71.158 391.61C71.9409 391.681 72.7225 391.462 73.355 390.995C73.355 390.995 76.664 393.227 78.63 390.809C78.63 390.809 82.123 391.584 75.904 375.829C75.904 375.829 73.522 359.129 72.209 355.842C69.709 349.621 71.474 332.57 72.009 328.915C72.87 322.806 72.409 312.398 70.835 304.346C69.684 298.575 72.801 287.679 73.952 280.981C76.317 266.881 80.952 230.881 80.373 224.071L82.288 224.743C83.0863 224.756 83.8692 224.522 84.53 224.074Z" />
|
||||
<circle
|
||||
className="body-part-circle"
|
||||
cx="82.004"
|
||||
cy="120"
|
||||
r={CIRCLE_RADIUS}
|
||||
id={BodyPart[BodyPart.CHEST]}
|
||||
/>
|
||||
<circle
|
||||
className="body-part-circle"
|
||||
cx="82.004"
|
||||
cy="181"
|
||||
r={CIRCLE_RADIUS}
|
||||
id={BodyPart[BodyPart.HIP]}
|
||||
/>
|
||||
<circle
|
||||
className="body-part-circle"
|
||||
cx="82.004"
|
||||
cy="181"
|
||||
r={CIRCLE_RADIUS}
|
||||
id={BodyPart[BodyPart.WAIST]}
|
||||
/>
|
||||
<circle
|
||||
className="body-part-circle"
|
||||
cx="82"
|
||||
cy="50"
|
||||
r={CIRCLE_RADIUS}
|
||||
id={BodyPart[BodyPart.NECK]}
|
||||
/>
|
||||
<circle
|
||||
className="body-part-circle"
|
||||
cx="82"
|
||||
cy="50"
|
||||
r={CIRCLE_RADIUS}
|
||||
id={BodyPart[BodyPart.HMD]}
|
||||
/>
|
||||
<circle
|
||||
className="body-part-circle"
|
||||
cx="139.004"
|
||||
cy="170"
|
||||
r={CIRCLE_RADIUS}
|
||||
id={BodyPart[BodyPart.LEFT_HAND]}
|
||||
/>
|
||||
<circle
|
||||
className="body-part-circle"
|
||||
cx="122"
|
||||
cy="93"
|
||||
r={CIRCLE_RADIUS}
|
||||
id={BodyPart[BodyPart.LEFT_UPPER_ARM]}
|
||||
/>
|
||||
<circle
|
||||
className="body-part-circle"
|
||||
cx="122"
|
||||
cy="93"
|
||||
r={CIRCLE_RADIUS}
|
||||
id={BodyPart[BodyPart.LEFT_SHOULDER]}
|
||||
/>
|
||||
<circle
|
||||
className="body-part-circle"
|
||||
cx="139.004"
|
||||
cy="170"
|
||||
r={CIRCLE_RADIUS}
|
||||
id={BodyPart[BodyPart.LEFT_LOWER_ARM]}
|
||||
/>
|
||||
<circle
|
||||
className="body-part-circle"
|
||||
cx="97.004"
|
||||
cy="360"
|
||||
r={CIRCLE_RADIUS}
|
||||
id={BodyPart[BodyPart.LEFT_LOWER_LEG]}
|
||||
/>
|
||||
<circle
|
||||
className="body-part-circle"
|
||||
cx="103.004"
|
||||
cy="260"
|
||||
r={CIRCLE_RADIUS}
|
||||
id={BodyPart[BodyPart.LEFT_UPPER_LEG]}
|
||||
/>
|
||||
<circle
|
||||
className="body-part-circle"
|
||||
cx="97.004"
|
||||
cy="360"
|
||||
r={CIRCLE_RADIUS}
|
||||
id={BodyPart[BodyPart.LEFT_FOOT]}
|
||||
/>
|
||||
|
||||
<circle
|
||||
className="body-part-circle"
|
||||
cx="26.004"
|
||||
cy="170"
|
||||
r={CIRCLE_RADIUS}
|
||||
id={BodyPart[BodyPart.RIGHT_HAND]}
|
||||
/>
|
||||
|
||||
<circle
|
||||
className="body-part-circle"
|
||||
cx="43"
|
||||
cy="93"
|
||||
r={CIRCLE_RADIUS}
|
||||
id={BodyPart[BodyPart.RIGHT_UPPER_ARM]}
|
||||
/>
|
||||
<circle
|
||||
className="body-part-circle"
|
||||
cx="43"
|
||||
cy="93"
|
||||
r={CIRCLE_RADIUS}
|
||||
id={BodyPart[BodyPart.RIGHT_SHOULDER]}
|
||||
/>
|
||||
<circle
|
||||
className="body-part-circle"
|
||||
cx="26.004"
|
||||
cy="170"
|
||||
r={CIRCLE_RADIUS}
|
||||
id={BodyPart[BodyPart.RIGHT_LOWER_ARM]}
|
||||
/>
|
||||
<circle
|
||||
className="body-part-circle"
|
||||
cx="67.004"
|
||||
cy="360"
|
||||
r={CIRCLE_RADIUS}
|
||||
id={BodyPart[BodyPart.RIGHT_LOWER_LEG]}
|
||||
/>
|
||||
|
||||
<circle
|
||||
className="body-part-circle"
|
||||
cx="61.004"
|
||||
cy="260"
|
||||
r={CIRCLE_RADIUS}
|
||||
id={BodyPart[BodyPart.RIGHT_UPPER_LEG]}
|
||||
/>
|
||||
<circle
|
||||
className="body-part-circle"
|
||||
cx="67.004"
|
||||
cy="360"
|
||||
r={CIRCLE_RADIUS}
|
||||
id={BodyPart[BodyPart.RIGHT_FOOT]}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
43
src/components/commons/ProgressBar.tsx
Normal file
43
src/components/commons/ProgressBar.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import classNames from 'classnames';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function ProgressBar({
|
||||
progress,
|
||||
parts = 1,
|
||||
height = 10,
|
||||
}: {
|
||||
progress: number;
|
||||
parts?: number;
|
||||
height?: number;
|
||||
}) {
|
||||
const Bar = ({ index }: { index: number }) => {
|
||||
const value = useMemo(
|
||||
() => Math.min(Math.max((progress * parts) / 1 - index, 0), 1),
|
||||
[index, progress]
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className="flex relative flex-grow bg-background-50 rounded-lg overflow-hidden"
|
||||
style={{ height: `${height}px` }}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'bg-accent-background-20 rounded-lg overflow-hidden absolute top-0'
|
||||
)}
|
||||
style={{
|
||||
width: `${value * 100}%`,
|
||||
height: `${height}px`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-row gap-2">
|
||||
{Array.from({ length: parts }).map((_, key) => (
|
||||
<Bar index={key} key={key}></Bar>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
53
src/components/commons/Radio.tsx
Normal file
53
src/components/commons/Radio.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import classNames from 'classnames';
|
||||
import { Control, Controller } from 'react-hook-form';
|
||||
import { Typography } from './Typography';
|
||||
|
||||
export function Radio({
|
||||
control,
|
||||
name,
|
||||
label,
|
||||
value,
|
||||
desciption,
|
||||
}: {
|
||||
control: Control<any>;
|
||||
name: string;
|
||||
label: string;
|
||||
value: string | number;
|
||||
desciption?: string;
|
||||
}) {
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field: { onChange, ref, name, value: checked } }) => (
|
||||
<label
|
||||
className={classNames(
|
||||
'w-full bg-background-60 p-3 rounded-md flex gap-3 border-2',
|
||||
{
|
||||
'border-accent-background-30': value == checked,
|
||||
'border-transparent': value != checked,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
className="text-accent-background-30 focus:ring-transparent focus:ring-offset-transparent focus:outline-transparent"
|
||||
name={name}
|
||||
ref={ref}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
checked={value == checked}
|
||||
></input>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography bold>{label}</Typography>
|
||||
{desciption && (
|
||||
<Typography variant="standard" color="secondary">
|
||||
{desciption}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export type SelectOption = { label: string, value: any };
|
||||
|
||||
export const Select = React.forwardRef<HTMLSelectElement, { options: SelectOption[], label?: string }>(({ label, options, ...props }, ref) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-1 ">
|
||||
{label && <span className="text-field-title">{label}</span>}
|
||||
<select {...props} ref={ref} className="w-full mt-0 rounded-md border-purple-gray-600 text-field-title bg-purple-gray-600 shadow-sm focus:border-purple-gray-600 focus:ring focus:ring-purple-gray-600 focus:ring-opacity-50">
|
||||
{options && options.map(({ label, value }) => <option value={value} key={value}>{label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
13
src/components/commons/TipBox.tsx
Normal file
13
src/components/commons/TipBox.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ReactChild } from 'react';
|
||||
import { BulbIcon } from './icon/BulbIcon';
|
||||
|
||||
export function TipBox({ children }: { children: ReactChild }) {
|
||||
return (
|
||||
<div className="flex flex-row gap-4 bg-accent-background-50 p-4 rounded-md">
|
||||
<div className="fill-accent-background-20 flex flex-col justify-center">
|
||||
<BulbIcon></BulbIcon>
|
||||
</div>
|
||||
<div className="flex flex-col text-accent-background-10">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
src/components/commons/Typography.tsx
Normal file
43
src/components/commons/Typography.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import classNames from 'classnames';
|
||||
import { createElement, ReactNode, useMemo } from 'react';
|
||||
|
||||
export function Typography({
|
||||
variant = 'standard',
|
||||
bold = false,
|
||||
color = 'primary',
|
||||
children,
|
||||
}: {
|
||||
variant?: 'main-title' | 'section-title' | 'standard' | 'vr-accessible';
|
||||
bold?: boolean;
|
||||
block?: boolean;
|
||||
color?: 'primary' | 'secondary' | string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const tag = useMemo(() => {
|
||||
const tags = {
|
||||
'main-title': 'h1',
|
||||
'section-title': 'h2',
|
||||
standard: 'p',
|
||||
'vr-accessible': 'p',
|
||||
};
|
||||
return tags[variant];
|
||||
}, [variant]);
|
||||
|
||||
return createElement(
|
||||
tag,
|
||||
{
|
||||
className: classNames([
|
||||
variant === 'main-title' && 'text-main-title',
|
||||
variant === 'section-title' && 'text-section-title',
|
||||
variant === 'standard' &&
|
||||
(bold ? 'text-standard-bold' : 'text-standard'),
|
||||
variant === 'vr-accessible' &&
|
||||
(bold ? 'text-vr-accesible-bold' : 'text-vr-accesible'),
|
||||
color === 'primary' && 'text-background-10',
|
||||
color === 'secondary' && 'text-background-30',
|
||||
typeof color === 'string' && color,
|
||||
]),
|
||||
},
|
||||
children
|
||||
);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
|
||||
|
||||
export function ArrowDownIcon() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z" /></svg>
|
||||
)
|
||||
}
|
||||
48
src/components/commons/icon/ArrowIcons.tsx
Normal file
48
src/components/commons/icon/ArrowIcons.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
export function ArrowDownIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
version="1.1"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function ArrowLeftIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="12"
|
||||
height="10"
|
||||
viewBox="0 0 12 10"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 5C12 5.18941 11.921 5.37106 11.7804 5.50499C11.6397 5.63892 11.449 5.71416 11.2501 5.71416H2.56155L5.7817 8.77932C5.85142 8.84572 5.90673 8.92455 5.94446 9.0113C5.9822 9.09805 6.00162 9.19104 6.00162 9.28494C6.00162 9.37884 5.9822 9.47183 5.94446 9.55858C5.90673 9.64534 5.85142 9.72417 5.7817 9.79056C5.71197 9.85696 5.6292 9.90964 5.5381 9.94557C5.447 9.98151 5.34936 10 5.25075 10C5.15215 10 5.05451 9.98151 4.96341 9.94557C4.87231 9.90964 4.78954 9.85696 4.71981 9.79056L0.220316 5.50562C0.150479 5.43928 0.0950707 5.36048 0.0572652 5.27371C0.0194598 5.18695 0 5.09394 0 5C0 4.90606 0.0194598 4.81305 0.0572652 4.72629C0.0950707 4.63952 0.150479 4.56072 0.220316 4.49438L4.71981 0.209436C4.86063 0.0753365 5.05161 0 5.25075 0C5.4499 0 5.64088 0.0753365 5.7817 0.209436C5.92251 0.343536 6.00162 0.525414 6.00162 0.715059C6.00162 0.904705 5.92251 1.08658 5.7817 1.22068L2.56155 4.28584H11.2501C11.449 4.28584 11.6397 4.36108 11.7804 4.49502C11.921 4.62895 12 4.81059 12 5Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function ArrowRightIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="12"
|
||||
height="10"
|
||||
viewBox="0 0 12 10"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M-3.2717e-07 5C-3.2717e-07 4.81059 0.0790079 4.62894 0.219644 4.49501C0.360281 4.36108 0.551026 4.28584 0.749916 4.28584L9.43845 4.28584L6.2183 1.22068C6.14858 1.15428 6.09327 1.07545 6.05554 0.9887C6.0178 0.901945 5.99838 0.808961 5.99838 0.715058C5.99838 0.621155 6.0178 0.528172 6.05554 0.441417C6.09327 0.354662 6.14858 0.275834 6.2183 0.209435C6.28803 0.143036 6.3708 0.0903648 6.4619 0.0544297C6.553 0.0184946 6.65064 -2.87569e-07 6.74925 -2.87569e-07C6.84785 -2.87569e-07 6.94549 0.0184946 7.03659 0.0544297C7.12769 0.0903648 7.21046 0.143036 7.28019 0.209435L11.7797 4.49438C11.8495 4.56072 11.9049 4.63952 11.9427 4.72629C11.9805 4.81305 12 4.90606 12 5C12 5.09394 11.9805 5.18695 11.9427 5.27371C11.9049 5.36048 11.8495 5.43928 11.7797 5.50562L7.28019 9.79056C7.13937 9.92466 6.94839 10 6.74925 10C6.5501 10 6.35912 9.92466 6.2183 9.79056C6.07749 9.65646 5.99838 9.47459 5.99838 9.28494C5.99838 9.0953 6.07749 8.91342 6.2183 8.77932L9.43845 5.71416L0.749916 5.71416C0.551026 5.71416 0.360281 5.63891 0.219644 5.50498C0.0790079 5.37105 -3.2717e-07 5.18941 -3.2717e-07 5Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +1,64 @@
|
||||
import classNames from "classnames";
|
||||
import { useMemo } from "react";
|
||||
import classNames from 'classnames';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function BatteryIcon({ value }: { value: number }) {
|
||||
export function BatteryIcon({
|
||||
value,
|
||||
disabled = false,
|
||||
}: {
|
||||
value: number;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const col = useMemo(() => {
|
||||
const colorsMap: { [key: number]: string } = {
|
||||
0.4: 'fill-status-success',
|
||||
0.2: 'fill-status-warning',
|
||||
0: 'fill-status-critical',
|
||||
};
|
||||
|
||||
const col = useMemo(() => {
|
||||
const val = Object.keys(colorsMap)
|
||||
.filter((key) => +key < value)
|
||||
.sort((a, b) => +b - +a)[0];
|
||||
return disabled
|
||||
? 'fill-background-40'
|
||||
: colorsMap[+val] || 'fill-background-10';
|
||||
}, [value, disabled]);
|
||||
|
||||
const colorsMap: { [key: number]: string } = {
|
||||
0.4: 'fill-status-online',
|
||||
0.2: 'fill-status-warning',
|
||||
0: 'fill-status-error',
|
||||
}
|
||||
|
||||
const val = Object.keys(colorsMap).filter(key => +key < value).sort((a, b) => +b - +a)[0];
|
||||
return colorsMap[+val] || 'fill-primary-gray-600';
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<svg width="19" height="9" viewBox="0 0 19 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.0833 0H1.31203C0.995003 0.00131561 0.691347 0.121213 0.467167 0.333594C0.242986 0.545976 0.116428 0.83365 0.115039 1.134V7.383C0.114754 7.68458 0.240506 7.97399 0.464808 8.18799C0.689109 8.40198 0.993714 8.52315 1.31203 8.525H11.0833V0Z" fill="#4C3755"/>
|
||||
<path d="M15.0005 8.525C15.3175 8.52368 15.6212 8.40379 15.8454 8.19141C16.0696 7.97902 16.1961 7.69135 16.1975 7.391V5.968H17.9972V2.558H16.1975V1.134C16.1961 0.83365 16.0696 0.545976 15.8454 0.333594C15.6212 0.121213 15.3175 0.00131561 15.0005 0H10.9672V8.525H15.0005Z" fill="#4C3755"/>
|
||||
<mask id="mask0_4_39" style={{ 'maskType': 'alpha', }} maskUnits="userSpaceOnUse" x="0" y="0" width="18" height="9">
|
||||
<path d="M11.0833 0H1.31203C0.995003 0.00131561 0.691347 0.121213 0.467167 0.333594C0.242986 0.545976 0.116428 0.83365 0.115039 1.134V7.383C0.114754 7.68458 0.240506 7.97399 0.464808 8.18799C0.689109 8.40198 0.993714 8.52315 1.31203 8.525H11.0833V0Z" fill="#4C3755"/>
|
||||
<path d="M15.0005 8.525C15.3175 8.52368 15.6212 8.40379 15.8454 8.19141C16.0696 7.97902 16.1961 7.69135 16.1975 7.391V5.968H17.9972V2.558H16.1975V1.134C16.1961 0.83365 16.0696 0.545976 15.8454 0.333594C15.6212 0.121213 15.3175 0.00131561 15.0005 0H10.9672V8.525H15.0005Z" fill="#4C3755"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_4_39)" className={classNames(col, 'opacity-100')}>
|
||||
<rect width={value * 18} height="9"/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
width="19"
|
||||
height="9"
|
||||
viewBox="0 0 19 9"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.0833 0H1.31203C0.995003 0.00131561 0.691347 0.121213 0.467167 0.333594C0.242986 0.545976 0.116428 0.83365 0.115039 1.134V7.383C0.114754 7.68458 0.240506 7.97399 0.464808 8.18799C0.689109 8.40198 0.993714 8.52315 1.31203 8.525H11.0833V0Z"
|
||||
fill="#3D6381"
|
||||
/>
|
||||
<path
|
||||
d="M15.0005 8.525C15.3175 8.52368 15.6212 8.40379 15.8454 8.19141C16.0696 7.97902 16.1961 7.69135 16.1975 7.391V5.968H17.9972V2.558H16.1975V1.134C16.1961 0.83365 16.0696 0.545976 15.8454 0.333594C15.6212 0.121213 15.3175 0.00131561 15.0005 0H10.9672V8.525H15.0005Z"
|
||||
fill="#3D6381"
|
||||
/>
|
||||
<mask
|
||||
id="mask0_4_39"
|
||||
style={{ maskType: 'alpha' }}
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="18"
|
||||
height="9"
|
||||
>
|
||||
<path
|
||||
d="M11.0833 0H1.31203C0.995003 0.00131561 0.691347 0.121213 0.467167 0.333594C0.242986 0.545976 0.116428 0.83365 0.115039 1.134V7.383C0.114754 7.68458 0.240506 7.97399 0.464808 8.18799C0.689109 8.40198 0.993714 8.52315 1.31203 8.525H11.0833V0Z"
|
||||
fill="#3D6381"
|
||||
/>
|
||||
<path
|
||||
d="M15.0005 8.525C15.3175 8.52368 15.6212 8.40379 15.8454 8.19141C16.0696 7.97902 16.1961 7.69135 16.1975 7.391V5.968H17.9972V2.558H16.1975V1.134C16.1961 0.83365 16.0696 0.545976 15.8454 0.333594C15.6212 0.121213 15.3175 0.00131561 15.0005 0H10.9672V8.525H15.0005Z"
|
||||
fill="#3D6381"
|
||||
/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_4_39)" className={classNames(col, 'opacity-100')}>
|
||||
<rect width={value * 18} height="9" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
12
src/components/commons/icon/BulbIcon.tsx
Normal file
12
src/components/commons/icon/BulbIcon.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export function BulbIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={20}
|
||||
height={20}
|
||||
viewBox="0 0 352 512"
|
||||
>
|
||||
<path d="M96.06 454.35c.01 6.29 1.87 12.45 5.36 17.69l17.09 25.69a31.99 31.99 0 0 0 26.64 14.28h61.71a31.99 31.99 0 0 0 26.64-14.28l17.09-25.69a31.989 31.989 0 0 0 5.36-17.69l.04-38.35H96.01l.05 38.35zM0 176c0 44.37 16.45 84.85 43.56 115.78 16.52 18.85 42.36 58.23 52.21 91.45.04.26.07.52.11.78h160.24c.04-.26.07-.51.11-.78 9.85-33.22 35.69-72.6 52.21-91.45C335.55 260.85 352 220.37 352 176 352 78.61 272.91-.3 175.45 0 73.44.31 0 82.97 0 176zm176-80c-44.11 0-80 35.89-80 80 0 8.84-7.16 16-16 16s-16-7.16-16-16c0-61.76 50.24-112 112-112 8.84 0 16 7.16 16 16s-7.16 16-16 16z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
19
src/components/commons/icon/CheckIcon.tsx
Normal file
19
src/components/commons/icon/CheckIcon.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
export function CheckIcon(props: any) {
|
||||
return (
|
||||
<svg
|
||||
width="9"
|
||||
height="7"
|
||||
viewBox="0 0 9 7"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_548_761)">
|
||||
<path d="M3.05666 6.86296L0.132685 3.81321C0.0906355 3.76962 0.0572652 3.71778 0.034494 3.66068C0.0117228 3.60358 0 3.54235 0 3.48051C0 3.41867 0.0117228 3.35744 0.034494 3.30035C0.0572652 3.24325 0.0906355 3.19141 0.132685 3.14781L0.770644 2.48241C0.812442 2.43856 0.862143 2.40375 0.916885 2.38C0.971628 2.35625 1.03033 2.34402 1.08962 2.34402C1.14891 2.34402 1.20762 2.35625 1.26236 2.38C1.3171 2.40375 1.3668 2.43856 1.4086 2.48241L3.37883 4.53739L7.59946 0.135219C7.64126 0.0913603 7.69096 0.0565546 7.7457 0.0328039C7.80044 0.00905319 7.85915 -0.00317383 7.91844 -0.00317383C7.97773 -0.00317383 8.03643 0.00905319 8.09117 0.0328039C8.14592 0.0565546 8.19562 0.0913603 8.23742 0.135219L8.87537 0.800618C8.91742 0.844214 8.95079 0.896052 8.97357 0.95315C8.99634 1.01025 9.00806 1.07148 9.00806 1.13332C9.00806 1.19516 8.99634 1.25639 8.97357 1.31349C8.95079 1.37058 8.91742 1.42242 8.87537 1.46602L3.69303 6.86296C3.65126 6.90657 3.60166 6.94116 3.54706 6.96477C3.49246 6.98837 3.43394 7.00052 3.37484 7.00052C3.31574 7.00052 3.25722 6.98837 3.20263 6.96477C3.14803 6.94116 3.09843 6.90657 3.05666 6.86296Z" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_548_761">
|
||||
<rect width="9" height="7" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
|
||||
export function CircleIcon(props: any) {
|
||||
return (
|
||||
<svg {...props} width="10" height="10" viewBox="0 0 6 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 6C4.65685 6 6 4.65685 6 3C6 1.34315 4.65685 0 3 0C1.34315 0 0 1.34315 0 3C0 4.65685 1.34315 6 3 6Z" fill="#50E897"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 6 6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M3 6C4.65685 6 6 4.65685 6 3C6 1.34315 4.65685 0 3 0C1.34315 0 0 1.34315 0 3C0 4.65685 1.34315 6 3 6Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
|
||||
|
||||
|
||||
export function CloseIcon({ className, size = 35 }: { className?: string, size?: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} className={className} viewBox="0 0 31 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.3804 17.8804L12.619 11.119" stroke="#C0A1D8" strokeLinecap="round"/>
|
||||
<path d="M12.6196 17.8804L19.381 11.119" stroke="#C0A1D8" strokeLinecap="round"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
export function CloseIcon({
|
||||
className,
|
||||
size = 35,
|
||||
}: {
|
||||
className?: string;
|
||||
size?: number;
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
className={className}
|
||||
viewBox="0 0 31 29"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19.3804 17.8804L12.619 11.119"
|
||||
stroke="#C0A1D8"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M12.6196 17.8804L19.381 11.119"
|
||||
stroke="#C0A1D8"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
|
||||
|
||||
export function CrossIcon() {
|
||||
return ( <svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd"></path></svg>)
|
||||
}
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
|
||||
export function CubeIcon(props: any) {
|
||||
return (
|
||||
<svg {...props} width="20" height="20" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.835 3.752L8.377 0.852002C8.31295 0.823464 8.24362 0.808716 8.1735 0.808716C8.10338 0.808716 8.03405 0.823464 7.97 0.852002L1.509 3.752C1.45917 3.77586 1.4169 3.81302 1.38684 3.85938C1.35678 3.90573 1.34011 3.95949 1.33867 4.01472C1.33722 4.06995 1.35105 4.12451 1.37863 4.17237C1.40622 4.22024 1.44649 4.25956 1.495 4.286L7.938 7.679C8.00997 7.71812 8.09058 7.73862 8.1725 7.73862C8.25442 7.73862 8.33503 7.71812 8.407 7.679L14.85 4.286C14.8985 4.25947 14.9387 4.22008 14.9662 4.17215C14.9937 4.12423 15.0074 4.06964 15.0059 4.01441C15.0043 3.95918 14.9875 3.90545 14.9574 3.85915C14.9272 3.81284 14.8849 3.77577 14.835 3.752Z" />
|
||||
<path d="M9.04301 15.774L15.537 12.338C15.5774 12.3112 15.6104 12.2748 15.6331 12.232C15.6558 12.1892 15.6674 12.1414 15.667 12.093V5.429C15.6697 5.38221 15.6601 5.33552 15.6392 5.29359C15.6182 5.25166 15.5867 5.21596 15.5476 5.19006C15.5085 5.16415 15.4634 5.14894 15.4166 5.14595C15.3698 5.14296 15.3231 5.15228 15.281 5.173L8.78701 8.519C8.74073 8.54365 8.70214 8.58059 8.6755 8.62576C8.64886 8.67092 8.63519 8.72257 8.63601 8.775V15.529C8.63442 15.5774 8.64577 15.6253 8.6689 15.6679C8.69203 15.7104 8.7261 15.746 8.76758 15.771C8.80907 15.796 8.85647 15.8094 8.90489 15.8099C8.95331 15.8105 9.00099 15.7981 9.04301 15.774Z"/>
|
||||
<path d="M0.67101 5.428V12.088C0.670579 12.1364 0.682243 12.1842 0.704943 12.227C0.727644 12.2698 0.760664 12.3062 0.80101 12.333L7.29601 15.769C7.33755 15.7933 7.38493 15.8059 7.43307 15.8055C7.48122 15.805 7.52834 15.7915 7.5694 15.7663C7.61046 15.7412 7.64392 15.7053 7.66622 15.6627C7.68851 15.62 7.69881 15.5721 7.69601 15.524V8.771C7.69683 8.71856 7.68317 8.66692 7.65652 8.62175C7.62988 8.57659 7.59129 8.53965 7.54501 8.515L1.05501 5.169C1.01276 5.14923 0.966102 5.14076 0.919596 5.14439C0.87309 5.14802 0.828319 5.16364 0.789646 5.18973C0.750973 5.21581 0.719718 5.25147 0.698927 5.29323C0.678135 5.33498 0.668517 5.38142 0.67101 5.428Z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 16 16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M14.835 3.752L8.377 0.852002C8.31295 0.823464 8.24362 0.808716 8.1735 0.808716C8.10338 0.808716 8.03405 0.823464 7.97 0.852002L1.509 3.752C1.45917 3.77586 1.4169 3.81302 1.38684 3.85938C1.35678 3.90573 1.34011 3.95949 1.33867 4.01472C1.33722 4.06995 1.35105 4.12451 1.37863 4.17237C1.40622 4.22024 1.44649 4.25956 1.495 4.286L7.938 7.679C8.00997 7.71812 8.09058 7.73862 8.1725 7.73862C8.25442 7.73862 8.33503 7.71812 8.407 7.679L14.85 4.286C14.8985 4.25947 14.9387 4.22008 14.9662 4.17215C14.9937 4.12423 15.0074 4.06964 15.0059 4.01441C15.0043 3.95918 14.9875 3.90545 14.9574 3.85915C14.9272 3.81284 14.8849 3.77577 14.835 3.752Z" />
|
||||
<path d="M9.04301 15.774L15.537 12.338C15.5774 12.3112 15.6104 12.2748 15.6331 12.232C15.6558 12.1892 15.6674 12.1414 15.667 12.093V5.429C15.6697 5.38221 15.6601 5.33552 15.6392 5.29359C15.6182 5.25166 15.5867 5.21596 15.5476 5.19006C15.5085 5.16415 15.4634 5.14894 15.4166 5.14595C15.3698 5.14296 15.3231 5.15228 15.281 5.173L8.78701 8.519C8.74073 8.54365 8.70214 8.58059 8.6755 8.62576C8.64886 8.67092 8.63519 8.72257 8.63601 8.775V15.529C8.63442 15.5774 8.64577 15.6253 8.6689 15.6679C8.69203 15.7104 8.7261 15.746 8.76758 15.771C8.80907 15.796 8.85647 15.8094 8.90489 15.8099C8.95331 15.8105 9.00099 15.7981 9.04301 15.774Z" />
|
||||
<path d="M0.67101 5.428V12.088C0.670579 12.1364 0.682243 12.1842 0.704943 12.227C0.727644 12.2698 0.760664 12.3062 0.80101 12.333L7.29601 15.769C7.33755 15.7933 7.38493 15.8059 7.43307 15.8055C7.48122 15.805 7.52834 15.7915 7.5694 15.7663C7.61046 15.7412 7.64392 15.7053 7.66622 15.6627C7.68851 15.62 7.69881 15.5721 7.69601 15.524V8.771C7.69683 8.71856 7.68317 8.66692 7.65652 8.62175C7.62988 8.57659 7.59129 8.53965 7.54501 8.515L1.05501 5.169C1.01276 5.14923 0.966102 5.14076 0.919596 5.14439C0.87309 5.14802 0.828319 5.16364 0.789646 5.18973C0.750973 5.21581 0.719718 5.25147 0.698927 5.29323C0.678135 5.33498 0.668517 5.38142 0.67101 5.428Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
12
src/components/commons/icon/EyeIcon.tsx
Normal file
12
src/components/commons/icon/EyeIcon.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export function EyeIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="14"
|
||||
height="10"
|
||||
viewBox="0 0 14 10"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M9.09817 4.99914C9.09817 6.11133 8.15709 7.01294 6.9962 7.01294C5.83532 7.01294 4.89424 6.11133 4.89424 4.99914C4.89424 3.88693 5.83532 2.98533 6.9962 2.98533C8.15709 2.98532 9.09817 3.88694 9.09817 4.99914ZM7 0.806091C5.79804 0.811423 4.55217 1.10403 3.37279 1.66426C2.49711 2.09735 1.64372 2.70838 0.90293 3.46257C0.539093 3.84756 0.0750283 4.40501 0 4.99979C0.00886667 5.515 0.561517 6.15093 0.90293 6.53703C1.5976 7.2616 2.42877 7.85557 3.37279 8.33578C4.47262 8.86954 5.68997 9.17685 7 9.19395C8.2031 9.18853 9.44869 8.89255 10.6268 8.33578C11.5024 7.90269 12.3563 7.29122 13.0971 6.53703C13.4609 6.15204 13.925 5.59457 14 4.99979C13.9911 4.48458 13.4385 3.84863 13.0971 3.46254C12.4024 2.73797 11.5708 2.14446 10.6268 1.66423C9.52751 1.13088 8.30716 0.82568 7 0.806091ZM6.99911 1.84732C8.8205 1.84732 10.297 3.25891 10.297 5.00025C10.297 6.74157 8.8205 8.15316 6.99911 8.15316C5.17773 8.15316 3.70124 6.74156 3.70124 5.00025C3.70124 3.25891 5.17773 1.84732 6.99911 1.84732Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
12
src/components/commons/icon/FootIcon.tsx
Normal file
12
src/components/commons/icon/FootIcon.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export function FootIcon({ width }: { width?: number }) {
|
||||
return (
|
||||
<svg
|
||||
width={width || 28}
|
||||
viewBox="0 0 28 28"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="28" height="28" rx="2" fill="#56407B" />
|
||||
<path d="M18.5981 6.05798L18.1461 7.57104C18.0991 7.72904 18.0361 7.92496 18.0061 8.00696C17.8731 8.3476 17.7678 8.69847 17.6911 9.05603C17.6078 9.35662 17.595 9.67231 17.6538 9.97864C17.7126 10.285 17.8414 10.5737 18.0301 10.822C18.0651 10.866 18.2771 11.2161 18.5031 11.6021C18.7291 11.9881 18.9321 12.3339 18.9581 12.3719C19.0375 12.4967 19.1065 12.6279 19.1641 12.764C19.2954 13.1107 19.3199 13.4887 19.2346 13.8494C19.1492 14.2101 18.9578 14.5369 18.6851 14.788C18.3588 15.1236 17.9785 15.4021 17.5601 15.6119C16.885 15.93 16.3258 16.4504 15.9601 17.101C15.8219 17.345 15.6665 17.579 15.4951 17.801C15.3751 17.967 15.1531 18.292 15.0011 18.524C14.7241 18.945 14.5201 19.2441 14.3791 19.4301C14.0713 19.8357 13.7085 20.1964 13.3011 20.502L12.9011 20.796C12.4511 21.131 12.4491 21.132 12.2011 21.146C12.0364 21.1349 11.8735 21.1855 11.7441 21.288C11.6483 21.3637 11.5381 21.4192 11.4201 21.451C11.3022 21.4829 11.179 21.4904 11.0581 21.473C10.8581 21.473 10.8231 21.4731 10.7951 21.451C10.7435 21.4122 10.7026 21.3609 10.6761 21.302C10.6746 21.2885 10.6682 21.276 10.6581 21.267C10.6411 21.254 10.6301 21.26 10.5821 21.306C10.5023 21.376 10.4081 21.4276 10.3061 21.457C10.2078 21.4797 10.1066 21.4871 10.0061 21.479H9.7921L9.7121 21.438C9.67073 21.4185 9.63235 21.3933 9.5981 21.363L9.5641 21.328H9.3391C9.0311 21.328 8.9941 21.32 8.8991 21.228C8.84921 21.1797 8.79222 21.1392 8.7301 21.108C8.61137 21.0413 8.50891 20.9491 8.4301 20.838C8.41279 20.7935 8.40728 20.7453 8.4141 20.698L8.4201 20.604L8.5061 20.515C8.5961 20.423 8.6061 20.39 8.5811 20.337C8.53854 20.293 8.48119 20.2662 8.4201 20.262C8.32553 20.2388 8.24225 20.1828 8.1851 20.104C8.09834 20.0317 8.03903 19.9318 8.0171 19.821C7.98191 19.6196 8.00125 19.4124 8.0731 19.2209C8.1321 19.0639 8.1321 19.064 8.3671 19.063C8.57004 19.0815 8.77415 19.0437 8.9571 18.954C9.02435 18.9097 9.10263 18.8851 9.1831 18.8829C9.2331 18.8829 9.2711 18.862 9.5831 18.671C9.83458 18.5055 10.1027 18.3666 10.3831 18.257L10.4881 18.223L10.5591 18.0229C10.7588 17.4891 10.9019 16.9357 10.9861 16.3719C11.0496 15.9465 11.1422 15.5259 11.2631 15.113C11.3728 14.7304 11.5064 14.355 11.6631 13.989C12.0182 13.0652 12.2417 12.096 12.3271 11.11C12.3801 10.764 12.4431 10.3661 12.4661 10.2271C12.4891 10.0881 12.5171 9.89894 12.5281 9.80994C12.5391 9.72094 12.5631 9.51606 12.5821 9.35706C12.6011 9.19806 12.6291 8.96499 12.6421 8.83899C12.6551 8.71299 12.6771 8.51101 12.6901 8.38501C12.7441 7.88501 12.8191 7.11596 12.8711 6.53796C12.8901 6.31196 12.9111 6.09799 12.9151 6.06299L12.9221 6H18.6161L18.5981 6.05798Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
20
src/components/commons/icon/FrontOfChair.tsx
Normal file
20
src/components/commons/icon/FrontOfChair.tsx
Normal file
File diff suppressed because one or more lines are too long
@@ -1,9 +1,12 @@
|
||||
|
||||
|
||||
export function GearIcon() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 14 13" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.00099 11.9C7.31948 11.9003 7.63041 11.997 7.89283 12.1775C8.15524 12.3579 8.35682 12.6137 8.47099 12.911C9.11419 12.757 9.72885 12.5018 10.292 12.155C10.2026 11.9531 10.1559 11.7348 10.155 11.514C10.1546 11.2516 10.2198 10.9933 10.3448 10.7626C10.4697 10.5319 10.6505 10.3362 10.8705 10.1932C11.0905 10.0503 11.3427 9.96464 11.6043 9.94417C11.8659 9.92369 12.1284 9.96902 12.368 10.076C12.7147 9.51424 12.9699 8.90091 13.124 8.259C12.8268 8.1447 12.5711 7.9431 12.3906 7.68071C12.2102 7.41833 12.1134 7.10745 12.113 6.789C12.1134 6.47055 12.2102 6.15968 12.3906 5.89729C12.5711 5.6349 12.8268 5.4333 13.124 5.319C12.97 4.6758 12.7148 4.06115 12.368 3.498C12.1284 3.60499 11.8659 3.65031 11.6043 3.62983C11.3427 3.60936 11.0905 3.52374 10.8705 3.38078C10.6505 3.23782 10.4697 3.04207 10.3448 2.81138C10.2198 2.58068 10.1546 2.32237 10.155 2.06C10.1543 1.83899 10.201 1.62041 10.292 1.419C9.72669 1.07498 9.11135 0.820948 8.46799 0.666C8.35405 0.963474 8.15253 1.21938 7.89007 1.39989C7.6276 1.5804 7.31654 1.67703 6.99799 1.677C6.67886 1.67742 6.36711 1.581 6.10393 1.40049C5.84075 1.21998 5.63853 0.963872 5.52399 0.666C4.882 0.821335 4.26872 1.07752 3.70699 1.425C3.83599 1.71551 3.87394 2.03828 3.81586 2.35079C3.75778 2.66329 3.60638 2.95087 3.38162 3.17563C3.15686 3.40039 2.86928 3.55178 2.55678 3.60987C2.24427 3.66795 1.9215 3.62999 1.63099 3.501C1.28471 4.06442 1.02957 4.67899 0.874993 5.322C1.17184 5.43574 1.42719 5.63685 1.60733 5.89878C1.78746 6.1607 1.8839 6.47111 1.8839 6.789C1.8839 7.10689 1.78746 7.4173 1.60733 7.67922C1.42719 7.94115 1.17184 8.14226 0.874993 8.256C1.02899 8.8992 1.28417 9.51385 1.63099 10.077C1.92149 9.94858 2.24405 9.91106 2.55627 9.96935C2.86849 10.0276 3.15577 10.179 3.38036 10.4036C3.60495 10.6282 3.75634 10.9155 3.81464 11.2277C3.87294 11.5399 3.83541 11.8625 3.70699 12.153C4.26924 12.4985 4.88243 12.7533 5.52399 12.908C5.6399 12.6107 5.84296 12.3554 6.10653 12.1755C6.37011 11.9956 6.68189 11.8996 7.00099 11.9ZM4.45399 6.817C4.4536 6.31238 4.60287 5.81897 4.88294 5.3992C5.16301 4.97942 5.56128 4.65214 6.02738 4.45876C6.49348 4.26537 7.00646 4.21457 7.50144 4.31277C7.99642 4.41098 8.45115 4.65378 8.80811 5.01046C9.16508 5.36715 9.40824 5.82169 9.50683 6.31659C9.60542 6.81149 9.55502 7.32451 9.362 7.79076C9.16898 8.25701 8.84201 8.65554 8.42246 8.93594C8.00291 9.21634 7.50962 9.366 7.00499 9.366C6.67012 9.36613 6.3385 9.3003 6.02907 9.17228C5.71964 9.04425 5.43846 8.85653 5.20158 8.61983C4.96469 8.38313 4.77675 8.1021 4.64848 7.79277C4.52021 7.48344 4.45412 7.15187 4.45399 6.817Z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 14 13"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M7.00099 11.9C7.31948 11.9003 7.63041 11.997 7.89283 12.1775C8.15524 12.3579 8.35682 12.6137 8.47099 12.911C9.11419 12.757 9.72885 12.5018 10.292 12.155C10.2026 11.9531 10.1559 11.7348 10.155 11.514C10.1546 11.2516 10.2198 10.9933 10.3448 10.7626C10.4697 10.5319 10.6505 10.3362 10.8705 10.1932C11.0905 10.0503 11.3427 9.96464 11.6043 9.94417C11.8659 9.92369 12.1284 9.96902 12.368 10.076C12.7147 9.51424 12.9699 8.90091 13.124 8.259C12.8268 8.1447 12.5711 7.9431 12.3906 7.68071C12.2102 7.41833 12.1134 7.10745 12.113 6.789C12.1134 6.47055 12.2102 6.15968 12.3906 5.89729C12.5711 5.6349 12.8268 5.4333 13.124 5.319C12.97 4.6758 12.7148 4.06115 12.368 3.498C12.1284 3.60499 11.8659 3.65031 11.6043 3.62983C11.3427 3.60936 11.0905 3.52374 10.8705 3.38078C10.6505 3.23782 10.4697 3.04207 10.3448 2.81138C10.2198 2.58068 10.1546 2.32237 10.155 2.06C10.1543 1.83899 10.201 1.62041 10.292 1.419C9.72669 1.07498 9.11135 0.820948 8.46799 0.666C8.35405 0.963474 8.15253 1.21938 7.89007 1.39989C7.6276 1.5804 7.31654 1.67703 6.99799 1.677C6.67886 1.67742 6.36711 1.581 6.10393 1.40049C5.84075 1.21998 5.63853 0.963872 5.52399 0.666C4.882 0.821335 4.26872 1.07752 3.70699 1.425C3.83599 1.71551 3.87394 2.03828 3.81586 2.35079C3.75778 2.66329 3.60638 2.95087 3.38162 3.17563C3.15686 3.40039 2.86928 3.55178 2.55678 3.60987C2.24427 3.66795 1.9215 3.62999 1.63099 3.501C1.28471 4.06442 1.02957 4.67899 0.874993 5.322C1.17184 5.43574 1.42719 5.63685 1.60733 5.89878C1.78746 6.1607 1.8839 6.47111 1.8839 6.789C1.8839 7.10689 1.78746 7.4173 1.60733 7.67922C1.42719 7.94115 1.17184 8.14226 0.874993 8.256C1.02899 8.8992 1.28417 9.51385 1.63099 10.077C1.92149 9.94858 2.24405 9.91106 2.55627 9.96935C2.86849 10.0276 3.15577 10.179 3.38036 10.4036C3.60495 10.6282 3.75634 10.9155 3.81464 11.2277C3.87294 11.5399 3.83541 11.8625 3.70699 12.153C4.26924 12.4985 4.88243 12.7533 5.52399 12.908C5.6399 12.6107 5.84296 12.3554 6.10653 12.1755C6.37011 11.9956 6.68189 11.8996 7.00099 11.9ZM4.45399 6.817C4.4536 6.31238 4.60287 5.81897 4.88294 5.3992C5.16301 4.97942 5.56128 4.65214 6.02738 4.45876C6.49348 4.26537 7.00646 4.21457 7.50144 4.31277C7.99642 4.41098 8.45115 4.65378 8.80811 5.01046C9.16508 5.36715 9.40824 5.82169 9.50683 6.31659C9.60542 6.81149 9.55502 7.32451 9.362 7.79076C9.16898 8.25701 8.84201 8.65554 8.42246 8.93594C8.00291 9.21634 7.50962 9.366 7.00499 9.366C6.67012 9.36613 6.3385 9.3003 6.02907 9.17228C5.71964 9.04425 5.43846 8.85653 5.20158 8.61983C4.96469 8.38313 4.77675 8.1021 4.64848 7.79277C4.52021 7.48344 4.45412 7.15187 4.45399 6.817Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
32
src/components/commons/icon/LoaderIcon.tsx
Normal file
32
src/components/commons/icon/LoaderIcon.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
export function LoaderIcon({
|
||||
youSpinMeRightRoundBabyRightRound = false,
|
||||
}: {
|
||||
youSpinMeRightRoundBabyRightRound?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
width="19"
|
||||
height="19"
|
||||
viewBox="0 0 19 19"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={classNames({
|
||||
'animate-spin': youSpinMeRightRoundBabyRightRound,
|
||||
})}
|
||||
>
|
||||
<path d="M16.0312 8.90625H13.0625C12.7359 8.90625 12.4688 9.17344 12.4688 9.5C12.4688 9.82656 12.7359 10.0938 13.0625 10.0938H16.0312C16.3578 10.0938 16.625 9.82656 16.625 9.5C16.625 9.17344 16.3578 8.90625 16.0312 8.90625Z" />
|
||||
<path d="M6.53125 9.5C6.53125 9.17344 6.26406 8.90625 5.9375 8.90625H2.96875C2.64219 8.90625 2.375 9.17344 2.375 9.5C2.375 9.82656 2.64219 10.0938 2.96875 10.0938H5.9375C6.26406 10.0938 6.53125 9.82656 6.53125 9.5Z" />
|
||||
<path d="M9.5 12.4688C9.17344 12.4688 8.90625 12.7359 8.90625 13.0625V16.0312C8.90625 16.3578 9.17344 16.625 9.5 16.625C9.82656 16.625 10.0938 16.3578 10.0938 16.0312V13.0625C10.0938 12.7359 9.82656 12.4688 9.5 12.4688Z" />
|
||||
<path d="M9.5 2.375C9.17344 2.375 8.90625 2.64219 8.90625 2.96875V5.9375C8.90625 6.26406 9.17344 6.53125 9.5 6.53125C9.82656 6.53125 10.0938 6.26406 10.0938 5.9375V2.96875C10.0938 2.64219 9.82656 2.375 9.5 2.375Z" />
|
||||
<path d="M12.0716 8.01562C12.2349 8.30137 12.5986 8.39785 12.8843 8.23457L15.456 6.75019C15.7417 6.58691 15.8382 6.22324 15.6749 5.9375C15.5117 5.65176 15.148 5.55527 14.8623 5.71855L12.2906 7.20293C12.0048 7.36992 11.9083 7.72988 12.0716 8.01562Z" />
|
||||
<path d="M6.92836 10.9844C6.76508 10.6986 6.40141 10.6021 6.11567 10.7654L3.54399 12.2498C3.25825 12.4131 3.16176 12.7768 3.32505 13.0625C3.48833 13.3482 3.852 13.4447 4.13774 13.2814L6.70942 11.7971C6.99516 11.6301 7.09165 11.2701 6.92836 10.9844Z" />
|
||||
<path d="M11.7971 12.2869C11.6338 12.0012 11.2702 11.9047 10.9844 12.068C10.6987 12.2312 10.6022 12.5949 10.7655 12.8807L12.2498 15.4523C12.4131 15.7381 12.7768 15.8346 13.0625 15.6713C13.3483 15.508 13.4448 15.1443 13.2815 14.8586L11.7971 12.2869Z" />
|
||||
<path d="M6.75024 3.54765C6.58696 3.26191 6.22329 3.16543 5.93754 3.32871C5.6518 3.49199 5.55532 3.85566 5.7186 4.1414L7.20297 6.71308C7.36626 6.99882 7.72993 7.09531 8.01567 6.93203C8.30141 6.76875 8.3979 6.40507 8.23461 6.11933L6.75024 3.54765Z" />
|
||||
<path d="M10.9844 6.92832C11.2702 7.0916 11.6301 6.99512 11.7971 6.70937L13.2815 4.13769C13.4448 3.85195 13.3483 3.49199 13.0625 3.325C12.7768 3.16172 12.4168 3.2582 12.2498 3.54394L10.7655 6.11562C10.6022 6.40137 10.6987 6.76504 10.9844 6.92832Z" />
|
||||
<path d="M8.01567 12.0717C7.72993 11.9084 7.36997 12.0049 7.20297 12.2906L5.7186 14.8623C5.55532 15.148 5.6518 15.508 5.93754 15.675C6.22329 15.8383 6.58325 15.7418 6.75024 15.456L8.23461 12.8844C8.3979 12.5986 8.30141 12.235 8.01567 12.0717Z" />
|
||||
<path d="M15.4523 12.2498L12.8807 10.7654C12.5949 10.6021 12.235 10.6986 12.068 10.9844C11.9047 11.2701 12.0012 11.6301 12.2869 11.7971L14.8586 13.2814C15.1443 13.4447 15.5043 13.3482 15.6713 13.0625C15.8346 12.7768 15.7381 12.4168 15.4523 12.2498Z" />
|
||||
<path d="M3.54765 6.75019L6.11933 8.23457C6.40507 8.39785 6.76504 8.30137 6.93203 8.01562C7.09902 7.72988 6.99882 7.36992 6.71308 7.20293L4.1414 5.71855C3.85566 5.55527 3.4957 5.65176 3.32871 5.9375C3.16543 6.22324 3.26191 6.5832 3.54765 6.75019Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,17 @@
|
||||
|
||||
|
||||
|
||||
export function MaximiseIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg width="35" height="35" className={className} viewBox="0 0 31 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 11.5H14C13.1716 11.5 12.5 12.1716 12.5 13V17C12.5 17.8284 13.1716 18.5 14 18.5H18C18.8284 18.5 19.5 17.8284 19.5 17V13C19.5 12.1716 18.8284 11.5 18 11.5Z" stroke="#C0A1D8"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
width="35"
|
||||
height="35"
|
||||
className={className}
|
||||
viewBox="0 0 31 29"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18 11.5H14C13.1716 11.5 12.5 12.1716 12.5 13V17C12.5 17.8284 13.1716 18.5 14 18.5H18C18.8284 18.5 19.5 17.8284 19.5 17V13C19.5 12.1716 18.8284 11.5 18 11.5Z"
|
||||
stroke="#C0A1D8"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
|
||||
|
||||
|
||||
export function MinimiseIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg width="35" height="35" className={className} viewBox="0 0 31 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.5 15.5H10.5" stroke="#C0A1D8" strokeLinecap="round"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
width="35"
|
||||
height="35"
|
||||
className={className}
|
||||
viewBox="0 0 31 29"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M20.5 15.5H10.5" stroke="#C0A1D8" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
|
||||
|
||||
export function RecordIcon() {
|
||||
return (
|
||||
<svg width="33" height="29" viewBox="0 0 24 24">
|
||||
<path d="M20.84 2.18L16.91 2.96L19.65 6.5L21.62 6.1L20.84 2.18M13.97 3.54L12 3.93L14.75 7.46L16.71 7.07L13.97 3.54M9.07 4.5L7.1 4.91L9.85 8.44L11.81 8.05L9.07 4.5M4.16 5.5L3.18 5.69A2 2 0 0 0 1.61 8.04L2 10L6.9 9.03L4.16 5.5M2 10V20C2 21.11 2.9 22 4 22H20C21.11 22 22 21.11 22 20V10H2Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<svg width="33" height="29" viewBox="0 0 24 24">
|
||||
<path d="M20.84 2.18L16.91 2.96L19.65 6.5L21.62 6.1L20.84 2.18M13.97 3.54L12 3.93L14.75 7.46L16.71 7.07L13.97 3.54M9.07 4.5L7.1 4.91L9.85 8.44L11.81 8.05L9.07 4.5M4.16 5.5L3.18 5.69A2 2 0 0 0 1.61 8.04L2 10L6.9 9.03L4.16 5.5M2 10V20C2 21.11 2.9 22 4 22H20C21.11 22 22 21.11 22 20V10H2Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,57 @@
|
||||
|
||||
|
||||
export function ResetIcon() {
|
||||
return (
|
||||
<svg width="33" height="29" viewBox="0 0 33 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.39801 3.061V11.524H10.861" stroke="white" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M5.93801 18.576C6.85461 21.1713 8.58991 23.3983 10.8824 24.9215C13.1749 26.4447 15.9003 27.1816 18.648 27.021C21.3957 26.8604 24.0168 25.8111 26.1162 24.0312C28.2157 22.2514 29.6797 19.8373 30.2878 17.153C30.8958 14.4686 30.6149 11.6593 29.4874 9.14845C28.3599 6.63762 26.4468 4.56127 24.0365 3.23233C21.6262 1.90339 18.8493 1.39384 16.1242 1.7805C13.3991 2.16715 10.8735 3.42904 8.92801 5.376L2.39301 11.523" stroke="white" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
)
|
||||
return (
|
||||
<svg
|
||||
width="33"
|
||||
height="29"
|
||||
viewBox="0 0 33 29"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.39801 3.061V11.524H10.861"
|
||||
stroke="white"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M5.93801 18.576C6.85461 21.1713 8.58991 23.3983 10.8824 24.9215C13.1749 26.4447 15.9003 27.1816 18.648 27.021C21.3957 26.8604 24.0168 25.8111 26.1162 24.0312C28.2157 22.2514 29.6797 19.8373 30.2878 17.153C30.8958 14.4686 30.6149 11.6593 29.4874 9.14845C28.3599 6.63762 26.4468 4.56127 24.0365 3.23233C21.6262 1.90339 18.8493 1.39384 16.1242 1.7805C13.3991 2.16715 10.8735 3.42904 8.92801 5.376L2.39301 11.523"
|
||||
stroke="white"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function QuickResetIcon() {
|
||||
return (
|
||||
<svg width="33" height="29" viewBox="0 0 33 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.39804 3.04599V11.509H10.861" stroke="white" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M5.93804 18.561C6.85464 21.1563 8.58994 23.3833 10.8824 24.9065C13.1749 26.4297 15.9003 27.1666 18.648 27.006C21.3957 26.8454 24.0168 25.7961 26.1163 24.0162C28.2157 22.2363 29.6798 19.8223 30.2878 17.1379C30.8959 14.4536 30.615 11.6443 29.4874 9.13344C28.3599 6.6226 26.4468 4.54626 24.0365 3.21732C21.6262 1.88837 18.8493 1.37883 16.1242 1.76548C13.3991 2.15213 10.8735 3.41402 8.92804 5.36099L2.39304 11.508" stroke="white" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M16.633 9.33699C17.879 11.837 14.133 13.083 14.133 15.581C14.133 18.079 16.633 19.327 16.633 19.327C15.407 16.855 19.133 15.581 19.133 13.083C19.133 10.585 16.633 9.33699 16.633 9.33699ZM20.377 13.083C21.626 15.583 17.877 16.829 17.877 19.327H21.626C22.126 19.327 22.875 18.703 22.875 16.827C22.875 14.337 20.377 13.083 20.377 13.083Z" fill="white"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
width="33"
|
||||
height="29"
|
||||
viewBox="0 0 33 29"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.39804 3.04599V11.509H10.861"
|
||||
stroke="white"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M5.93804 18.561C6.85464 21.1563 8.58994 23.3833 10.8824 24.9065C13.1749 26.4297 15.9003 27.1666 18.648 27.006C21.3957 26.8454 24.0168 25.7961 26.1163 24.0162C28.2157 22.2363 29.6798 19.8223 30.2878 17.1379C30.8959 14.4536 30.615 11.6443 29.4874 9.13344C28.3599 6.6226 26.4468 4.54626 24.0365 3.21732C21.6262 1.88837 18.8493 1.37883 16.1242 1.76548C13.3991 2.15213 10.8735 3.41402 8.92804 5.36099L2.39304 11.508"
|
||||
stroke="white"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M16.633 9.33699C17.879 11.837 14.133 13.083 14.133 15.581C14.133 18.079 16.633 19.327 16.633 19.327C15.407 16.855 19.133 15.581 19.133 13.083C19.133 10.585 16.633 9.33699 16.633 9.33699ZM20.377 13.083C21.626 15.583 17.877 16.829 17.877 19.327H21.626C22.126 19.327 22.875 18.703 22.875 16.827C22.875 14.337 20.377 13.083 20.377 13.083Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
|
||||
|
||||
|
||||
export function SlimeVRIcon({ drag }: { drag?: boolean }) {
|
||||
return (
|
||||
<svg width="49" height="29" viewBox="0 0 49 29" fill="none" xmlns="http://www.w3.org/2000/svg" data-tauri-drag-region={drag}>
|
||||
<path d="M2 26.996C10.44 25.59 29.16 23.1571 46.509 26.9091C46.509 26.9091 48.89 -0.199966 35.761 2.14503" stroke="#A44FED" strokeWidth="3" strokeLinecap="round"/>
|
||||
<path d="M7.52161 15.0107L12.3649 9.20459L17.5044 13.9572" stroke="#A44FED" strokeWidth="3.00157" strokeLinecap="round"/>
|
||||
<path d="M27.9566 14.1435L33.7372 9.27062L37.9695 14.8458" stroke="#A44FED" strokeWidth="3.00136" strokeLinecap="round"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
width="49"
|
||||
height="29"
|
||||
viewBox="0 0 49 29"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
data-tauri-drag-region={drag}
|
||||
>
|
||||
<path
|
||||
d="M2 26.996C10.44 25.59 29.16 23.1571 46.509 26.9091C46.509 26.9091 48.89 -0.199966 35.761 2.14503"
|
||||
stroke="#A44FED"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M7.52161 15.0107L12.3649 9.20459L17.5044 13.9572"
|
||||
stroke="#A44FED"
|
||||
strokeWidth="3.00157"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M27.9566 14.1435L33.7372 9.27062L37.9695 14.8458"
|
||||
stroke="#A44FED"
|
||||
strokeWidth="3.00136"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
15
src/components/commons/icon/SquaresIcon.tsx
Normal file
15
src/components/commons/icon/SquaresIcon.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
export function SquaresIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M6.425 0.125H0.125V6.425H6.425V0.125Z" />
|
||||
<path d="M6.425 9.57495H0.125V15.875H6.425V9.57495Z" />
|
||||
<path d="M9.57501 9.57495H15.875V15.875H9.57501V9.57495Z" />
|
||||
<path d="M15.875 0.125H9.57501V6.425H15.875V0.125Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
12
src/components/commons/icon/SteamIcon.tsx
Normal file
12
src/components/commons/icon/SteamIcon.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export function SteamIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="18"
|
||||
viewBox="0 0 20 18"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M17.6562 5.32031C17.6562 6.10938 17.3903 6.7832 16.8583 7.3418C16.3263 7.90039 15.6845 8.17969 14.933 8.17969C14.1815 8.17969 13.5398 7.90039 13.0078 7.3418C12.4758 6.7832 12.2098 6.10938 12.2098 5.32031C12.2098 4.53125 12.4758 3.85742 13.0078 3.29883C13.5398 2.74023 14.1815 2.46094 14.933 2.46094C15.6845 2.46094 16.3263 2.74023 16.8583 3.29883C17.3903 3.85742 17.6562 4.53125 17.6562 5.32031ZM9.0625 14.0156C9.0625 13.2031 8.79092 12.5117 8.24777 11.9414C7.70461 11.3711 7.04613 11.0859 6.27232 11.0859C6.07143 11.0859 5.87054 11.1094 5.66964 11.1562L6.83036 11.6484C7.40327 11.8906 7.81064 12.3066 8.05246 12.8965C8.29427 13.4863 8.29985 14.0781 8.0692 14.6719C7.83854 15.2734 7.44048 15.6992 6.875 15.9492C6.30952 16.1992 5.74405 16.2031 5.17857 15.9609C5.02232 15.8984 4.79167 15.8027 4.48661 15.6738C4.18155 15.5449 3.95461 15.4492 3.8058 15.3867C4.0439 15.8555 4.38244 16.2324 4.82143 16.5176C5.26042 16.8027 5.74405 16.9453 6.27232 16.9453C7.04613 16.9453 7.70461 16.6602 8.24777 16.0898C8.79092 15.5195 9.0625 14.8281 9.0625 14.0156ZM18.3259 5.33203C18.3259 4.34766 17.9929 3.50586 17.327 2.80664C16.6611 2.10742 15.8594 1.75781 14.9219 1.75781C13.9769 1.75781 13.1715 2.10742 12.5056 2.80664C11.8397 3.50586 11.5067 4.34766 11.5067 5.33203C11.5067 6.32422 11.8397 7.16797 12.5056 7.86328C13.1715 8.55859 13.9769 8.90625 14.9219 8.90625C15.8594 8.90625 16.6611 8.55859 17.327 7.86328C17.9929 7.16797 18.3259 6.32422 18.3259 5.33203ZM20 5.33203C20 6.80859 19.5033 8.06641 18.51 9.10547C17.5167 10.1445 16.3207 10.6641 14.9219 10.6641L10.0446 14.4023C9.95536 15.4102 9.54985 16.2617 8.82812 16.957C8.1064 17.6523 7.25446 18 6.27232 18C5.37202 18 4.57589 17.7031 3.88393 17.1094C3.19196 16.5156 2.75298 15.7656 2.56696 14.8594L0 13.7812V8.75391L4.34152 10.5938C4.92932 10.2188 5.57292 10.0312 6.27232 10.0312C6.36905 10.0312 6.49926 10.0391 6.66295 10.0547L9.83259 5.28516C9.84747 3.82422 10.3516 2.57812 11.3449 1.54688C12.3382 0.515625 13.5305 0 14.9219 0C16.3207 0 17.5167 0.521484 18.51 1.56445C19.5033 2.60742 20 3.86328 20 5.33203Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +1,68 @@
|
||||
import classNames from "classnames";
|
||||
import { useMemo } from "react"
|
||||
import classNames from 'classnames';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function WifiIcon({
|
||||
value,
|
||||
disabled = false,
|
||||
}: {
|
||||
value: number;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const percent = useMemo(
|
||||
() =>
|
||||
value
|
||||
? Math.max(
|
||||
Math.min(((value - -95) * (100 - 0)) / (-40 - -95) + 0, 100)
|
||||
) / 100
|
||||
: 0,
|
||||
[value]
|
||||
);
|
||||
|
||||
export function WifiIcon({ value }: { value: number }) {
|
||||
const y = useMemo(() => (percent ? (1 - percent) * 13 : 0), [percent]);
|
||||
|
||||
const percent = useMemo(() => value ? Math.max(Math.min((value - -95) * (100 - 0) / (-40 - -95) + 0, 100)) / 100 : 0, [value])
|
||||
const col = useMemo(() => {
|
||||
const colorsMap: { [key: number]: string } = {
|
||||
0.4: 'fill-status-success',
|
||||
0.2: 'fill-status-warning',
|
||||
0: 'fill-status-critical',
|
||||
};
|
||||
|
||||
const y = useMemo(() => percent ? (1 - percent) * 13 : 0 , [percent])
|
||||
const val = Object.keys(colorsMap)
|
||||
.filter((key) => +key < percent)
|
||||
.sort((a, b) => +b - +a)[0];
|
||||
return disabled
|
||||
? 'fill-background-40'
|
||||
: colorsMap[+val] || 'fill-background-10';
|
||||
}, [percent, disabled]);
|
||||
|
||||
const col = useMemo(() => {
|
||||
|
||||
const colorsMap: { [key: number]: string } = {
|
||||
0.4: 'fill-status-online',
|
||||
0.2: 'fill-status-warning',
|
||||
0: 'fill-status-error',
|
||||
}
|
||||
|
||||
const val = Object.keys(colorsMap).filter(key => +key < percent).sort((a, b) => +b - +a)[0];
|
||||
return colorsMap[+val] || 'fill-primary-gray-600';
|
||||
}, [percent])
|
||||
|
||||
return (
|
||||
<svg width="16" height="13" viewBox="0 0 16 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.799 12.378L15.585 2.67801C13.3492 0.95947 10.6129 0.01903 7.793 1.00136e-05C4.9725 0.0172 2.23528 0.95782 0 2.67801L7.786 12.378L7.793 12.385L7.799 12.378Z" fill="#4C3755"/>
|
||||
<mask id="mask0_0_1" style={{ 'maskType': 'alpha', }} maskUnits="userSpaceOnUse" x="0" width="16" height="13" className={classNames(col, 'opacity-100')} >
|
||||
<path d="M0 2.712L7.782 12.392V12.407L7.795 12.396L15.577 2.716C13.3449 0.980306 10.6044 0.026036 7.777 0C4.95656 0.021826 2.22242 0.975276 0 2.712Z"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_0_1)" className={classNames(col)}>
|
||||
<path style={{ transform: `translateY(${y}px)`}} d="M0 2.712L7.782 12.392V12.407L7.795 12.396L15.577 2.716C13.3449 0.980306 10.6044 0.026036 7.777 0C4.95656 0.021826 2.22242 0.975276 0 2.712Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="13"
|
||||
viewBox="0 0 16 13"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7.799 12.378L15.585 2.67801C13.3492 0.95947 10.6129 0.01903 7.793 1.00136e-05C4.9725 0.0172 2.23528 0.95782 0 2.67801L7.786 12.378L7.793 12.385L7.799 12.378Z"
|
||||
fill="#3D6381"
|
||||
/>
|
||||
<mask
|
||||
id="mask0_0_1"
|
||||
style={{ maskType: 'alpha' }}
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
width="16"
|
||||
height="13"
|
||||
className={classNames(col, 'opacity-100')}
|
||||
>
|
||||
<path d="M0 2.712L7.782 12.392V12.407L7.795 12.396L15.577 2.716C13.3449 0.980306 10.6044 0.026036 7.777 0C4.95656 0.021826 2.22242 0.975276 0 2.712Z" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_0_1)" className={classNames(col)}>
|
||||
<path
|
||||
style={{ transform: `translateY(${y}px)` }}
|
||||
d="M0 2.712L7.782 12.392V12.407L7.795 12.396L15.577 2.716C13.3449 0.980306 10.6044 0.026036 7.777 0C4.95656 0.021826 2.22242 0.975276 0 2.712Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
12
src/components/commons/icon/WrenchIcons.tsx
Normal file
12
src/components/commons/icon/WrenchIcons.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export function WrenchIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M16.4015 0.321751C14.4783 -0.370266 12.2436 0.0539342 10.7026 1.595C9.1615 3.13605 8.73728 5.3707 9.42928 7.29395L0 16.7233L3.27673 20L12.706 10.5707C14.6293 11.2627 16.864 10.8385 18.405 9.2974C19.9461 7.75635 20.3703 5.5217 19.6783 3.59845L16.3624 6.91435L14.0006 5.99942L13.0856 3.63762L16.4015 0.321751ZM3.93633 16.0637C4.31238 16.4397 4.31238 17.0494 3.93633 17.4254C3.5603 17.8015 2.95062 17.8015 2.57458 17.4254C2.19853 17.0494 2.19853 16.4397 2.57458 16.0637C2.95062 15.6876 3.5603 15.6876 3.93633 16.0637Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
56
src/components/home/Home.tsx
Normal file
56
src/components/home/Home.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { TrackerDataT } from 'solarxr-protocol';
|
||||
import { useConfig } from '../../hooks/config';
|
||||
import { useTrackers } from '../../hooks/tracker';
|
||||
import { Typography } from '../commons/Typography';
|
||||
import { TrackerCard } from '../tracker/TrackerCard';
|
||||
import { TrackersTable } from '../tracker/TrackersTable';
|
||||
|
||||
export function Home() {
|
||||
const { config } = useConfig();
|
||||
const { useAssignedTrackers } = useTrackers();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const asignedTrackers = useAssignedTrackers();
|
||||
|
||||
const sendToSettings = (tracker: TrackerDataT) => {
|
||||
navigate(
|
||||
`/tracker/${tracker.trackerId?.trackerNum}/${tracker.trackerId?.deviceId?.id}`
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto flex flex-col gap-2">
|
||||
{asignedTrackers.length === 0 && (
|
||||
<div className="flex px-5 pt-5 justify-center">
|
||||
<Typography variant="standard">
|
||||
No trackers detected or assigned
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!config?.debug && (
|
||||
<div className="grid sm:grid-cols-1 md:grid-cols-2 gap-3 px-4 my-4">
|
||||
{asignedTrackers.map(({ tracker, device }, index) => (
|
||||
<TrackerCard
|
||||
key={index}
|
||||
tracker={tracker}
|
||||
device={device}
|
||||
onClick={() => sendToSettings(tracker)}
|
||||
smol
|
||||
interactable
|
||||
/>
|
||||
))}{' '}
|
||||
</div>
|
||||
)}
|
||||
{config?.debug && (
|
||||
<div className="flex px-5 pt-5 justify-center overflow-x-auto">
|
||||
<TrackersTable
|
||||
flatTrackers={asignedTrackers}
|
||||
clickedTracker={(tracker) => sendToSettings(tracker)}
|
||||
></TrackersTable>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
50
src/components/home/ResetButton.tsx
Normal file
50
src/components/home/ResetButton.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { ResetRequestT, ResetType, RpcMessage } from 'solarxr-protocol';
|
||||
import { useWebsocketAPI } from '../../hooks/websocket-api';
|
||||
import { BigButton } from '../commons/BigButton';
|
||||
import { QuickResetIcon, ResetIcon } from '../commons/icon/ResetIcon';
|
||||
|
||||
export function ResetButton({ type }: { type: ResetType }) {
|
||||
const timerid = useRef<NodeJS.Timer | null>(null);
|
||||
const [reseting, setReseting] = useState(false);
|
||||
const [timer, setTimer] = useState(0);
|
||||
const { sendRPCPacket } = useWebsocketAPI();
|
||||
|
||||
const reset = () => {
|
||||
const req = new ResetRequestT();
|
||||
req.resetType = type;
|
||||
setReseting(true);
|
||||
if (type !== ResetType.Quick) {
|
||||
if (timerid.current) clearInterval(timerid.current);
|
||||
timerid.current = setInterval(() => {
|
||||
setTimer((timer) => {
|
||||
if (timer + 1 === 3) {
|
||||
if (timerid.current) clearInterval(timerid.current);
|
||||
sendRPCPacket(RpcMessage.ResetRequest, req);
|
||||
setTimer(0);
|
||||
setReseting(false);
|
||||
}
|
||||
return timer + 1;
|
||||
});
|
||||
}, 1000);
|
||||
} else {
|
||||
sendRPCPacket(RpcMessage.ResetRequest, req);
|
||||
setReseting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BigButton
|
||||
text={
|
||||
!reseting
|
||||
? type === ResetType.Quick
|
||||
? 'Quick Reset'
|
||||
: 'Reset'
|
||||
: `${3 - timer}`
|
||||
}
|
||||
icon={type === ResetType.Quick ? <QuickResetIcon /> : <ResetIcon />}
|
||||
onClick={reset}
|
||||
disabled={reseting}
|
||||
></BigButton>
|
||||
);
|
||||
}
|
||||
250
src/components/onboarding/BodyAssignment.tsx
Normal file
250
src/components/onboarding/BodyAssignment.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import { useMemo } from 'react';
|
||||
import { BodyPart } from 'solarxr-protocol';
|
||||
import { FlatDeviceTracker } from '../../hooks/app';
|
||||
import { useTrackers } from '../../hooks/tracker';
|
||||
import { BodyInteractions } from '../commons/BodyInteractions';
|
||||
import { TrackerPartCard } from './TrackerPartCard';
|
||||
|
||||
export function BodyAssignment({
|
||||
advanced,
|
||||
onRoleSelected,
|
||||
onlyAssigned = false,
|
||||
}: {
|
||||
advanced: boolean;
|
||||
onlyAssigned: boolean;
|
||||
onRoleSelected: (role: BodyPart) => void;
|
||||
}) {
|
||||
const { useAssignedTrackers } = useTrackers();
|
||||
|
||||
const assignedTrackers = useAssignedTrackers();
|
||||
|
||||
const trackerPartGrouped = useMemo(
|
||||
() =>
|
||||
assignedTrackers.reduce<{ [key: number]: FlatDeviceTracker[] }>(
|
||||
(curr, td) => {
|
||||
if (!td && onlyAssigned) return curr;
|
||||
|
||||
const key = td.tracker.info?.bodyPart || BodyPart.NONE;
|
||||
return {
|
||||
...curr,
|
||||
[key]: [...(curr[key] || []), td],
|
||||
};
|
||||
},
|
||||
{}
|
||||
),
|
||||
[assignedTrackers]
|
||||
);
|
||||
|
||||
const assignedRoles = useMemo(
|
||||
() =>
|
||||
assignedTrackers.map(
|
||||
({ tracker }) => tracker.info?.bodyPart || BodyPart.NONE,
|
||||
{}
|
||||
),
|
||||
[assignedTrackers]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<BodyInteractions
|
||||
assignedRoles={assignedRoles}
|
||||
leftControls={
|
||||
<div className="flex flex-col justify-between h-full">
|
||||
<div className="flex flex-col gap-3">
|
||||
{advanced && (
|
||||
<TrackerPartCard
|
||||
label="HMD"
|
||||
onlyAssigned={onlyAssigned}
|
||||
td={trackerPartGrouped[BodyPart.HMD]}
|
||||
role={BodyPart.HMD}
|
||||
onClick={() => onRoleSelected(BodyPart.HMD)}
|
||||
direction="right"
|
||||
/>
|
||||
)}
|
||||
{advanced && (
|
||||
<TrackerPartCard
|
||||
label="NECK"
|
||||
onlyAssigned={onlyAssigned}
|
||||
td={trackerPartGrouped[BodyPart.NECK]}
|
||||
role={BodyPart.NECK}
|
||||
onClick={() => onRoleSelected(BodyPart.NECK)}
|
||||
direction="right"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{advanced && (
|
||||
<TrackerPartCard
|
||||
label="RIGHT SHOULDER"
|
||||
onlyAssigned={onlyAssigned}
|
||||
td={trackerPartGrouped[BodyPart.RIGHT_SHOULDER]}
|
||||
role={BodyPart.RIGHT_SHOULDER}
|
||||
onClick={() => onRoleSelected(BodyPart.RIGHT_SHOULDER)}
|
||||
direction="right"
|
||||
/>
|
||||
)}
|
||||
<TrackerPartCard
|
||||
label="RIGHT UPPER ARM"
|
||||
onlyAssigned={onlyAssigned}
|
||||
td={trackerPartGrouped[BodyPart.RIGHT_UPPER_ARM]}
|
||||
role={BodyPart.RIGHT_UPPER_ARM}
|
||||
onClick={() => onRoleSelected(BodyPart.RIGHT_UPPER_ARM)}
|
||||
direction="right"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<TrackerPartCard
|
||||
label="RIGHT LOWER ARM"
|
||||
onlyAssigned={onlyAssigned}
|
||||
td={trackerPartGrouped[BodyPart.RIGHT_LOWER_ARM]}
|
||||
role={BodyPart.RIGHT_LOWER_ARM}
|
||||
onClick={() => onRoleSelected(BodyPart.RIGHT_LOWER_ARM)}
|
||||
direction="right"
|
||||
/>
|
||||
|
||||
{advanced && (
|
||||
<TrackerPartCard
|
||||
label="RIGHT HAND"
|
||||
onlyAssigned={onlyAssigned}
|
||||
td={trackerPartGrouped[BodyPart.RIGHT_HAND]}
|
||||
role={BodyPart.RIGHT_HAND}
|
||||
onClick={() => onRoleSelected(BodyPart.RIGHT_HAND)}
|
||||
direction="right"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<TrackerPartCard
|
||||
label="RIGHT_UPPER_LEG"
|
||||
onlyAssigned={onlyAssigned}
|
||||
td={trackerPartGrouped[BodyPart.RIGHT_UPPER_LEG]}
|
||||
role={BodyPart.RIGHT_UPPER_LEG}
|
||||
onClick={() => onRoleSelected(BodyPart.RIGHT_UPPER_LEG)}
|
||||
direction="right"
|
||||
/>
|
||||
|
||||
<TrackerPartCard
|
||||
label="RIGHT LOWER LEG"
|
||||
onlyAssigned={onlyAssigned}
|
||||
td={trackerPartGrouped[BodyPart.RIGHT_LOWER_LEG]}
|
||||
role={BodyPart.RIGHT_LOWER_LEG}
|
||||
onClick={() => onRoleSelected(BodyPart.RIGHT_LOWER_LEG)}
|
||||
direction="right"
|
||||
/>
|
||||
<TrackerPartCard
|
||||
label="RIGHT FOOT"
|
||||
onlyAssigned={onlyAssigned}
|
||||
td={trackerPartGrouped[BodyPart.RIGHT_FOOT]}
|
||||
role={BodyPart.RIGHT_FOOT}
|
||||
onClick={() => onRoleSelected(BodyPart.RIGHT_FOOT)}
|
||||
direction="right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
rightControls={
|
||||
<div className="flex flex-col justify-between h-full">
|
||||
<TrackerPartCard
|
||||
label="CHEST"
|
||||
onlyAssigned={onlyAssigned}
|
||||
td={trackerPartGrouped[BodyPart.CHEST]}
|
||||
role={BodyPart.CHEST}
|
||||
onClick={() => onRoleSelected(BodyPart.CHEST)}
|
||||
direction="left"
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{advanced && (
|
||||
<TrackerPartCard
|
||||
label="LEFT SHOULDER"
|
||||
onlyAssigned={onlyAssigned}
|
||||
td={trackerPartGrouped[BodyPart.LEFT_SHOULDER]}
|
||||
role={BodyPart.LEFT_SHOULDER}
|
||||
onClick={() => onRoleSelected(BodyPart.LEFT_SHOULDER)}
|
||||
direction="left"
|
||||
/>
|
||||
)}
|
||||
|
||||
<TrackerPartCard
|
||||
label="LEFT UPPER ARM"
|
||||
onlyAssigned={onlyAssigned}
|
||||
td={trackerPartGrouped[BodyPart.LEFT_UPPER_ARM]}
|
||||
role={BodyPart.LEFT_UPPER_ARM}
|
||||
onClick={() => onRoleSelected(BodyPart.LEFT_UPPER_ARM)}
|
||||
direction="left"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<TrackerPartCard
|
||||
label="LEFT LOWER ARM"
|
||||
onlyAssigned={onlyAssigned}
|
||||
td={trackerPartGrouped[BodyPart.LEFT_LOWER_ARM]}
|
||||
role={BodyPart.LEFT_LOWER_ARM}
|
||||
onClick={() => onRoleSelected(BodyPart.LEFT_LOWER_ARM)}
|
||||
direction="left"
|
||||
/>
|
||||
{advanced && (
|
||||
<TrackerPartCard
|
||||
label="LEFT HAND"
|
||||
onlyAssigned={onlyAssigned}
|
||||
td={trackerPartGrouped[BodyPart.LEFT_HAND]}
|
||||
onClick={() => onRoleSelected(BodyPart.LEFT_HAND)}
|
||||
role={BodyPart.LEFT_HAND}
|
||||
direction="left"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<TrackerPartCard
|
||||
label="WAIST"
|
||||
onlyAssigned={onlyAssigned}
|
||||
td={trackerPartGrouped[BodyPart.WAIST]}
|
||||
onClick={() => onRoleSelected(BodyPart.WAIST)}
|
||||
role={BodyPart.WAIST}
|
||||
direction="left"
|
||||
/>
|
||||
<TrackerPartCard
|
||||
label="HIP"
|
||||
onlyAssigned={onlyAssigned}
|
||||
td={trackerPartGrouped[BodyPart.HIP]}
|
||||
onClick={() => onRoleSelected(BodyPart.HIP)}
|
||||
role={BodyPart.HIP}
|
||||
direction="left"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<TrackerPartCard
|
||||
label="LEFT UPPER LEG"
|
||||
onlyAssigned={onlyAssigned}
|
||||
td={trackerPartGrouped[BodyPart.LEFT_UPPER_LEG]}
|
||||
role={BodyPart.LEFT_UPPER_LEG}
|
||||
onClick={() => onRoleSelected(BodyPart.LEFT_UPPER_LEG)}
|
||||
direction="left"
|
||||
/>
|
||||
|
||||
<TrackerPartCard
|
||||
label="LEFT LOWER LEG"
|
||||
onlyAssigned={onlyAssigned}
|
||||
td={trackerPartGrouped[BodyPart.LEFT_LOWER_LEG]}
|
||||
role={BodyPart.LEFT_LOWER_LEG}
|
||||
onClick={() => onRoleSelected(BodyPart.LEFT_LOWER_LEG)}
|
||||
direction="left"
|
||||
/>
|
||||
<TrackerPartCard
|
||||
label="LEFT FOOT"
|
||||
onlyAssigned={onlyAssigned}
|
||||
td={trackerPartGrouped[BodyPart.LEFT_FOOT]}
|
||||
role={BodyPart.LEFT_FOOT}
|
||||
onClick={() => onRoleSelected(BodyPart.LEFT_FOOT)}
|
||||
direction="left"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
></BodyInteractions>
|
||||
</>
|
||||
);
|
||||
}
|
||||
19
src/components/onboarding/OnboardingContextProvicer.tsx
Normal file
19
src/components/onboarding/OnboardingContextProvicer.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ReactChild } from 'react';
|
||||
import {
|
||||
OnboardingContextC,
|
||||
useProvideOnboarding,
|
||||
} from '../../hooks/onboarding';
|
||||
|
||||
export function OnboardingContextProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactChild;
|
||||
}) {
|
||||
const context = useProvideOnboarding();
|
||||
|
||||
return (
|
||||
<OnboardingContextC.Provider value={context}>
|
||||
{children}
|
||||
</OnboardingContextC.Provider>
|
||||
);
|
||||
}
|
||||
27
src/components/onboarding/OnboardingLayout.tsx
Normal file
27
src/components/onboarding/OnboardingLayout.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ReactChild } from 'react';
|
||||
import { useLayout } from '../../hooks/layout';
|
||||
import { useOnboarding } from '../../hooks/onboarding';
|
||||
import { MainLayoutRoute } from '../MainLayout';
|
||||
import { TopBar } from '../TopBar';
|
||||
|
||||
export function OnboardingLayout({ children }: { children: ReactChild }) {
|
||||
const { layoutHeight, ref } = useLayout<HTMLDivElement>();
|
||||
const { state } = useOnboarding();
|
||||
|
||||
return !state.alonePage ? (
|
||||
<>
|
||||
<TopBar progress={state.progress}></TopBar>
|
||||
<div
|
||||
ref={ref}
|
||||
className="flex-grow pt-10 mx-4"
|
||||
style={{ height: layoutHeight }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<MainLayoutRoute widgets={false}>
|
||||
<div className="flex-grow pt-10 mx-4">{children}</div>
|
||||
</MainLayoutRoute>
|
||||
);
|
||||
}
|
||||
92
src/components/onboarding/TrackerPartCard.tsx
Normal file
92
src/components/onboarding/TrackerPartCard.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import classNames from 'classnames';
|
||||
import { MouseEventHandler, useEffect, useMemo, useState } from 'react';
|
||||
import { BodyPart, TrackerDataT } from 'solarxr-protocol';
|
||||
import { FlatDeviceTracker } from '../../hooks/app';
|
||||
import { useTracker } from '../../hooks/tracker';
|
||||
import { Typography } from '../commons/Typography';
|
||||
|
||||
function Tracker({
|
||||
tracker,
|
||||
updateVelocity,
|
||||
}: {
|
||||
tracker: TrackerDataT;
|
||||
updateVelocity: (velocity: number) => void;
|
||||
}) {
|
||||
const { useVelocity } = useTracker(tracker);
|
||||
|
||||
const velocity = useVelocity();
|
||||
|
||||
useEffect(() => {
|
||||
updateVelocity(velocity);
|
||||
}, [velocity]);
|
||||
|
||||
return (
|
||||
<Typography>
|
||||
{`${tracker.info?.customName || tracker.info?.displayName}` || 'no name'}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
export function TrackerPartCard({
|
||||
td,
|
||||
label,
|
||||
role,
|
||||
direction,
|
||||
onlyAssigned,
|
||||
onClick,
|
||||
}: {
|
||||
td: FlatDeviceTracker[];
|
||||
label: string;
|
||||
role: BodyPart;
|
||||
onlyAssigned: boolean;
|
||||
direction: 'left' | 'right';
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
}) {
|
||||
const [velocities, setVelocities] = useState<number[]>([]);
|
||||
|
||||
const updateVelocity = (vel: number) => {
|
||||
if (velocities.length > 3) {
|
||||
velocities.shift();
|
||||
}
|
||||
velocities.push(vel);
|
||||
setVelocities(velocities);
|
||||
};
|
||||
|
||||
const globalVelocity = useMemo(
|
||||
() => velocities.reduce((curr, v) => curr + v, 0) / (td?.length || 1),
|
||||
[velocities, td]
|
||||
);
|
||||
|
||||
const showCard = useMemo(
|
||||
() => (onlyAssigned && td && td.length > 0) || !onlyAssigned,
|
||||
[onlyAssigned, td]
|
||||
);
|
||||
|
||||
return (
|
||||
(showCard && (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col gap-1 control w-32 hover:bg-background-50 p-2 rounded-md',
|
||||
direction === 'left' ? 'items-start' : 'items-end'
|
||||
)}
|
||||
id={BodyPart[role]}
|
||||
onClick={onClick}
|
||||
style={{
|
||||
boxShadow: `0px 0px ${globalVelocity * 3}px ${
|
||||
globalVelocity * 3
|
||||
}px #183951`,
|
||||
}}
|
||||
>
|
||||
<Typography color="secondary">{label}</Typography>
|
||||
{td?.map(({ tracker }, index) => (
|
||||
<Tracker
|
||||
tracker={tracker}
|
||||
key={index}
|
||||
updateVelocity={(vel) => updateVelocity(vel)}
|
||||
/>
|
||||
))}
|
||||
{!td && <Typography>Unassigned</Typography>}
|
||||
</div>
|
||||
)) || <></>
|
||||
);
|
||||
}
|
||||
224
src/components/onboarding/pages/ConnectTracker.tsx
Normal file
224
src/components/onboarding/pages/ConnectTracker.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
CloseSerialRequestT,
|
||||
OpenSerialRequestT,
|
||||
RpcMessage,
|
||||
SerialUpdateResponseT,
|
||||
SetWifiRequestT,
|
||||
} from 'solarxr-protocol';
|
||||
import { useLayout } from '../../../hooks/layout';
|
||||
import { useOnboarding } from '../../../hooks/onboarding';
|
||||
import { useTrackers } from '../../../hooks/tracker';
|
||||
import { useWebsocketAPI } from '../../../hooks/websocket-api';
|
||||
import { ArrowLink } from '../../commons/ArrowLink';
|
||||
import { Button } from '../../commons/Button';
|
||||
import { LoaderIcon } from '../../commons/icon/LoaderIcon';
|
||||
import { TipBox } from '../../commons/TipBox';
|
||||
import { Typography } from '../../commons/Typography';
|
||||
import { TrackerCard } from '../../tracker/TrackerCard';
|
||||
|
||||
const BOTTOM_HEIGHT = 80;
|
||||
type ConnectionStatus =
|
||||
| 'CONNECTING'
|
||||
| 'CONNECTED'
|
||||
| 'HANDSHAKE'
|
||||
| 'ERROR'
|
||||
| 'START-CONNECTING';
|
||||
|
||||
const statusLabelMap = {
|
||||
['CONNECTING']: 'Sending wifi credentials',
|
||||
['CONNECTED']: 'Connected to WiFi',
|
||||
['ERROR']: 'Unable to connect to Wifi',
|
||||
['START-CONNECTING']: 'Looking for trackers',
|
||||
['HANDSHAKE']: 'Connected to the Server',
|
||||
};
|
||||
|
||||
export function ConnectTrackersPage() {
|
||||
const { layoutHeight, ref } = useLayout<HTMLDivElement>();
|
||||
const { trackers, useConnectedTrackers } = useTrackers();
|
||||
const { applyProgress, state, skipSetup } = useOnboarding();
|
||||
const navigate = useNavigate();
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
const [isSerialOpen, setSerialOpen] = useState(false);
|
||||
const [connectionStatus, setConnectionStatus] =
|
||||
useState<ConnectionStatus>('START-CONNECTING');
|
||||
|
||||
applyProgress(0.4);
|
||||
|
||||
const connectedTrackers = useConnectedTrackers();
|
||||
|
||||
useEffect(() => {
|
||||
if (!state.wifi) {
|
||||
navigate('/onboarding/wifi-creds');
|
||||
}
|
||||
|
||||
sendRPCPacket(RpcMessage.OpenSerialRequest, new OpenSerialRequestT());
|
||||
return () => {
|
||||
sendRPCPacket(RpcMessage.CloseSerialRequest, new CloseSerialRequestT());
|
||||
};
|
||||
}, []);
|
||||
|
||||
useRPCPacket(
|
||||
RpcMessage.SerialUpdateResponse,
|
||||
(data: SerialUpdateResponseT) => {
|
||||
if (data.closed) {
|
||||
setSerialOpen(false);
|
||||
setConnectionStatus('START-CONNECTING');
|
||||
setTimeout(() => {
|
||||
sendRPCPacket(RpcMessage.OpenSerialRequest, new OpenSerialRequestT());
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
if (!data.closed && !isSerialOpen) {
|
||||
setSerialOpen(true);
|
||||
setConnectionStatus('START-CONNECTING');
|
||||
}
|
||||
|
||||
if (data.log) {
|
||||
const log = data.log as string;
|
||||
if (connectionStatus === 'START-CONNECTING' && state.wifi) {
|
||||
setConnectionStatus('CONNECTING');
|
||||
if (!state.wifi) return;
|
||||
const wifi = new SetWifiRequestT();
|
||||
wifi.ssid = state.wifi.ssid;
|
||||
wifi.password = state.wifi.password;
|
||||
sendRPCPacket(RpcMessage.SetWifiRequest, wifi);
|
||||
}
|
||||
|
||||
if (log.includes('Connected successfully to SSID')) {
|
||||
setConnectionStatus('CONNECTED');
|
||||
}
|
||||
|
||||
if (log.includes('Handshake successful')) {
|
||||
setConnectionStatus('HANDSHAKE');
|
||||
setTimeout(() => {
|
||||
setConnectionStatus('START-CONNECTING');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
if (
|
||||
// eslint-disable-next-line quotes
|
||||
log.includes("Can't connect from any credentials")
|
||||
) {
|
||||
setConnectionStatus('ERROR');
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => {
|
||||
if (!isSerialOpen)
|
||||
sendRPCPacket(RpcMessage.OpenSerialRequest, new OpenSerialRequestT());
|
||||
else clearInterval(id);
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(id);
|
||||
};
|
||||
}, [isSerialOpen, sendRPCPacket]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="flex gap-10 w-full max-w-5xl">
|
||||
<div className="flex flex-col w-96">
|
||||
<ArrowLink to="/onboarding/wifi-creds">
|
||||
Go Back to WiFi credentials
|
||||
</ArrowLink>
|
||||
<Typography variant="main-title">Connect trackers</Typography>
|
||||
<Typography color="secondary">
|
||||
Now onto the fun part, connecting all the trackers!
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
Simply connect all that aren't connected yet, through a USB port.
|
||||
</Typography>
|
||||
<div className="flex flex-col gap-2 py-5">
|
||||
<ArrowLink
|
||||
to="/onboarding/connect"
|
||||
direction="right"
|
||||
variant="boxed"
|
||||
>
|
||||
I have other types of trackers
|
||||
</ArrowLink>
|
||||
<ArrowLink to="/settings/serial" direction="right" variant="boxed">
|
||||
I'm having trouble connecting!
|
||||
</ArrowLink>
|
||||
</div>
|
||||
<TipBox>
|
||||
Not sure which tracker is which? Shake a tracker and it will
|
||||
highlight the corresponding item.
|
||||
</TipBox>
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-xl bg-background-70 h-16 flex gap-2 p-3 lg:w-full mt-4',
|
||||
connectionStatus === 'ERROR' && 'border-2 border-status-critical'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col justify-center fill-background-10">
|
||||
<LoaderIcon
|
||||
youSpinMeRightRoundBabyRightRound={connectionStatus !== 'ERROR'}
|
||||
></LoaderIcon>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Typography bold>USB Tracker</Typography>
|
||||
<Typography color="secondary">
|
||||
{statusLabelMap[connectionStatus]}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow">
|
||||
<Typography color="secondary" bold>
|
||||
{connectedTrackers.length} trackers connected
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
className="flex-grow overflow-y-scroll"
|
||||
ref={ref}
|
||||
style={{ height: layoutHeight - BOTTOM_HEIGHT }}
|
||||
>
|
||||
<div className="grid lg:grid-cols-2 md:grid-cols-1 gap-2 mx-3 pt-3">
|
||||
{Array.from({
|
||||
...connectedTrackers,
|
||||
length: Math.max(trackers.length, 20),
|
||||
}).map((tracker, index) => (
|
||||
<div key={index}>
|
||||
{!tracker && (
|
||||
<div className="rounded-xl bg-background-70 h-16"></div>
|
||||
)}
|
||||
{tracker && (
|
||||
<TrackerCard
|
||||
tracker={tracker.tracker}
|
||||
device={tracker.device}
|
||||
smol
|
||||
></TrackerCard>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{ height: BOTTOM_HEIGHT }}
|
||||
className="flex items-center w-full"
|
||||
>
|
||||
<div className="w-full flex">
|
||||
<div className="flex flex-grow">
|
||||
<Button variant="secondary" to="/" onClick={skipSetup}>
|
||||
Skip setup
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="primary" to="/onboarding/mounting/auto">
|
||||
I connected all my trackers
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
28
src/components/onboarding/pages/Done.tsx
Normal file
28
src/components/onboarding/pages/Done.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { useOnboarding } from '../../../hooks/onboarding';
|
||||
import { Button } from '../../commons/Button';
|
||||
import { SlimeVRIcon } from '../../commons/icon/SimevrIcon';
|
||||
import { Typography } from '../../commons/Typography';
|
||||
|
||||
export function DonePage() {
|
||||
const { applyProgress, skipSetup } = useOnboarding();
|
||||
|
||||
applyProgress(1);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center">
|
||||
<div className="flex flex-col gap-5 items-center z-10">
|
||||
<SlimeVRIcon></SlimeVRIcon>
|
||||
<Typography variant="main-title">You're all set!</Typography>
|
||||
<div className="flex flex-col items-center">
|
||||
<Typography color="secondary">
|
||||
Enjoy your full body experience
|
||||
</Typography>
|
||||
</div>
|
||||
<Button variant="primary" to="/" onClick={skipSetup}>
|
||||
Close the guide
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
49
src/components/onboarding/pages/EnterVR.tsx
Normal file
49
src/components/onboarding/pages/EnterVR.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { BodyPart } from 'solarxr-protocol';
|
||||
import { useOnboarding } from '../../../hooks/onboarding';
|
||||
import { useTrackers } from '../../../hooks/tracker';
|
||||
import { ArrowLink } from '../../commons/ArrowLink';
|
||||
import { Button } from '../../commons/Button';
|
||||
import { Typography } from '../../commons/Typography';
|
||||
|
||||
export function EnterVRPage() {
|
||||
const { applyProgress, skipSetup } = useOnboarding();
|
||||
|
||||
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 h-full justify-center items-center">
|
||||
<div className="flex gap-8">
|
||||
<div className="flex flex-col max-w-md gap-3">
|
||||
<ArrowLink to="/onboarding/trackers-assign" direction="left">
|
||||
Go Back to Tracker assignent
|
||||
</ArrowLink>
|
||||
<Typography variant="main-title">Time to enter VR!</Typography>
|
||||
<Typography color="secondary">
|
||||
Put on all your trackers and then enter VR!
|
||||
</Typography>
|
||||
</div>
|
||||
{/* <div className="flex flex-col flex-grow gap-3 rounded-xl fill-background-50">
|
||||
<Typography variant="main-title">Illustration HERE</Typography>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full py-4 flex flex-row">
|
||||
<div className="flex flex-grow">
|
||||
<Button variant="secondary" to="/" onClick={skipSetup}>
|
||||
Skip setup
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="primary" to="/onboarding/mounting/auto">
|
||||
I'm ready
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
59
src/components/onboarding/pages/Home.tsx
Normal file
59
src/components/onboarding/pages/Home.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { useOnboarding } from '../../../hooks/onboarding';
|
||||
import { Button } from '../../commons/Button';
|
||||
import { SlimeVRIcon } from '../../commons/icon/SimevrIcon';
|
||||
import { Typography } from '../../commons/Typography';
|
||||
|
||||
export function HomePage() {
|
||||
const { applyProgress, skipSetup } = useOnboarding();
|
||||
|
||||
applyProgress(0.1);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center">
|
||||
<div className="flex flex-col gap-5 items-center z-10">
|
||||
<SlimeVRIcon></SlimeVRIcon>
|
||||
<Typography variant="main-title">Welcome to SlimeVR</Typography>
|
||||
<div className="flex flex-col items-center">
|
||||
<Typography color="secondary">Bringing full-body tracking</Typography>
|
||||
<Typography color="secondary">to everyone</Typography>
|
||||
</div>
|
||||
<Button variant="primary" to="/onboarding/wifi-creds">
|
||||
Lets get setup!
|
||||
</Button>
|
||||
<NavLink to="/" onClick={skipSetup}>
|
||||
<Typography color="secondary">Skip setup</Typography>
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="absolute bg-accent-background-50 w-full rounded-full"
|
||||
style={{
|
||||
bottom: 'calc(-300vw / 1.04)',
|
||||
height: '300vw',
|
||||
width: '300vw',
|
||||
}}
|
||||
></div>
|
||||
<img
|
||||
className="absolute"
|
||||
src="/images/slime-girl.png"
|
||||
style={{
|
||||
width: '40%',
|
||||
maxWidth: 800,
|
||||
bottom: '1%',
|
||||
left: '9%',
|
||||
}}
|
||||
/>
|
||||
<img
|
||||
className="absolute"
|
||||
src="/images/slimes.png"
|
||||
style={{
|
||||
width: '40%',
|
||||
maxWidth: 800,
|
||||
bottom: '1%',
|
||||
right: '9%',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
src/components/onboarding/pages/ResetTutorial.tsx
Normal file
47
src/components/onboarding/pages/ResetTutorial.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useOnboarding } from '../../../hooks/onboarding';
|
||||
import { ArrowLink } from '../../commons/ArrowLink';
|
||||
import { Button } from '../../commons/Button';
|
||||
import { Typography } from '../../commons/Typography';
|
||||
|
||||
export function ResetTutorialPage() {
|
||||
const { applyProgress, skipSetup } = useOnboarding();
|
||||
|
||||
applyProgress(0.8);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center">
|
||||
<div className="flex flex-col w-full h-full justify-center px-20">
|
||||
<div className="flex gap-8">
|
||||
<div className="flex flex-col max-w-md gap-3">
|
||||
<ArrowLink to="/onboarding/mounting/auto" direction="left">
|
||||
Go Back to Mounting calibration
|
||||
</ArrowLink>
|
||||
<Typography variant="main-title">
|
||||
Reset tutorial
|
||||
<span className="mx-2 p-1 bg-accent-background-30 text-standard rounded-md">
|
||||
Work in progress
|
||||
</span>
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
This feature isn't done, just press continue
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full py-4 flex flex-row">
|
||||
<div className="flex flex-grow">
|
||||
<Button variant="secondary" to="/" onClick={skipSetup}>
|
||||
Skip setup
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="primary" to="/onboarding/body-proportions/auto">
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
92
src/components/onboarding/pages/WifiCreds.tsx
Normal file
92
src/components/onboarding/pages/WifiCreds.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useOnboarding } from '../../../hooks/onboarding';
|
||||
import { ArrowLink } from '../../commons/ArrowLink';
|
||||
import { Button } from '../../commons/Button';
|
||||
import { Input } from '../../commons/Input';
|
||||
import { Typography } from '../../commons/Typography';
|
||||
|
||||
export interface WifiForm {
|
||||
ssid: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export function WifiCredsPage() {
|
||||
const navigate = useNavigate();
|
||||
const { applyProgress, state, setWifiCredentials, skipSetup } =
|
||||
useOnboarding();
|
||||
const { register, reset, handleSubmit, formState } = useForm<WifiForm>({
|
||||
defaultValues: {},
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
applyProgress(0.2);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.wifi) {
|
||||
reset({
|
||||
ssid: state.wifi.ssid,
|
||||
password: state.wifi.password,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const submitWifiCreds = (value: WifiForm) => {
|
||||
setWifiCredentials(value.ssid, value.password);
|
||||
navigate('/onboarding/connect-trackers');
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
className="flex flex-col w-full h-full"
|
||||
onSubmit={handleSubmit(submitWifiCreds)}
|
||||
>
|
||||
<div className="flex flex-col w-full h-full justify-center items-center">
|
||||
<div className="flex gap-10">
|
||||
<div className="flex flex-col max-w-sm">
|
||||
<ArrowLink to="/onboarding/home" direction="left">
|
||||
Go Back to introduction
|
||||
</ArrowLink>
|
||||
<Typography variant="main-title">Input WiFi credentials</Typography>
|
||||
<Typography color="secondary">
|
||||
The Trackers will use these credentials to connect wirelessly
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
please use the credentials that you are currently connected to
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex flex-col bg-background-70 gap-3 p-10 rounded-xl max-w-sm">
|
||||
<Input
|
||||
{...register('ssid', { required: true })}
|
||||
type="text"
|
||||
label="SSID"
|
||||
placeholder="Enter SSID"
|
||||
/>
|
||||
<Input
|
||||
{...register('password', { required: true })}
|
||||
type="password"
|
||||
label="Password"
|
||||
placeholder="Enter password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full py-4 flex flex-row">
|
||||
<div className="flex flex-grow">
|
||||
<Button variant="secondary" to="/" onClick={skipSetup}>
|
||||
Skip setup
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="secondary" to="/onboarding/trackers-assign">
|
||||
Skip wifi settings
|
||||
</Button>
|
||||
<Button type="submit" variant="primary" disabled={!formState.isValid}>
|
||||
Submit!
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
FC,
|
||||
MouseEventHandler,
|
||||
ReactChild,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useElemSize } from '../../../../hooks/layout';
|
||||
import { CheckIcon } from '../../../commons/icon/CheckIcon';
|
||||
import { CrossIcon } from '../../../commons/icon/CrossIcon';
|
||||
import { Typography } from '../../../commons/Typography';
|
||||
import { DoneStep } from './autobone-steps/Done';
|
||||
import { PreparationStep } from './autobone-steps/Preparation';
|
||||
import { PutTrackersOnStep } from './autobone-steps/PutTrackersOn';
|
||||
import { Recording } from './autobone-steps/Recording';
|
||||
import { StartRecording } from './autobone-steps/StartRecording';
|
||||
import { VerifyResultsStep } from './autobone-steps/VerifyResults';
|
||||
|
||||
export function StepDot({
|
||||
active,
|
||||
done,
|
||||
onClick,
|
||||
}: {
|
||||
active?: boolean;
|
||||
done?: boolean;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex h-4 w-4 rounded-full justify-center items-center fill-background-10 transition-all',
|
||||
active || done ? 'bg-accent-background-20 ' : 'bg-background-60'
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{active && (
|
||||
<div className="flex h-2 w-2 rounded-full bg-background-10"></div>
|
||||
)}
|
||||
{done && <CheckIcon />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function StepContainer({
|
||||
children,
|
||||
width,
|
||||
active,
|
||||
type,
|
||||
step,
|
||||
variant,
|
||||
}: {
|
||||
type: 'numbered' | 'fullsize';
|
||||
variant: 'alone' | 'onboarding';
|
||||
children: ReactChild;
|
||||
width: number;
|
||||
active: boolean;
|
||||
step: number;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'step-container transition-transform duration-500 w-full p-8 rounded-lg flex gap-4 h-full',
|
||||
!active && 'opacity-40 pointer-events-none',
|
||||
variant === 'onboarding' && 'bg-background-70',
|
||||
variant === 'alone' && 'bg-background-60'
|
||||
)}
|
||||
style={{
|
||||
minWidth: width,
|
||||
width,
|
||||
}}
|
||||
>
|
||||
{type === 'numbered' && (
|
||||
<div className="flex flex-col">
|
||||
<div className="bg-accent-background-40 rounded-full h-8 w-8 flex flex-col items-center justify-center">
|
||||
<Typography variant="section-title" bold>
|
||||
{step + 1}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type StepComponentType = FC<{
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
variant: 'alone' | 'onboarding';
|
||||
}>;
|
||||
type Step = { type: 'numbered' | 'fullsize'; component: StepComponentType };
|
||||
|
||||
export function AutoboneStepper({
|
||||
variant,
|
||||
}: {
|
||||
variant: 'alone' | 'onboarding';
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const { width } = useElemSize(ref);
|
||||
const [steps, setSteps] = useState(0);
|
||||
const [step, setStep] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
const steps = ref.current.getElementsByClassName('step-container');
|
||||
setSteps(steps.length);
|
||||
}, [ref]);
|
||||
|
||||
const stepsComponents: Step[] = [
|
||||
{ type: 'numbered', component: PutTrackersOnStep },
|
||||
{ type: 'numbered', component: PreparationStep },
|
||||
{ type: 'numbered', component: StartRecording },
|
||||
{ type: 'fullsize', component: Recording },
|
||||
{ type: 'numbered', component: VerifyResultsStep },
|
||||
{ type: 'fullsize', component: DoneStep },
|
||||
];
|
||||
|
||||
const nextStep = () => {
|
||||
if (step + 1 === steps) return;
|
||||
setStep(step + 1);
|
||||
};
|
||||
|
||||
const prevStep = () => {
|
||||
if (step - 1 < 0) return;
|
||||
setStep(step - 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-4">
|
||||
<div className="w-full flex" ref={ref}>
|
||||
<div
|
||||
className="transition-transform duration-500 flex gap-8"
|
||||
style={{ transform: `translateX(-${(width + 32) * step}px)` }}
|
||||
>
|
||||
{stepsComponents.map(({ type, component: StepComponent }, index) => (
|
||||
<StepContainer
|
||||
variant={variant}
|
||||
key={index}
|
||||
type={type}
|
||||
width={width}
|
||||
active={index === step}
|
||||
step={step}
|
||||
>
|
||||
<StepComponent
|
||||
variant={variant}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
/>
|
||||
</StepContainer>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center items-center gap-2">
|
||||
{Array.from({ length: steps }).map((_, index) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
{index !== 0 && (
|
||||
<div className="w-5 h-1 bg-background-50 rounded-full"></div>
|
||||
)}
|
||||
<StepDot
|
||||
active={index === step}
|
||||
done={index < step}
|
||||
// onClick={() => setStep(index)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
AutoboneContextC,
|
||||
useProvideAutobone,
|
||||
} from '../../../../hooks/autobone';
|
||||
import { useOnboarding } from '../../../../hooks/onboarding';
|
||||
import { ArrowLink } from '../../../commons/ArrowLink';
|
||||
import { Button } from '../../../commons/Button';
|
||||
import { Typography } from '../../../commons/Typography';
|
||||
import { AutoboneStepper } from './AutoboneStepper';
|
||||
|
||||
export function AutomaticProportionsPage() {
|
||||
const { applyProgress, skipSetup, state } = useOnboarding();
|
||||
const context = useProvideAutobone();
|
||||
|
||||
applyProgress(0.9);
|
||||
|
||||
return (
|
||||
<AutoboneContextC.Provider value={context}>
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center">
|
||||
<div className="flex flex-col w-full h-full justify-center max-w-3xl gap-5">
|
||||
<div className="flex flex-col max-w-lg gap-3">
|
||||
{!state.alonePage && (
|
||||
<ArrowLink to="/onboarding/reset-tutorial" direction="left">
|
||||
Go Back to Reset tutorial
|
||||
</ArrowLink>
|
||||
)}
|
||||
<Typography variant="main-title">Measure your body</Typography>
|
||||
<div>
|
||||
<Typography color="secondary">
|
||||
For SlimeVR trackers to work, we need to know the length of your
|
||||
bones.
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
This short calibration will measure it for you.
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<AutoboneStepper
|
||||
variant={state.alonePage ? 'alone' : 'onboarding'}
|
||||
></AutoboneStepper>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full py-4 flex flex-row">
|
||||
<div className="flex flex-grow">
|
||||
{!state.alonePage && (
|
||||
<Button variant="secondary" to="/" onClick={skipSetup}>
|
||||
Skip setup
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
to="/onboarding/body-proportions/manual"
|
||||
>
|
||||
Manual calibration
|
||||
</Button>
|
||||
{!state.alonePage && (
|
||||
<Button variant="primary" to="/onboarding/done">
|
||||
Continue
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AutoboneContextC.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
MouseEventHandler,
|
||||
ReactChild,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
ChangeSkeletonConfigRequestT,
|
||||
RpcMessage,
|
||||
SkeletonBone,
|
||||
SkeletonConfigRequestT,
|
||||
SkeletonConfigResponseT,
|
||||
} from 'solarxr-protocol';
|
||||
import { useWebsocketAPI } from '../../../../hooks/websocket-api';
|
||||
import { Typography } from '../../../commons/Typography';
|
||||
|
||||
export const skeletonBoneLabels = {
|
||||
[SkeletonBone.NONE]: 'None',
|
||||
[SkeletonBone.HEAD]: 'Head shift',
|
||||
[SkeletonBone.NECK]: 'Neck length',
|
||||
[SkeletonBone.TORSO]: 'Torso length',
|
||||
[SkeletonBone.CHEST]: 'Chest distance',
|
||||
[SkeletonBone.WAIST]: 'Waist distance',
|
||||
[SkeletonBone.HIP_OFFSET]: 'Hip offset',
|
||||
[SkeletonBone.HIPS_WIDTH]: 'Hips width',
|
||||
[SkeletonBone.LEGS_LENGTH]: 'Legs length',
|
||||
[SkeletonBone.KNEE_HEIGHT]: 'Knee height',
|
||||
[SkeletonBone.FOOT_LENGTH]: 'Foot length',
|
||||
[SkeletonBone.FOOT_SHIFT]: 'Foot shift',
|
||||
[SkeletonBone.SKELETON_OFFSET]: 'Skeleton offset',
|
||||
[SkeletonBone.CONTROLLER_DISTANCE_Z]: 'Controller distance z',
|
||||
[SkeletonBone.CONTROLLER_DISTANCE_Y]: 'Controller distance y',
|
||||
[SkeletonBone.FOREARM_LENGTH]: 'Forearm distance',
|
||||
[SkeletonBone.SHOULDERS_DISTANCE]: 'Shoulders distance',
|
||||
[SkeletonBone.SHOULDERS_WIDTH]: 'Shoulders width',
|
||||
[SkeletonBone.UPPER_ARM_LENGTH]: 'Upper arm length',
|
||||
[SkeletonBone.ELBOW_OFFSET]: 'Elbow offset',
|
||||
};
|
||||
|
||||
function IncrementButton({
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
children: ReactChild;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
'p-3 rounded-lg w-16 h-16 flex flex-col justify-center items-center bg-background-60 hover:bg-opacity-50'
|
||||
)}
|
||||
>
|
||||
<Typography variant="main-title" bold>
|
||||
{children}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BodyProportions({
|
||||
precise,
|
||||
variant = 'onboarding',
|
||||
}: {
|
||||
precise: boolean;
|
||||
variant: 'onboarding' | 'alone';
|
||||
}) {
|
||||
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
|
||||
const [config, setConfig] = useState<Omit<
|
||||
SkeletonConfigResponseT,
|
||||
'pack'
|
||||
> | null>(null);
|
||||
const [selectedBone, setSelectedBone] = useState(SkeletonBone.HEAD);
|
||||
const bodyParts = useMemo(() => {
|
||||
return (
|
||||
config?.skeletonParts.map(({ bone, value }) => ({
|
||||
bone,
|
||||
label: skeletonBoneLabels[bone],
|
||||
value,
|
||||
})) || []
|
||||
);
|
||||
}, [config]);
|
||||
|
||||
useRPCPacket(
|
||||
RpcMessage.SkeletonConfigResponse,
|
||||
(data: SkeletonConfigResponseT) => {
|
||||
setConfig(data);
|
||||
console.log(data);
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
sendRPCPacket(
|
||||
RpcMessage.SkeletonConfigRequest,
|
||||
new SkeletonConfigRequestT()
|
||||
);
|
||||
}, []);
|
||||
|
||||
const roundedStep = (value: number, step: number, add: boolean) => {
|
||||
if (!add) {
|
||||
return (Math.round(value * 200) - step * 2) / 200;
|
||||
} else {
|
||||
return (Math.round(value * 200) + step * 2) / 200;
|
||||
}
|
||||
};
|
||||
|
||||
const updateConfigValue = (configChange: ChangeSkeletonConfigRequestT) => {
|
||||
sendRPCPacket(RpcMessage.ChangeSkeletonConfigRequest, configChange);
|
||||
const conf = { ...config } as Omit<SkeletonConfigResponseT, 'pack'> | null;
|
||||
const b = conf?.skeletonParts?.find(({ bone }) => bone == selectedBone);
|
||||
if (!b || !conf) return;
|
||||
b.value = configChange.value;
|
||||
setConfig(conf);
|
||||
};
|
||||
|
||||
const increment = async (value: number, v: number) => {
|
||||
const configChange = new ChangeSkeletonConfigRequestT();
|
||||
|
||||
configChange.bone = selectedBone;
|
||||
configChange.value = roundedStep(value, v, true);
|
||||
|
||||
updateConfigValue(configChange);
|
||||
};
|
||||
|
||||
const decrement = (value: number, v: number) => {
|
||||
const configChange = new ChangeSkeletonConfigRequestT();
|
||||
|
||||
configChange.bone = selectedBone;
|
||||
configChange.value = value - v / 100;
|
||||
|
||||
updateConfigValue(configChange);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<div className="flex flex-col overflow-y-scroll overflow-x-hidden max-h-[450px] w-full px-1 gap-3 pb-16">
|
||||
{bodyParts.map(({ label, bone, value }) => (
|
||||
<div className="flex" key={bone}>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex gap-2 transition-opacity duration-300',
|
||||
selectedBone != bone && 'opacity-0'
|
||||
)}
|
||||
>
|
||||
{!precise && (
|
||||
<IncrementButton onClick={() => decrement(value, 10)}>
|
||||
-10
|
||||
</IncrementButton>
|
||||
)}
|
||||
<IncrementButton onClick={() => decrement(value, 1)}>
|
||||
-1
|
||||
</IncrementButton>
|
||||
{precise && (
|
||||
<IncrementButton onClick={() => decrement(value, 0.5)}>
|
||||
-0.5
|
||||
</IncrementButton>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-grow flex-col px-2"
|
||||
onClick={() => setSelectedBone(bone)}
|
||||
>
|
||||
<div
|
||||
key={bone}
|
||||
className={classNames(
|
||||
'p-3 rounded-lg h-16 flex w-full items-center justify-between px-6 transition-colors duration-300 bg-background-60',
|
||||
(selectedBone == bone && 'opacity-100') || 'opacity-50'
|
||||
)}
|
||||
>
|
||||
<Typography variant="section-title" bold>
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography variant="main-title" bold>
|
||||
{Number(value * 100)
|
||||
.toFixed(1)
|
||||
.replace(/[.,]0$/, '')}{' '}
|
||||
CM
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex gap-2 transition-opacity duration-300',
|
||||
selectedBone != bone && 'opacity-0'
|
||||
)}
|
||||
>
|
||||
{precise && (
|
||||
<IncrementButton onClick={() => increment(value, 0.5)}>
|
||||
+0.5
|
||||
</IncrementButton>
|
||||
)}
|
||||
<IncrementButton onClick={() => increment(value, 1)}>
|
||||
+1
|
||||
</IncrementButton>
|
||||
{!precise && (
|
||||
<IncrementButton onClick={() => increment(value, 10)}>
|
||||
+10
|
||||
</IncrementButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-0 h-20 w-full pointer-events-none">
|
||||
<div
|
||||
className={classNames(
|
||||
'w-full h-full bg-gradient-to-b from-transparent opacity-100',
|
||||
variant === 'onboarding' && 'to-background-80',
|
||||
variant === 'alone' && 'to-background-70'
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useOnboarding } from '../../../../hooks/onboarding';
|
||||
import { ArrowLink } from '../../../commons/ArrowLink';
|
||||
import { Button } from '../../../commons/Button';
|
||||
import { CheckBox } from '../../../commons/Checkbox';
|
||||
import { PersonFrontIcon } from '../../../commons/PersonFrontIcon';
|
||||
import { Typography } from '../../../commons/Typography';
|
||||
import { BodyProportions } from './BodyProportions';
|
||||
|
||||
export function ManualProportionsPage() {
|
||||
const { applyProgress, skipSetup, state } = useOnboarding();
|
||||
|
||||
applyProgress(0.9);
|
||||
|
||||
const { control, watch } = useForm<{ precise: boolean }>({
|
||||
defaultValues: { precise: false },
|
||||
});
|
||||
const { precise } = watch();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center">
|
||||
<div className="flex flex-col w-full h-full max-w-5xl justify-center">
|
||||
<div className="flex gap-8 justify-center">
|
||||
<div className="flex flex-col w-full max-w-2xl gap-3 items-center">
|
||||
<div className="flex flex-col">
|
||||
{!state.alonePage && (
|
||||
<ArrowLink to="/onboarding/reset-tutorial" direction="left">
|
||||
Go Back to Reset tutorial
|
||||
</ArrowLink>
|
||||
)}
|
||||
<Typography variant="main-title">
|
||||
Manual Body Proportions
|
||||
</Typography>
|
||||
<CheckBox
|
||||
control={control}
|
||||
label="Precision adjust"
|
||||
name="precise"
|
||||
variant="toggle"
|
||||
></CheckBox>
|
||||
</div>
|
||||
<BodyProportions
|
||||
precise={precise}
|
||||
variant={state.alonePage ? 'alone' : 'onboarding'}
|
||||
></BodyProportions>
|
||||
</div>
|
||||
<div className="flex-col flex-grow gap-3 rounded-xl fill-background-50 items-center hidden md:flex">
|
||||
<PersonFrontIcon width={200}></PersonFrontIcon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full py-4 flex flex-row">
|
||||
<div className="flex flex-grow">
|
||||
{!state.alonePage && (
|
||||
<Button variant="secondary" to="/" onClick={skipSetup}>
|
||||
Skip setup
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
to="/onboarding/body-proportions/auto"
|
||||
>
|
||||
Automatic calibration
|
||||
</Button>
|
||||
{!state.alonePage && (
|
||||
<Button variant="primary" to="/onboarding/done">
|
||||
Continue
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Typography } from '../../../../commons/Typography';
|
||||
|
||||
export function DoneStep() {
|
||||
return (
|
||||
<div className="flex flex-col items-center w-full justify-center gap-5">
|
||||
<div className="flex gap-1 flex-col justify-center items-center">
|
||||
<Typography variant="section-title">
|
||||
Body measured and saved!
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
Your body proportions calibtation is complete
|
||||
</Typography>
|
||||
</div>
|
||||
{/* <Button variant="primary">Continue to next step</Button> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Button } from '../../../../commons/Button';
|
||||
import { FromtOfChairIcon } from '../../../../commons/icon/FrontOfChair';
|
||||
import { Typography } from '../../../../commons/Typography';
|
||||
|
||||
export function PreparationStep({ nextStep }: { nextStep: () => void }) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col flex-grow">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<Typography variant="main-title" bold>
|
||||
Preparation
|
||||
</Typography>
|
||||
<div>
|
||||
<Typography color="secondary">
|
||||
Grab a chair and stand in front of it. such that you can
|
||||
</Typography>
|
||||
<Typography color="secondary">sit down at any moment.</Typography>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex">
|
||||
<Button variant="primary" onClick={nextStep}>
|
||||
I’m in front of a chair
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-16">
|
||||
<FromtOfChairIcon />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Button } from '../../../../commons/Button';
|
||||
import { PersonFrontIcon } from '../../../../commons/PersonFrontIcon';
|
||||
import { TipBox } from '../../../../commons/TipBox';
|
||||
import { Typography } from '../../../../commons/Typography';
|
||||
|
||||
export function PutTrackersOnStep({ nextStep }: { nextStep: () => void }) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col flex-grow">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<Typography variant="main-title" bold>
|
||||
Put on your trackers
|
||||
</Typography>
|
||||
<div>
|
||||
<Typography color="secondary">
|
||||
To calibrate your proportions, we're gonna use the trackers you
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
just assigned. Put on all your trackers, you can see which are
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
which in the figure to the right.
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<TipBox>
|
||||
Not sure which tracker is which? Shake a tracker and it will
|
||||
highlight the corresponding item.
|
||||
</TipBox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex">
|
||||
<Button variant="primary" onClick={nextStep}>
|
||||
I have all my trackers on
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-16">
|
||||
<PersonFrontIcon width={150}></PersonFrontIcon>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useAutobone } from '../../../../../hooks/autobone';
|
||||
import { ProgressBar } from '../../../../commons/ProgressBar';
|
||||
import { Typography } from '../../../../commons/Typography';
|
||||
|
||||
export function Recording({ nextStep }: { nextStep: () => void }) {
|
||||
const { progress, hasCalibration, hasRecording } = useAutobone();
|
||||
|
||||
useEffect(() => {
|
||||
if (progress === 1 && hasCalibration) {
|
||||
nextStep();
|
||||
}
|
||||
}, [progress, hasCalibration]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center w-full justify-between">
|
||||
<div className="flex gap-1 flex-col justify-center items-center">
|
||||
<div className="flex text-status-critical justify-center items-center gap-1">
|
||||
<div className="w-2 h-2 rounded-lg bg-status-critical"></div>
|
||||
<Typography color="text-status-critical">REC</Typography>
|
||||
</div>
|
||||
<Typography variant="section-title">We're recording...</Typography>
|
||||
<Typography color="secondary">Make the moves shown below</Typography>
|
||||
</div>
|
||||
<Typography color="secondary">
|
||||
Do squads (we probably want illustrations here)
|
||||
</Typography>
|
||||
<div className="flex flex-col gap-2 items-center w-full max-w-[150px]">
|
||||
<ProgressBar progress={progress} height={2}></ProgressBar>
|
||||
<Typography color="secondary">
|
||||
{!hasCalibration && hasRecording
|
||||
? 'Processing the result'
|
||||
: '15 seconds left'}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useAutobone } from '../../../../../hooks/autobone';
|
||||
import { Button } from '../../../../commons/Button';
|
||||
import { TipBox } from '../../../../commons/TipBox';
|
||||
import { Typography } from '../../../../commons/Typography';
|
||||
|
||||
export function StartRecording({ nextStep }: { nextStep: () => void }) {
|
||||
const { startRecording } = useAutobone();
|
||||
|
||||
const start = () => {
|
||||
nextStep();
|
||||
startRecording();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col flex-grow">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<Typography variant="main-title" bold>
|
||||
Make some moves
|
||||
</Typography>
|
||||
<div>
|
||||
<Typography color="secondary">
|
||||
We're now going to record some specific poses and
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
moves. these will be prompted in the next screen.
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<TipBox>
|
||||
Make sure you do not move your heels, they must stay at the same
|
||||
place while recording.
|
||||
</TipBox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex">
|
||||
<Button variant="primary" onClick={start}>
|
||||
Start Recording
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import classNames from 'classnames';
|
||||
import { useEffect } from 'react';
|
||||
import { useAutobone } from '../../../../../hooks/autobone';
|
||||
import { Button } from '../../../../commons/Button';
|
||||
import { Typography } from '../../../../commons/Typography';
|
||||
|
||||
export function VerifyResultsStep({
|
||||
nextStep,
|
||||
prevStep,
|
||||
variant,
|
||||
}: {
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
variant: 'onboarding' | 'alone';
|
||||
}) {
|
||||
const {
|
||||
startRecording,
|
||||
hasCalibration,
|
||||
bodyParts,
|
||||
hasRecording,
|
||||
applyProcessing,
|
||||
} = useAutobone();
|
||||
|
||||
const apply = () => {
|
||||
applyProcessing();
|
||||
nextStep();
|
||||
};
|
||||
|
||||
const redo = () => {
|
||||
startRecording();
|
||||
prevStep();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col flex-grow justify-between">
|
||||
<div className="flex flex-col gap-4 max-w-sm">
|
||||
<Typography variant="main-title" bold>
|
||||
Verify results
|
||||
</Typography>
|
||||
<div>
|
||||
<Typography color="secondary">
|
||||
Check the results below, do they look correct?
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full items-center flex-col">
|
||||
<div className="flex flex-col pt-1 gap-2 justify-center w-full max-w-xs">
|
||||
<Typography bold>Recording results</Typography>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col w-full p-4 rounded-md gap-2',
|
||||
variant === 'onboarding' && 'bg-background-60',
|
||||
variant === 'alone' && 'bg-background-50'
|
||||
)}
|
||||
>
|
||||
{bodyParts?.map(({ bone, label, value }) => (
|
||||
<div className="flex justify-between" key={bone}>
|
||||
<Typography color="secondary">{label}</Typography>
|
||||
<Typography bold>{(value * 100).toFixed(2)} CM</Typography>
|
||||
</div>
|
||||
))}
|
||||
{!hasCalibration && hasRecording && (
|
||||
<Typography>Processing recording...</Typography>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={variant === 'onboarding' ? 'secondary' : 'tierciary'}
|
||||
onClick={redo}
|
||||
>
|
||||
Redo recording
|
||||
</Button>
|
||||
<Button variant="primary" onClick={apply}>
|
||||
They're correct
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useOnboarding } from '../../../../hooks/onboarding';
|
||||
import { ArrowLink } from '../../../commons/ArrowLink';
|
||||
import { Button } from '../../../commons/Button';
|
||||
import { Typography } from '../../../commons/Typography';
|
||||
|
||||
export function AutomaticMountingPage() {
|
||||
const { applyProgress, skipSetup, state } = useOnboarding();
|
||||
|
||||
applyProgress(0.7);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center">
|
||||
<div className="flex flex-col w-full h-full justify-center px-20">
|
||||
<div className="flex gap-8">
|
||||
<div className="flex flex-col max-w-md gap-3">
|
||||
{!state.alonePage && (
|
||||
<ArrowLink to="/onboarding/enter-vr" direction="left">
|
||||
Go Back to Enter VR
|
||||
</ArrowLink>
|
||||
)}
|
||||
<Typography variant="main-title">
|
||||
Mount calibration!{' '}
|
||||
<span className="p-1 bg-accent-background-30 text-standard rounded-md">
|
||||
Work in progress
|
||||
</span>
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
This feature isn't done, just choose manual mounting.
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full py-4 flex flex-row">
|
||||
<div className="flex flex-grow">
|
||||
{!state.alonePage && (
|
||||
<Button variant="secondary" to="/" onClick={skipSetup}>
|
||||
Skip setup
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
to="/onboarding/mounting/manual"
|
||||
>
|
||||
Manually set mounting
|
||||
</Button>
|
||||
{!state.alonePage && (
|
||||
<Button variant="primary" to="/onboarding/reset-tutorial">
|
||||
Next step
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
117
src/components/onboarding/pages/mounting/ManualMounting.tsx
Normal file
117
src/components/onboarding/pages/mounting/ManualMounting.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { Quaternion } from 'math3d';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { AssignTrackerRequestT, BodyPart, RpcMessage } from 'solarxr-protocol';
|
||||
import { FlatDeviceTracker } from '../../../../hooks/app';
|
||||
import { useOnboarding } from '../../../../hooks/onboarding';
|
||||
import { useTrackers } from '../../../../hooks/tracker';
|
||||
import { useWebsocketAPI } from '../../../../hooks/websocket-api';
|
||||
import { QuaternionToQuatT } from '../../../../maths/quaternion';
|
||||
import { ArrowLink } from '../../../commons/ArrowLink';
|
||||
import { Button } from '../../../commons/Button';
|
||||
import { TipBox } from '../../../commons/TipBox';
|
||||
import { Typography } from '../../../commons/Typography';
|
||||
import { BodyAssignment } from '../../BodyAssignment';
|
||||
import { MountingSelectionMenu } from './MountingSelectionMenu';
|
||||
|
||||
export function ManualMountingPage() {
|
||||
const { applyProgress, skipSetup, state } = useOnboarding();
|
||||
const { sendRPCPacket } = useWebsocketAPI();
|
||||
|
||||
const [selectedRole, setSelectRole] = useState<BodyPart>(BodyPart.NONE);
|
||||
|
||||
applyProgress(0.7);
|
||||
|
||||
const { useAssignedTrackers } = useTrackers();
|
||||
const assignedTrackers = useAssignedTrackers();
|
||||
|
||||
const trackerPartGrouped = useMemo(
|
||||
() =>
|
||||
assignedTrackers.reduce<{ [key: number]: FlatDeviceTracker[] }>(
|
||||
(curr, td) => {
|
||||
const key = td.tracker.info?.bodyPart || BodyPart.NONE;
|
||||
return {
|
||||
...curr,
|
||||
[key]: [...(curr[key] || []), td],
|
||||
};
|
||||
},
|
||||
{}
|
||||
),
|
||||
[assignedTrackers]
|
||||
);
|
||||
|
||||
const onDirectionSelected = (mountingOrientation: number) => {
|
||||
(trackerPartGrouped[selectedRole] || []).forEach((td) => {
|
||||
const assignreq = new AssignTrackerRequestT();
|
||||
|
||||
assignreq.bodyPosition = td.tracker.info?.bodyPart || BodyPart.NONE;
|
||||
assignreq.mountingRotation = QuaternionToQuatT(
|
||||
Quaternion.Euler(0, +mountingOrientation, 0)
|
||||
);
|
||||
assignreq.trackerId = td.tracker.trackerId;
|
||||
sendRPCPacket(RpcMessage.AssignTrackerRequest, assignreq);
|
||||
});
|
||||
|
||||
setSelectRole(BodyPart.NONE);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MountingSelectionMenu
|
||||
isOpen={selectedRole !== BodyPart.NONE}
|
||||
onClose={() => setSelectRole(BodyPart.NONE)}
|
||||
onDirectionSelected={onDirectionSelected}
|
||||
></MountingSelectionMenu>
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center">
|
||||
<div className="flex flex-col w-full h-full justify-center items-center">
|
||||
<div className="flex md:gap-8">
|
||||
<div className="flex flex-col w-full max-w-md gap-3">
|
||||
{!state.alonePage && (
|
||||
<ArrowLink to="/onboarding/enter-vr" direction="left">
|
||||
Go Back to Enter VR
|
||||
</ArrowLink>
|
||||
)}
|
||||
<Typography variant="main-title">Manual Mounting</Typography>
|
||||
<Typography color="secondary">
|
||||
Click on every tracker and select which way they are mounted
|
||||
</Typography>
|
||||
<TipBox>
|
||||
Not sure which tracker is which? Shake a tracker and it will
|
||||
highlight the corresponding item.
|
||||
</TipBox>
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow gap-3 rounded-xl fill-background-50">
|
||||
<BodyAssignment
|
||||
onlyAssigned={true}
|
||||
advanced={true}
|
||||
onRoleSelected={setSelectRole}
|
||||
></BodyAssignment>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full pb-4 flex flex-row">
|
||||
<div className="flex flex-grow">
|
||||
{!state.alonePage && (
|
||||
<Button variant="secondary" to="/" onClick={skipSetup}>
|
||||
Skip setup
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
to="/onboarding/mounting/auto"
|
||||
>
|
||||
Automatic mounting
|
||||
</Button>
|
||||
{!state.alonePage && (
|
||||
<Button variant="primary" to="/onboarding/reset-tutorial">
|
||||
Next step
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import classNames from 'classnames';
|
||||
import { MouseEventHandler } from 'react';
|
||||
import ReactModal from 'react-modal';
|
||||
import { useElemSize, useLayout } from '../../../../hooks/layout';
|
||||
import { Button } from '../../../commons/Button';
|
||||
import { FootIcon } from '../../../commons/icon/FootIcon';
|
||||
import { Typography } from '../../../commons/Typography';
|
||||
|
||||
function MoutingOrientationCard({
|
||||
orientation,
|
||||
onClick,
|
||||
}: {
|
||||
orientation: string;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className="h-32 bg-background-60 rounded-md flex justify-between p-4 hover:bg-background-50"
|
||||
>
|
||||
<div className="flex flex-col justify-center">
|
||||
<Typography variant="main-title">{orientation}</Typography>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center fill-white">
|
||||
<FootIcon width={58}></FootIcon>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MountingSelectionMenu({
|
||||
isOpen = true,
|
||||
onClose,
|
||||
onDirectionSelected,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onDirectionSelected: (direction: number) => void;
|
||||
}) {
|
||||
const { ref: refTrackers, layoutHeight: trackersHeight } =
|
||||
useLayout<HTMLDivElement>();
|
||||
const { ref: refOptions, height: optionsHeight } =
|
||||
useElemSize<HTMLDivElement>();
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
isOpen={isOpen}
|
||||
shouldCloseOnOverlayClick
|
||||
shouldCloseOnEsc
|
||||
onRequestClose={onClose}
|
||||
overlayClassName={classNames(
|
||||
'fixed top-0 right-0 left-0 bottom-0 flex flex-col items-center w-full h-full bg-black bg-opacity-90 z-20'
|
||||
)}
|
||||
className={classNames(
|
||||
'focus:ring-transparent focus:ring-offset-transparent focus:outline-transparent outline-none mt-20 z-10'
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full h-full flex-col ">
|
||||
<Typography variant="main-title" bold>
|
||||
Where do you want this tracker to be?
|
||||
</Typography>
|
||||
<div
|
||||
className="flex w-full flex-col flex-grow items-center gap-3 justify-center"
|
||||
ref={refTrackers}
|
||||
style={{ height: trackersHeight - optionsHeight }}
|
||||
>
|
||||
<div className="grid grid-cols-2 grid-rows-2 gap-6 w-full">
|
||||
<MoutingOrientationCard
|
||||
orientation="LEFT"
|
||||
onClick={() => onDirectionSelected(90)}
|
||||
/>
|
||||
<MoutingOrientationCard
|
||||
orientation="RIGHT"
|
||||
onClick={() => onDirectionSelected(-90)}
|
||||
/>
|
||||
<MoutingOrientationCard
|
||||
orientation="FRONT"
|
||||
onClick={() => onDirectionSelected(180)}
|
||||
/>
|
||||
<MoutingOrientationCard
|
||||
orientation="BACK"
|
||||
onClick={() => onDirectionSelected(0)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex w-full justify-between absolute bottom-0 left-0 p-10 z-0"
|
||||
onClick={onClose}
|
||||
ref={refOptions}
|
||||
>
|
||||
<div className="flex flex-col justify-end pointer-events-auto">
|
||||
<Button variant="primary" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ReactModal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
AssignTrackerRequestT,
|
||||
BodyPart,
|
||||
QuatT,
|
||||
RpcMessage,
|
||||
TrackerIdT,
|
||||
} from 'solarxr-protocol';
|
||||
import { FlatDeviceTracker } from '../../../../hooks/app';
|
||||
import { useOnboarding } from '../../../../hooks/onboarding';
|
||||
import { useTrackers } from '../../../../hooks/tracker';
|
||||
import { useWebsocketAPI } from '../../../../hooks/websocket-api';
|
||||
import { ArrowLink } from '../../../commons/ArrowLink';
|
||||
import { Button } from '../../../commons/Button';
|
||||
import { CheckBox } from '../../../commons/Checkbox';
|
||||
import { TipBox } from '../../../commons/TipBox';
|
||||
import { Typography } from '../../../commons/Typography';
|
||||
import { BodyAssignment } from '../../BodyAssignment';
|
||||
import { TrackerSelectionMenu } from './TrackerSelectionMenu';
|
||||
|
||||
export function TrackersAssignPage() {
|
||||
const { useAssignedTrackers, trackers } = useTrackers();
|
||||
const { applyProgress, skipSetup, state } = useOnboarding();
|
||||
const { sendRPCPacket } = useWebsocketAPI();
|
||||
|
||||
const { control, watch } = useForm<{ advanced: boolean }>({
|
||||
defaultValues: { advanced: false },
|
||||
});
|
||||
const { advanced } = watch();
|
||||
const [selectedRole, setSelectRole] = useState<BodyPart>(BodyPart.NONE);
|
||||
const assignedTrackers = useAssignedTrackers();
|
||||
|
||||
const trackerPartGrouped = useMemo(
|
||||
() =>
|
||||
assignedTrackers.reduce<{ [key: number]: FlatDeviceTracker[] }>(
|
||||
(curr, td) => {
|
||||
const key = td.tracker.info?.bodyPart || BodyPart.NONE;
|
||||
return {
|
||||
...curr,
|
||||
[key]: [...(curr[key] || []), td],
|
||||
};
|
||||
},
|
||||
{}
|
||||
),
|
||||
[assignedTrackers]
|
||||
);
|
||||
|
||||
const onTrackerSelected = (tracker: FlatDeviceTracker | null) => {
|
||||
const assign = (
|
||||
role: BodyPart,
|
||||
rotation: QuatT | null,
|
||||
trackerId: TrackerIdT | null
|
||||
) => {
|
||||
const assignreq = new AssignTrackerRequestT();
|
||||
|
||||
assignreq.bodyPosition = role;
|
||||
assignreq.mountingRotation = rotation;
|
||||
assignreq.trackerId = trackerId;
|
||||
sendRPCPacket(RpcMessage.AssignTrackerRequest, assignreq);
|
||||
};
|
||||
|
||||
(trackerPartGrouped[selectedRole] || []).forEach((td) =>
|
||||
assign(
|
||||
BodyPart.NONE,
|
||||
td.tracker.info?.mountingOrientation || null,
|
||||
td.tracker.trackerId
|
||||
)
|
||||
);
|
||||
|
||||
if (!tracker) {
|
||||
setSelectRole(BodyPart.NONE);
|
||||
return;
|
||||
}
|
||||
|
||||
assign(
|
||||
selectedRole,
|
||||
tracker.tracker.info?.mountingOrientation || null,
|
||||
tracker.tracker.trackerId
|
||||
);
|
||||
setSelectRole(BodyPart.NONE);
|
||||
};
|
||||
|
||||
applyProgress(0.5);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TrackerSelectionMenu
|
||||
bodyPart={selectedRole}
|
||||
isOpen={selectedRole !== BodyPart.NONE}
|
||||
onClose={() => setSelectRole(BodyPart.NONE)}
|
||||
onTrackerSelected={onTrackerSelected}
|
||||
></TrackerSelectionMenu>
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center">
|
||||
<div className="flex flex-col w-full h-full justify-center items-center">
|
||||
<div className="flex md:gap-8">
|
||||
<div className="flex flex-col max-w-sm gap-3">
|
||||
{!state.alonePage && (
|
||||
<ArrowLink to="/onboarding/wifi-creds" direction="left">
|
||||
Go Back to Wifi Credentials
|
||||
</ArrowLink>
|
||||
)}
|
||||
<Typography variant="main-title">Assign trackers</Typography>
|
||||
<Typography color="secondary">
|
||||
Let's choose which tracker goes where. Click on a location where
|
||||
you want to place a tracker
|
||||
</Typography>
|
||||
<div className="flex gap-1">
|
||||
<Typography>{assignedTrackers.length}</Typography>
|
||||
<Typography color="secondary">
|
||||
of {trackers.length} trackers assigned
|
||||
</Typography>
|
||||
</div>
|
||||
<TipBox>
|
||||
Not sure which tracker is which? Shake a tracker and it will
|
||||
highlight the corresponding item.
|
||||
</TipBox>
|
||||
<CheckBox
|
||||
control={control}
|
||||
label="Show advanced assign locations"
|
||||
name="advanced"
|
||||
variant="toggle"
|
||||
></CheckBox>
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow gap-3 rounded-xl fill-background-50">
|
||||
<BodyAssignment
|
||||
onlyAssigned={false}
|
||||
advanced={advanced}
|
||||
onRoleSelected={setSelectRole}
|
||||
></BodyAssignment>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full py-4 flex flex-row">
|
||||
<div className="flex flex-grow">
|
||||
{!state.alonePage && (
|
||||
<Button variant="secondary" to="/" onClick={skipSetup}>
|
||||
Skip setup
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
{!state.alonePage && (
|
||||
<Button variant="primary" to="/onboarding/enter-vr">
|
||||
I Assigned all the trackers
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import classNames from 'classnames';
|
||||
import ReactModal from 'react-modal';
|
||||
import { BodyPart } from 'solarxr-protocol';
|
||||
import { FlatDeviceTracker } from '../../../../hooks/app';
|
||||
import { useElemSize, useLayout } from '../../../../hooks/layout';
|
||||
import { useTrackers } from '../../../../hooks/tracker';
|
||||
import { Button } from '../../../commons/Button';
|
||||
import { TipBox } from '../../../commons/TipBox';
|
||||
import { Typography } from '../../../commons/Typography';
|
||||
import { TrackerCard } from '../../../tracker/TrackerCard';
|
||||
|
||||
export function TrackerSelectionMenu({
|
||||
isOpen = true,
|
||||
onClose,
|
||||
onTrackerSelected,
|
||||
bodyPart,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
bodyPart: BodyPart;
|
||||
onClose: () => void;
|
||||
onTrackerSelected: (tracker: FlatDeviceTracker | null) => void;
|
||||
}) {
|
||||
const { ref: refTrackers, layoutHeight: trackersHeight } =
|
||||
useLayout<HTMLDivElement>();
|
||||
const { ref: refOptions, height: optionsHeight } =
|
||||
useElemSize<HTMLDivElement>();
|
||||
|
||||
const { useAssignedTrackers, useUnassignedTrackers } = useTrackers();
|
||||
|
||||
const unassignedTrackers = useUnassignedTrackers();
|
||||
const assignedTrackers = useAssignedTrackers();
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
isOpen={isOpen}
|
||||
shouldCloseOnOverlayClick
|
||||
shouldCloseOnEsc
|
||||
onRequestClose={onClose}
|
||||
overlayClassName={classNames(
|
||||
'fixed top-0 right-0 left-0 bottom-0 flex flex-col items-center w-full h-full bg-black bg-opacity-90 z-20'
|
||||
)}
|
||||
className={classNames(
|
||||
'focus:ring-transparent focus:ring-offset-transparent focus:outline-transparent outline-none mt-20 z-10'
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full h-full flex-col ">
|
||||
<div className="flex w-full flex-col flex-grow items-center gap-3">
|
||||
<Typography variant="main-title" bold>
|
||||
Which tracker to assign to the {BodyPart[bodyPart]}?
|
||||
</Typography>
|
||||
<div className="relative">
|
||||
<div
|
||||
className="w-full h-full min-w-[700px] overflow-y-auto p-2 flex flex-col gap-6"
|
||||
ref={refTrackers}
|
||||
style={{ height: trackersHeight - optionsHeight }}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
{unassignedTrackers.length && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Typography>Unassigned Trackers</Typography>
|
||||
<div className=" grid grid-cols-2 gap-3">
|
||||
{unassignedTrackers.map((fd, index) => (
|
||||
<TrackerCard
|
||||
key={index}
|
||||
tracker={fd.tracker}
|
||||
device={fd.device}
|
||||
onClick={() => onTrackerSelected(fd)}
|
||||
smol
|
||||
interactable
|
||||
outlined={
|
||||
bodyPart ===
|
||||
(fd.tracker.info?.bodyPart || BodyPart.NONE)
|
||||
}
|
||||
></TrackerCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Typography>Assigned Trackers</Typography>
|
||||
<div className=" grid grid-cols-2 gap-3">
|
||||
{assignedTrackers.map((fd, index) => (
|
||||
<TrackerCard
|
||||
key={index}
|
||||
tracker={fd.tracker}
|
||||
device={fd.device}
|
||||
onClick={() => onTrackerSelected(fd)}
|
||||
smol
|
||||
interactable
|
||||
outlined={
|
||||
bodyPart ===
|
||||
(fd.tracker.info?.bodyPart || BodyPart.NONE)
|
||||
}
|
||||
></TrackerCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute px-2 pr-4 bottom-0 h-10 w-full border-b-[1px] border-background-40">
|
||||
<div className="w-full h-full bg-gradient-to-b from-transparent to-black opacity-50"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex w-full justify-between absolute bottom-0 left-0 p-10 z-0"
|
||||
onClick={onClose}
|
||||
ref={refOptions}
|
||||
>
|
||||
<div className="w-full max-w-sm">
|
||||
<TipBox>
|
||||
Not sure which tracker is which? Shake a tracker and it will
|
||||
highlight the corresponding item.
|
||||
</TipBox>
|
||||
</div>
|
||||
<div className="flex flex-col justify-end pointer-events-auto">
|
||||
<Button variant="primary" onClick={() => onTrackerSelected(null)}>
|
||||
Do not assign
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ReactModal>
|
||||
);
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import classNames from "classnames";
|
||||
import { useMemo, useState } from "react";
|
||||
import { AutoBoneEpochResponseT, AutoBoneProcessRequestT, AutoBoneProcessStatusResponseT, AutoBoneProcessType, RpcMessage, SkeletonConfigRequestT, SkeletonPartT } from "solarxr-protocol";
|
||||
import { useWebsocketAPI } from "../../hooks/websocket-api";
|
||||
import { Button } from "../commons/Button";
|
||||
import { AppModal } from "../Modal";
|
||||
import { bodyPartLabels } from "./BodyProportions";
|
||||
|
||||
export function AutomaticCalibration() {
|
||||
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
|
||||
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const [isProcessRunning, setProcessRunning] = useState(false);
|
||||
const [hasRecording, setHasRecording] = useState(false);
|
||||
const [hasCalibration, setHasCalibration] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [skeletonParts, setSkeletonParts] = useState<SkeletonPartT[] | null>(null);
|
||||
|
||||
const bodyParts = useMemo(() => {
|
||||
return skeletonParts?.map(({ bone, value }) => ({ bone, label: bodyPartLabels[bone], value })) || []
|
||||
}, [skeletonParts])
|
||||
|
||||
const startProcess = (processType: AutoBoneProcessType) => {
|
||||
// Don't allow multiple processes at once (for now atleast)
|
||||
if (isProcessRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessRunning(true);
|
||||
setProgress(0);
|
||||
|
||||
const processRequest = new AutoBoneProcessRequestT();
|
||||
processRequest.processType = processType;
|
||||
|
||||
sendRPCPacket(RpcMessage.AutoBoneProcessRequest, processRequest)
|
||||
}
|
||||
|
||||
const startRecording = () => {
|
||||
setHasRecording(false);
|
||||
startProcess(AutoBoneProcessType.RECORD);
|
||||
}
|
||||
|
||||
const startProcessing = () => {
|
||||
setHasCalibration(false);
|
||||
startProcess(AutoBoneProcessType.PROCESS);
|
||||
}
|
||||
|
||||
useRPCPacket(RpcMessage.AutoBoneProcessStatusResponse, (data: AutoBoneProcessStatusResponseT) => {
|
||||
if (data.completed) {
|
||||
setProcessRunning(false);
|
||||
setProgress(1);
|
||||
}
|
||||
|
||||
if (data.processType) {
|
||||
if (data.message) {
|
||||
console.log(AutoBoneProcessType[data.processType], ": ", data.message);
|
||||
}
|
||||
|
||||
if (data.total > 0 && data.current >= 0) {
|
||||
setProgress(data.current / data.total);
|
||||
}
|
||||
|
||||
if (data.completed) {
|
||||
console.log("Process ", AutoBoneProcessType[data.processType], " has completed");
|
||||
|
||||
switch (data.processType) {
|
||||
case AutoBoneProcessType.RECORD:
|
||||
setHasRecording(data.success);
|
||||
break;
|
||||
|
||||
case AutoBoneProcessType.PROCESS:
|
||||
setHasCalibration(data.success);
|
||||
break;
|
||||
|
||||
case AutoBoneProcessType.APPLY:
|
||||
// Update skeleton config when applied
|
||||
sendRPCPacket(RpcMessage.SkeletonConfigRequest, new SkeletonConfigRequestT())
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
useRPCPacket(RpcMessage.AutoBoneEpochResponse, (data: AutoBoneEpochResponseT) => {
|
||||
setProgress(data.currentEpoch/data.totalEpochs);
|
||||
|
||||
// Probably not necessary to show to the user
|
||||
console.log("Epoch ", data.currentEpoch, "/", data.totalEpochs, " (Error ", data.epochError, ")");
|
||||
|
||||
setSkeletonParts(data.adjustedSkeletonParts);
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="primary" onClick={() => setOpen(true)}>Automatic calibration</Button>
|
||||
<AppModal
|
||||
isOpen={isOpen}
|
||||
name={<>Automatic Calibration</>}
|
||||
onRequestClose={() => setOpen(false)}
|
||||
>
|
||||
<>
|
||||
<div className="flex w-full justify-center gap-3">
|
||||
<Button variant="primary" onClick={startRecording} disabled={isProcessRunning}>Start Recording</Button>
|
||||
<Button variant="primary" onClick={() => startProcess(AutoBoneProcessType.SAVE)} disabled={isProcessRunning || !hasRecording}>Save Recording</Button>
|
||||
<Button variant="primary" onClick={startProcessing} disabled={isProcessRunning}>Start Calibration</Button>
|
||||
</div>
|
||||
<div className="flex flex-col w-full h-12 p-2">
|
||||
<div className="w-full rounded-full h-full overflow-hidden relative bg-purple-gray-800">
|
||||
<div className={classNames("h-full top-0 left-0 bg-purple-gray-300", { 'transition-all': progress > 0})} style={{width: `${progress * 100}%`}}></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col w-full p-2">
|
||||
{bodyParts.map(({label, bone, value}) =>
|
||||
<div key={bone} className="px-3 rounded-lg py-2 hover:bg-purple-gray-600">
|
||||
<div className="flex flex-row gap-5">
|
||||
<div className="flex flex-grow justify-start items-center text-field-title">{label}</div>
|
||||
<div className="flex justify-center items-center w-16 text-field-title">{`${Number(value * 100).toFixed(1).replace(/[.,]0$/, "")}cm`}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex w-full justify-between mt-3">
|
||||
<Button variant="primary" onClick={() => setOpen(false)}>Close</Button>
|
||||
<Button variant="primary" onClick={() => startProcess(AutoBoneProcessType.APPLY)} disabled={isProcessRunning || !hasCalibration}>Apply values</Button>
|
||||
</div>
|
||||
</>
|
||||
</AppModal>
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
import classNames from "classnames";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useWebsocketAPI } from "../../hooks/websocket-api";
|
||||
import { Button } from "../commons/Button";
|
||||
import { NumberSelector } from "../commons/NumberSelector";
|
||||
import { AutomaticCalibration } from "./AutomaticCalibration";
|
||||
import { BodyView } from "./BodyView";
|
||||
|
||||
import { ChangeSkeletonConfigRequestT, RpcMessage, SkeletonBone, SkeletonConfigRequestT, SkeletonConfigResponseT, SkeletonResetAllRequestT } from 'solarxr-protocol';
|
||||
|
||||
export const bodyPartLabels = {
|
||||
[SkeletonBone.NONE]: "None",
|
||||
[SkeletonBone.HEAD]: "Head shift",
|
||||
[SkeletonBone.NECK]: "Neck length",
|
||||
[SkeletonBone.TORSO]: "Torso length",
|
||||
[SkeletonBone.CHEST]: "Chest distance",
|
||||
[SkeletonBone.WAIST]: "Waist distance",
|
||||
[SkeletonBone.HIP_OFFSET]: "Hip offset",
|
||||
[SkeletonBone.HIPS_WIDTH]: "Hips width",
|
||||
[SkeletonBone.LEGS_LENGTH]: "Legs length",
|
||||
[SkeletonBone.KNEE_HEIGHT]: "Knee height",
|
||||
[SkeletonBone.FOOT_LENGTH]: "Foot length",
|
||||
[SkeletonBone.FOOT_SHIFT]: "Foot shift",
|
||||
[SkeletonBone.SKELETON_OFFSET]: "Skeleton offset",
|
||||
[SkeletonBone.CONTROLLER_DISTANCE_Z]: "Controller distance z",
|
||||
[SkeletonBone.CONTROLLER_DISTANCE_Y]: "Controller distance y",
|
||||
[SkeletonBone.FOREARM_LENGTH]: "Forearm distance",
|
||||
[SkeletonBone.SHOULDERS_DISTANCE]: "Shoulders distance",
|
||||
[SkeletonBone.SHOULDERS_WIDTH]: "Shoulders width",
|
||||
[SkeletonBone.UPPER_ARM_LENGTH]: "Upper arm length",
|
||||
[SkeletonBone.ELBOW_OFFSET]: "Elbow offset"
|
||||
}
|
||||
|
||||
type BodyProportionsForm = ({ [key: string]: number });
|
||||
|
||||
|
||||
export function BodyProportions() {
|
||||
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
|
||||
const [selectedBodyPart, setSelectedBodyPart] = useState<number | null>(null);
|
||||
const { control, reset, watch, handleSubmit } = useForm<BodyProportionsForm>({ defaultValues: Object.values(SkeletonBone).filter((bone) => !isNaN(+bone)).reduce((curr, bone) => ({ ...curr, [bone]: 0 }), {}) });
|
||||
const [config, setConfig] = useState<SkeletonConfigResponseT | null>(null);
|
||||
const [step, setStep] = useState(1);
|
||||
|
||||
const bodyParts = useMemo(() => {
|
||||
return config?.skeletonParts.map(({ bone, value }) => ({ bone, label: bodyPartLabels[bone], value })) || []
|
||||
}, [config])
|
||||
|
||||
|
||||
const toFormData = (data: SkeletonConfigResponseT): BodyProportionsForm => data.skeletonParts.reduce((curr, part) => ({ ...curr, [part.bone]: part.value }), {})
|
||||
|
||||
useRPCPacket(RpcMessage.SkeletonConfigResponse, (data: SkeletonConfigResponseT) => {
|
||||
setConfig(data);
|
||||
reset(toFormData(data));
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
sendRPCPacket(RpcMessage.SkeletonConfigRequest, new SkeletonConfigRequestT())
|
||||
}, [])
|
||||
|
||||
|
||||
const onSubmit = async (data: BodyProportionsForm) => {
|
||||
if (!config) return ;
|
||||
|
||||
const curr = toFormData(config);
|
||||
for (const key in curr) {
|
||||
if (curr[key] !== data[key]) {
|
||||
const configChange = new ChangeSkeletonConfigRequestT();
|
||||
|
||||
console.log(key);
|
||||
configChange.bone = +key;
|
||||
configChange.value = data[key];
|
||||
|
||||
await sendRPCPacket(RpcMessage.ChangeSkeletonConfigRequest, configChange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch(() => handleSubmit(onSubmit)());
|
||||
return () => subscription.unsubscribe();
|
||||
})
|
||||
|
||||
|
||||
const handleResetAll = () => {
|
||||
sendRPCPacket(RpcMessage.SkeletonResetAllRequest, new SkeletonResetAllRequestT());
|
||||
}
|
||||
|
||||
|
||||
const onMouseEnterSelector = (id: number) => {
|
||||
setSelectedBodyPart(id);
|
||||
}
|
||||
|
||||
const roundedStep = (value: number, add: boolean) => {
|
||||
if (!add) {
|
||||
return (Math.round(value * 200) - step * 2) / 200;
|
||||
} else {
|
||||
return (Math.round(value * 200) + step * 2) / 200;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex flex-col overflow-y-auto">
|
||||
<div className="flex px-8 pt-8 text-secondary-heading">
|
||||
Body Proportions
|
||||
</div>
|
||||
<div className="flex p-5 gap-8 justify-center">
|
||||
<div className="h-1/2 sticky top-5 rounded-lg w-72 justify-center p-10 hidden sm:flex bg-purple-gray-900">
|
||||
<BodyView selectedBodyPart={selectedBodyPart || 0}></BodyView>
|
||||
</div>
|
||||
<div className="flex-col flex gap-4">
|
||||
<div className="flex gap-3 ">
|
||||
<AutomaticCalibration></AutomaticCalibration>
|
||||
<Button variant="primary" onClick={handleResetAll}>Reset all</Button>
|
||||
</div>
|
||||
<div className="flex flex-col rounded-lg py-2">
|
||||
<div className="text-field-title">Precision</div>
|
||||
<div className="flex rounded-lg overflow-hidden flex-grow h-10 cursor-pointer bg-purple-gray-600 text-field-title">
|
||||
<div onClick={() => setStep(0.5)} className={classNames("hover:bg-purple-gray-500 flex flex-grow justify-center items-center ", { 'bg-purple-gray-400': step === 0.5 })}>0.5</div>
|
||||
<div onClick={() => setStep(1)} className={classNames("hover:bg-purple-gray-500 flex flex-grow justify-center items-center", { 'bg-purple-gray-400': step === 1 })}>1</div>
|
||||
<div onClick={() => setStep(5)} className={classNames("hover:bg-purple-gray-500 flex flex-grow justify-center items-center", { 'bg-purple-gray-400': step === 5 })}>5</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
{bodyParts.map(({label, bone}) =>
|
||||
<div key={bone} onMouseEnter={() => onMouseEnterSelector(bone)} className={classNames('px-3 rounded-lg py-2', { 'bg-purple-gray-700 ': bone === selectedBodyPart })}>
|
||||
<NumberSelector
|
||||
variant="big"
|
||||
control={control}
|
||||
label={label}
|
||||
min={-1}
|
||||
max={1.5}
|
||||
step={roundedStep}
|
||||
name={`${bone}`}
|
||||
valueLabelFormat={(value) => `${Number(value * 100).toFixed(1).replace(/[.,]0$/, "")}cm` }
|
||||
></NumberSelector>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,10 +1,10 @@
|
||||
import { ReactChild } from "react";
|
||||
import { AppContextC, useProvideAppContext } from "../../hooks/app";
|
||||
import { ReactChild } from 'react';
|
||||
import { AppContextC, useProvideAppContext } from '../../hooks/app';
|
||||
|
||||
export function AppContextProvider({ children }: { children: ReactChild }) {
|
||||
const context = useProvideAppContext();
|
||||
|
||||
const context = useProvideAppContext();
|
||||
|
||||
return <AppContextC.Provider value={context}>{children}</AppContextC.Provider>
|
||||
|
||||
}
|
||||
return (
|
||||
<AppContextC.Provider value={context}>{children}</AppContextC.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
16
src/components/providers/ConfigContext.tsx
Normal file
16
src/components/providers/ConfigContext.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ReactChild, useEffect } from 'react';
|
||||
import { ConfigContextC, useConfigProvider } from '../../hooks/config';
|
||||
|
||||
export function ConfigContextProvider({ children }: { children: ReactChild }) {
|
||||
const context = useConfigProvider();
|
||||
|
||||
useEffect(() => {
|
||||
context.loadConfig();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ConfigContextC.Provider value={context}>
|
||||
{children}
|
||||
</ConfigContextC.Provider>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +1,26 @@
|
||||
import { ReactChild } from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { useLayout } from "../../hooks/layout";
|
||||
import { CloseIcon } from "../commons/icon/CloseIcon";
|
||||
import { Navbar } from "../Navbar";
|
||||
import { SettingsSidebar } from "./SettingsSidebar";
|
||||
import { ReactChild } from 'react';
|
||||
import { useLayout } from '../../hooks/layout';
|
||||
import { Navbar } from '../Navbar';
|
||||
import { TopBar } from '../TopBar';
|
||||
import { SettingsSidebar } from './SettingsSidebar';
|
||||
|
||||
export function SettingsLayoutRoute({ children }: { children: ReactChild }) {
|
||||
const { layoutHeight, ref } = useLayout<HTMLDivElement>();
|
||||
|
||||
const { layoutHeight, ref } = useLayout<HTMLDivElement>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar></Navbar>
|
||||
<div ref={ref} className='flex-grow' style={{ height: layoutHeight }}>
|
||||
<div className="flex h-full">
|
||||
return (
|
||||
<>
|
||||
<TopBar></TopBar>
|
||||
<div ref={ref} className="flex-grow" style={{ height: layoutHeight }}>
|
||||
<div className="flex h-full pb-3">
|
||||
<Navbar></Navbar>
|
||||
<div className="h-full w-full gap-2 flex">
|
||||
<SettingsSidebar></SettingsSidebar>
|
||||
<div className="flex flex-grow gap-10 flex-col rounded-tl-3xl overflow-hidden bg-purple-gray-800">
|
||||
<div className="relative overflow-y-auto overflow-x-hidden">
|
||||
{children}
|
||||
<div className="absolute top-0 right-0 p-5">
|
||||
<NavLink to="/" className="flex gap-5 group cursor-pointer">
|
||||
<div className="flex rounded-full bg-purple-gray-600 fill-purple-gray-100 group-hover:fill-purple-gray-200"><CloseIcon size={50}></CloseIcon></div>
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col overflow-y-auto pr-2">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
29
src/components/settings/SettingsPageLayout.tsx
Normal file
29
src/components/settings/SettingsPageLayout.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import classNames from 'classnames';
|
||||
import { ReactChild } from 'react';
|
||||
|
||||
export function SettingsPageLayout({
|
||||
children,
|
||||
className,
|
||||
icon,
|
||||
...props
|
||||
}: {
|
||||
children: ReactChild;
|
||||
icon: ReactChild;
|
||||
} & React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'bg-background-70 rounded-lg p-8 flex gap-8 w-full',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="w-10 h-10 bg-accent-background-40 flex justify-center items-center rounded-full fill-background-10">
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-col w-full">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +1,67 @@
|
||||
import { NavLink } from "react-router-dom"
|
||||
import classNames from 'classnames';
|
||||
import { ReactChild, useMemo } from 'react';
|
||||
import { NavLink, useLocation, useMatch } from 'react-router-dom';
|
||||
import { Typography } from '../commons/Typography';
|
||||
|
||||
export function SettingsLink({
|
||||
to,
|
||||
scrollTo,
|
||||
children,
|
||||
}: {
|
||||
to: string;
|
||||
scrollTo?: string;
|
||||
children: ReactChild;
|
||||
}) {
|
||||
const { state } = useLocation();
|
||||
const doesMatch = useMatch({
|
||||
path: to,
|
||||
});
|
||||
|
||||
const isActive = useMemo(() => {
|
||||
const typedState: { scrollTo?: string } = state as any;
|
||||
return (
|
||||
(doesMatch && !scrollTo && !typedState?.scrollTo) ||
|
||||
(doesMatch && typedState?.scrollTo == scrollTo)
|
||||
);
|
||||
}, [state, doesMatch]);
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
to={to}
|
||||
state={{ scrollTo }}
|
||||
className={classNames('pl-5 py-2 hover:bg-background-60 rounded-lg', {
|
||||
'bg-background-60': isActive,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsSidebar() {
|
||||
return (
|
||||
<div className="flex flex-col px-8 w-72 gap-8 pb-5 overflow-y-auto">
|
||||
<div className="flex flex-col gap-3 pt-8 text-section-indicator">
|
||||
<div className="flex text-">TRACKER SETTINGS</div>
|
||||
<div className="flex flex-col gap-2 font-medium text-extra-emphasised">
|
||||
<NavLink to="/settings/trackers" state={{ scrollTo: 'steamvr' }} className="pl-5 py-2 hover:text-field-title hover:bg-purple-gray-700 rounded-lg">SteamVR</NavLink>
|
||||
<NavLink to="/settings/trackers" state={{ scrollTo: 'filtering' }} className="pl-5 py-2 hover:text-field-title hover:bg-purple-gray-700 rounded-lg">Filtering</NavLink>
|
||||
<NavLink to="/settings/serial" className="pl-5 py-2 hover:text-field-title hover:bg-purple-gray-700 rounded-lg">Serial</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="flex flex-col gap-3 pt-4 ">
|
||||
<div className="flex">USER INTERFACE</div>
|
||||
<div className="flex flex-col gap-2 font-medium">
|
||||
<NavLink to="" className="pl-3 py-2 hover:bg-primary-5 rounded-lg">Widgets</NavLink>
|
||||
</div>
|
||||
</div> */}
|
||||
return (
|
||||
<div className="flex flex-col px-5 w-[280px] min-w-[280px] py-5 gap-3 overflow-y-auto bg-background-70 rounded-lg">
|
||||
<Typography variant="main-title">Settings</Typography>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Typography variant="section-title">General</Typography>
|
||||
<div className="flex flex-col gap-2">
|
||||
<SettingsLink to="/settings/trackers" scrollTo="steamvr">
|
||||
SteamVR
|
||||
</SettingsLink>
|
||||
<SettingsLink to="/settings/trackers" scrollTo="mechanics">
|
||||
Tracker mechanics
|
||||
</SettingsLink>
|
||||
<SettingsLink to="/settings/trackers" scrollTo="interface">
|
||||
Interface
|
||||
</SettingsLink>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Typography variant="section-title">Utilities</Typography>
|
||||
<div className="flex flex-col gap-2">
|
||||
<SettingsLink to="/settings/serial">Serial Console</SettingsLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
257
src/components/settings/pages/GeneralSettings.tsx
Normal file
257
src/components/settings/pages/GeneralSettings.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
ChangeSettingsRequestT,
|
||||
FilteringSettingsT,
|
||||
FilteringType,
|
||||
RpcMessage,
|
||||
SettingsRequestT,
|
||||
SettingsResponseT,
|
||||
SteamVRTrackersSettingT,
|
||||
} from 'solarxr-protocol';
|
||||
import { useConfig } from '../../../hooks/config';
|
||||
import { useWebsocketAPI } from '../../../hooks/websocket-api';
|
||||
import { CheckBox } from '../../commons/Checkbox';
|
||||
import { SquaresIcon } from '../../commons/icon/SquaresIcon';
|
||||
import { SteamIcon } from '../../commons/icon/SteamIcon';
|
||||
import { WrenchIcon } from '../../commons/icon/WrenchIcons';
|
||||
import { NumberSelector } from '../../commons/NumberSelector';
|
||||
import { Radio } from '../../commons/Radio';
|
||||
import { Typography } from '../../commons/Typography';
|
||||
import { SettingsPageLayout } from '../SettingsPageLayout';
|
||||
|
||||
interface SettingsForm {
|
||||
trackers: {
|
||||
waist: boolean;
|
||||
chest: boolean;
|
||||
legs: boolean;
|
||||
knees: boolean;
|
||||
elbows: boolean;
|
||||
};
|
||||
filtering: {
|
||||
type: number;
|
||||
intensity: number;
|
||||
ticks: number;
|
||||
};
|
||||
interface: {
|
||||
devmode: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export function GeneralSettings() {
|
||||
const { config, setConfig } = useConfig();
|
||||
const { state } = useLocation();
|
||||
const pageRef = useRef<HTMLFormElement | null>(null);
|
||||
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
const { register, reset, control, watch, handleSubmit } =
|
||||
useForm<SettingsForm>({
|
||||
defaultValues: {
|
||||
trackers: {
|
||||
waist: false,
|
||||
chest: false,
|
||||
elbows: false,
|
||||
knees: false,
|
||||
legs: false,
|
||||
},
|
||||
filtering: { intensity: 0, ticks: 0 },
|
||||
interface: { devmode: false },
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (values: SettingsForm) => {
|
||||
const settings = new ChangeSettingsRequestT();
|
||||
|
||||
if (values.trackers) {
|
||||
const trackers = new SteamVRTrackersSettingT();
|
||||
trackers.waist = values.trackers.waist;
|
||||
trackers.chest = values.trackers.chest;
|
||||
trackers.legs = values.trackers.legs;
|
||||
trackers.knees = values.trackers.knees;
|
||||
trackers.elbows = values.trackers.elbows;
|
||||
settings.steamVrTrackers = trackers;
|
||||
}
|
||||
|
||||
const filtering = new FilteringSettingsT();
|
||||
filtering.type = values.filtering.type;
|
||||
filtering.intensity = values.filtering.intensity;
|
||||
filtering.ticks = values.filtering.ticks;
|
||||
|
||||
settings.filtering = filtering;
|
||||
sendRPCPacket(RpcMessage.ChangeSettingsRequest, settings);
|
||||
|
||||
setConfig({ debug: values.interface.devmode });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch(() => handleSubmit(onSubmit)());
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT());
|
||||
}, []);
|
||||
|
||||
useRPCPacket(RpcMessage.SettingsResponse, (settings: SettingsResponseT) => {
|
||||
reset({
|
||||
...(settings.steamVrTrackers
|
||||
? { trackers: settings.steamVrTrackers }
|
||||
: {}),
|
||||
...(settings.filtering ? { filtering: settings.filtering } : {}),
|
||||
interface: {
|
||||
devmode: config?.debug,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Handle scrolling to selected page
|
||||
useEffect(() => {
|
||||
const typedState: { scrollTo: string } = state as any;
|
||||
if (!pageRef.current || !typedState || !typedState.scrollTo) {
|
||||
return;
|
||||
}
|
||||
const elem = pageRef.current.querySelector(`#${typedState.scrollTo}`);
|
||||
if (elem) {
|
||||
elem.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
return (
|
||||
<form className="flex flex-col gap-2 w-full" ref={pageRef}>
|
||||
<SettingsPageLayout icon={<SteamIcon></SteamIcon>} id="steamvr">
|
||||
<>
|
||||
<Typography variant="main-title">SteamVR</Typography>
|
||||
<Typography bold>SteamVR trackers</Typography>
|
||||
<div className="flex flex-col py-2">
|
||||
<Typography color="secondary">
|
||||
Enable or disable specific tracking parts.
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
Useful if you want more control over what SlimeVR does.
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pt-3">
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="trackers.waist"
|
||||
label="Waist"
|
||||
/>
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="trackers.chest"
|
||||
label="Chest"
|
||||
/>
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="trackers.legs"
|
||||
label="Feet"
|
||||
/>
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="trackers.knees"
|
||||
label="Knees"
|
||||
/>
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="trackers.elbows"
|
||||
label="Elbows"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</SettingsPageLayout>
|
||||
<SettingsPageLayout icon={<WrenchIcon></WrenchIcon>} id="mechanics">
|
||||
<>
|
||||
<Typography variant="main-title">Tracker mechanics</Typography>
|
||||
<Typography bold>Filtering</Typography>
|
||||
<div className="flex flex-col pt-2 pb-4">
|
||||
<Typography color="secondary">
|
||||
Choose the filtering type for your trackers.
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
Extrapolation predicts movement while interpolation smoothens
|
||||
movement.
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography>Filtering type</Typography>
|
||||
<div className="flex md:flex-row flex-col gap-3 pt-2">
|
||||
<Radio
|
||||
control={control}
|
||||
name="filtering.type"
|
||||
label="No filtering"
|
||||
desciption="Use measurements as is, will not do any filtering."
|
||||
value={FilteringType.NONE}
|
||||
></Radio>
|
||||
<Radio
|
||||
control={control}
|
||||
name="filtering.type"
|
||||
label="Smoothing"
|
||||
desciption="Smooths the movements but adds some latency."
|
||||
value={FilteringType.INTERPOLATION}
|
||||
></Radio>
|
||||
<Radio
|
||||
control={control}
|
||||
name="filtering.type"
|
||||
label="Prediction"
|
||||
desciption="Reduces latency and makes movements more snappy, but may increase jitter."
|
||||
value={FilteringType.EXTRAPOLATION}
|
||||
></Radio>
|
||||
</div>
|
||||
<div className="flex gap-5 pt-5 md:flex-row flex-col">
|
||||
<NumberSelector
|
||||
control={control}
|
||||
name="filtering.intensity"
|
||||
label="Intensity"
|
||||
valueLabelFormat={(value) => `${value} %`}
|
||||
min={0}
|
||||
max={100}
|
||||
step={10}
|
||||
/>
|
||||
<NumberSelector
|
||||
control={control}
|
||||
name="filtering.ticks"
|
||||
label="Latency"
|
||||
valueLabelFormat={(value) => `${value} ticks`}
|
||||
min={0}
|
||||
max={50}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</SettingsPageLayout>
|
||||
<SettingsPageLayout icon={<SquaresIcon></SquaresIcon>} id="interface">
|
||||
<>
|
||||
<Typography variant="main-title">Interface</Typography>
|
||||
<Typography bold>Developer Mode</Typography>
|
||||
<div className="flex flex-col">
|
||||
<Typography color="secondary">
|
||||
This mode can be useful if you need in-depth data or to interact
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
with connected trackers on a more advanced level
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid sm:grid-cols-2 gap-3 pt-3">
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
control={control}
|
||||
outlined
|
||||
name="interface.devmode"
|
||||
label="Developper mode"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</SettingsPageLayout>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -1,94 +1,104 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { CloseSerialRequestT, OpenSerialRequestT, RpcMessage, SerialUpdateResponseT, SetWifiRequestT } from "solarxr-protocol";
|
||||
import { useLayout } from "../../../hooks/layout";
|
||||
import { useWebsocketAPI } from "../../../hooks/websocket-api";
|
||||
import { Button } from "../../commons/Button";
|
||||
import { Input } from "../../commons/Input";
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
CloseSerialRequestT,
|
||||
OpenSerialRequestT,
|
||||
RpcMessage,
|
||||
SerialUpdateResponseT,
|
||||
SetWifiRequestT,
|
||||
} from 'solarxr-protocol';
|
||||
import { useLayout } from '../../../hooks/layout';
|
||||
import { useWebsocketAPI } from '../../../hooks/websocket-api';
|
||||
import { Button } from '../../commons/Button';
|
||||
import { Input } from '../../commons/Input';
|
||||
import { Typography } from '../../commons/Typography';
|
||||
|
||||
export interface WifiForm {
|
||||
ssid: string;
|
||||
password: string;
|
||||
ssid: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export function Serial() {
|
||||
const {
|
||||
layoutHeight,
|
||||
layoutWidth,
|
||||
ref: consoleRef,
|
||||
} = useLayout<HTMLDivElement>();
|
||||
|
||||
const { layoutHeight, layoutWidth, ref: consoleRef } = useLayout<HTMLDivElement>();
|
||||
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
|
||||
// const consoleRef = useRef<HTMLPreElement>(null);
|
||||
const [consoleContent, setConsole] = useState('');
|
||||
const [isSerialOpen, setSerialOpen] = useState(false);
|
||||
|
||||
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
|
||||
// const consoleRef = useRef<HTMLPreElement>(null);
|
||||
const [consoleContent, setConsole] = useState("");
|
||||
const [isSerialOpen, setSerialOpen] = useState(false);
|
||||
const { register, handleSubmit } = useForm<WifiForm>({ defaultValues: {} });
|
||||
useEffect(() => {
|
||||
sendRPCPacket(RpcMessage.OpenSerialRequest, new OpenSerialRequestT());
|
||||
return () => {
|
||||
sendRPCPacket(RpcMessage.CloseSerialRequest, new CloseSerialRequestT());
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
sendRPCPacket(RpcMessage.OpenSerialRequest, new OpenSerialRequestT())
|
||||
return () => {
|
||||
sendRPCPacket(RpcMessage.CloseSerialRequest, new CloseSerialRequestT());
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
useRPCPacket(RpcMessage.SerialUpdateResponse, (data: SerialUpdateResponseT) => {
|
||||
if (data.closed) {
|
||||
setSerialOpen(false)
|
||||
setTimeout(() => {
|
||||
sendRPCPacket(RpcMessage.OpenSerialRequest, new OpenSerialRequestT())
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
if (!data.closed) {
|
||||
setSerialOpen(true);
|
||||
}
|
||||
|
||||
if (data.log && consoleRef.current) {
|
||||
setConsole((console) => console + data.log)
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (consoleRef.current)
|
||||
consoleRef.current.scrollTo({ top: consoleRef.current.scrollHeight })
|
||||
}, [consoleContent])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => {
|
||||
if (!isSerialOpen)
|
||||
sendRPCPacket(RpcMessage.OpenSerialRequest, new OpenSerialRequestT())
|
||||
else
|
||||
clearInterval(id);
|
||||
useRPCPacket(
|
||||
RpcMessage.SerialUpdateResponse,
|
||||
(data: SerialUpdateResponseT) => {
|
||||
if (data.closed) {
|
||||
setSerialOpen(false);
|
||||
setTimeout(() => {
|
||||
sendRPCPacket(RpcMessage.OpenSerialRequest, new OpenSerialRequestT());
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearInterval(id);
|
||||
}
|
||||
}, [isSerialOpen, sendRPCPacket])
|
||||
if (!data.closed) {
|
||||
setSerialOpen(true);
|
||||
}
|
||||
|
||||
const sendWifiCredentials = (value: WifiForm) => {
|
||||
const wifi = new SetWifiRequestT();
|
||||
|
||||
wifi.password = value.password;
|
||||
wifi.ssid = value.ssid;
|
||||
|
||||
sendRPCPacket(RpcMessage.SetWifiRequest, wifi)
|
||||
if (data.log && consoleRef.current) {
|
||||
setConsole((console) => console + data.log);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (consoleRef.current)
|
||||
consoleRef.current.scrollTo({
|
||||
top: consoleRef.current.scrollHeight,
|
||||
});
|
||||
}, [consoleContent]);
|
||||
|
||||
return (
|
||||
<form className="flex flex-col h-full gap-2" onSubmit={handleSubmit(sendWifiCredentials)}>
|
||||
<div ref={consoleRef} style={{ height: layoutHeight, width: layoutWidth }} className="overflow-x-auto overflow-y-auto flex select-text pl-3">
|
||||
<pre>
|
||||
{isSerialOpen ? consoleContent : 'Connection to serial lost, Reconnecting...'}
|
||||
</pre>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 p-3" >
|
||||
<Input {...register('ssid', { required: true })} type="text" placeholder="SSID"></Input>
|
||||
<Input {...register('password', { required: true })} type="password" placeholder="Password"></Input>
|
||||
<Button variant="primary" type="submit">Send</Button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => {
|
||||
if (!isSerialOpen)
|
||||
sendRPCPacket(RpcMessage.OpenSerialRequest, new OpenSerialRequestT());
|
||||
else clearInterval(id);
|
||||
}, 1000);
|
||||
|
||||
}
|
||||
return () => {
|
||||
clearInterval(id);
|
||||
};
|
||||
}, [isSerialOpen, sendRPCPacket]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-2 flex-grow bg-background-70 p-5 rounded-md">
|
||||
<Typography variant="main-title">Serial Console</Typography>
|
||||
<Typography color="secondary">
|
||||
This is a live information feed for serial communication. May be useful
|
||||
if you need to know the firmware is acting up.
|
||||
</Typography>
|
||||
<div
|
||||
className="w-full bg-background-80 rounded-lg overflow-x-auto overflow-y-auto"
|
||||
ref={consoleRef}
|
||||
style={{ height: layoutHeight }}
|
||||
>
|
||||
<div
|
||||
className="flex select-text "
|
||||
style={{ maxWidth: layoutWidth - 30 }}
|
||||
>
|
||||
<pre>
|
||||
{isSerialOpen
|
||||
? consoleContent
|
||||
: 'Connection to serial lost, Reconnecting...'}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { ChangeSettingsRequestT, FilteringSettingsT, RpcMessage, SettingsRequestT, SettingsResponseT, SteamVRTrackersSettingT } from "solarxr-protocol";
|
||||
import { useWebsocketAPI } from "../../../hooks/websocket-api";
|
||||
import { CheckBox } from "../../commons/Checkbox";
|
||||
import { NumberSelector } from "../../commons/NumberSelector";
|
||||
import { Select } from "../../commons/Select";
|
||||
|
||||
interface SettingsForm {
|
||||
trackers: {
|
||||
waist: boolean,
|
||||
chest: boolean,
|
||||
legs: boolean,
|
||||
knees: boolean,
|
||||
elbows: boolean,
|
||||
}
|
||||
filtering: {
|
||||
type: number;
|
||||
intensity: number;
|
||||
ticks: number;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function TrackersSettings() {
|
||||
|
||||
const { state } = useLocation();
|
||||
const pageRef = useRef<HTMLFormElement | null>(null);
|
||||
|
||||
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
const { register, reset, control, watch, handleSubmit } = useForm<SettingsForm>({ defaultValues: { filtering: { intensity: 0, ticks: 0 } } });
|
||||
|
||||
const onSubmit = (values: SettingsForm) => {
|
||||
const settings = new ChangeSettingsRequestT();
|
||||
|
||||
if (values.trackers) {
|
||||
const trackers = new SteamVRTrackersSettingT();
|
||||
trackers.waist = values.trackers.waist;
|
||||
trackers.chest = values.trackers.chest;
|
||||
trackers.legs = values.trackers.legs;
|
||||
trackers.knees = values.trackers.knees;
|
||||
trackers.elbows = values.trackers.elbows;
|
||||
settings.steamVrTrackers = trackers;
|
||||
}
|
||||
|
||||
const filtering = new FilteringSettingsT();
|
||||
filtering.type = values.filtering.type;
|
||||
filtering.intensity = values.filtering.intensity;
|
||||
filtering.ticks = values.filtering.ticks;
|
||||
|
||||
settings.filtering = filtering;
|
||||
sendRPCPacket(RpcMessage.ChangeSettingsRequest, settings)
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch(() => handleSubmit(onSubmit)());
|
||||
return () => subscription.unsubscribe();
|
||||
}, [])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT());
|
||||
}, [])
|
||||
|
||||
useRPCPacket(RpcMessage.SettingsResponse, (settings: SettingsResponseT) => {
|
||||
reset({
|
||||
...(settings.steamVrTrackers ? {trackers: settings.steamVrTrackers} : {}),
|
||||
...(settings.filtering ? {filtering: settings.filtering} : {})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const typedState: { scrollTo: string } = state as any;
|
||||
|
||||
if (!pageRef.current || (!typedState || !typedState.scrollTo))
|
||||
return ;
|
||||
|
||||
const elem = pageRef.current.querySelector(`#${typedState.scrollTo}`)
|
||||
if (elem)
|
||||
elem.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [state])
|
||||
|
||||
|
||||
return (
|
||||
<form className="px-8 flex flex-col gap-20 py-4" ref={pageRef}>
|
||||
<div className="flex flex-col gap-2" id="steamvr">
|
||||
<div className="flex gap-5 text-secondary-heading">
|
||||
SteamVR Trackers
|
||||
</div>
|
||||
<div className="flex flex-col text-default">
|
||||
<p>Enable or disable specific tracking parts.</p>
|
||||
<p>Useful if you want more control over what SlimeVR does.</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-5 pt-5">
|
||||
<CheckBox outlined {...register('trackers.waist')} label="Waist" />
|
||||
<CheckBox outlined {...register('trackers.chest')} label="Chest"/>
|
||||
<CheckBox outlined {...register('trackers.legs')} label="Legs"/>
|
||||
<CheckBox outlined {...register('trackers.knees')} label="Knees"/>
|
||||
<CheckBox outlined {...register('trackers.elbows')} label="Elbows"/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2" id="filtering">
|
||||
<div className="flex gap-5 text-secondary-heading">
|
||||
Filtering
|
||||
</div>
|
||||
<div className="flex flex-col text-default">
|
||||
<p>Choose the filtering type for your trackers.</p>
|
||||
<p>Extrapolation predicts movement while interpolation smoothens movement.</p>
|
||||
</div>
|
||||
<div className="flex gap-5 pt-5">
|
||||
<Select {...register('filtering.type')} label="Filtering Type" options={[{ label: 'None', value: 0 }, { label: 'Interpolation', value: 1 }, {label: 'Extrapolation', value: 2 }]}></Select>
|
||||
<NumberSelector variant="smol" control={control} name="filtering.intensity" label="Intensity" valueLabelFormat={(value) => `${value}%`} min={0} max={100} step={10}></NumberSelector>
|
||||
<NumberSelector variant="smol" control={control} name="filtering.ticks" label="Ticks" min={0} max={80} step={1}></NumberSelector>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
82
src/components/tracker/SingleTrackerBodyAssignmentMenu.tsx
Normal file
82
src/components/tracker/SingleTrackerBodyAssignmentMenu.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import classNames from 'classnames';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import ReactModal from 'react-modal';
|
||||
import { BodyPart } from 'solarxr-protocol';
|
||||
import { Button } from '../commons/Button';
|
||||
import { CheckBox } from '../commons/Checkbox';
|
||||
import { TipBox } from '../commons/TipBox';
|
||||
import { Typography } from '../commons/Typography';
|
||||
import { BodyAssignment } from '../onboarding/BodyAssignment';
|
||||
|
||||
export function SingleTrackerBodyAssignmentMenu({
|
||||
isOpen = true,
|
||||
onClose,
|
||||
onRoleSelected,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onRoleSelected: (role: BodyPart) => void;
|
||||
}) {
|
||||
const { control, watch } = useForm<{ advanced: boolean }>({
|
||||
defaultValues: { advanced: false },
|
||||
});
|
||||
const { advanced } = watch();
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
isOpen={isOpen}
|
||||
shouldCloseOnOverlayClick
|
||||
shouldCloseOnEsc
|
||||
onRequestClose={onClose}
|
||||
overlayClassName={classNames(
|
||||
'fixed top-0 right-0 left-0 bottom-0 flex flex-col items-center w-full h-full justify-center bg-black bg-opacity-90 z-20'
|
||||
)}
|
||||
className={classNames(
|
||||
'focus:ring-transparent focus:ring-offset-transparent focus:outline-transparent outline-none mt-20 z-10'
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full h-full flex-col gap-10 px-3">
|
||||
<div className="flex flex-col w-full h-full justify-center items-center">
|
||||
<div className="flex gap-8">
|
||||
<div className="flex flex-col max-w-sm gap-3">
|
||||
<Typography variant="main-title" bold>
|
||||
Where do you want this tracker to be?
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
Choose a location where you want this tracker to be assigned.
|
||||
Alternatively you can choose to manage all trackers at once
|
||||
instead of one by one.
|
||||
</Typography>
|
||||
<CheckBox
|
||||
control={control}
|
||||
label="Show advanced assign locations"
|
||||
name="advanced"
|
||||
variant="toggle"
|
||||
></CheckBox>
|
||||
<div className="flex">
|
||||
<Button variant="secondary" to="/onboarding/trackers-assign">
|
||||
Manage all trackers
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow gap-3 rounded-xl fill-background-50">
|
||||
<BodyAssignment
|
||||
onlyAssigned={false}
|
||||
advanced={advanced}
|
||||
onRoleSelected={onRoleSelected}
|
||||
></BodyAssignment>
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => onRoleSelected(BodyPart.NONE)}
|
||||
>
|
||||
Unassign tracker
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ReactModal>
|
||||
);
|
||||
}
|
||||
30
src/components/tracker/TrackerBattery.tsx
Normal file
30
src/components/tracker/TrackerBattery.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { BatteryIcon } from '../commons/icon/BatteryIcon';
|
||||
import { Typography } from '../commons/Typography';
|
||||
|
||||
export function TrackerBattery({
|
||||
value,
|
||||
disabled,
|
||||
}: {
|
||||
value: number;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col justify-around">
|
||||
<BatteryIcon value={value} disabled={disabled} />
|
||||
</div>
|
||||
{!disabled && (
|
||||
<div className="w-10">
|
||||
<Typography color="secondary">
|
||||
{(value * 100).toFixed(0)} %
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
{disabled && (
|
||||
<div className="flex flex-col justify-center w-10">
|
||||
<div className="w-7 h-1 bg-background-30 rounded-full"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,96 +1,151 @@
|
||||
import classNames from "classnames";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { BodyPart, DeviceDataT, TrackerDataT, TrackerStatus } from "solarxr-protocol";
|
||||
import { WifiIcon } from "../commons/icon/WifiIcon";
|
||||
import { BatteryIcon } from "../commons/icon/BatteryIcon";
|
||||
import { TrackerSettings } from "./TrackerSettings";
|
||||
import { IconButton } from "../commons/ButtonIcon";
|
||||
import { GearIcon } from "../commons/icon/GearIcon";
|
||||
import { QuaternionFromQuatT } from "../../maths/quaternion";
|
||||
import { MouseEventHandler } from 'react';
|
||||
import {
|
||||
DeviceDataT,
|
||||
TrackerDataT,
|
||||
TrackerStatus as TrackerStatusEnum,
|
||||
} from 'solarxr-protocol';
|
||||
import { Typography } from '../commons/Typography';
|
||||
import { TrackerBattery } from './TrackerBattery';
|
||||
import { TrackerWifi } from './TrackerWifi';
|
||||
import { TrackerStatus } from './TrackerStatus';
|
||||
import { FootIcon } from '../commons/icon/FootIcon';
|
||||
import classNames from 'classnames';
|
||||
import { useTracker } from '../../hooks/tracker';
|
||||
|
||||
function TrackerBig({
|
||||
device,
|
||||
tracker,
|
||||
}: {
|
||||
tracker: TrackerDataT;
|
||||
device?: DeviceDataT;
|
||||
}) {
|
||||
const { useName } = useTracker(tracker);
|
||||
|
||||
export function TrackerCard({ tracker, device }: { tracker: TrackerDataT, device?: DeviceDataT }) {
|
||||
const trackerName = useName();
|
||||
|
||||
const previousRot = useRef<{ x: number, y: number, z: number, w: number }>(tracker.rotation!)
|
||||
const [velocity, setVelocity] = useState<number>(0);
|
||||
const [rots, setRotation] = useState<number[]>([]);
|
||||
|
||||
const statusClass = useMemo(() => {
|
||||
const statusMap: { [key: number]: string } = {
|
||||
[TrackerStatus.NONE]: 'bg-purple-gray-900',
|
||||
[TrackerStatus.BUSY]: 'bg-status-warning',
|
||||
[TrackerStatus.ERROR]: 'bg-status-error',
|
||||
[TrackerStatus.DISCONNECTED]: 'bg-purple-gray-900',
|
||||
[TrackerStatus.OCCLUDED]: 'bg-status-warning',
|
||||
[TrackerStatus.OK]: 'bg-status-online'
|
||||
}
|
||||
return statusMap[tracker.status];
|
||||
}, [tracker.status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tracker.rotation) {
|
||||
const rot = QuaternionFromQuatT(tracker.rotation).mul(QuaternionFromQuatT(previousRot.current).inverse());
|
||||
const dif = Math.min(1, (rot.x ** 2 + rot.y ** 2 + rot.z ** 2) * 2.5)
|
||||
// Use sum of rotation of last 3 frames (0.3sec) for smoother movement and better detection of slow movement.
|
||||
if (rots.length === 3) {
|
||||
rots.shift();
|
||||
}
|
||||
rots.push(dif);
|
||||
setRotation(rots);
|
||||
setVelocity(rots.reduce((a, b) => a + b));
|
||||
previousRot.current = tracker.rotation;
|
||||
}
|
||||
}, [tracker.rotation])
|
||||
|
||||
const trackerName = useMemo(() => {
|
||||
if (device?.customName)
|
||||
return device.customName;
|
||||
if (tracker.info?.bodyPart)
|
||||
return BodyPart[tracker.info?.bodyPart];
|
||||
return device?.hardwareInfo?.displayName || 'NONE';
|
||||
|
||||
}, [tracker.info, device?.customName, device?.hardwareInfo?.displayName])
|
||||
|
||||
return (
|
||||
<TrackerSettings tracker={tracker} device={device} >
|
||||
<div className={classNames("flex rounded-l-md rounded-r-xl", statusClass)}>
|
||||
<div className="flex rounded-r-md py-3 ml-[5px] pr-4 pl-4 w-full gap-4 bg-purple-gray-700">
|
||||
<div className="flex flex-grow flex-col truncate gap-2">
|
||||
<div className="flex text-field-title">{trackerName}</div>
|
||||
<div className="flex flex-row gap-4 text-default">
|
||||
{device && device.hardwareStatus &&
|
||||
<>
|
||||
<div className="flex gap-2 flex-grow">
|
||||
{device.hardwareStatus.rssi && <div className="flex flex-col justify-around">
|
||||
<WifiIcon value={device.hardwareStatus?.rssi} />
|
||||
</div>}
|
||||
{device.hardwareStatus.ping && <div className="flex w-10">{device.hardwareStatus.ping} ms</div>}
|
||||
</div>
|
||||
{device.hardwareStatus.batteryPctEstimate &&
|
||||
<div className="flex w-1/3 gap-2">
|
||||
<div className="flex flex-col justify-around">
|
||||
<BatteryIcon value={device.hardwareStatus.batteryPctEstimate / 100}/>
|
||||
</div>
|
||||
<div className="flex">{((device.hardwareStatus.batteryPctEstimate)).toFixed(0)} %</div>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
}
|
||||
<div className="flex w-1/3 gap-0.5 justify-around flex-col">
|
||||
<div className="w-full rounded-full h-1 bg-purple-gray-600">
|
||||
<div className="h-1 rounded-full bg-accent-darker" style={{width: `${velocity * 100}%`}}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{tracker.info?.editable &&
|
||||
<div className="flex flex-col flex-shrink justify-around">
|
||||
<IconButton className="fill-purple-gray-300" icon={<GearIcon/>}/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
return (
|
||||
<div className="flex flex-col justify-center rounded-md py-3 pr-4 pl-4 w-full gap-2 box-border my-8 px-6 h-32">
|
||||
<div className="flex justify-center fill-background-10">
|
||||
<FootIcon></FootIcon>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Typography bold>{trackerName}</Typography>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<TrackerStatus status={tracker.status}></TrackerStatus>
|
||||
</div>
|
||||
<div className="flex text-default justify-center gap-5 flex-wrap">
|
||||
{device && device.hardwareStatus && (
|
||||
<>
|
||||
{device.hardwareStatus.batteryPctEstimate && (
|
||||
<TrackerBattery
|
||||
value={device.hardwareStatus.batteryPctEstimate / 100}
|
||||
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
|
||||
/>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
{device.hardwareStatus.rssi && device.hardwareStatus.ping && (
|
||||
<TrackerWifi
|
||||
rssi={device.hardwareStatus.rssi}
|
||||
ping={device.hardwareStatus.ping}
|
||||
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
|
||||
></TrackerWifi>
|
||||
)}
|
||||
</div>
|
||||
</TrackerSettings>
|
||||
)
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
function TrackerSmol({
|
||||
device,
|
||||
tracker,
|
||||
}: {
|
||||
tracker: TrackerDataT;
|
||||
device?: DeviceDataT;
|
||||
}) {
|
||||
const { useName } = useTracker(tracker);
|
||||
|
||||
const trackerName = useName();
|
||||
|
||||
return (
|
||||
<div className="flex rounded-md py-3 px-5 w-full gap-4 h-16">
|
||||
<div className="flex flex-col justify-center items-center fill-background-10">
|
||||
<FootIcon></FootIcon>
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow">
|
||||
<Typography bold>{trackerName}</Typography>
|
||||
<TrackerStatus status={tracker.status}></TrackerStatus>
|
||||
</div>
|
||||
{device && device.hardwareStatus && (
|
||||
<>
|
||||
<div className="flex flex-col justify-center items-center">
|
||||
{device.hardwareStatus.batteryPctEstimate && (
|
||||
<TrackerBattery
|
||||
value={device.hardwareStatus.batteryPctEstimate / 100}
|
||||
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col justify-center items-center">
|
||||
{device.hardwareStatus.rssi && device.hardwareStatus.ping && (
|
||||
<TrackerWifi
|
||||
rssi={device.hardwareStatus.rssi}
|
||||
ping={device.hardwareStatus.ping}
|
||||
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
|
||||
></TrackerWifi>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TrackerCard({
|
||||
tracker,
|
||||
device,
|
||||
smol = false,
|
||||
interactable = false,
|
||||
outlined = false,
|
||||
onClick,
|
||||
bg = 'bg-background-60',
|
||||
shakeHighlight = true,
|
||||
}: {
|
||||
tracker: TrackerDataT;
|
||||
device?: DeviceDataT;
|
||||
smol?: boolean;
|
||||
interactable?: boolean;
|
||||
outlined?: boolean;
|
||||
bg?: string;
|
||||
shakeHighlight?: boolean;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
}) {
|
||||
const { useVelocity } = useTracker(tracker);
|
||||
|
||||
const velocity = useVelocity();
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
'rounded-lg',
|
||||
interactable && 'hover:bg-background-50',
|
||||
outlined && 'outline outline-2 outline-accent-background-40',
|
||||
bg
|
||||
)}
|
||||
style={
|
||||
shakeHighlight
|
||||
? {
|
||||
boxShadow: `0px 0px ${velocity * 8}px ${velocity * 8}px #183951`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{smol && <TrackerSmol tracker={tracker} device={device}></TrackerSmol>}
|
||||
{!smol && <TrackerBig tracker={tracker} device={device}></TrackerBig>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,86 +1,232 @@
|
||||
import { ReactChild, useMemo, useState } from "react";
|
||||
import { Button } from "../commons/Button";
|
||||
import { AppModal } from "../Modal";
|
||||
import { Select } from "../commons/Select";
|
||||
import { useWebsocketAPI } from "../../hooks/websocket-api";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { AssignTrackerRequestT, BodyPart, DeviceDataT, RpcMessage, TrackerDataT } from "solarxr-protocol";
|
||||
import { FixEuler, QuaternionFromQuatT, QuaternionToQuatT } from "../../maths/quaternion";
|
||||
import { Quaternion } from "math3d";
|
||||
|
||||
import { Quaternion } from 'math3d';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { AssignTrackerRequestT, BodyPart, RpcMessage } from 'solarxr-protocol';
|
||||
import { useDebouncedEffect } from '../../hooks/timeout';
|
||||
import { useTrackerFromId } from '../../hooks/tracker';
|
||||
import { useWebsocketAPI } from '../../hooks/websocket-api';
|
||||
import {
|
||||
FixEuler,
|
||||
QuaternionFromQuatT,
|
||||
QuaternionToQuatT,
|
||||
} from '../../maths/quaternion';
|
||||
import { ArrowLink } from '../commons/ArrowLink';
|
||||
import { Button } from '../commons/Button';
|
||||
import { FootIcon } from '../commons/icon/FootIcon';
|
||||
import { Input } from '../commons/Input';
|
||||
import { Typography } from '../commons/Typography';
|
||||
import { MountingSelectionMenu } from '../onboarding/pages/mounting/MountingSelectionMenu';
|
||||
import { SingleTrackerBodyAssignmentMenu } from './SingleTrackerBodyAssignmentMenu';
|
||||
import { TrackerCard } from './TrackerCard';
|
||||
|
||||
const rotationToQuatMap = {
|
||||
FRONT: 180,
|
||||
LEFT: 90,
|
||||
RIGHT: -90,
|
||||
BACK: 0
|
||||
}
|
||||
FRONT: 180,
|
||||
LEFT: 90,
|
||||
RIGHT: -90,
|
||||
BACK: 0,
|
||||
};
|
||||
|
||||
export function TrackerSettings({ tracker, device, children }: { tracker: TrackerDataT, device?: DeviceDataT, children: ReactChild }) {
|
||||
const rotationsLabels = {
|
||||
[rotationToQuatMap.BACK]: 'Back',
|
||||
[rotationToQuatMap.FRONT]: 'Front',
|
||||
[rotationToQuatMap.LEFT]: 'Left',
|
||||
[rotationToQuatMap.RIGHT]: 'Right',
|
||||
};
|
||||
|
||||
const { sendRPCPacket } = useWebsocketAPI();
|
||||
const { register, handleSubmit, reset } = useForm({ defaultValues: { bodyPosition: 0, mountingRotation: rotationToQuatMap.BACK } });
|
||||
export function TrackerSettingsPage() {
|
||||
const { sendRPCPacket } = useWebsocketAPI();
|
||||
const [firstLoad, setFirstLoad] = useState(false);
|
||||
const [selectRotation, setSelectRotation] = useState<boolean>(false);
|
||||
const [selectBodypart, setSelectBodypart] = useState<boolean>(false);
|
||||
const { trackernum, deviceid } = useParams<{
|
||||
trackernum: string;
|
||||
deviceid: string;
|
||||
}>();
|
||||
const { register, watch, reset } = useForm<{ trackerName: string | null }>({
|
||||
defaultValues: { trackerName: null },
|
||||
reValidateMode: 'onSubmit',
|
||||
});
|
||||
const { trackerName } = watch();
|
||||
|
||||
const tracker = useTrackerFromId(trackernum, deviceid);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const onDirectionSelected = (mountingOrientation: number) => {
|
||||
if (!tracker) return;
|
||||
|
||||
const positions = useMemo(() => Object.keys(BodyPart).filter((position: string) => isNaN(+position)).map((role, index) =>( { label: role, value: index })), [])
|
||||
const rotations = useMemo(() => [
|
||||
{ label: 'FRONT', value: rotationToQuatMap.FRONT },
|
||||
{ label: 'LEFT', value: rotationToQuatMap.LEFT },
|
||||
{ label: 'RIGHT', value: rotationToQuatMap.RIGHT },
|
||||
{ label: 'BACK', value: rotationToQuatMap.BACK }
|
||||
], []);
|
||||
const assignreq = new AssignTrackerRequestT();
|
||||
assignreq.mountingRotation = QuaternionToQuatT(
|
||||
Quaternion.Euler(0, +mountingOrientation, 0)
|
||||
);
|
||||
assignreq.bodyPosition = tracker?.tracker.info?.bodyPart || BodyPart.NONE;
|
||||
assignreq.trackerId = tracker?.tracker.trackerId;
|
||||
sendRPCPacket(RpcMessage.AssignTrackerRequest, assignreq);
|
||||
setSelectRotation(false);
|
||||
};
|
||||
|
||||
const onRoleSelected = (role: BodyPart) => {
|
||||
if (!tracker) return;
|
||||
|
||||
const handleSaveSettings = ({ bodyPosition, mountingRotation }: { bodyPosition: number, mountingRotation: number }) => {
|
||||
const assignreq = new AssignTrackerRequestT();
|
||||
const assignreq = new AssignTrackerRequestT();
|
||||
assignreq.bodyPosition = role;
|
||||
assignreq.trackerId = tracker?.tracker.trackerId;
|
||||
sendRPCPacket(RpcMessage.AssignTrackerRequest, assignreq);
|
||||
setSelectBodypart(false);
|
||||
};
|
||||
|
||||
assignreq.bodyPosition = bodyPosition;
|
||||
const currRotation = useMemo(
|
||||
() =>
|
||||
tracker?.tracker.info?.mountingOrientation
|
||||
? FixEuler(
|
||||
QuaternionFromQuatT(tracker.tracker.info?.mountingOrientation)
|
||||
.eulerAngles.y
|
||||
)
|
||||
: rotationToQuatMap.BACK,
|
||||
[tracker?.tracker.info?.mountingOrientation]
|
||||
);
|
||||
|
||||
|
||||
assignreq.mountingRotation = QuaternionToQuatT(Quaternion.Euler(0, +mountingRotation, 0));
|
||||
assignreq.trackerId = tracker.trackerId;
|
||||
useDebouncedEffect(
|
||||
() => {
|
||||
if (!tracker) return;
|
||||
if (trackerName == tracker.tracker.info?.customName) return;
|
||||
const assignreq = new AssignTrackerRequestT();
|
||||
assignreq.bodyPosition = tracker?.tracker.info?.bodyPart || BodyPart.NONE;
|
||||
assignreq.displayName = trackerName;
|
||||
assignreq.trackerId = tracker?.tracker.trackerId;
|
||||
sendRPCPacket(RpcMessage.AssignTrackerRequest, assignreq);
|
||||
},
|
||||
[trackerName],
|
||||
1000
|
||||
);
|
||||
|
||||
sendRPCPacket(RpcMessage.AssignTrackerRequest, assignreq)
|
||||
setOpen(false);
|
||||
useEffect(() => {
|
||||
if (tracker && !firstLoad) setFirstLoad(true);
|
||||
}, [tracker, firstLoad]);
|
||||
|
||||
useEffect(() => {
|
||||
if (firstLoad) {
|
||||
console.log('hey');
|
||||
reset({
|
||||
trackerName: tracker?.tracker.info?.customName as string | null,
|
||||
});
|
||||
}
|
||||
}, [firstLoad]);
|
||||
|
||||
|
||||
const openSettings = () => {
|
||||
|
||||
if (!tracker.info?.editable)
|
||||
return;
|
||||
|
||||
setOpen(true)
|
||||
reset({
|
||||
bodyPosition: tracker.info?.bodyPart,
|
||||
mountingRotation: tracker.info?.mountingOrientation ? FixEuler(QuaternionFromQuatT(tracker.info?.mountingOrientation!).eulerAngles.y) : rotationToQuatMap.BACK
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div onClick={openSettings} className="cursor-pointer">
|
||||
{children}
|
||||
return (
|
||||
<form className="h-full overflow-y-auto">
|
||||
<SingleTrackerBodyAssignmentMenu
|
||||
isOpen={selectBodypart}
|
||||
onClose={() => setSelectBodypart(false)}
|
||||
onRoleSelected={onRoleSelected}
|
||||
></SingleTrackerBodyAssignmentMenu>
|
||||
<MountingSelectionMenu
|
||||
isOpen={selectRotation}
|
||||
onClose={() => setSelectRotation(false)}
|
||||
onDirectionSelected={onDirectionSelected}
|
||||
></MountingSelectionMenu>
|
||||
<div className="flex gap-2 md:h-full flex-wrap md:flex-row ">
|
||||
<div className="flex flex-col w-full md:max-w-xs gap-2">
|
||||
{tracker && (
|
||||
<TrackerCard
|
||||
bg={'bg-background-70'}
|
||||
device={tracker?.device}
|
||||
tracker={tracker?.tracker}
|
||||
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>
|
||||
<AppModal
|
||||
isOpen={open}
|
||||
onRequestClose={() => setOpen(false)}
|
||||
name={<>{tracker.info?.bodyPart ? `${BodyPart[tracker.info?.bodyPart]} Settings` : 'Tracker Settings'}</>}
|
||||
>
|
||||
<form onSubmit={handleSubmit(handleSaveSettings)} className="flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-5">
|
||||
<Select {...register("bodyPosition")} label="Tracker role" options={positions}></Select>
|
||||
<Select {...register("mountingRotation")} label="Mounting rotation" options={rotations}></Select>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Button variant="primary" type="submit" >Save</Button>
|
||||
<Button variant="primary" type="button" onClick={() => setOpen(false)}>Close</Button>
|
||||
</div>
|
||||
</form>
|
||||
</AppModal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<Button variant="primary" disabled>
|
||||
Update now
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col bg-background-70 p-3 rounded-lg gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Typography color="secondary">Manufacturer</Typography>
|
||||
<Typography>
|
||||
{tracker?.device?.hardwareInfo?.manufacturer}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<Typography color="secondary">Display name</Typography>
|
||||
<Typography>{tracker?.tracker.info?.displayName}</Typography>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<Typography color="secondary">Custom name</Typography>
|
||||
<Typography>
|
||||
{tracker?.tracker.info?.customName || '--'}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow bg-background-70 rounded-lg p-5 gap-3">
|
||||
<ArrowLink to="/">Go back to trackers list</ArrowLink>
|
||||
<Typography variant="main-title">Tracker settigns</Typography>
|
||||
<div className="flex flex-col gap-2 w-full mt-3">
|
||||
<Typography variant="section-title">Assignment</Typography>
|
||||
<Typography color="secondary">
|
||||
What part of the body the tracker is assigned to.
|
||||
</Typography>
|
||||
<div className="flex justify-between bg-background-80 w-full p-3 rounded-lg">
|
||||
<div className="flex gap-3 items-center">
|
||||
<FootIcon></FootIcon>
|
||||
<Typography>
|
||||
{BodyPart[tracker?.tracker.info?.bodyPart || BodyPart.NONE]}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setSelectBodypart(true)}
|
||||
>
|
||||
Edit assignment
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 w-full mt-3">
|
||||
<Typography variant="section-title">Mounting position</Typography>
|
||||
<Typography color="secondary">
|
||||
Where is the tracker mounted?
|
||||
</Typography>
|
||||
<div className="flex justify-between bg-background-80 w-full p-3 rounded-lg">
|
||||
<div className="flex gap-3 items-center">
|
||||
<FootIcon></FootIcon>
|
||||
<Typography>{rotationsLabels[currRotation]}</Typography>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setSelectRotation(true)}
|
||||
>
|
||||
Edit mounting
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 w-full mt-3">
|
||||
<Typography variant="section-title">Tracker name</Typography>
|
||||
<Typography color="secondary">
|
||||
{'Give it a cute nickname :)'}
|
||||
</Typography>
|
||||
<Input
|
||||
placeholder="NightyBeast's left leg"
|
||||
type="text"
|
||||
autocomplete={false}
|
||||
{...register('trackerName')}
|
||||
></Input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user