mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-05 18:01:56 +02:00
Session flightlist (#1407)
Co-authored-by: sctanf <36978460+sctanf@users.noreply.github.com> Co-authored-by: Butterscotch! <bscotchvanilla@gmail.com> Co-authored-by: Aed <145398159+Aed-1@users.noreply.github.com>
This commit is contained in:
63
Cargo.lock
generated
63
Cargo.lock
generated
@@ -1273,7 +1273,7 @@ dependencies = [
|
||||
"cc",
|
||||
"memchr",
|
||||
"rustc_version",
|
||||
"toml 0.9.0",
|
||||
"toml 0.9.2",
|
||||
"vswhom",
|
||||
"winreg 0.55.0",
|
||||
]
|
||||
@@ -1431,6 +1431,19 @@ dependencies = [
|
||||
"miniz_oxide 0.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flexi_logger"
|
||||
version = "0.29.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88a5a6882b2e137c4f2664562995865084eb5a00611fba30c582ef10354c4ad8"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"log",
|
||||
"nu-ansi-term",
|
||||
"regex",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -2770,6 +2783,15 @@ version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
@@ -4380,6 +4402,7 @@ dependencies = [
|
||||
"const_format",
|
||||
"dirs-next",
|
||||
"discord-sdk",
|
||||
"flexi_logger",
|
||||
"glob",
|
||||
"itertools",
|
||||
"libloading 0.8.5",
|
||||
@@ -4759,7 +4782,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"tauri-winres",
|
||||
"toml 0.9.0",
|
||||
"toml 0.9.2",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -4817,7 +4840,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"toml 0.9.0",
|
||||
"toml 0.9.2",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -4857,7 +4880,7 @@ dependencies = [
|
||||
"tauri-plugin",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.12",
|
||||
"toml 0.9.0",
|
||||
"toml 0.9.2",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -5067,7 +5090,7 @@ dependencies = [
|
||||
"serde_with",
|
||||
"swift-rs",
|
||||
"thiserror 2.0.12",
|
||||
"toml 0.9.0",
|
||||
"toml 0.9.2",
|
||||
"url",
|
||||
"urlpattern",
|
||||
"uuid",
|
||||
@@ -5284,9 +5307,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.9.0"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f271e09bde39ab52250160a67e88577e0559ad77e9085de6e9051a2c4353f8f8"
|
||||
checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac"
|
||||
dependencies = [
|
||||
"indexmap 2.6.0",
|
||||
"serde",
|
||||
@@ -5294,7 +5317,7 @@ dependencies = [
|
||||
"toml_datetime 0.7.0",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow 0.7.11",
|
||||
"winnow 0.7.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5348,16 +5371,16 @@ dependencies = [
|
||||
"serde_spanned 0.6.9",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_write",
|
||||
"winnow 0.7.11",
|
||||
"winnow 0.7.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5c1c469eda89749d2230d8156a5969a69ffe0d6d01200581cdc6110674d293e"
|
||||
checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30"
|
||||
dependencies = [
|
||||
"winnow 0.7.11",
|
||||
"winnow 0.7.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5368,9 +5391,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b679217f2848de74cabd3e8fc5e6d66f40b7da40f8e1954d92054d9010690fd5"
|
||||
checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
@@ -6430,9 +6453,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.11"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd"
|
||||
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -6656,7 +6679,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"windows-sys 0.59.0",
|
||||
"winnow 0.7.11",
|
||||
"winnow 0.7.12",
|
||||
"xdg-home",
|
||||
"zbus_macros 5.4.0",
|
||||
"zbus_names 4.2.0",
|
||||
@@ -6711,7 +6734,7 @@ checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"winnow 0.7.11",
|
||||
"winnow 0.7.12",
|
||||
"zvariant 5.7.0",
|
||||
]
|
||||
|
||||
@@ -6819,7 +6842,7 @@ dependencies = [
|
||||
"endi",
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"winnow 0.7.11",
|
||||
"winnow 0.7.12",
|
||||
"zvariant_derive 5.7.0",
|
||||
"zvariant_utils 3.2.1",
|
||||
]
|
||||
@@ -6871,5 +6894,5 @@ dependencies = [
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 2.0.87",
|
||||
"winnow 0.7.11",
|
||||
"winnow 0.7.12",
|
||||
]
|
||||
|
||||
@@ -16,15 +16,15 @@
|
||||
"@sentry/vite-plugin": "^2.22.7",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tanstack/react-query": "^5.48.0",
|
||||
"@tauri-apps/api": "~2",
|
||||
"@tauri-apps/plugin-dialog": "~2",
|
||||
"@tauri-apps/plugin-fs": "~2",
|
||||
"@tauri-apps/plugin-http": "~2",
|
||||
"@tauri-apps/api": "^2.0.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||
"@tauri-apps/plugin-fs": "2.4.1",
|
||||
"@tauri-apps/plugin-opener": "^2.4.0",
|
||||
"@tauri-apps/plugin-http": "^2.5.0",
|
||||
"@tauri-apps/plugin-log": "~2",
|
||||
"@tauri-apps/plugin-opener": "~2",
|
||||
"@tauri-apps/plugin-os": "~2",
|
||||
"@tauri-apps/plugin-shell": "~2",
|
||||
"@tauri-apps/plugin-store": "~2",
|
||||
"@tauri-apps/plugin-os": "^2.0.0",
|
||||
"@tauri-apps/plugin-shell": "^2.3.0",
|
||||
"@tauri-apps/plugin-store": "^2.4.1",
|
||||
"@tweenjs/tween.js": "^25.0.0",
|
||||
"@twemoji/svg": "^15.0.0",
|
||||
"ajv": "^8.17.1",
|
||||
|
||||
@@ -238,9 +238,9 @@ reset-reset_all_warning_default-v2 =
|
||||
Are you sure you want to do this?
|
||||
|
||||
reset-full = Full Reset
|
||||
reset-mounting = Reset Mounting
|
||||
reset-mounting-feet = Reset Feet Mounting
|
||||
reset-mounting-fingers = Reset Fingers Mounting
|
||||
reset-mounting = Mounting Calibration
|
||||
reset-mounting-feet = Feet Calibration
|
||||
reset-mounting-fingers = Fingers Calibration
|
||||
reset-yaw = Yaw Reset
|
||||
|
||||
## Serial detection stuff
|
||||
@@ -262,6 +262,7 @@ navbar-settings = Settings
|
||||
|
||||
## Biovision hierarchy recording
|
||||
bvh-start_recording = Record BVH
|
||||
bvh-stop_recording = Save BVH recording
|
||||
bvh-recording = Recording...
|
||||
bvh-save_title = Save BVH recording
|
||||
|
||||
@@ -277,8 +278,8 @@ widget-overlay-is_mirrored_label = Display Overlay as Mirror
|
||||
## Widget: Drift compensation
|
||||
widget-drift_compensation-clear = Clear drift compensation
|
||||
|
||||
## Widget: Clear Reset Mounting
|
||||
widget-clear_mounting = Clear Reset Mounting
|
||||
## Widget: Clear Mounting calibration
|
||||
widget-clear_mounting = Clear mounting calibration
|
||||
|
||||
## Widget: Developer settings
|
||||
widget-developer_mode = Developer Mode
|
||||
@@ -455,6 +456,7 @@ mounting_selection_menu-close = Close
|
||||
## Sidebar settings
|
||||
settings-sidebar-title = Settings
|
||||
settings-sidebar-general = General
|
||||
settings-sidebar-steamvr = SteamVR
|
||||
settings-sidebar-tracker_mechanics = Tracker mechanics
|
||||
settings-sidebar-stay_aligned = Stay Aligned
|
||||
settings-sidebar-fk_settings = Tracking settings
|
||||
@@ -462,9 +464,12 @@ settings-sidebar-gesture_control = Gesture control
|
||||
settings-sidebar-interface = Interface
|
||||
settings-sidebar-osc_router = OSC router
|
||||
settings-sidebar-osc_trackers = VRChat OSC Trackers
|
||||
settings-sidebar-osc_vmc = VMC
|
||||
settings-sidebar-utils = Utilities
|
||||
settings-sidebar-serial = Serial console
|
||||
settings-sidebar-appearance = Appearance
|
||||
settings-sidebar-home = Home Screen
|
||||
settings-sidebar-checklist = Tracking checklist
|
||||
settings-sidebar-notifications = Notifications
|
||||
settings-sidebar-behavior = Behavior
|
||||
settings-sidebar-firmware-tool = DIY Firmware Tool
|
||||
@@ -654,7 +659,7 @@ settings-general-gesture_control-yawResetTaps = Taps for yaw reset
|
||||
settings-general-gesture_control-fullResetEnabled = Enable tap to full reset
|
||||
settings-general-gesture_control-fullResetDelay = Full reset delay
|
||||
settings-general-gesture_control-fullResetTaps = Taps for full reset
|
||||
settings-general-gesture_control-mountingResetEnabled = Enable tap to reset mounting
|
||||
settings-general-gesture_control-mountingResetEnabled = Enable tap to mounting calibration
|
||||
settings-general-gesture_control-mountingResetDelay = Mounting reset delay
|
||||
settings-general-gesture_control-mountingResetTaps = Taps for mounting reset
|
||||
# The number of trackers that can have higher acceleration before a tap is rejected
|
||||
@@ -879,6 +884,16 @@ settings-utils-advanced-open_logs = Logs folder
|
||||
settings-utils-advanced-open_logs-description = Open SlimeVR's logs folder in file explorer, containing the logs of the app
|
||||
settings-utils-advanced-open_logs-label = Open folder
|
||||
|
||||
## Home Screen
|
||||
settings-home-list-layout = Trackers list layout
|
||||
settings-home-list-layout-desc = Select one of the possible layouts of the home screen
|
||||
settings-home-list-layout-grid = Grid
|
||||
settings-home-list-layout-table = Table
|
||||
|
||||
## Tracking Checlist
|
||||
settings-tracking_checklist-active_steps = Active Steps
|
||||
settings-tracking_checklist-active_steps-desc = List all the steps that will show in the tracking checklist. You can either disable or enable ignorable steps
|
||||
|
||||
## Setup/onboarding menu
|
||||
onboarding-skip = Skip setup
|
||||
onboarding-continue = Continue
|
||||
@@ -1132,7 +1147,11 @@ onboarding-automatic_mounting-done-description = Your mounting calibration is co
|
||||
onboarding-automatic_mounting-done-restart = Try again
|
||||
onboarding-automatic_mounting-mounting_reset-title = Mounting Reset
|
||||
onboarding-automatic_mounting-mounting_reset-step-0 = 1. Squat in a "skiing" pose with your legs bent, your upper body tilted forwards, and your arms bent.
|
||||
onboarding-automatic_mounting-mounting_reset-step-1 = 2. Press the "Reset Mounting" button and wait for 3 seconds before the trackers' mounting orientations will reset.
|
||||
onboarding-automatic_mounting-mounting_reset-step-1 = 2. Press the "Mounting calibration" button and wait for 3 seconds before the trackers' mounting orientations will reset.
|
||||
|
||||
onboarding-automatic_mounting-mounting_reset-feet-step-0 = 1. Stand on your toes with both feet pointing forward. Alternatively you can do it siting on a chair.
|
||||
onboarding-automatic_mounting-mounting_reset-feet-step-1 = 2. Press the "Feet calibration" button and wait for 3 seconds before the trackers' mounting orientations will reset.
|
||||
|
||||
onboarding-automatic_mounting-preparation-title = Preparation
|
||||
onboarding-automatic_mounting-preparation-v2-step-0 = 1. Press the "Full Reset" button.
|
||||
onboarding-automatic_mounting-preparation-v2-step-1 = 2. Stand upright with your arms to your sides. Make sure to look forward.
|
||||
@@ -1305,6 +1324,8 @@ onboarding-stay_aligned-done = Done
|
||||
|
||||
## Home
|
||||
home-no_trackers = No trackers detected or assigned
|
||||
home-settings = Home Page Settings
|
||||
home-settings-close = Close
|
||||
|
||||
## Trackers Still On notification
|
||||
trackers_still_on-modal-title = Trackers still on
|
||||
@@ -1522,3 +1543,58 @@ error_collection_modal-description_v2 = { settings-interface-behavior-error_trac
|
||||
You can change this setting later in the Behavior section of the settings page.
|
||||
error_collection_modal-confirm = I agree
|
||||
error_collection_modal-cancel = I don't want to
|
||||
|
||||
|
||||
tracking_checklist = Tracking Checklist
|
||||
tracking_checklist-settings = Tracking Checklist Settings
|
||||
tracking_checklist-settings-close = Close
|
||||
tracking_checklist-status-incomplete = You are not prepared to use SlimeVR!
|
||||
tracking_checklist-status-partial = {$count ->
|
||||
[one] You have 1 warning!
|
||||
*[many] You have {$count} warnings!
|
||||
}
|
||||
tracking_checklist-status-complete = You are prepared to use SlimeVR!
|
||||
tracking_checklist-MOUNTING_CALIBRATION = Perform a mounting calibration
|
||||
tracking_checklist-FEET_MOUNTING_CALIBRATION = Perform a feet mounting calibration
|
||||
tracking_checklist-FULL_RESET = Perform a full Reset
|
||||
tracking_checklist-FULL_RESET-desc = Some Trackers need a reset to be performed
|
||||
tracking_checklist-STEAMVR_DISCONNECTED = SteamVR not running
|
||||
tracking_checklist-STEAMVR_DISCONNECTED-desc = SteamVR is not running. Are you using it for vr?
|
||||
tracking_checklist-STEAMVR_DISCONNECTED-open = Launch SteamVR
|
||||
tracking_checklist-TRACKERS_REST_CALIBRATION = Calibrate your trackers
|
||||
tracking_checklist-TRACKERS_REST_CALIBRATION-desc = You didn't perform tracker calibration. Please let your Slimes (highlighted in yellow) rest on a stable surface for a few secconds.
|
||||
tracking_checklist-TRACKER_ERROR = Trackers with Errors
|
||||
tracking_checklist-TRACKER_ERROR-desc = Some of your trackers have an error. Please restart the tracker.
|
||||
tracking_checklist-VRCHAT_SETTINGS = Configure VRChat settings
|
||||
tracking_checklist-VRCHAT_SETTINGS-desc = You have misconfigured VRchat Settings! This can impact your tracking experience.
|
||||
tracking_checklist-VRCHAT_SETTINGS-open = Go to VRChat Warnings
|
||||
tracking_checklist-UNASSIGNED_HMD = VR Headset not assigned to Head
|
||||
tracking_checklist-UNASSIGNED_HMD-desc = The VR headset should be assigned as a head tracker.
|
||||
tracking_checklist-NETWORK_PROFILE_PUBLIC = Change your network profile
|
||||
tracking_checklist-NETWORK_PROFILE_PUBLIC-desc = {$count ->
|
||||
[one] Your network profile is currently set to Public ({$adapters}).
|
||||
This is not recommended for SlimeVR to function properly.
|
||||
<PublicFixLink>See how to fix it here.</PublicFixLink>
|
||||
*[many] Some of your network adapters are set to public:
|
||||
{$adapters}
|
||||
This is not recommended for SlimeVR to function properly.
|
||||
<PublicFixLink>See how to fix it here.</PublicFixLink>
|
||||
}
|
||||
tracking_checklist-NETWORK_PROFILE_PUBLIC-open = Open Control Panel
|
||||
tracking_checklist-STAY_ALIGNED_CONFIGURED = Configure Stay Aligned
|
||||
tracking_checklist-STAY_ALIGNED_CONFIGURED-desc = Record the Stay Aligned poses for an improved imu drift
|
||||
tracking_checklist-STAY_ALIGNED_CONFIGURED-open = Open Stay Aligned Wizard
|
||||
|
||||
tracking_checklist-ignore = Ignore
|
||||
|
||||
preview-mocap_mode_soon = Mocap Mode (Soon™)
|
||||
preview-disable_render = Disable rendering
|
||||
preview-disabled_render = Rendering disabled
|
||||
|
||||
toolbar-mounting_calibration = Mounting Calibration
|
||||
toolbar-mounting_calibration-default = Body
|
||||
toolbar-mounting_calibration-feet = Feet
|
||||
toolbar-mounting_calibration-fingers = Fingers
|
||||
toolbar-drift_reset = Drift Reset
|
||||
toolbar-assigned_trackers = {$count} trackers assigned
|
||||
toolbar-unassigned_trackers = {$count} trackers unassigned
|
||||
|
||||
BIN
gui/public/images/mounting/MountingFeets.webp
Normal file
BIN
gui/public/images/mounting/MountingFeets.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
gui/public/images/mounting/MountingFeetsSide.webp
Normal file
BIN
gui/public/images/mounting/MountingFeetsSide.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
BIN
gui/public/sounds/full-reset/end-full-reset-with-tail.ogg
Normal file
BIN
gui/public/sounds/full-reset/end-full-reset-with-tail.ogg
Normal file
Binary file not shown.
BIN
gui/public/sounds/full-reset/full-click-1.ogg
Normal file
BIN
gui/public/sounds/full-reset/full-click-1.ogg
Normal file
Binary file not shown.
BIN
gui/public/sounds/full-reset/full-click-2.ogg
Normal file
BIN
gui/public/sounds/full-reset/full-click-2.ogg
Normal file
Binary file not shown.
BIN
gui/public/sounds/full-reset/full-click-3.ogg
Normal file
BIN
gui/public/sounds/full-reset/full-click-3.ogg
Normal file
Binary file not shown.
BIN
gui/public/sounds/full-reset/init-full-reset-with-tail.ogg
Normal file
BIN
gui/public/sounds/full-reset/init-full-reset-with-tail.ogg
Normal file
Binary file not shown.
BIN
gui/public/sounds/mew.ogg
Normal file
BIN
gui/public/sounds/mew.ogg
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
gui/public/sounds/mounting-reset/mount-click-1.ogg
Normal file
BIN
gui/public/sounds/mounting-reset/mount-click-1.ogg
Normal file
Binary file not shown.
BIN
gui/public/sounds/mounting-reset/mount-click-2.ogg
Normal file
BIN
gui/public/sounds/mounting-reset/mount-click-2.ogg
Normal file
Binary file not shown.
BIN
gui/public/sounds/mounting-reset/mount-click-3.ogg
Normal file
BIN
gui/public/sounds/mounting-reset/mount-click-3.ogg
Normal file
Binary file not shown.
BIN
gui/public/sounds/tracking/pause.ogg
Normal file
BIN
gui/public/sounds/tracking/pause.ogg
Normal file
Binary file not shown.
BIN
gui/public/sounds/tracking/play.ogg
Normal file
BIN
gui/public/sounds/tracking/play.ogg
Normal file
Binary file not shown.
BIN
gui/public/sounds/yaw-reset/yaw-reset.ogg
Normal file
BIN
gui/public/sounds/yaw-reset/yaw-reset.ogg
Normal file
Binary file not shown.
@@ -28,15 +28,14 @@ shadow-rs = "0.35"
|
||||
[dependencies]
|
||||
serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tauri = { version = "2", features = ["devtools", "tray-icon", "image-png", "rustls-tls"] }
|
||||
tauri-runtime = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-fs = "2"
|
||||
tauri-plugin-http = "2"
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-os = "2"
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-store = "2"
|
||||
tauri = { version = "2.0", features = ["devtools", "tray-icon", "image-png", "rustls-tls"] }
|
||||
tauri-runtime = "2.0"
|
||||
tauri-plugin-dialog = "2.0"
|
||||
tauri-plugin-fs = "2.4.1"
|
||||
tauri-plugin-os = "2.0"
|
||||
tauri-plugin-shell = "2.3.0"
|
||||
tauri-plugin-store = "2.0"
|
||||
flexi_logger = "0.29"
|
||||
log-panics = { version = "2", features = ["with-backtrace"] }
|
||||
log = "0.4"
|
||||
clap = { version = "4.0.29", features = ["derive"] }
|
||||
@@ -55,6 +54,8 @@ dirs-next = "2.0.0"
|
||||
discord-sdk = "0.3.6"
|
||||
tokio = { version = "1.37.0", features = ["time"] }
|
||||
itertools = "0.13.0"
|
||||
tauri-plugin-opener = "2.4.0"
|
||||
tauri-plugin-http = "2.5.0"
|
||||
tauri-plugin-log = "2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
|
||||
@@ -41,6 +41,17 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "opener:allow-open-url",
|
||||
"allow": [
|
||||
{
|
||||
"url": "steam:*"
|
||||
},
|
||||
{
|
||||
"url": "ms-settings:network"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
|
||||
@@ -39,7 +39,6 @@ import { OSCRouterSettings } from './components/settings/pages/OSCRouterSettings
|
||||
import * as os from '@tauri-apps/plugin-os';
|
||||
import { VMCSettings } from './components/settings/pages/VMCSettings';
|
||||
import { MountingChoose } from './components/onboarding/pages/mounting/MountingChoose';
|
||||
import { StatusProvider } from './components/providers/StatusSystemContext';
|
||||
import { VersionUpdateModal } from './components/VersionUpdateModal';
|
||||
import { CalibrationTutorialPage } from './components/onboarding/pages/CalibrationTutorial';
|
||||
import { AssignmentTutorialPage } from './components/onboarding/pages/assignment-preparation/AssignmentTutorial';
|
||||
@@ -61,6 +60,9 @@ import { FirmwareUpdate } from './components/firmware-update/FirmwareUpdate';
|
||||
import { ConnectionLost } from './components/onboarding/pages/ConnectionLost';
|
||||
import { VRCWarningsPage } from './components/vrc/VRCWarningsPage';
|
||||
import { StayAlignedSetup } from './components/onboarding/pages/stay-aligned/StayAlignedSetup';
|
||||
import { TrackingChecklistProvider } from './components/tracking-checklist/TrackingChecklistProvider';
|
||||
import { HomeScreenSettings } from './components/settings/pages/HomeScreenSettings';
|
||||
import { ChecklistPage } from './components/tracking-checklist/TrackingChecklist';
|
||||
|
||||
export const GH_REPO = 'SlimeVR/SlimeVR-Server';
|
||||
export const VersionContext = createContext('');
|
||||
@@ -83,7 +85,7 @@ function Layout() {
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<MainLayout isMobile={isMobile}>
|
||||
<MainLayout isMobile={isMobile} full>
|
||||
<Home />
|
||||
</MainLayout>
|
||||
}
|
||||
@@ -91,7 +93,7 @@ function Layout() {
|
||||
<Route
|
||||
path="/firmware-update"
|
||||
element={
|
||||
<MainLayout isMobile={isMobile} widgets={false}>
|
||||
<MainLayout isMobile={isMobile}>
|
||||
<FirmwareUpdate />
|
||||
</MainLayout>
|
||||
}
|
||||
@@ -99,11 +101,19 @@ function Layout() {
|
||||
<Route
|
||||
path="/vr-mode"
|
||||
element={
|
||||
<MainLayout isMobile={isMobile}>
|
||||
<MainLayout isMobile={isMobile} full>
|
||||
<VRModePage />
|
||||
</MainLayout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/checklist"
|
||||
element={
|
||||
<MainLayout isMobile={isMobile}>
|
||||
<ChecklistPage />
|
||||
</MainLayout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/tracker/:trackernum/:deviceid"
|
||||
element={
|
||||
@@ -115,7 +125,7 @@ function Layout() {
|
||||
<Route
|
||||
path="/vrc-warnings"
|
||||
element={
|
||||
<MainLayout isMobile={isMobile} widgets={false}>
|
||||
<MainLayout isMobile={isMobile}>
|
||||
<VRCWarningsPage />
|
||||
</MainLayout>
|
||||
}
|
||||
@@ -135,6 +145,7 @@ function Layout() {
|
||||
<Route path="osc/vrchat" element={<VRCOSCSettings />} />
|
||||
<Route path="osc/vmc" element={<VMCSettings />} />
|
||||
<Route path="interface" element={<InterfaceSettings />} />
|
||||
<Route path="interface/home" element={<HomeScreenSettings />} />
|
||||
<Route path="advanced" element={<AdvancedSettings />} />
|
||||
</Route>
|
||||
<Route
|
||||
@@ -297,7 +308,7 @@ export default function App() {
|
||||
<WebSocketApiContext.Provider value={websocketAPI}>
|
||||
<AppContextProvider>
|
||||
<OnboardingContextProvider>
|
||||
<StatusProvider>
|
||||
<TrackingChecklistProvider>
|
||||
<VersionContext.Provider value={updateFound}>
|
||||
<div className="h-full w-full text-standard bg-background-80 text-background-10">
|
||||
<Preload />
|
||||
@@ -305,7 +316,7 @@ export default function App() {
|
||||
{websocketAPI.isConnected && <Layout />}
|
||||
</div>
|
||||
</VersionContext.Provider>
|
||||
</StatusProvider>
|
||||
</TrackingChecklistProvider>
|
||||
</OnboardingContextProvider>
|
||||
</AppContextProvider>
|
||||
</WebSocketApiContext.Provider>
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Localized } from '@fluent/react';
|
||||
import { ClearMountingResetRequestT, RpcMessage } from 'solarxr-protocol';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import { BigButton } from './commons/BigButton';
|
||||
import { TrashIcon } from './commons/icon/TrashIcon';
|
||||
import { Quaternion } from 'three';
|
||||
import { QuaternionFromQuatT, similarQuaternions } from '@/maths/quaternion';
|
||||
import { useMemo } from 'react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { assignedTrackersAtom } from '@/store/app-store';
|
||||
|
||||
const _q = new Quaternion();
|
||||
|
||||
export function ClearMountingButton() {
|
||||
const { sendRPCPacket } = useWebsocketAPI();
|
||||
const assignedTrackers = useAtomValue(assignedTrackersAtom);
|
||||
|
||||
const trackerWithMounting = useMemo(
|
||||
() =>
|
||||
assignedTrackers.some(
|
||||
(d) =>
|
||||
!similarQuaternions(
|
||||
QuaternionFromQuatT(d?.tracker.info?.mountingResetOrientation),
|
||||
_q
|
||||
)
|
||||
),
|
||||
[assignedTrackers]
|
||||
);
|
||||
|
||||
const clearMounting = () => {
|
||||
const record = new ClearMountingResetRequestT();
|
||||
sendRPCPacket(RpcMessage.ClearMountingResetRequest, record);
|
||||
};
|
||||
|
||||
return (
|
||||
<Localized id={'widget-clear_mounting'}>
|
||||
<BigButton
|
||||
icon={<TrashIcon size={20} />}
|
||||
onClick={clearMounting}
|
||||
disabled={!trackerWithMounting}
|
||||
/>
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +1,67 @@
|
||||
:root {
|
||||
--toolbar-h: 120px;
|
||||
}
|
||||
|
||||
.main-layout {
|
||||
display: grid;
|
||||
grid-template:
|
||||
't t' var(--topbar-h)
|
||||
's c' calc(100% - var(--topbar-h))
|
||||
'n c' calc(100% - var(--topbar-h))
|
||||
/ var(--navbar-w) calc(100% - var(--navbar-w));
|
||||
|
||||
&:has(.widgets) {
|
||||
&.full {
|
||||
grid-template:
|
||||
't t t' var(--topbar-h)
|
||||
's c w' calc(100% - var(--topbar-h))
|
||||
/ var(--navbar-w) calc(100% - var(--navbar-w) - var(--widget-w)) var(
|
||||
--widget-w
|
||||
);
|
||||
'n b s' var(--toolbar-h)
|
||||
'n c s' calc(100% - var(--topbar-h) - var(--toolbar-h))
|
||||
/ var(--navbar-w) calc(100% - var(--navbar-w) - var(--right-section-w)) var(--right-section-w);
|
||||
}
|
||||
|
||||
@screen nsm {
|
||||
--right-section-w: 40%;
|
||||
}
|
||||
|
||||
@screen sm {
|
||||
--right-section-w: 35%;
|
||||
}
|
||||
|
||||
@screen md {
|
||||
--right-section-w: 30%;
|
||||
}
|
||||
|
||||
@screen lg {
|
||||
--right-section-w: 25%;
|
||||
}
|
||||
|
||||
@screen xl {
|
||||
--right-section-w: 22%;
|
||||
}
|
||||
|
||||
@screen mobile {
|
||||
--checklist-h: 30px;
|
||||
|
||||
&.checklist-ok {
|
||||
--checklist-h: 0px;
|
||||
}
|
||||
|
||||
&.full {
|
||||
grid-template:
|
||||
't' var(--topbar-h)
|
||||
'l' var(--checklist-h)
|
||||
'b' var(--toolbar-h)
|
||||
'c' calc(
|
||||
100% - var(--topbar-h) - var(--checklist-h) - var(--toolbar-h) - var(
|
||||
--navbar-h
|
||||
)
|
||||
)
|
||||
'n' calc(var(--navbar-h))
|
||||
/ 100%;
|
||||
}
|
||||
|
||||
grid-template:
|
||||
't' var(--topbar-h)
|
||||
'c' calc(100% - var(--topbar-h) - var(--navbar-h))
|
||||
's' calc(var(--navbar-h))
|
||||
'n' calc(var(--navbar-h))
|
||||
/ 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,20 +9,25 @@ import {
|
||||
import { Navbar } from './Navbar';
|
||||
import { TopBar } from './TopBar';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import { WidgetsComponent } from './WidgetsComponent';
|
||||
import './MainLayout.scss';
|
||||
import { Toolbar } from './Toolbar';
|
||||
import { Sidebar } from './Sidebar';
|
||||
import { TrackingChecklistMobile } from './tracking-checklist/TrackingChecklist';
|
||||
import { useTrackingChecklist } from '@/hooks/tracking-checklist';
|
||||
|
||||
export function MainLayout({
|
||||
children,
|
||||
background = true,
|
||||
widgets = true,
|
||||
full = false,
|
||||
isMobile = undefined,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
background?: boolean;
|
||||
isMobile?: boolean;
|
||||
widgets?: boolean;
|
||||
showToolbarSettings?: boolean;
|
||||
full?: boolean;
|
||||
}) {
|
||||
const { completion } = useTrackingChecklist();
|
||||
const { sendRPCPacket } = useWebsocketAPI();
|
||||
const [ProportionsLastPageOpen, setProportionsLastPageOpen] = useState(true);
|
||||
|
||||
@@ -58,33 +63,42 @@ export function MainLayout({
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="main-layout w-full h-screen">
|
||||
<div
|
||||
className={classNames('main-layout w-full h-screen', full && 'full', {
|
||||
'checklist-ok': completion === 'complete',
|
||||
})}
|
||||
>
|
||||
<div style={{ gridArea: 't' }}>
|
||||
<TopBar />
|
||||
</div>
|
||||
<div style={{ gridArea: 's' }} className="overflow-y-auto">
|
||||
<div style={{ gridArea: 'n' }} className="overflow-y-auto">
|
||||
<Navbar />
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{ gridArea: 'c' }}
|
||||
className={classNames(
|
||||
'overflow-y-auto mr-2 my-2 mobile:m-0',
|
||||
'flex flex-col rounded-xl',
|
||||
background && 'bg-background-70'
|
||||
'flex flex-col rounded-md',
|
||||
background && 'bg-background-70',
|
||||
{ 'rounded-t-none': !isMobile && full }
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{!isMobile && widgets && (
|
||||
<div
|
||||
style={{ gridArea: 'w' }}
|
||||
className="overflow-y-auto mr-2 my-2 rounded-xl bg-background-70 flex flex-col gap-2 p-2 widgets"
|
||||
>
|
||||
<WidgetsComponent />
|
||||
{full && isMobile && completion !== 'complete' && (
|
||||
<TrackingChecklistMobile />
|
||||
)}
|
||||
{full && (
|
||||
<div style={{ gridArea: 'b' }}>
|
||||
<Toolbar />
|
||||
</div>
|
||||
)}
|
||||
{!isMobile && full && (
|
||||
<div style={{ gridArea: 's' }} className="mr-2">
|
||||
<Sidebar />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ import { useLocalization } from '@fluent/react';
|
||||
import classnames from 'classnames';
|
||||
import { ReactNode } from 'react';
|
||||
import { NavLink, useMatch } from 'react-router-dom';
|
||||
import { CubeIcon } from './commons/icon/CubeIcon';
|
||||
import { GearIcon } from './commons/icon/GearIcon';
|
||||
import { HumanIcon } from './commons/icon/HumanIcon';
|
||||
import { RulerIcon } from './commons/icon/RulerIcon';
|
||||
import { SparkleIcon } from './commons/icon/SparkleIcon';
|
||||
import { WrenchIcon } from './commons/icon/WrenchIcons';
|
||||
import { useBreakpoint } from '@/hooks/breakpoint';
|
||||
import { useConfig } from '@/hooks/config';
|
||||
import { HomeIcon } from './commons/icon/HomeIcon';
|
||||
import { SkiIcon } from './commons/icon/SkiIcon';
|
||||
|
||||
export function NavButton({
|
||||
to,
|
||||
@@ -34,7 +34,7 @@ export function NavButton({
|
||||
state={state}
|
||||
className={classnames(
|
||||
'flex flex-col justify-center xs:gap-4 mobile:gap-2',
|
||||
'xs:w-[85px] mobile:w-[80px] mobile:h-[80px]',
|
||||
'xs:w-[85px] mobile:w-[65px] mobile:h-[65px]',
|
||||
'xs:py-3 mobile:py-4 rounded-md mobile:rounded-b-none group select-text',
|
||||
{
|
||||
'bg-accent-background-50 fill-accent-background-20': doesMatch,
|
||||
@@ -44,16 +44,16 @@ export function NavButton({
|
||||
>
|
||||
<div className="flex justify-around">
|
||||
<div
|
||||
className={classnames('scale-150', {
|
||||
className={classnames('scale-[150%]', {
|
||||
'fill-accent-lighter': doesMatch,
|
||||
'fill-background-50': !doesMatch,
|
||||
'fill-background-40': !doesMatch,
|
||||
})}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classnames('text-center', {
|
||||
className={classnames('text-center mobile:hidden', {
|
||||
'text-accent-background-10': doesMatch,
|
||||
'text-background-10': !doesMatch,
|
||||
})}
|
||||
@@ -70,7 +70,7 @@ export function MainLinks() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavButton to="/" icon={<CubeIcon />}>
|
||||
<NavButton to="/" icon={<HomeIcon />}>
|
||||
{l10n.getString('navbar-home')}
|
||||
</NavButton>
|
||||
<NavButton
|
||||
@@ -84,7 +84,7 @@ export function MainLinks() {
|
||||
to="/onboarding/mounting/choose"
|
||||
match="/onboarding/mounting/*"
|
||||
state={{ alonePage: true }}
|
||||
icon={<WrenchIcon />}
|
||||
icon={<SkiIcon />}
|
||||
>
|
||||
{l10n.getString('navbar-mounting')}
|
||||
</NavButton>
|
||||
|
||||
251
gui/src/components/Sidebar.tsx
Normal file
251
gui/src/components/Sidebar.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
import { useTrackingChecklist } from '@/hooks/tracking-checklist';
|
||||
import { TrackingChecklist } from './tracking-checklist/TrackingChecklist';
|
||||
import { SkeletonVisualizerWidget } from './widgets/SkeletonVisualizerWidget';
|
||||
import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Typography } from './commons/Typography';
|
||||
import { useLocaleConfig } from '@/i18n/config';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import {
|
||||
RpcMessage,
|
||||
SkeletonConfigRequestT,
|
||||
SkeletonConfigResponseT,
|
||||
} from 'solarxr-protocol';
|
||||
import { Tooltip } from './commons/Tooltip';
|
||||
import { Vector3 } from 'three';
|
||||
import { RecordIcon } from './commons/icon/RecordIcon';
|
||||
import { PauseIcon } from './commons/icon/PauseIcon';
|
||||
import { HumanIcon } from './commons/icon/HumanIcon';
|
||||
import { EyeIcon } from './commons/icon/EyeIcon';
|
||||
import { useConfig } from '@/hooks/config';
|
||||
import { useBHV } from '@/hooks/bvh';
|
||||
import { usePauseTracking } from '@/hooks/pause-tracking';
|
||||
import { PlayIcon } from './commons/icon/PlayIcon';
|
||||
|
||||
export function PreviewControls({ open }: { open: boolean }) {
|
||||
const [userHeight, setUserHeight] = useState('');
|
||||
const { currentLocales } = useLocaleConfig();
|
||||
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
|
||||
|
||||
const {
|
||||
state: bvhState,
|
||||
toggle: toggleBVH,
|
||||
available: bvhAvailable,
|
||||
} = useBHV();
|
||||
const { paused, toggle: toggleTracking } = usePauseTracking();
|
||||
|
||||
const { cmFormat } = useMemo(() => {
|
||||
const cmFormat = Intl.NumberFormat(currentLocales, {
|
||||
style: 'unit',
|
||||
unit: 'centimeter',
|
||||
maximumFractionDigits: 1,
|
||||
});
|
||||
return { cmFormat };
|
||||
}, [currentLocales]);
|
||||
useRPCPacket(
|
||||
RpcMessage.SkeletonConfigResponse,
|
||||
(data: SkeletonConfigResponseT) => {
|
||||
if (data.userHeight)
|
||||
setUserHeight(cmFormat.format((data.userHeight * 100) / 0.936));
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
sendRPCPacket(
|
||||
RpcMessage.SkeletonConfigRequest,
|
||||
new SkeletonConfigRequestT()
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
preferedDirection="bottom"
|
||||
content={
|
||||
<Typography id="onboarding-manual_proportions-estimated_height" />
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'h-10 bg-background-60 p-4 flex items-center rounded-lg justify-center cursor-help w-fit top-2 left-2 absolute',
|
||||
{
|
||||
'opacity-0': !open,
|
||||
'opacity-100': open,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Typography variant="section-title">{userHeight}</Typography>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className="absolute bottom-0 pb-4 flex justify-center w-full">
|
||||
<div className="flex bg-background-80 bg-opacity-70 rounded-lg gap-2 px-4 py-2 items-center fill-background-10">
|
||||
{bvhAvailable && (
|
||||
<Tooltip
|
||||
content={
|
||||
<Typography
|
||||
variant="section-title"
|
||||
id={
|
||||
bvhState === 'idle'
|
||||
? 'bvh-start_recording'
|
||||
: 'bvh-stop_recording'
|
||||
}
|
||||
/>
|
||||
}
|
||||
preferedDirection="top"
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex justify-center items-center w-10 h-10 rounded-full hover:bg-background-60 cursor-pointer',
|
||||
{ 'bg-background-60': bvhState !== 'idle' }
|
||||
)}
|
||||
onClick={() => toggleBVH()}
|
||||
>
|
||||
{bvhState === 'idle' && <RecordIcon width={20} />}
|
||||
{bvhState !== 'idle' && (
|
||||
<div className="w-5 h-5 rounded-full bg-status-critical animate-pulse" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip
|
||||
content={
|
||||
<Typography
|
||||
variant="section-title"
|
||||
id={paused ? 'tracking-paused' : 'tracking-unpaused'}
|
||||
/>
|
||||
}
|
||||
preferedDirection="top"
|
||||
>
|
||||
<div
|
||||
className="flex justify-center items-center w-14 h-14 rounded-full bg-background-60 hover:bg-background-50 cursor-pointer"
|
||||
onClick={() => toggleTracking()}
|
||||
>
|
||||
{!paused && <PauseIcon width={25} />}
|
||||
{paused && <PlayIcon width={25} />}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={
|
||||
<Typography
|
||||
variant="section-title"
|
||||
id="preview-mocap_mode_soon"
|
||||
/>
|
||||
}
|
||||
preferedDirection="top"
|
||||
>
|
||||
<div className="flex justify-center items-center w-10 h-10 rounded-full cursor-not-allowed">
|
||||
<HumanIcon width={20} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PreviewSection({ open }: { open: boolean }) {
|
||||
const { config, setConfig } = useConfig();
|
||||
const [disabledRender, setDisabledRender] = useState(config?.skeletonPreview);
|
||||
|
||||
const toggleRender = () => {
|
||||
setConfig({ skeletonPreview: disabledRender });
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// need useLayoutEffect to make sure that the state is corect before the first render of the skeleton
|
||||
setDisabledRender(!config?.skeletonPreview);
|
||||
}, [config]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'transition-opacity duration-500 delay-500 h-full relative',
|
||||
{
|
||||
'opacity-0': !open,
|
||||
'opacity-100': open,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<SkeletonVisualizerWidget
|
||||
disabled={disabledRender}
|
||||
toggleDisabled={() => toggleRender()}
|
||||
onInit={(context) => {
|
||||
context.addView({
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
position: new Vector3(3, 2.5, -3),
|
||||
onHeightChange(v, newHeight) {
|
||||
v.controls.target.set(0, newHeight / 2.2, 0.1);
|
||||
const scale = Math.max(1, newHeight) / 1.3;
|
||||
v.camera.zoom = 1 / scale;
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
preferedDirection="bottom"
|
||||
content={<Typography id="preview-disable_render" />}
|
||||
>
|
||||
<div
|
||||
className="flex justify-center items-center w-10 h-10 cursor-pointer rounded-full fill-background-10 absolute right-2 top-2 bg-background-60 hover:bg-background-50"
|
||||
onClick={() => toggleRender()}
|
||||
>
|
||||
<EyeIcon width={18} closed={!disabledRender} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<PreviewControls open={open} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Sidebar() {
|
||||
const { completion } = useTrackingChecklist();
|
||||
const [closed, setClosed] = useState(true);
|
||||
const [closing, setClosing] = useState(false);
|
||||
|
||||
const closedHight = '90px';
|
||||
const checklistSize = closed ? closedHight : 'calc(100% - 16px)';
|
||||
const previewSize = closed ? `calc(100% - ${closedHight} - 24px)` : '0%';
|
||||
|
||||
const toggleClosed = () => setClosed((closed) => !closed);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setClosing(true);
|
||||
const ref = setTimeout(() => setClosing(false), 1000);
|
||||
return () => {
|
||||
clearTimeout(ref);
|
||||
setClosing(false);
|
||||
};
|
||||
}, [closed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (completion === 'complete') {
|
||||
setClosed(true);
|
||||
} else if (completion === 'incomplete') {
|
||||
setClosed(false);
|
||||
}
|
||||
}, [completion]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="transition-[height] duration-500 rounded-lg my-2 bg-background-70 overflow-clip"
|
||||
style={{ height: checklistSize }}
|
||||
>
|
||||
<TrackingChecklist
|
||||
closed={closed}
|
||||
closing={closing}
|
||||
toggleClosed={toggleClosed}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="transition-[height] duration-500 rounded-lg my-2 bg-background-70 overflow-clip"
|
||||
style={{ height: previewSize }}
|
||||
>
|
||||
<PreviewSection open={closed} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
202
gui/src/components/Toolbar.tsx
Normal file
202
gui/src/components/Toolbar.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import { Typography } from './commons/Typography';
|
||||
import classNames from 'classnames';
|
||||
import { ResetType } from 'solarxr-protocol';
|
||||
import {
|
||||
BODY_PARTS_GROUPS,
|
||||
MountingResetGroup,
|
||||
ResetBtnStatus,
|
||||
useReset,
|
||||
UseResetOptions,
|
||||
} from '@/hooks/reset';
|
||||
import { Tooltip } from './commons/Tooltip';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { assignedTrackersAtom } from '@/store/app-store';
|
||||
import { useBreakpoint } from '@/hooks/breakpoint';
|
||||
import { useMemo } from 'react';
|
||||
import { ResetButtonIcon } from './home/ResetButton';
|
||||
|
||||
const MAINBUTTON_CLASSES = ({ disabled }: { disabled: boolean }) =>
|
||||
classNames(
|
||||
'relative overflow-clip',
|
||||
'flex h-full items-center justify-center gap-2 px-4 bg-background-60 rounded-lg fill-background-10 aspect-square md:aspect-auto',
|
||||
{
|
||||
'cursor-pointer hover:bg-background-50 bg-background-60': !disabled,
|
||||
'cursor-not-allowed bg-background-70 brightness-75': disabled,
|
||||
}
|
||||
);
|
||||
|
||||
function ButtonProgress({
|
||||
progress,
|
||||
status,
|
||||
}: {
|
||||
progress: number;
|
||||
status: ResetBtnStatus;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute top-0 left-0 w-0 h-full bg-accent-background-20 opacity-50'
|
||||
)}
|
||||
style={{
|
||||
width: `${progress * 100}%`,
|
||||
transition:
|
||||
status === 'counting'
|
||||
? 'width 0.3s cubic-bezier(0.68, -0.8, 0.32, 1.8)'
|
||||
: 'width 1s linear',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BasicResetButton(options: UseResetOptions & { customName?: string }) {
|
||||
const { isMd } = useBreakpoint('md');
|
||||
const {
|
||||
triggerReset,
|
||||
status,
|
||||
name: resetName,
|
||||
timer,
|
||||
progress: resetProress,
|
||||
disabled,
|
||||
duration,
|
||||
} = useReset(options);
|
||||
|
||||
const progress = status === 'counting' ? resetProress / duration : 0;
|
||||
|
||||
const name = options.customName || resetName;
|
||||
|
||||
const skiReset =
|
||||
options.type === ResetType.Mounting && options.group === 'default';
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
disabled={isMd}
|
||||
content={<Typography textAlign="text-center" id={name} />}
|
||||
preferedDirection="top"
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
MAINBUTTON_CLASSES({ disabled }),
|
||||
'rounded-lg',
|
||||
'absolute'
|
||||
)}
|
||||
style={{
|
||||
animationIterationCount: 1,
|
||||
}}
|
||||
onClick={() => !disabled && triggerReset()}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
'animate-spin-ccw': !skiReset && status === 'finished',
|
||||
'animate-skiing': skiReset && status === 'finished',
|
||||
'opacity-0': status === 'counting',
|
||||
})}
|
||||
style={{
|
||||
animationIterationCount: 1,
|
||||
}}
|
||||
>
|
||||
<ResetButtonIcon {...options} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classNames('hidden md:block relative', {
|
||||
'opacity-0': status === 'counting',
|
||||
})}
|
||||
>
|
||||
<Typography
|
||||
variant="section-title"
|
||||
textAlign="text-center"
|
||||
id={name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ButtonProgress progress={progress} status={status} />
|
||||
<div
|
||||
className={classNames(
|
||||
{
|
||||
'opacity-0': status !== 'counting',
|
||||
'animate-timer-tick': status === 'counting',
|
||||
},
|
||||
'absolute top-0 h-full flex items-center justify-center'
|
||||
)}
|
||||
>
|
||||
<Typography variant="main-title" textAlign="text-center">
|
||||
{timer}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export function Toolbar() {
|
||||
const assignedTrackers = useAtomValue(assignedTrackersAtom);
|
||||
|
||||
const { visibleGroups, groupVisibility } = useMemo(() => {
|
||||
const groupVisibility = Object.keys(BODY_PARTS_GROUPS)
|
||||
.filter((k) => ['fingers'].includes(k))
|
||||
.reduce(
|
||||
(curr, key) => {
|
||||
const group = key as MountingResetGroup;
|
||||
curr[group] = assignedTrackers.some(
|
||||
({ tracker }) =>
|
||||
tracker.info?.bodyPart &&
|
||||
BODY_PARTS_GROUPS[group].includes(tracker.info?.bodyPart)
|
||||
);
|
||||
|
||||
return curr;
|
||||
},
|
||||
{} as Record<MountingResetGroup, boolean>
|
||||
);
|
||||
|
||||
return {
|
||||
groupVisibility,
|
||||
visibleGroups: Object.values(groupVisibility).filter((v) => v).length,
|
||||
};
|
||||
}, [assignedTrackers]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex mobile:py-2 flex-col items-center bg-background-70 rounded-t-lg h-[var(--toolbar-h)] mr-2 xs:mt-2 mobile:mr-0">
|
||||
<div className="px-3 py-3 w-full flex gap-4 justify-center md:justify-start">
|
||||
<div className="flex-col flex gap-1 md:w-[60%]">
|
||||
<Typography variant="section-title" id="toolbar-drift_reset" />
|
||||
<div className="gap-2 md:h-[72px] h-[62px] w-full grid-cols-2 grid">
|
||||
<BasicResetButton type={ResetType.Full} />
|
||||
<BasicResetButton type={ResetType.Yaw} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-col flex gap-1 md:flex-grow">
|
||||
<Typography
|
||||
variant="section-title"
|
||||
id="toolbar-mounting_calibration"
|
||||
/>
|
||||
<div
|
||||
className="gap-2 md:h-[72px] h-[62px] w-full md:grid flex"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(calc(2 + ${visibleGroups}), 1fr)`,
|
||||
}}
|
||||
>
|
||||
<BasicResetButton
|
||||
type={ResetType.Mounting}
|
||||
group={'default'}
|
||||
customName="toolbar-mounting_calibration-default"
|
||||
/>
|
||||
<BasicResetButton
|
||||
type={ResetType.Mounting}
|
||||
group={'feet'}
|
||||
customName="toolbar-mounting_calibration-feet"
|
||||
/>
|
||||
{groupVisibility['fingers'] && (
|
||||
<BasicResetButton
|
||||
type={ResetType.Mounting}
|
||||
group={'fingers'}
|
||||
customName="toolbar-mounting_calibration-fingers"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { BVHButton } from './BVHButton';
|
||||
import { TrackingPauseButton } from './TrackingPauseButton';
|
||||
import { ResetButton } from './home/ResetButton';
|
||||
import { OverlayWidget } from './widgets/OverlayWidget';
|
||||
import { TipBox } from './commons/TipBox';
|
||||
import { DeveloperModeWidget } from './widgets/DeveloperModeWidget';
|
||||
import { useConfig } from '@/hooks/config';
|
||||
import { ResetType, StatusData } from 'solarxr-protocol';
|
||||
import { useMemo } from 'react';
|
||||
import { parseStatusToLocale, useStatusContext } from '@/hooks/status-system';
|
||||
import { ClearMountingButton } from './ClearMountingButton';
|
||||
import { ToggleableSkeletonVisualizerWidget } from './widgets/SkeletonVisualizerWidget';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { flatTrackersAtom } from '@/store/app-store';
|
||||
import { A } from './commons/A';
|
||||
|
||||
function UnprioritizedStatuses() {
|
||||
const { l10n } = useLocalization();
|
||||
const trackers = useAtomValue(flatTrackersAtom);
|
||||
const { statuses } = useStatusContext();
|
||||
const unprioritizedStatuses = useMemo(
|
||||
() => Object.values(statuses).filter((status) => !status.prioritized),
|
||||
[statuses]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-3 mb-2">
|
||||
{unprioritizedStatuses.map((status) => (
|
||||
<Localized
|
||||
id={`status_system-${StatusData[status.dataType]}`}
|
||||
vars={parseStatusToLocale(status, trackers, l10n)}
|
||||
key={status.id}
|
||||
elems={{
|
||||
PublicFixLink: (
|
||||
<A href="https://docs.slimevr.dev/common-issues.html#network-profile-is-currently-set-to-public" />
|
||||
),
|
||||
}}
|
||||
>
|
||||
<TipBox whitespace={false} hideIcon>
|
||||
{`Warning, you should fix ${StatusData[status.dataType]}`}
|
||||
</TipBox>
|
||||
</Localized>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function WidgetsComponent() {
|
||||
const { config } = useConfig();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-2 w-full [&>*:nth-child(odd):last-of-type]:col-span-full">
|
||||
<ResetButton type={ResetType.Yaw} size="big" />
|
||||
<ResetButton type={ResetType.Full} size="big" />
|
||||
<ResetButton type={ResetType.Mounting} size="big" />
|
||||
<ResetButton
|
||||
type={ResetType.Mounting}
|
||||
size="big"
|
||||
bodyPartsToReset="feet"
|
||||
/>
|
||||
<ResetButton
|
||||
type={ResetType.Mounting}
|
||||
size="big"
|
||||
bodyPartsToReset="fingers"
|
||||
/>
|
||||
<ClearMountingButton />
|
||||
{!window.__ANDROID__?.isThere() && <BVHButton />}
|
||||
<TrackingPauseButton />
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<OverlayWidget />
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<ToggleableSkeletonVisualizerWidget height={400} />
|
||||
</div>
|
||||
<UnprioritizedStatuses />
|
||||
{config?.debug && (
|
||||
<div className="w-full">
|
||||
<DeveloperModeWidget />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,23 @@
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import classNames from 'classnames';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export function A({ href, children }: { href?: string; children?: ReactNode }) {
|
||||
export function A({
|
||||
href,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
href?: string;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
onClick={() =>
|
||||
href && openUrl(href).catch(() => window.open(href, '_blank'))
|
||||
href && open(href).catch(() => window.open(href, '_blank'))
|
||||
}
|
||||
className="underline"
|
||||
className={classNames(className, 'underline', 'cursor-pointer')}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
|
||||
@@ -2,6 +2,7 @@ import classNames from 'classnames';
|
||||
import React, { ReactNode, useMemo } from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { LoaderIcon, SlimeState } from './icon/LoaderIcon';
|
||||
import { Localized, LocalizedProps } from '@fluent/react';
|
||||
|
||||
function ButtonContent({
|
||||
loading,
|
||||
@@ -17,11 +18,11 @@ function ButtonContent({
|
||||
<div
|
||||
className={classNames(
|
||||
{ 'opacity-0': loading },
|
||||
'flex flex-row gap-2 justify-center'
|
||||
'flex flex-row gap-2 justify-center items-center'
|
||||
)}
|
||||
>
|
||||
{icon && (
|
||||
<div className="flex justify-center items-center fill-background-10 w-5 h-5">
|
||||
<div className="flex justify-center items-center fill-background-10 w-5">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
@@ -44,7 +45,9 @@ export type ButtonProps = {
|
||||
loading?: boolean;
|
||||
rounded?: boolean;
|
||||
state?: any;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
id?: string;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement> &
|
||||
Omit<LocalizedProps, 'id'>;
|
||||
|
||||
export function Button({
|
||||
children,
|
||||
@@ -55,6 +58,10 @@ export function Button({
|
||||
state = {},
|
||||
icon,
|
||||
rounded = false,
|
||||
attrs,
|
||||
id,
|
||||
vars,
|
||||
elems,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
const classes = useMemo(() => {
|
||||
@@ -95,7 +102,7 @@ export function Button({
|
||||
);
|
||||
}, [variant, disabled, rounded, props.className]);
|
||||
|
||||
return to ? (
|
||||
const content = to ? (
|
||||
<NavLink
|
||||
to={to}
|
||||
className={classes}
|
||||
@@ -103,14 +110,26 @@ export function Button({
|
||||
onClick={(ev) => disabled && ev.preventDefault()}
|
||||
>
|
||||
<ButtonContent icon={icon} loading={loading}>
|
||||
{id && (
|
||||
<Localized attrs={attrs} vars={vars} elems={elems} id={id}>
|
||||
{children}
|
||||
</Localized>
|
||||
)}
|
||||
{!id && children}
|
||||
</ButtonContent>
|
||||
</NavLink>
|
||||
) : (
|
||||
<button type="button" {...props} className={classes} disabled={disabled}>
|
||||
<ButtonContent icon={icon} loading={loading}>
|
||||
{id && (
|
||||
<Localized attrs={attrs} vars={vars} elems={elems} id={id}>
|
||||
{children}
|
||||
</Localized>
|
||||
)}
|
||||
{!id && children}
|
||||
</ButtonContent>
|
||||
</button>
|
||||
);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { forwardRef, useMemo } from 'react';
|
||||
import { Control, Controller } from 'react-hook-form';
|
||||
|
||||
export const CHECKBOX_CLASSES = classNames(
|
||||
'bg-background-50 border-background-50 rounded-md w-5 h-5 text-accent-background-30 focus:border-accent-background-40 focus:ring-transparent focus:ring-offset-transparent focus:outline-transparent'
|
||||
'bg-background-50 border-background-50 cursor-pointer rounded-md w-5 h-5 text-accent-background-30 focus:border-accent-background-40 focus:ring-transparent focus:ring-offset-transparent focus:outline-transparent'
|
||||
);
|
||||
|
||||
export const CheckboxInternal = forwardRef<
|
||||
@@ -34,7 +34,9 @@ export const CheckboxInternal = forwardRef<
|
||||
const classes = useMemo(() => {
|
||||
const vriantsMap = {
|
||||
checkbox: {
|
||||
checkbox: CHECKBOX_CLASSES,
|
||||
checkbox: classNames(CHECKBOX_CLASSES, {
|
||||
'brightness-50 hover:cursor-not-allowed': disabled,
|
||||
}),
|
||||
toggle: '',
|
||||
pin: '',
|
||||
},
|
||||
@@ -47,7 +49,7 @@ export const CheckboxInternal = forwardRef<
|
||||
},
|
||||
};
|
||||
return vriantsMap[variant];
|
||||
}, [variant]);
|
||||
}, [variant, disabled]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
86
gui/src/components/commons/FirmwareIcon.tsx
Normal file
86
gui/src/components/commons/FirmwareIcon.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
DeviceDataT,
|
||||
TrackerDataT,
|
||||
TrackerStatus as TrackerStatusEnum,
|
||||
} from 'solarxr-protocol';
|
||||
import { Typography } from './Typography';
|
||||
import classNames from 'classnames';
|
||||
import { DownloadIcon } from './icon/DownloadIcon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAppContext } from '@/hooks/app';
|
||||
import { Tooltip } from './Tooltip';
|
||||
import { Localized } from '@fluent/react';
|
||||
import { checkForUpdate } from '@/hooks/firmware-update';
|
||||
|
||||
function UpdateIcon({
|
||||
showUpdate,
|
||||
}: {
|
||||
showUpdate:
|
||||
| 'can-update'
|
||||
| 'low-battery'
|
||||
| 'updated'
|
||||
| 'unavailable'
|
||||
| 'blocked';
|
||||
}) {
|
||||
const content = (
|
||||
<div className="relative">
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute rounded-full h-6 w-6 left-1 top-1 bg-accent-background-10 animate-[ping_2s_linear_3]',
|
||||
showUpdate !== 'can-update' && 'hidden'
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute rounded-full h-8 w-8 justify-center flex items-center',
|
||||
showUpdate === 'low-battery'
|
||||
? 'cursor-not-allowed bg-background-80 outline-2 outline-status-critical outline'
|
||||
: 'hover:bg-background-40 hover:cursor-pointer bg-background-50'
|
||||
)}
|
||||
>
|
||||
<DownloadIcon width={15} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return showUpdate !== 'can-update' ? (
|
||||
<Tooltip
|
||||
preferedDirection="top"
|
||||
content={
|
||||
<Localized id={'tracker-settings-update-low-battery'}>
|
||||
<Typography />
|
||||
</Localized>
|
||||
}
|
||||
>
|
||||
<div className="absolute right-5 -top-2.5">{content}</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Link to="/firmware-update" className="absolute right-5 -top-2.5">
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function FirmwareIcon({
|
||||
tracker,
|
||||
device,
|
||||
}: {
|
||||
tracker: TrackerDataT;
|
||||
device?: DeviceDataT;
|
||||
}) {
|
||||
const { currentFirmwareRelease } = useAppContext();
|
||||
|
||||
const showUpdate =
|
||||
tracker.status !== TrackerStatusEnum.DISCONNECTED &&
|
||||
currentFirmwareRelease &&
|
||||
device &&
|
||||
checkForUpdate(currentFirmwareRelease, device);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showUpdate &&
|
||||
showUpdate !== 'unavailable' &&
|
||||
showUpdate !== 'updated' && <UpdateIcon showUpdate={'can-update'} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -53,8 +53,9 @@ export function Bar({
|
||||
}) {
|
||||
const value = useMemo(
|
||||
() => Math.min(Math.max((progress * parts) / 1 - index, 0), 1),
|
||||
[index, progress]
|
||||
[index, progress, parts]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
|
||||
@@ -8,17 +8,23 @@ import {
|
||||
useLayoutEffect,
|
||||
MutableRefObject,
|
||||
useMemo,
|
||||
createElement,
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Typography } from './Typography';
|
||||
import { CloseIcon } from './icon/CloseIcon';
|
||||
|
||||
type Direction = 'top' | 'left' | 'right' | 'bottom';
|
||||
interface TooltipProps {
|
||||
content: ReactNode;
|
||||
children: ReactElement;
|
||||
preferedDirection: 'top' | 'left' | 'right' | 'bottom';
|
||||
preferedDirection: Direction;
|
||||
blockedDirections?: Direction[];
|
||||
mode?: 'corner' | 'center';
|
||||
variant?: 'auto' | 'drawer' | 'floating';
|
||||
disabled?: boolean;
|
||||
tag?: string;
|
||||
spacing?: number;
|
||||
}
|
||||
|
||||
interface TooltipPos {
|
||||
@@ -77,12 +83,12 @@ const clamp = (v: number, min: number, max: number) =>
|
||||
|
||||
const getFloatingTooltipPosition = (
|
||||
preferedDirection: TooltipProps['preferedDirection'],
|
||||
blockedDirections: Direction[],
|
||||
mode: TooltipProps['mode'],
|
||||
childrenRect: DOMRect,
|
||||
tooltipRect: DOMRect
|
||||
tooltipRect: DOMRect,
|
||||
spacing: number
|
||||
) => {
|
||||
const spacing = 10;
|
||||
|
||||
const getPosition = (
|
||||
direction: TooltipProps['preferedDirection']
|
||||
): TooltipPos => {
|
||||
@@ -135,9 +141,10 @@ const getFloatingTooltipPosition = (
|
||||
const pos = getPosition(preferedDirection);
|
||||
if (isNotInside({ ...pos, height: tooltipRect.height }, windowRect)) {
|
||||
const [firstPos] = ['left', 'top', 'right', 'bottom']
|
||||
.filter((dir) => !blockedDirections.includes(dir as Direction))
|
||||
.map((dir) => ({
|
||||
dir,
|
||||
area: getPosition(dir as TooltipProps['preferedDirection']),
|
||||
area: getPosition(dir as Direction),
|
||||
}))
|
||||
.toSorted(
|
||||
(a, b) =>
|
||||
@@ -226,12 +233,17 @@ const getFloatingTooltipPosition = (
|
||||
export function FloatingTooltip({
|
||||
childRef,
|
||||
preferedDirection,
|
||||
blockedDirections = [],
|
||||
mode,
|
||||
children,
|
||||
spacing,
|
||||
}: {
|
||||
childRef: MutableRefObject<HTMLDivElement | null>;
|
||||
childRef: MutableRefObject<HTMLElement | null>;
|
||||
children: ReactNode;
|
||||
} & Pick<TooltipProps, 'mode' | 'preferedDirection'>) {
|
||||
} & Pick<
|
||||
TooltipProps,
|
||||
'mode' | 'preferedDirection' | 'blockedDirections' | 'spacing'
|
||||
>) {
|
||||
const tooltipRef = useRef<HTMLDivElement | null>(null);
|
||||
const [tooltipStyle, setTooltipStyle] = useState<TooltipPos | undefined>();
|
||||
|
||||
@@ -245,9 +257,11 @@ export function FloatingTooltip({
|
||||
setTooltipStyle(
|
||||
getFloatingTooltipPosition(
|
||||
preferedDirection,
|
||||
blockedDirections,
|
||||
mode,
|
||||
childrenRect,
|
||||
tooltipRect
|
||||
tooltipRect,
|
||||
spacing ?? 20
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -314,7 +328,7 @@ export function DrawerTooltip({
|
||||
childRef,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
childRef: MutableRefObject<HTMLDivElement | null>;
|
||||
childRef: MutableRefObject<HTMLElement | null>;
|
||||
}) {
|
||||
const touchTimestamp = useRef<number>(0);
|
||||
const touchTimeout = useRef<number>(0);
|
||||
@@ -377,17 +391,12 @@ export function DrawerTooltip({
|
||||
if (childRef.current && childRef.current.children[0]) {
|
||||
const elem = childRef.current.children[0] as HTMLElement;
|
||||
|
||||
elem.addEventListener('mousedown', touchStart); // for debug on desktop
|
||||
elem.addEventListener('mouseup', touchEnd); // for debug on desktop
|
||||
elem.addEventListener('scroll', scroll);
|
||||
|
||||
elem.addEventListener('click', touchEnd);
|
||||
elem.addEventListener('touchstart', touchStart);
|
||||
elem.addEventListener('touchend', touchEnd);
|
||||
|
||||
return () => {
|
||||
elem.removeEventListener('mousedown', touchStart); // for debug on desktop
|
||||
elem.removeEventListener('mouseup', touchEnd); // for debug on desktop
|
||||
elem.removeEventListener('scroll', scroll);
|
||||
|
||||
elem.removeEventListener('touchstart', touchStart);
|
||||
@@ -396,6 +405,7 @@ export function DrawerTooltip({
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
// FIXME: Completely broken not sure why. Will be solved when tooltips on mobile actually work
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -444,33 +454,53 @@ export function Tooltip({
|
||||
content,
|
||||
children,
|
||||
preferedDirection,
|
||||
blockedDirections = [],
|
||||
mode = 'center',
|
||||
variant = 'auto',
|
||||
disabled = false,
|
||||
tag = 'div',
|
||||
spacing = 20,
|
||||
}: TooltipProps) {
|
||||
const childRef = useRef<HTMLDivElement | null>(null);
|
||||
const childRef = useRef<HTMLElement | null>(null);
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
|
||||
const portal = createPortal(
|
||||
isMobile ? (
|
||||
let portal = null;
|
||||
if (variant === 'auto') {
|
||||
portal = isMobile ? (
|
||||
<DrawerTooltip childRef={childRef}>{content}</DrawerTooltip>
|
||||
) : (
|
||||
<FloatingTooltip
|
||||
preferedDirection={preferedDirection}
|
||||
blockedDirections={blockedDirections}
|
||||
mode={mode}
|
||||
childRef={childRef}
|
||||
spacing={spacing}
|
||||
>
|
||||
{content}
|
||||
</FloatingTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'drawer')
|
||||
portal = <DrawerTooltip childRef={childRef}>{content}</DrawerTooltip>;
|
||||
|
||||
if (variant === 'floating')
|
||||
portal = (
|
||||
<FloatingTooltip
|
||||
blockedDirections={blockedDirections}
|
||||
preferedDirection={preferedDirection}
|
||||
mode={mode}
|
||||
childRef={childRef}
|
||||
spacing={spacing}
|
||||
>
|
||||
{content}
|
||||
</FloatingTooltip>
|
||||
),
|
||||
document.body
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="contents" ref={childRef}>
|
||||
{children}
|
||||
</div>
|
||||
{!disabled && portal}
|
||||
{createElement(tag, { className: 'contents', ref: childRef }, children)}
|
||||
{!disabled && createPortal(portal, document.body)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useConfig } from '@/hooks/config';
|
||||
import { Localized, LocalizedProps } from '@fluent/react';
|
||||
import classNames from 'classnames';
|
||||
import { createElement, ReactNode, useMemo } from 'react';
|
||||
|
||||
@@ -12,6 +13,10 @@ export function Typography({
|
||||
truncate = false,
|
||||
textAlign,
|
||||
sentryMask = false,
|
||||
id,
|
||||
attrs,
|
||||
elems,
|
||||
vars,
|
||||
}: {
|
||||
variant?:
|
||||
| 'main-title'
|
||||
@@ -39,7 +44,8 @@ export function Typography({
|
||||
| 'text-end';
|
||||
children?: ReactNode;
|
||||
sentryMask?: boolean;
|
||||
}) {
|
||||
id?: string;
|
||||
} & Omit<LocalizedProps, 'id'>) {
|
||||
const tag = useMemo(() => {
|
||||
const tags = {
|
||||
'main-title': 'h1',
|
||||
@@ -52,7 +58,7 @@ export function Typography({
|
||||
}, [variant]);
|
||||
const { config } = useConfig();
|
||||
|
||||
return createElement(
|
||||
const element = createElement(
|
||||
tag,
|
||||
{
|
||||
className: classNames([
|
||||
@@ -71,12 +77,22 @@ export function Typography({
|
||||
whitespace,
|
||||
textAlign,
|
||||
italic && 'italic',
|
||||
truncate && 'leading-3 text-ellipsis',
|
||||
truncate && 'leading-[1.2rem] text-ellipsis',
|
||||
truncate && (config?.textSize ?? 12) > 12 && 'line-clamp-1',
|
||||
truncate && (config?.textSize ?? 12) <= 12 && 'line-clamp-2',
|
||||
sentryMask && 'sentry-mask',
|
||||
]),
|
||||
},
|
||||
children || []
|
||||
children || id || []
|
||||
);
|
||||
|
||||
if (id) {
|
||||
return (
|
||||
<Localized id={id} attrs={attrs} elems={elems} vars={vars}>
|
||||
{element}
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
12
gui/src/components/commons/icon/ChecklistIcon.tsx
Normal file
12
gui/src/components/commons/icon/ChecklistIcon.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export function Checklist({ size = 24 }: { size?: number }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={size}
|
||||
viewBox="0 -960 960 960"
|
||||
width={size}
|
||||
>
|
||||
<path d="M222-200 80-342l56-56 85 85 170-170 56 57-225 226Zm0-320L80-662l56-56 85 85 170-170 56 57-225 226Zm298 240v-80h360v80H520Zm0-320v-80h360v80H520Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
12
gui/src/components/commons/icon/ClearIcon.tsx
Normal file
12
gui/src/components/commons/icon/ClearIcon.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export function ClearIcon({ size = 24 }: { size?: number }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={size}
|
||||
width={size}
|
||||
viewBox="0 -960 960 960"
|
||||
>
|
||||
<path d="M440-520h80v-280q0-17-11.5-28.5T480-840q-17 0-28.5 11.5T440-800v280ZM200-360h560v-80H200v80Zm-58 240h98v-80q0-17 11.5-28.5T280-240q17 0 28.5 11.5T320-200v80h120v-80q0-17 11.5-28.5T480-240q17 0 28.5 11.5T520-200v80h120v-80q0-17 11.5-28.5T680-240q17 0 28.5 11.5T720-200v80h98l-40-160H182l-40 160Zm676 80H142q-39 0-63-31t-14-69l55-220v-80q0-33 23.5-56.5T200-520h160v-280q0-50 35-85t85-35q50 0 85 35t35 85v280h160q33 0 56.5 23.5T840-440v80l55 220q13 38-11.5 69T818-40Zm-58-400H200h560Zm-240-80h-80 80Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
export function GearIcon() {
|
||||
export function GearIcon({ size = 20 }: { size?: number }) {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 14 13"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
|
||||
12
gui/src/components/commons/icon/HomeIcon.tsx
Normal file
12
gui/src/components/commons/icon/HomeIcon.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export function HomeIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24px"
|
||||
viewBox="0 -960 960 960"
|
||||
width="24px"
|
||||
>
|
||||
<path d="M160-120v-480l320-240 320 240v480H560v-280H400v280H160Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
12
gui/src/components/commons/icon/LayoutIcon.tsx
Normal file
12
gui/src/components/commons/icon/LayoutIcon.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export function LayoutIcon({ size = 16 }: { size?: number }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path d="M1 2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2Zm8 9a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1v-3ZM1 8a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V8Zm8-6a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1V2Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
12
gui/src/components/commons/icon/SkiIcon.tsx
Normal file
12
gui/src/components/commons/icon/SkiIcon.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export function SkiIcon({ size = 24 }: { size?: number }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={size}
|
||||
width={size}
|
||||
viewBox="0 -960 960 960"
|
||||
>
|
||||
<path d="M740-40q-26 0-50.5-4T642-56L80-261l20-57 276 101 69-178-143-149q-27-28-21.5-66.5T320-669l139-80q17-10 34.5-11.5T528-755q17 6 29.5 19t18.5 31l13 43q13 43 42.5 76t70.5 50l21-64 57 18-45 138q-74-12-131-58t-84-114l-101 58 121 138-89 230 124 45 84-257q14 5 28 9t29 7l-85 262 31 11q18 6 37.5 9.5T740-100q26 0 49.5-5t45.5-15l45 45q-32 17-67 26t-73 9Zm-80-660q-33 0-56.5-23.5T580-780q0-33 23.5-56.5T660-860q33 0 56.5 23.5T740-780q0 33-23.5 56.5T660-700Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +1,28 @@
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { Link, NavLink, useNavigate } from 'react-router-dom';
|
||||
import { StatusData, TrackerDataT } from 'solarxr-protocol';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { NavLink, useNavigate } from 'react-router-dom';
|
||||
import { TrackerDataT } from 'solarxr-protocol';
|
||||
import { useConfig } from '@/hooks/config';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { TrackerCard } from '@/components/tracker/TrackerCard';
|
||||
import { TrackersTable } from '@/components/tracker/TrackersTable';
|
||||
import {
|
||||
parseStatusToLocale,
|
||||
trackerStatusRelated,
|
||||
useStatusContext,
|
||||
} from '@/hooks/status-system';
|
||||
import { useMemo } from 'react';
|
||||
import { WarningBox } from '@/components/commons/TipBox';
|
||||
import { HeadsetIcon } from '@/components/commons/icon/HeadsetIcon';
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { flatTrackersAtom } from '@/store/app-store';
|
||||
import { useVRCConfig } from '@/hooks/vrc-config';
|
||||
|
||||
const DONT_REPEAT_STATUSES = [StatusData.StatusTrackerReset];
|
||||
import {
|
||||
assignedTrackersAtom,
|
||||
unassignedTrackersAtom,
|
||||
} from '@/store/app-store';
|
||||
import { useTrackingChecklist } from '@/hooks/tracking-checklist';
|
||||
import { Checklist } from '@/components/commons/icon/ChecklistIcon';
|
||||
import { useState } from 'react';
|
||||
import { HomeSettingsModal } from './HomeSettingsModal';
|
||||
import { LayoutIcon } from '@/components/commons/icon/LayoutIcon';
|
||||
|
||||
export function Home() {
|
||||
const { l10n } = useLocalization();
|
||||
const { config } = useConfig();
|
||||
const trackers = useAtomValue(flatTrackersAtom);
|
||||
const { statuses } = useStatusContext();
|
||||
const { invalidConfig } = useVRCConfig();
|
||||
const trackers = useAtomValue(assignedTrackersAtom);
|
||||
const unassignedTrackers = useAtomValue(unassignedTrackersAtom);
|
||||
const { highlightedTrackers } = useTrackingChecklist();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const sendToSettings = (tracker: TrackerDataT) => {
|
||||
@@ -34,60 +31,39 @@ export function Home() {
|
||||
);
|
||||
};
|
||||
|
||||
const filteredStatuses = useMemo(() => {
|
||||
const dontRepeat = new Map(DONT_REPEAT_STATUSES.map((x) => [x, false]));
|
||||
return Object.entries(statuses).filter(([, value]) => {
|
||||
if (dontRepeat.get(value.dataType)) return false;
|
||||
if (dontRepeat.has(value.dataType)) dontRepeat.set(value.dataType, true);
|
||||
return true;
|
||||
});
|
||||
}, [statuses]);
|
||||
const settingsOpenState = useState(false);
|
||||
const [, setSettingsOpen] = settingsOpenState;
|
||||
|
||||
return (
|
||||
<div className="relative h-full">
|
||||
<HomeSettingsModal open={settingsOpenState} />
|
||||
<NavLink
|
||||
to="/vr-mode"
|
||||
className="xs:hidden absolute z-50 h-12 w-12 rounded-full bg-accent-background-30 bottom-3 right-3 flex justify-center items-center fill-background-10"
|
||||
>
|
||||
<HeadsetIcon />
|
||||
</NavLink>
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div
|
||||
className={classNames(
|
||||
'px-3 pt-3 gap-3 w-full grid md:grid-cols-2 mobile:grid-cols-1'
|
||||
)}
|
||||
<NavLink
|
||||
to="/checklist"
|
||||
className="xs:hidden absolute z-50 h-12 w-12 rounded-full bg-accent-background-30 bottom-[70px] right-3 flex justify-center items-center fill-background-10"
|
||||
>
|
||||
{filteredStatuses
|
||||
.filter(([, status]) => status.prioritized)
|
||||
.map(([, status]) => (
|
||||
<Localized
|
||||
key={status.id}
|
||||
id={`status_system-${StatusData[status.dataType]}`}
|
||||
vars={parseStatusToLocale(status, trackers, l10n)}
|
||||
>
|
||||
<WarningBox whitespace={false}>
|
||||
{`Warning, you should fix ${StatusData[status.dataType]}`}
|
||||
</WarningBox>
|
||||
</Localized>
|
||||
))}
|
||||
{invalidConfig && (
|
||||
<WarningBox whitespace={false}>
|
||||
<div className="flex gap-2 justify-between items-center w-full">
|
||||
<div className="flex">
|
||||
<Localized id={'vrc_config-invalid'} />
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Link to="/vrc-warnings">
|
||||
<div className="rounded-md p-2 bg-background-90 bg-opacity-15 hover:bg-background-10 hover:bg-opacity-25 text-nowrap">
|
||||
<Localized id={'vrc_config-show_more'} />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</WarningBox>
|
||||
)}
|
||||
</div>
|
||||
<Checklist />
|
||||
</NavLink>
|
||||
<div className="overflow-y-auto flex flex-col gap-3">
|
||||
<div className="flex w-full gap-2 items-center px-4 h-5">
|
||||
<Typography
|
||||
color="secondary"
|
||||
id="toolbar-assigned_trackers"
|
||||
vars={{ count: trackers.length }}
|
||||
/>
|
||||
<div className="bg-background-50 h-[2px] rounded-lg flex-grow" />
|
||||
<div
|
||||
className="fill-background-30 hover:fill-background-20 cursor-pointer"
|
||||
onClick={() => setSettingsOpen(true)}
|
||||
>
|
||||
<LayoutIcon size={18} />
|
||||
</div>
|
||||
</div>
|
||||
{trackers.length === 0 && (
|
||||
<div className="flex px-5 pt-5 justify-center">
|
||||
<Typography variant="standard">
|
||||
@@ -96,7 +72,7 @@ export function Home() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!config?.debug && trackers.length > 0 && (
|
||||
{config?.homeLayout == 'default' && trackers.length > 0 && (
|
||||
<div className="grid sm:grid-cols-1 md:grid-cols-2 gap-4 px-5 my-5">
|
||||
{trackers.map(({ tracker, device }, index) => (
|
||||
<TrackerCard
|
||||
@@ -107,22 +83,69 @@ export function Home() {
|
||||
smol
|
||||
showUpdates
|
||||
interactable
|
||||
warning={Object.values(statuses).some((status) =>
|
||||
trackerStatusRelated(tracker, status)
|
||||
)}
|
||||
warning={
|
||||
!!highlightedTrackers?.trackers.find(
|
||||
(t) =>
|
||||
t?.deviceId?.id === tracker.trackerId?.deviceId?.id &&
|
||||
t?.trackerNum === tracker.trackerId?.trackerNum
|
||||
) && highlightedTrackers.step
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{config?.debug && trackers.length > 0 && (
|
||||
<div className="px-2 pt-5 overflow-y-scroll overflow-x-auto">
|
||||
|
||||
{config?.homeLayout === 'table' && trackers.length > 0 && (
|
||||
<div className="mx-2 overflow-x-auto">
|
||||
<TrackersTable
|
||||
flatTrackers={trackers}
|
||||
clickedTracker={(tracker) => sendToSettings(tracker)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{unassignedTrackers.length > 0 && (
|
||||
<>
|
||||
<div className="flex w-full gap-2 items-center px-4 h-5">
|
||||
<Typography
|
||||
color="secondary"
|
||||
id="toolbar-unassigned_trackers"
|
||||
vars={{ count: unassignedTrackers.length }}
|
||||
/>
|
||||
<div className="bg-background-50 h-[2px] rounded-lg flex-grow" />
|
||||
</div>
|
||||
{config?.homeLayout == 'default' && (
|
||||
<div className="grid sm:grid-cols-1 md:grid-cols-2 gap-4 px-5 my-3">
|
||||
{unassignedTrackers.map(({ tracker, device }, index) => (
|
||||
<TrackerCard
|
||||
key={index}
|
||||
tracker={tracker}
|
||||
device={device}
|
||||
onClick={() => sendToSettings(tracker)}
|
||||
smol
|
||||
showUpdates
|
||||
interactable
|
||||
warning={
|
||||
!!highlightedTrackers?.trackers.find(
|
||||
(t) =>
|
||||
t?.deviceId?.id === tracker.trackerId?.deviceId?.id &&
|
||||
t?.trackerNum === tracker.trackerId?.trackerNum
|
||||
) && highlightedTrackers.step
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{config?.homeLayout === 'table' && (
|
||||
<div className="mx-2 overflow-x-auto">
|
||||
<TrackersTable
|
||||
flatTrackers={unassignedTrackers}
|
||||
clickedTracker={(tracker) => sendToSettings(tracker)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
34
gui/src/components/home/HomeSettingsModal.tsx
Normal file
34
gui/src/components/home/HomeSettingsModal.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { BaseModal } from '@/components/commons/BaseModal';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { HomeLayoutSettings } from '@/components/settings/pages/HomeScreenSettings';
|
||||
|
||||
export function HomeSettingsModal({
|
||||
open,
|
||||
}: {
|
||||
open: [boolean, Dispatch<SetStateAction<boolean>>];
|
||||
}) {
|
||||
return (
|
||||
<BaseModal
|
||||
isOpen={open[0]}
|
||||
appendClasses={'max-w-xl w-full'}
|
||||
closeable
|
||||
onRequestClose={() => {
|
||||
open[1](false);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Typography variant="main-title" id="home-settings" />
|
||||
<HomeLayoutSettings />
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={() => open[1](false)}
|
||||
id="home-settings-close"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
@@ -1,219 +1,67 @@
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
BodyPart,
|
||||
ResetRequestT,
|
||||
ResetType,
|
||||
RpcMessage,
|
||||
StatusData,
|
||||
} from 'solarxr-protocol';
|
||||
import { useConfig } from '@/hooks/config';
|
||||
import { useCountdown } from '@/hooks/countdown';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import {
|
||||
playSoundOnResetEnded,
|
||||
playSoundOnResetStarted,
|
||||
} from '@/sounds/sounds';
|
||||
import { BigButton } from '@/components/commons/BigButton';
|
||||
import { Localized } from '@fluent/react';
|
||||
import { ResetType } from 'solarxr-protocol';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import {
|
||||
MountingResetIcon,
|
||||
YawResetIcon,
|
||||
FullResetIcon,
|
||||
} from '@/components/commons/icon/ResetIcon';
|
||||
import { useStatusContext } from '@/hooks/status-system';
|
||||
import classNames from 'classnames';
|
||||
import { useReset, UseResetOptions } from '@/hooks/reset';
|
||||
import {
|
||||
FullResetIcon,
|
||||
YawResetIcon,
|
||||
} from '@/components/commons/icon/ResetIcon';
|
||||
import { ReactNode } from 'react';
|
||||
import { SkiIcon } from '@/components/commons/icon/SkiIcon';
|
||||
import { FootIcon } from '@/components/commons/icon/FootIcon';
|
||||
import { FingersIcon } from '@/components/commons/icon/FingersIcon';
|
||||
|
||||
export function ResetButtonIcon(options: UseResetOptions) {
|
||||
if (options.type === ResetType.Mounting && !options.group)
|
||||
options.group = 'default';
|
||||
|
||||
if (options.type === ResetType.Yaw) return <YawResetIcon width={18} />;
|
||||
if (options.type === ResetType.Full) return <FullResetIcon width={18} />;
|
||||
if (options.type === ResetType.Mounting) {
|
||||
if (options.group === 'default') return <SkiIcon />;
|
||||
if (options.group === 'feet') return <FootIcon />;
|
||||
if (options.group === 'fingers') return <FingersIcon width={16} />;
|
||||
}
|
||||
}
|
||||
|
||||
export function ResetButton({
|
||||
type,
|
||||
size = 'big',
|
||||
bodyPartsToReset = 'default',
|
||||
className,
|
||||
onReseted,
|
||||
children,
|
||||
...options
|
||||
}: {
|
||||
className?: string;
|
||||
type: ResetType;
|
||||
size: 'big' | 'small';
|
||||
bodyPartsToReset?: 'default' | 'feet' | 'fingers';
|
||||
children?: ReactNode;
|
||||
onReseted?: () => void;
|
||||
}) {
|
||||
const { l10n } = useLocalization();
|
||||
const { sendRPCPacket } = useWebsocketAPI();
|
||||
const { statuses } = useStatusContext();
|
||||
const { config } = useConfig();
|
||||
const finishedTimeoutRef = useRef(-1);
|
||||
const [isFinished, setFinished] = useState(false);
|
||||
|
||||
const needsFullReset = useMemo(
|
||||
() =>
|
||||
type === ResetType.Mounting &&
|
||||
Object.values(statuses).some(
|
||||
(status) => status.dataType === StatusData.StatusTrackerReset
|
||||
),
|
||||
[statuses]
|
||||
} & UseResetOptions) {
|
||||
const { triggerReset, status, timer, disabled, name } = useReset(
|
||||
options,
|
||||
onReseted
|
||||
);
|
||||
|
||||
const feetBodyParts = [BodyPart.LEFT_FOOT, BodyPart.RIGHT_FOOT];
|
||||
const fingerBodyParts = [
|
||||
BodyPart.LEFT_THUMB_METACARPAL,
|
||||
BodyPart.LEFT_THUMB_PROXIMAL,
|
||||
BodyPart.LEFT_THUMB_DISTAL,
|
||||
BodyPart.LEFT_INDEX_PROXIMAL,
|
||||
BodyPart.LEFT_INDEX_INTERMEDIATE,
|
||||
BodyPart.LEFT_INDEX_DISTAL,
|
||||
BodyPart.LEFT_MIDDLE_PROXIMAL,
|
||||
BodyPart.LEFT_MIDDLE_INTERMEDIATE,
|
||||
BodyPart.LEFT_MIDDLE_DISTAL,
|
||||
BodyPart.LEFT_RING_PROXIMAL,
|
||||
BodyPart.LEFT_RING_INTERMEDIATE,
|
||||
BodyPart.LEFT_RING_DISTAL,
|
||||
BodyPart.LEFT_LITTLE_PROXIMAL,
|
||||
BodyPart.LEFT_LITTLE_INTERMEDIATE,
|
||||
BodyPart.LEFT_LITTLE_DISTAL,
|
||||
BodyPart.RIGHT_THUMB_METACARPAL,
|
||||
BodyPart.RIGHT_THUMB_PROXIMAL,
|
||||
BodyPart.RIGHT_THUMB_DISTAL,
|
||||
BodyPart.RIGHT_INDEX_PROXIMAL,
|
||||
BodyPart.RIGHT_INDEX_INTERMEDIATE,
|
||||
BodyPart.RIGHT_INDEX_DISTAL,
|
||||
BodyPart.RIGHT_MIDDLE_PROXIMAL,
|
||||
BodyPart.RIGHT_MIDDLE_INTERMEDIATE,
|
||||
BodyPart.RIGHT_MIDDLE_DISTAL,
|
||||
BodyPart.RIGHT_RING_PROXIMAL,
|
||||
BodyPart.RIGHT_RING_INTERMEDIATE,
|
||||
BodyPart.RIGHT_RING_DISTAL,
|
||||
BodyPart.RIGHT_LITTLE_PROXIMAL,
|
||||
BodyPart.RIGHT_LITTLE_INTERMEDIATE,
|
||||
BodyPart.RIGHT_LITTLE_DISTAL,
|
||||
];
|
||||
|
||||
const reset = () => {
|
||||
const req = new ResetRequestT();
|
||||
req.resetType = type;
|
||||
switch (bodyPartsToReset) {
|
||||
case 'default':
|
||||
// Server handles it. Usually all body parts except fingers.
|
||||
req.bodyParts = [];
|
||||
break;
|
||||
case 'feet':
|
||||
req.bodyParts = feetBodyParts;
|
||||
break;
|
||||
case 'fingers':
|
||||
req.bodyParts = fingerBodyParts;
|
||||
break;
|
||||
}
|
||||
sendRPCPacket(RpcMessage.ResetRequest, req);
|
||||
};
|
||||
|
||||
const { isCounting, startCountdown, timer } = useCountdown({
|
||||
duration: type === ResetType.Yaw ? 0 : undefined,
|
||||
onCountdownEnd: () => {
|
||||
maybePlaySoundOnResetEnd(type);
|
||||
reset();
|
||||
setFinished(true);
|
||||
if (finishedTimeoutRef.current !== -1)
|
||||
clearTimeout(finishedTimeoutRef.current);
|
||||
finishedTimeoutRef.current = setTimeout(() => {
|
||||
setFinished(false);
|
||||
finishedTimeoutRef.current = -1;
|
||||
}, 2000) as unknown as number;
|
||||
if (onReseted) onReseted();
|
||||
},
|
||||
});
|
||||
|
||||
const text = useMemo(() => {
|
||||
switch (type) {
|
||||
case ResetType.Yaw:
|
||||
return l10n.getString(
|
||||
'reset-yaw' +
|
||||
(bodyPartsToReset !== 'default' ? '-' + bodyPartsToReset : '')
|
||||
);
|
||||
case ResetType.Mounting:
|
||||
return l10n.getString(
|
||||
'reset-mounting' +
|
||||
(bodyPartsToReset !== 'default' ? '-' + bodyPartsToReset : '')
|
||||
);
|
||||
case ResetType.Full:
|
||||
return l10n.getString(
|
||||
'reset-full' +
|
||||
(bodyPartsToReset !== 'default' ? '-' + bodyPartsToReset : '')
|
||||
);
|
||||
}
|
||||
}, [type, bodyPartsToReset]);
|
||||
|
||||
const getIcon = () => {
|
||||
switch (type) {
|
||||
case ResetType.Yaw:
|
||||
return <YawResetIcon width={20} />;
|
||||
case ResetType.Mounting:
|
||||
switch (bodyPartsToReset) {
|
||||
case 'default':
|
||||
return <MountingResetIcon width={20} />;
|
||||
case 'feet':
|
||||
return <FootIcon width={30} />;
|
||||
case 'fingers':
|
||||
return <FingersIcon width={20} />;
|
||||
}
|
||||
}
|
||||
return <FullResetIcon width={20} />;
|
||||
};
|
||||
|
||||
const maybePlaySoundOnResetEnd = (type: ResetType) => {
|
||||
if (!config?.feedbackSound) return;
|
||||
playSoundOnResetEnded(type, config?.feedbackSoundVolume);
|
||||
};
|
||||
|
||||
const maybePlaySoundOnResetStart = () => {
|
||||
if (!config?.feedbackSound) return;
|
||||
if (type !== ResetType.Yaw)
|
||||
playSoundOnResetStarted(config?.feedbackSoundVolume);
|
||||
};
|
||||
|
||||
const triggerReset = () => {
|
||||
setFinished(false);
|
||||
startCountdown();
|
||||
maybePlaySoundOnResetStart();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (finishedTimeoutRef.current !== -1)
|
||||
clearTimeout(finishedTimeoutRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return size === 'small' ? (
|
||||
return (
|
||||
<Button
|
||||
icon={getIcon()}
|
||||
icon={<ResetButtonIcon {...options} />}
|
||||
onClick={triggerReset}
|
||||
className={classNames(
|
||||
'border-2',
|
||||
isFinished
|
||||
'border-2 py-[5px]',
|
||||
status === 'finished'
|
||||
? 'border-status-success'
|
||||
: 'transition-[border-color] duration-500 ease-in-out border-transparent',
|
||||
className
|
||||
)}
|
||||
variant="primary"
|
||||
disabled={isCounting || needsFullReset}
|
||||
disabled={disabled}
|
||||
>
|
||||
{!isCounting || type === ResetType.Yaw ? text : String(timer)}
|
||||
<div className="flex flex-col">
|
||||
<div className="opacity-0 h-0">
|
||||
{children || <Localized id={name} />}
|
||||
</div>
|
||||
{status !== 'counting' || options.type === ResetType.Yaw
|
||||
? children || <Localized id={name} />
|
||||
: String(timer)}
|
||||
</div>
|
||||
</Button>
|
||||
) : (
|
||||
<BigButton
|
||||
icon={getIcon()}
|
||||
onClick={triggerReset}
|
||||
className={classNames(
|
||||
'border-2',
|
||||
isFinished
|
||||
? 'border-status-success'
|
||||
: 'transition-[border-color] duration-500 ease-in-out border-transparent',
|
||||
className
|
||||
)}
|
||||
disabled={isCounting || needsFullReset}
|
||||
>
|
||||
{!isCounting || type === ResetType.Yaw ? text : String(timer)}
|
||||
</BigButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,8 +34,6 @@ export function OnboardingLayout({ children }: { children: ReactNode }) {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<MainLayout widgets={false} isMobile={isMobile}>
|
||||
{children}
|
||||
</MainLayout>
|
||||
<MainLayout isMobile={isMobile}>{children}</MainLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
grid-template:
|
||||
's t' var(--connect-tracker-layout-top)
|
||||
's c' calc(100% - var(--connect-tracker-layout-top))
|
||||
/ calc(var(--connect-tracker-layout-sidebar)) calc(
|
||||
100% - var(--connect-tracker-layout-sidebar)
|
||||
);
|
||||
/ calc(var(--connect-tracker-layout-sidebar)) calc(100% - var(
|
||||
--connect-tracker-layout-sidebar
|
||||
));
|
||||
|
||||
@screen mobile {
|
||||
grid-template:
|
||||
|
||||
@@ -5,8 +5,9 @@ import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
RpcMessage,
|
||||
StartWifiProvisioningRequestT,
|
||||
StatusData,
|
||||
StopWifiProvisioningRequestT,
|
||||
TrackingChecklistPublicNetworksT,
|
||||
TrackingChecklistStepId,
|
||||
WifiProvisioningStatus,
|
||||
WifiProvisioningStatusResponseT,
|
||||
} from 'solarxr-protocol';
|
||||
@@ -24,9 +25,9 @@ import './ConnectTracker.scss';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { connectedIMUTrackersAtom } from '@/store/app-store';
|
||||
import { BaseModal } from '@/components/commons/BaseModal';
|
||||
import { parseStatusToLocale, useStatusContext } from '@/hooks/status-system';
|
||||
import { A } from '@/components/commons/A';
|
||||
import { CONNECT_TRACKER } from '@/utils/tauri';
|
||||
import { useTrackingChecklist } from '@/hooks/tracking-checklist';
|
||||
|
||||
const statusLabelMap = {
|
||||
[WifiProvisioningStatus.NONE]:
|
||||
@@ -74,9 +75,34 @@ const statusProgressMap = {
|
||||
[WifiProvisioningStatus.COULD_NOT_FIND_SERVER]: 0.8,
|
||||
};
|
||||
|
||||
export function InvalidNetworkProfileWarning({
|
||||
extraData,
|
||||
}: {
|
||||
extraData: TrackingChecklistPublicNetworksT;
|
||||
}) {
|
||||
return (
|
||||
<div className="pt-4">
|
||||
<Localized
|
||||
id="tracking_checklist-NETWORK_PROFILE_PUBLIC-desc"
|
||||
elems={{
|
||||
PublicFixLink: (
|
||||
<A href="https://docs.slimevr.dev/common-issues.html#network-profile-is-currently-set-to-public" />
|
||||
),
|
||||
}}
|
||||
vars={{
|
||||
count: extraData.adapters.length,
|
||||
adapters: extraData.adapters.join(', '),
|
||||
}}
|
||||
>
|
||||
<WarningBox whitespace={false}>WARNING</WarningBox>
|
||||
</Localized>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ConnectTrackersPage() {
|
||||
const { l10n } = useLocalization();
|
||||
const { statuses } = useStatusContext();
|
||||
const { visibleSteps } = useTrackingChecklist();
|
||||
|
||||
const connectedIMUTrackers = useAtomValue(connectedIMUTrackersAtom);
|
||||
const { applyProgress, state } = useOnboarding();
|
||||
@@ -165,11 +191,13 @@ export function ConnectTrackersPage() {
|
||||
[connectedIMUTrackers.length]
|
||||
);
|
||||
|
||||
const filteredStatuses = useMemo(() => {
|
||||
return Object.entries(statuses).filter(
|
||||
([, value]) => value.dataType == StatusData.StatusPublicNetwork
|
||||
const invalidNetworkProfile = useMemo(() => {
|
||||
return visibleSteps.find(
|
||||
(step) =>
|
||||
step.id === TrackingChecklistStepId.NETWORK_PROFILE_PUBLIC &&
|
||||
!step.valid
|
||||
);
|
||||
}, [statuses]);
|
||||
}, [visibleSteps]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -243,24 +271,13 @@ export function ConnectTrackersPage() {
|
||||
>
|
||||
<TipBox>Conditional tip</TipBox>
|
||||
</Localized>
|
||||
{filteredStatuses.map(([, status]) => (
|
||||
<div className="pt-4">
|
||||
<Localized
|
||||
key={status.id}
|
||||
id={`status_system-${StatusData[status.dataType]}`}
|
||||
vars={parseStatusToLocale(status, connectedIMUTrackers, l10n)}
|
||||
elems={{
|
||||
PublicFixLink: (
|
||||
<A href="https://docs.slimevr.dev/common-issues.html#network-profile-is-currently-set-to-public" />
|
||||
),
|
||||
}}
|
||||
>
|
||||
<WarningBox whitespace={false}>
|
||||
{`Warning, you should fix ${StatusData[status.dataType]}`}
|
||||
</WarningBox>
|
||||
</Localized>
|
||||
</div>
|
||||
))}
|
||||
{invalidNetworkProfile && (
|
||||
<InvalidNetworkProfileWarning
|
||||
extraData={
|
||||
invalidNetworkProfile.extraData as TrackingChecklistPublicNetworksT
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-xl h-24 flex gap-2 p-3 lg:w-full mt-4 relative',
|
||||
|
||||
@@ -477,7 +477,6 @@ export function ManualProportionsPage() {
|
||||
<div className="h-14 flex flex-grow items-center">
|
||||
<ResetButton
|
||||
type={ResetType.Full}
|
||||
size="small"
|
||||
className="w-full h-full bg-background-50 hover:bg-background-40 text-background-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -69,11 +69,7 @@ export function PreparationStep({
|
||||
>
|
||||
{l10n.getString('onboarding-automatic_mounting-prev_step')}
|
||||
</Button>
|
||||
<ResetButton
|
||||
size="small"
|
||||
type={ResetType.Full}
|
||||
onReseted={nextStep}
|
||||
/>
|
||||
<ResetButton type={ResetType.Full} onReseted={nextStep} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -58,8 +58,8 @@ export function MountingResetStep({
|
||||
{l10n.getString('onboarding-automatic_mounting-prev_step')}
|
||||
</Button>
|
||||
<ResetButton
|
||||
size="small"
|
||||
type={ResetType.Mounting}
|
||||
group="default"
|
||||
onReseted={nextStep}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -69,11 +69,7 @@ export function PreparationStep({
|
||||
>
|
||||
{l10n.getString('onboarding-automatic_mounting-prev_step')}
|
||||
</Button>
|
||||
<ResetButton
|
||||
size="small"
|
||||
type={ResetType.Full}
|
||||
onReseted={nextStep}
|
||||
/>
|
||||
<ResetButton type={ResetType.Full} onReseted={nextStep} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,7 @@ export function PreparationStep({
|
||||
<Button variant={'secondary'} onClick={prevStep} />
|
||||
</Localized>
|
||||
|
||||
<ResetButton size="small" type={ResetType.Full} onReseted={nextStep} />
|
||||
<ResetButton type={ResetType.Full} onReseted={nextStep} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -54,8 +54,8 @@ export function VerifyMountingStep({
|
||||
{l10n.getString('onboarding-automatic_mounting-prev_step')}
|
||||
</Button>
|
||||
<ResetButton
|
||||
size="small"
|
||||
type={ResetType.Mounting}
|
||||
group="default"
|
||||
onReseted={nextStep}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -281,7 +281,7 @@ export function TrackersAssignPage() {
|
||||
<div className="flex flex-col w-full overflow-y-auto px-4 xs:items-center">
|
||||
<div className="flex mobile:flex-col md:gap-8 mobile:gap-4 mobile:pb-4">
|
||||
<div className="flex flex-col xs:max-w-sm gap-3">
|
||||
<Typography variant="main-title">
|
||||
<Typography variant="main-title" color="accent">
|
||||
{l10n.getString('onboarding-assign_trackers-title')}
|
||||
</Typography>
|
||||
<Typography>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { StatusSystemC, useProvideStatusContext } from '@/hooks/status-system';
|
||||
|
||||
export function StatusProvider({ children }: { children: ReactNode }) {
|
||||
const context = useProvideStatusContext();
|
||||
|
||||
return (
|
||||
<StatusSystemC.Provider value={context}>{children}</StatusSystemC.Provider>
|
||||
);
|
||||
}
|
||||
@@ -7,9 +7,9 @@
|
||||
grid-template:
|
||||
't t t' var(--topbar-h)
|
||||
'n s c' calc(100% - var(--topbar-h))
|
||||
/ var(--navbar-w) var(--settings-sidebar-w) calc(
|
||||
100% - var(--navbar-w) - var(--settings-sidebar-w)
|
||||
);
|
||||
/ var(--navbar-w) var(--settings-sidebar-w) calc(100% - var(--navbar-w) - var(
|
||||
--settings-sidebar-w
|
||||
));
|
||||
|
||||
@screen lg {
|
||||
--settings-sidebar-w: 270px;
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import classNames from 'classnames';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { NavLink, useLocation, useMatch } from 'react-router-dom';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { useVRCConfig } from '@/hooks/vrc-config';
|
||||
|
||||
export function SettingsLink({
|
||||
to,
|
||||
scrollTo,
|
||||
children,
|
||||
id,
|
||||
}: {
|
||||
id: string;
|
||||
to: string;
|
||||
scrollTo?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const { state } = useLocation();
|
||||
const doesMatch = useMatch({
|
||||
@@ -35,94 +34,118 @@ export function SettingsLink({
|
||||
'bg-background-60': isActive,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
<Typography id={id} />
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsSidebar() {
|
||||
const { l10n } = useLocalization();
|
||||
const { state: vrcConfigState } = useVRCConfig();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col px-5 py-5 gap-3 overflow-y-auto bg-background-70 rounded-lg h-full">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('settings-sidebar-title')}
|
||||
</Typography>
|
||||
<Typography variant="main-title" id="settings-sidebar-title" />
|
||||
<div className="flex flex-col gap-3">
|
||||
<Typography variant="section-title">
|
||||
{l10n.getString('settings-sidebar-general')}
|
||||
</Typography>
|
||||
<Typography variant="section-title" id="settings-sidebar-general" />
|
||||
<div className="flex flex-col gap-2">
|
||||
<SettingsLink to="/settings/trackers" scrollTo="steamvr">
|
||||
SteamVR
|
||||
</SettingsLink>
|
||||
<SettingsLink to="/settings/trackers" scrollTo="stayaligned">
|
||||
{l10n.getString('settings-sidebar-stay_aligned')}
|
||||
</SettingsLink>
|
||||
<SettingsLink to="/settings/trackers" scrollTo="mechanics">
|
||||
{l10n.getString('settings-sidebar-tracker_mechanics')}
|
||||
</SettingsLink>
|
||||
<SettingsLink to="/settings/trackers" scrollTo="fksettings">
|
||||
{l10n.getString('settings-sidebar-fk_settings')}
|
||||
</SettingsLink>
|
||||
<SettingsLink to="/settings/trackers" scrollTo="gestureControl">
|
||||
{l10n.getString('settings-sidebar-gesture_control')}
|
||||
</SettingsLink>
|
||||
<SettingsLink
|
||||
to="/settings/trackers"
|
||||
scrollTo="steamvr"
|
||||
id="settings-sidebar-steamvr"
|
||||
/>
|
||||
<SettingsLink
|
||||
to="/settings/trackers"
|
||||
scrollTo="stayaligned"
|
||||
id="settings-sidebar-stay_aligned"
|
||||
/>
|
||||
<SettingsLink
|
||||
to="/settings/trackers"
|
||||
scrollTo="mechanics"
|
||||
id="settings-sidebar-tracker_mechanics"
|
||||
/>
|
||||
<SettingsLink
|
||||
to="/settings/trackers"
|
||||
scrollTo="fksettings"
|
||||
id="settings-sidebar-fk_settings"
|
||||
/>
|
||||
<SettingsLink
|
||||
to="/settings/trackers"
|
||||
scrollTo="gestureControl"
|
||||
id="settings-sidebar-gesture_control"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Typography variant="section-title">
|
||||
{l10n.getString('settings-sidebar-interface')}
|
||||
</Typography>
|
||||
<Typography variant="section-title" id="settings-sidebar-interface" />
|
||||
<div className="flex flex-col gap-2">
|
||||
<SettingsLink to="/settings/interface" scrollTo="notifications">
|
||||
{l10n.getString('settings-sidebar-notifications')}
|
||||
</SettingsLink>
|
||||
<SettingsLink to="/settings/interface" scrollTo="behavior">
|
||||
{l10n.getString('settings-sidebar-behavior')}
|
||||
</SettingsLink>
|
||||
<SettingsLink to="/settings/interface" scrollTo="appearance">
|
||||
{l10n.getString('settings-sidebar-appearance')}
|
||||
</SettingsLink>
|
||||
<SettingsLink
|
||||
to="/settings/interface"
|
||||
scrollTo="notifications"
|
||||
id="settings-sidebar-notifications"
|
||||
/>
|
||||
<SettingsLink
|
||||
to="/settings/interface"
|
||||
scrollTo="behavior"
|
||||
id="settings-sidebar-behavior"
|
||||
/>
|
||||
<SettingsLink
|
||||
to="/settings/interface"
|
||||
scrollTo="appearance"
|
||||
id="settings-sidebar-appearance"
|
||||
/>
|
||||
<SettingsLink
|
||||
to="/settings/interface/home"
|
||||
scrollTo="home"
|
||||
id="settings-sidebar-home"
|
||||
/>
|
||||
<SettingsLink
|
||||
to="/settings/interface/home"
|
||||
scrollTo="checklist"
|
||||
id="settings-sidebar-checklist"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Typography variant="section-title">OSC</Typography>
|
||||
<div className="flex flex-col gap-2">
|
||||
<SettingsLink to="/settings/osc/router" scrollTo="router">
|
||||
{l10n.getString('settings-sidebar-osc_router')}
|
||||
</SettingsLink>
|
||||
<SettingsLink to="/settings/osc/vrchat" scrollTo="vrchat">
|
||||
{l10n.getString('settings-sidebar-osc_trackers')}
|
||||
</SettingsLink>
|
||||
<SettingsLink to="/settings/osc/vmc" scrollTo="vmc">
|
||||
VMC
|
||||
</SettingsLink>
|
||||
<SettingsLink
|
||||
to="/settings/osc/router"
|
||||
scrollTo="router"
|
||||
id="settings-sidebar-osc_router"
|
||||
/>
|
||||
<SettingsLink
|
||||
to="/settings/osc/vrchat"
|
||||
scrollTo="vrchat"
|
||||
id="settings-sidebar-osc_trackers"
|
||||
/>
|
||||
<SettingsLink
|
||||
to="/settings/osc/vmc"
|
||||
scrollTo="vmc"
|
||||
id="settings-sidebar-osc_vmc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Typography variant="section-title">
|
||||
{l10n.getString('settings-sidebar-utils')}
|
||||
</Typography>
|
||||
<Typography variant="section-title" id="settings-sidebar-utils" />
|
||||
<div className="flex flex-col gap-2">
|
||||
<SettingsLink to="/settings/serial">
|
||||
{l10n.getString('settings-sidebar-serial')}
|
||||
</SettingsLink>
|
||||
<SettingsLink to="/settings/firmware-tool">
|
||||
{l10n.getString('settings-sidebar-firmware-tool')}
|
||||
</SettingsLink>
|
||||
<SettingsLink to="/settings/serial" id="settings-sidebar-serial" />
|
||||
<SettingsLink
|
||||
to="/settings/firmware-tool"
|
||||
id="settings-sidebar-firmware-tool"
|
||||
/>
|
||||
</div>
|
||||
{vrcConfigState?.isSupported && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<SettingsLink to="/vrc-warnings">
|
||||
{l10n.getString('settings-sidebar-vrc_warnings')}
|
||||
</SettingsLink>
|
||||
<SettingsLink
|
||||
to="/vrc-warnings"
|
||||
id="settings-sidebar-vrc_warnings"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
<SettingsLink to="/settings/advanced">
|
||||
{l10n.getString('settings-sidebar-advanced')}
|
||||
</SettingsLink>
|
||||
<SettingsLink
|
||||
to="/settings/advanced"
|
||||
id="settings-sidebar-advanced"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
ModelRatiosT,
|
||||
ModelSettingsT,
|
||||
ModelTogglesT,
|
||||
ResetsSettingsT,
|
||||
RpcMessage,
|
||||
SettingsRequestT,
|
||||
SettingsResponseT,
|
||||
@@ -38,6 +37,11 @@ import {
|
||||
serializeStayAlignedSettings,
|
||||
deserializeStayAlignedSettings,
|
||||
} from './components/StayAlignedSettings';
|
||||
import {
|
||||
defaultResetSettings,
|
||||
loadResetSettings,
|
||||
ResetSettingsForm,
|
||||
} from '@/hooks/reset-settings';
|
||||
|
||||
export type SettingsForm = {
|
||||
trackers: {
|
||||
@@ -95,13 +99,7 @@ export type SettingsForm = {
|
||||
legTweaks: {
|
||||
correctionStrength: number;
|
||||
};
|
||||
resetsSettings: {
|
||||
resetMountingFeet: boolean;
|
||||
armsMountingResetMode: number;
|
||||
yawResetSmoothTime: number;
|
||||
saveMountingReset: boolean;
|
||||
resetHmdPitch: boolean;
|
||||
};
|
||||
resetsSettings: ResetSettingsForm;
|
||||
stayAligned: StayAlignedSettingsForm;
|
||||
};
|
||||
|
||||
@@ -156,22 +154,14 @@ const defaultValues: SettingsForm = {
|
||||
numberTrackersOverThreshold: 1,
|
||||
},
|
||||
legTweaks: { correctionStrength: 0.3 },
|
||||
resetsSettings: {
|
||||
resetMountingFeet: false,
|
||||
armsMountingResetMode: 0,
|
||||
yawResetSmoothTime: 0.0,
|
||||
saveMountingReset: false,
|
||||
resetHmdPitch: false,
|
||||
},
|
||||
resetsSettings: defaultResetSettings,
|
||||
stayAligned: defaultStayAlignedSettings,
|
||||
};
|
||||
|
||||
export function GeneralSettings() {
|
||||
const { l10n } = useLocalization();
|
||||
const { config } = useConfig();
|
||||
// const { state } = useLocation();
|
||||
const { currentLocales } = useLocaleConfig();
|
||||
// const pageRef = useRef<HTMLFormElement | null>(null);
|
||||
|
||||
const percentageFormat = new Intl.NumberFormat(currentLocales, {
|
||||
style: 'percent',
|
||||
@@ -288,17 +278,7 @@ export function GeneralSettings() {
|
||||
settings.stayAligned = serializeStayAlignedSettings(values.stayAligned);
|
||||
|
||||
if (values.resetsSettings) {
|
||||
const resetsSettings = new ResetsSettingsT();
|
||||
resetsSettings.resetMountingFeet =
|
||||
values.resetsSettings.resetMountingFeet;
|
||||
resetsSettings.armsMountingResetMode =
|
||||
values.resetsSettings.armsMountingResetMode;
|
||||
resetsSettings.yawResetSmoothTime =
|
||||
values.resetsSettings.yawResetSmoothTime;
|
||||
resetsSettings.saveMountingReset =
|
||||
values.resetsSettings.saveMountingReset;
|
||||
resetsSettings.resetHmdPitch = values.resetsSettings.resetHmdPitch;
|
||||
settings.resetsSettings = resetsSettings;
|
||||
settings.resetsSettings = loadResetSettings(values.resetsSettings);
|
||||
}
|
||||
|
||||
sendRPCPacket(RpcMessage.ChangeSettingsRequest, settings);
|
||||
|
||||
190
gui/src/components/settings/pages/HomeScreenSettings.tsx
Normal file
190
gui/src/components/settings/pages/HomeScreenSettings.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import { CheckBox } from '@/components/commons/Checkbox';
|
||||
import { CheckIcon } from '@/components/commons/icon/CheckIcon';
|
||||
import { HomeIcon } from '@/components/commons/icon/HomeIcon';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import {
|
||||
SettingsPageLayout,
|
||||
SettingsPagePaneLayout,
|
||||
} from '@/components/settings/SettingsPageLayout';
|
||||
import { Config, useConfig } from '@/hooks/config';
|
||||
import {
|
||||
trackingchecklistIdtoLabel,
|
||||
useTrackingChecklist,
|
||||
} from '@/hooks/tracking-checklist';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import classNames from 'classnames';
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { TrackingChecklistStepId } from 'solarxr-protocol';
|
||||
|
||||
type StepsForm = { steps: Record<TrackingChecklistStepId, boolean> };
|
||||
export function TrackingChecklistSettings({
|
||||
variant,
|
||||
}: {
|
||||
variant: 'settings' | 'modal';
|
||||
}) {
|
||||
const { l10n } = useLocalization();
|
||||
const { ignoredSteps, steps, ignoreStep } = useTrackingChecklist();
|
||||
|
||||
const { control, reset, handleSubmit } = useForm<StepsForm>({
|
||||
defaultValues: {
|
||||
steps: steps.reduce(
|
||||
(curr, { id }) => ({ [id]: !ignoredSteps.includes(id), ...curr }),
|
||||
{}
|
||||
),
|
||||
},
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset({
|
||||
steps: steps.reduce(
|
||||
(curr, { id }) => ({ [id]: !ignoredSteps.includes(id), ...curr }),
|
||||
{}
|
||||
),
|
||||
});
|
||||
}, [ignoredSteps]);
|
||||
|
||||
const onSubmit = (values: StepsForm) => {
|
||||
for (const [id, value] of Object.entries(values.steps)) {
|
||||
const stepId = +id;
|
||||
if (!stepId) continue;
|
||||
|
||||
// doing it this way prevents calling ignore step for every step.
|
||||
// that prevent sending a packet for steps that didnt change
|
||||
if (!value && !ignoredSteps.includes(stepId)) {
|
||||
ignoreStep(stepId, true);
|
||||
}
|
||||
|
||||
if (value && ignoredSteps.includes(stepId)) {
|
||||
ignoreStep(stepId, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col pt-4 pb-2">
|
||||
<Typography bold id="settings-tracking_checklist-active_steps" />
|
||||
<Typography
|
||||
color="secondary"
|
||||
id="settings-tracking_checklist-active_steps-desc"
|
||||
/>
|
||||
</div>
|
||||
<form
|
||||
className="grid md:grid-cols-2 gap-2 grid-rows-3 mobile:flex flex-col"
|
||||
onChange={handleSubmit(onSubmit)}
|
||||
>
|
||||
{steps
|
||||
.filter((step) => step.enabled)
|
||||
.map((step) => (
|
||||
<div key={step.id}>
|
||||
<CheckBox
|
||||
control={control}
|
||||
name={`steps.${step.id}`}
|
||||
disabled={!step.ignorable || !step.enabled}
|
||||
label={l10n.getString(trackingchecklistIdtoLabel[step.id])}
|
||||
outlined
|
||||
color={variant === 'settings' ? 'primary' : 'secondary'}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LayoutSelector({
|
||||
children,
|
||||
name,
|
||||
active = false,
|
||||
onClick,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
name: string;
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'w-40 aspect-video bg-background-70 flex-col flex rounded-lg border-2 group cursor-pointer',
|
||||
{
|
||||
'border-accent-background-20': active,
|
||||
'border-background-50 hover:border-background-40': !active,
|
||||
}
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="px-2 pt-2 pb-1">
|
||||
<Typography id={name} />
|
||||
</div>
|
||||
<div
|
||||
className={classNames('h-[2px] w-full mb-2', {
|
||||
'group-hover:bg-background-40 bg-background-50': !active,
|
||||
'bg-accent-background-20': active,
|
||||
})}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HomeLayoutSettings() {
|
||||
const { config, setConfig } = useConfig();
|
||||
|
||||
const setLayout = (layout: Config['homeLayout']) =>
|
||||
setConfig({ homeLayout: layout });
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col pt-4 pb-2">
|
||||
<Typography bold id="settings-home-list-layout" />
|
||||
<Typography color="secondary" id="settings-home-list-layout-desc" />
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<LayoutSelector
|
||||
name="settings-home-list-layout-grid"
|
||||
active={config?.homeLayout === 'default'}
|
||||
onClick={() => setLayout('default')}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-2 p-2">
|
||||
<div className="h-2 rounded-lg bg-background-40" />
|
||||
<div className="h-2 rounded-lg bg-background-40" />
|
||||
<div className="h-2 rounded-lg bg-background-40" />
|
||||
<div className="h-2 rounded-lg bg-background-40" />
|
||||
</div>
|
||||
</LayoutSelector>
|
||||
<LayoutSelector
|
||||
name="settings-home-list-layout-table"
|
||||
active={config?.homeLayout === 'table'}
|
||||
onClick={() => setLayout('table')}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-2 p-2">
|
||||
<div className="h-2 rounded-lg bg-background-40" />
|
||||
<div className="h-2 rounded-lg bg-background-40" />
|
||||
<div className="h-2 rounded-lg bg-background-40" />
|
||||
<div className="h-2 rounded-lg bg-background-40" />
|
||||
</div>
|
||||
</LayoutSelector>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HomeScreenSettings() {
|
||||
return (
|
||||
<SettingsPageLayout>
|
||||
<div className="flex flex-col gap-2">
|
||||
<SettingsPagePaneLayout icon={<HomeIcon />}>
|
||||
<Typography variant="main-title" id="home-settings" />
|
||||
<HomeLayoutSettings />
|
||||
</SettingsPagePaneLayout>
|
||||
<SettingsPagePaneLayout icon={<CheckIcon size={18} />}>
|
||||
<Typography variant="main-title" id="tracking_checklist" />
|
||||
<TrackingChecklistSettings variant="settings" />
|
||||
</SettingsPagePaneLayout>
|
||||
</div>
|
||||
</SettingsPageLayout>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { ArrowRightLeftIcon } from '@/components/commons/icon/ArrowIcons';
|
||||
import { isTrayAvailable } from '@/utils/tauri';
|
||||
import { isTauri } from '@tauri-apps/api/core';
|
||||
import { TauriFileInput } from '@/components/commons/TauriFileInput';
|
||||
import { DeveloperModeWidget } from '@/components/widgets/DeveloperModeWidget';
|
||||
|
||||
interface InterfaceSettingsForm {
|
||||
appearance: {
|
||||
@@ -304,7 +305,8 @@ export function InterfaceSettings() {
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid sm:grid-cols-2 pb-4">
|
||||
<div className="grid grid-cols-1 gap-2 pb-4 w-full">
|
||||
<div className="">
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
control={control}
|
||||
@@ -315,6 +317,8 @@ export function InterfaceSettings() {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{config?.debug && <DeveloperModeWidget />}
|
||||
</div>
|
||||
|
||||
<Typography variant="section-title">
|
||||
{l10n.getString('settings-interface-behavior-error_tracking')}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
DeviceDataT,
|
||||
TrackerDataT,
|
||||
TrackerStatus as TrackerStatusEnum,
|
||||
TrackingChecklistStepT,
|
||||
} from 'solarxr-protocol';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { TrackerBattery } from './TrackerBattery';
|
||||
@@ -12,61 +13,10 @@ import { TrackerStatus } from './TrackerStatus';
|
||||
import classNames from 'classnames';
|
||||
import { useTracker } from '@/hooks/tracker';
|
||||
import { BodyPartIcon } from '@/components/commons/BodyPartIcon';
|
||||
import { DownloadIcon } from '@/components/commons/icon/DownloadIcon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAppContext } from '@/hooks/app';
|
||||
import { Tooltip } from '@/components/commons/Tooltip';
|
||||
import { Localized } from '@fluent/react';
|
||||
import { checkForUpdate } from '@/hooks/firmware-update';
|
||||
|
||||
function UpdateIcon({
|
||||
showUpdate,
|
||||
}: {
|
||||
showUpdate:
|
||||
| 'can-update'
|
||||
| 'low-battery'
|
||||
| 'updated'
|
||||
| 'unavailable'
|
||||
| 'blocked';
|
||||
}) {
|
||||
const content = (
|
||||
<div className="relative">
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute rounded-full h-6 w-6 left-1 top-1 bg-accent-background-10 animate-[ping_2s_linear_infinite]',
|
||||
showUpdate !== 'can-update' && 'hidden'
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute rounded-full h-8 w-8 justify-center flex items-center',
|
||||
showUpdate === 'low-battery'
|
||||
? 'cursor-not-allowed bg-background-80 outline-2 outline-status-critical outline'
|
||||
: 'hover:bg-background-40 hover:cursor-pointer bg-background-50'
|
||||
)}
|
||||
>
|
||||
<DownloadIcon width={15} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return showUpdate !== 'can-update' ? (
|
||||
<Tooltip
|
||||
preferedDirection="top"
|
||||
content={
|
||||
<Localized id={'tracker-settings-update-low-battery'}>
|
||||
<Typography />
|
||||
</Localized>
|
||||
}
|
||||
>
|
||||
<div className="absolute right-5 -top-2.5">{content}</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Link to="/firmware-update" className="absolute right-5 -top-2.5">
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
import { FirmwareIcon } from '@/components/commons/FirmwareIcon';
|
||||
import { WarningIcon } from '@/components/commons/icon/WarningIcon';
|
||||
import { trackingchecklistIdtoLabel } from '@/hooks/tracking-checklist';
|
||||
|
||||
function TrackerBig({
|
||||
device,
|
||||
@@ -125,21 +75,39 @@ function TrackerBig({
|
||||
function TrackerSmol({
|
||||
device,
|
||||
tracker,
|
||||
warning,
|
||||
}: {
|
||||
tracker: TrackerDataT;
|
||||
device?: DeviceDataT;
|
||||
warning?: TrackingChecklistStepT | boolean;
|
||||
}) {
|
||||
const { useName } = useTracker(tracker);
|
||||
|
||||
const trackerName = useName();
|
||||
|
||||
return (
|
||||
<div className="flex rounded-md py-3 px-4 w-full gap-4 h-16">
|
||||
<div className="flex flex-col justify-center items-center fill-background-10">
|
||||
<BodyPartIcon bodyPart={tracker.info?.bodyPart} />
|
||||
<div className="flex rounded-md py-3 px-4 w-full gap-4 h-[70px]">
|
||||
<div className="flex flex-col justify-center items-center fill-background-10 relative">
|
||||
{warning && (
|
||||
<div className="absolute -right-2 -bottom-3 text-status-warning ">
|
||||
<WarningIcon width={20} />
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow justify-center">
|
||||
<Typography bold truncate>
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
'border-[3px] border-opacity-80 rounded-md overflow-clip',
|
||||
{
|
||||
'border-status-warning': warning,
|
||||
'border-transparent': !warning,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<BodyPartIcon bodyPart={tracker.info?.bodyPart} width={40} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col flex-grow justify-center gap-1">
|
||||
<Typography bold truncate variant="section-title">
|
||||
{trackerName}
|
||||
</Typography>
|
||||
<TrackerStatus status={tracker.status} />
|
||||
@@ -191,19 +159,12 @@ export function TrackerCard({
|
||||
bg?: string;
|
||||
shakeHighlight?: boolean;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
warning?: boolean;
|
||||
warning?: TrackingChecklistStepT | boolean;
|
||||
showUpdates?: boolean;
|
||||
}) {
|
||||
const { currentFirmwareRelease } = useAppContext();
|
||||
const { useVelocity } = useTracker(tracker);
|
||||
const velocity = useVelocity();
|
||||
|
||||
const showUpdate =
|
||||
showUpdates &&
|
||||
tracker.status !== TrackerStatusEnum.DISCONNECTED &&
|
||||
currentFirmwareRelease &&
|
||||
device &&
|
||||
checkForUpdate(currentFirmwareRelease, device);
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
@@ -212,8 +173,6 @@ export function TrackerCard({
|
||||
'rounded-lg overflow-hidden transition-[box-shadow] duration-200 ease-linear',
|
||||
interactable && 'hover:bg-background-50 cursor-pointer',
|
||||
outlined && 'outline outline-2 outline-accent-background-40',
|
||||
warning &&
|
||||
'outline outline-2 -outline-offset-2 outline-status-warning',
|
||||
bg
|
||||
)}
|
||||
style={
|
||||
@@ -226,12 +185,26 @@ export function TrackerCard({
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{smol && <TrackerSmol tracker={tracker} device={device} />}
|
||||
{smol && (
|
||||
<Tooltip
|
||||
preferedDirection="top"
|
||||
disabled={!warning}
|
||||
spacing={5}
|
||||
content={
|
||||
typeof warning === 'object' && (
|
||||
<div className="flex gap-1 items-center text-status-warning">
|
||||
<WarningIcon width={20} />
|
||||
<Typography id={trackingchecklistIdtoLabel[warning.id]} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<TrackerSmol tracker={tracker} device={device} warning={warning} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{!smol && <TrackerBig tracker={tracker} device={device} />}
|
||||
</div>
|
||||
{showUpdate &&
|
||||
showUpdate !== 'unavailable' &&
|
||||
showUpdate !== 'updated' && <UpdateIcon showUpdate={showUpdate} />}
|
||||
{showUpdates && <FirmwareIcon tracker={tracker} device={device} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export function TrackerWifi({
|
||||
ping,
|
||||
rssiShowNumeric,
|
||||
disabled,
|
||||
textColor = 'secondary',
|
||||
textColor = 'primary',
|
||||
}: {
|
||||
rssi: number | null;
|
||||
ping: number | null;
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import classNames from 'classnames';
|
||||
import { IPv4 } from 'ip-num';
|
||||
import { MouseEventHandler, ReactNode, useMemo, useState } from 'react';
|
||||
import {
|
||||
BodyPart,
|
||||
TrackerDataT,
|
||||
TrackerIdT,
|
||||
TrackerStatus as TrackerStatusEnum,
|
||||
} from 'solarxr-protocol';
|
||||
import { IPv4 } from 'ip-num/IPNumber';
|
||||
import { createContext, ReactNode, useContext, useMemo } from 'react';
|
||||
import { useConfig } from '@/hooks/config';
|
||||
import { useTracker } from '@/hooks/tracker';
|
||||
import { BodyPartIcon } from '@/components/commons/BodyPartIcon';
|
||||
@@ -16,37 +9,22 @@ import { formatVector3 } from '@/utils/formatting';
|
||||
import { TrackerBattery } from './TrackerBattery';
|
||||
import { TrackerStatus } from './TrackerStatus';
|
||||
import { TrackerWifi } from './TrackerWifi';
|
||||
import { trackerStatusRelated, useStatusContext } from '@/hooks/status-system';
|
||||
import { FlatDeviceTracker } from '@/store/app-store';
|
||||
import { StayAlignedInfo } from '@/components/stay-aligned/StayAlignedInfo';
|
||||
|
||||
enum DisplayColumn {
|
||||
NAME,
|
||||
TYPE,
|
||||
BATTERY,
|
||||
PING,
|
||||
TPS,
|
||||
ROTATION,
|
||||
TEMPERATURE,
|
||||
LINEAR_ACCELERATION,
|
||||
POSITION,
|
||||
STAY_ALIGNED,
|
||||
URL,
|
||||
}
|
||||
|
||||
const displayColumns: { [k: string]: boolean } = {
|
||||
[DisplayColumn.NAME]: true,
|
||||
[DisplayColumn.TYPE]: true,
|
||||
[DisplayColumn.BATTERY]: true,
|
||||
[DisplayColumn.PING]: true,
|
||||
[DisplayColumn.TPS]: true,
|
||||
[DisplayColumn.ROTATION]: true,
|
||||
[DisplayColumn.TEMPERATURE]: true,
|
||||
[DisplayColumn.LINEAR_ACCELERATION]: true,
|
||||
[DisplayColumn.POSITION]: true,
|
||||
[DisplayColumn.STAY_ALIGNED]: true,
|
||||
[DisplayColumn.URL]: true,
|
||||
};
|
||||
import { Tooltip } from '@/components/commons/Tooltip';
|
||||
import { WarningIcon } from '@/components/commons/icon/WarningIcon';
|
||||
import { FirmwareIcon } from '@/components/commons/FirmwareIcon';
|
||||
import {
|
||||
BodyPart,
|
||||
TrackerDataT,
|
||||
TrackerStatus as TrackerStatusEnum,
|
||||
TrackingChecklistStepT,
|
||||
} from 'solarxr-protocol';
|
||||
import {
|
||||
highlightedTrackers,
|
||||
trackingchecklistIdtoLabel,
|
||||
useTrackingChecklist,
|
||||
} from '@/hooks/tracking-checklist';
|
||||
|
||||
const isHMD = ({ tracker }: FlatDeviceTracker) =>
|
||||
tracker.info?.isHmd || tracker.info?.bodyPart === BodyPart.HEAD;
|
||||
@@ -58,16 +36,37 @@ const isSlime = ({ device }: FlatDeviceTracker) =>
|
||||
const getTrackerName = ({ tracker }: FlatDeviceTracker) =>
|
||||
tracker?.info?.customName?.toString() || '';
|
||||
|
||||
export function TrackerNameCell({ tracker }: { tracker: TrackerDataT }) {
|
||||
export function TrackerNameCell({
|
||||
tracker,
|
||||
warning,
|
||||
}: {
|
||||
tracker: TrackerDataT;
|
||||
warning: TrackingChecklistStepT | boolean;
|
||||
}) {
|
||||
const { useName } = useTracker(tracker);
|
||||
|
||||
const name = useName();
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-2">
|
||||
<div className="flex flex-col justify-center items-center fill-background-10">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col justify-center items-center fill-background-10 relative">
|
||||
{warning && (
|
||||
<div className="absolute -left-2 -top-1 text-status-warning ">
|
||||
<WarningIcon width={16} />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
'border-[2px] border-opacity-80 rounded-md overflow-clip',
|
||||
{
|
||||
'border-status-warning': warning,
|
||||
'border-transparent': !warning,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<BodyPartIcon bodyPart={tracker.info?.bodyPart} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow">
|
||||
<Typography bold whitespace="whitespace-nowrap">
|
||||
{name}
|
||||
@@ -103,60 +102,195 @@ export function TrackerRotCell({
|
||||
);
|
||||
}
|
||||
|
||||
export function RowContainer({
|
||||
function Header({
|
||||
name,
|
||||
className,
|
||||
first = false,
|
||||
last = false,
|
||||
show = true,
|
||||
}: {
|
||||
first?: boolean;
|
||||
last?: boolean;
|
||||
name: string;
|
||||
className?: string;
|
||||
show?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<th
|
||||
className={classNames('text-start px-2', {
|
||||
hidden: !show,
|
||||
'pl-4': first,
|
||||
'pr-4': last,
|
||||
})}
|
||||
>
|
||||
<div className={className}>
|
||||
<Typography id={name} whitespace="whitespace-nowrap" />
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
function Cell({
|
||||
children,
|
||||
rounded = 'none',
|
||||
hover,
|
||||
tracker,
|
||||
onClick,
|
||||
onMouseOver,
|
||||
onMouseOut,
|
||||
warning,
|
||||
first = false,
|
||||
last = false,
|
||||
show = true,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
rounded?: 'left' | 'right' | 'none';
|
||||
hover: boolean;
|
||||
tracker: TrackerDataT;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
onMouseOver?: MouseEventHandler<HTMLDivElement>;
|
||||
onMouseOut?: MouseEventHandler<HTMLDivElement>;
|
||||
warning: boolean;
|
||||
first?: boolean;
|
||||
last?: boolean;
|
||||
show?: boolean;
|
||||
}) {
|
||||
const { tracker } = useContext(TrackerRowProvider);
|
||||
const { useVelocity } = useTracker(tracker);
|
||||
|
||||
const velocity = useVelocity();
|
||||
|
||||
return (
|
||||
<td className={classNames('py-2 group overflow-hidden', { hidden: !show })}>
|
||||
<div
|
||||
className={classNames(
|
||||
'py-1',
|
||||
rounded === 'left' && 'pl-3',
|
||||
rounded === 'right' && 'pr-3',
|
||||
'overflow-hidden'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseOver}
|
||||
onMouseLeave={onMouseOut}
|
||||
style={{
|
||||
boxShadow: `0px 0px ${Math.floor(velocity * 8)}px ${Math.floor(
|
||||
velocity * 8
|
||||
)}px rgb(var(--accent-background-30))`,
|
||||
}}
|
||||
className={classNames(
|
||||
'h-[50px] flex flex-col justify-center px-3 transition-[box-shadow] duration-200 ease-linear',
|
||||
rounded === 'left' && 'rounded-l-lg border-l-2',
|
||||
rounded === 'right' && 'rounded-r-lg border-r-2',
|
||||
hover ? 'bg-background-50 cursor-pointer' : 'bg-background-60',
|
||||
(warning &&
|
||||
'border-status-warning border-solid border-t-2 border-b-2') ||
|
||||
'border-transparent'
|
||||
{ 'rounded-l-md ml-3': first, 'rounded-r-md mr-3': last },
|
||||
'bg-background-60 group-hover:bg-background-50 hover:cursor-pointer p-2 h-[50px] flex items-center'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
const TrackerRowProvider = createContext<FlatDeviceTracker>(undefined as never);
|
||||
|
||||
function Row({
|
||||
data,
|
||||
highlightedTrackers,
|
||||
clickedTracker,
|
||||
}: {
|
||||
data: FlatDeviceTracker;
|
||||
highlightedTrackers: highlightedTrackers | undefined;
|
||||
clickedTracker: (tracker: TrackerDataT) => void;
|
||||
}) {
|
||||
const { config } = useConfig();
|
||||
const fontColor = config?.devSettings?.highContrast ? 'primary' : 'secondary';
|
||||
const moreInfo = config?.devSettings?.moreInfo;
|
||||
|
||||
const { tracker, device } = data;
|
||||
|
||||
const warning =
|
||||
!!highlightedTrackers?.trackers.find(
|
||||
(t) =>
|
||||
t?.deviceId?.id === tracker.trackerId?.deviceId?.id &&
|
||||
t?.trackerNum === tracker.trackerId?.trackerNum
|
||||
) && highlightedTrackers.step;
|
||||
|
||||
return (
|
||||
<TrackerRowProvider.Provider value={data}>
|
||||
<Tooltip
|
||||
disabled={!warning}
|
||||
preferedDirection="top"
|
||||
content={
|
||||
warning && (
|
||||
<div className="flex gap-1 items-center text-status-warning">
|
||||
<WarningIcon width={20} />
|
||||
<Typography id={trackingchecklistIdtoLabel[warning.id]} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
tag="tr"
|
||||
spacing={-5}
|
||||
>
|
||||
<>
|
||||
<div className="relative z-10">
|
||||
<div className="absolute top-2 left-5">
|
||||
<FirmwareIcon tracker={tracker} device={device} />
|
||||
</div>
|
||||
</div>
|
||||
<tr className="group" onClick={() => clickedTracker(tracker)}>
|
||||
<Cell first>
|
||||
<TrackerNameCell tracker={tracker} warning={warning} />
|
||||
</Cell>
|
||||
<Cell>
|
||||
<Typography color={fontColor}>
|
||||
{device?.hardwareInfo?.manufacturer || '--'}
|
||||
</Typography>
|
||||
</Cell>
|
||||
<Cell>
|
||||
{device?.hardwareStatus?.batteryPctEstimate != null && (
|
||||
<TrackerBattery
|
||||
value={device.hardwareStatus.batteryPctEstimate / 100}
|
||||
voltage={device.hardwareStatus.batteryVoltage}
|
||||
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
|
||||
textColor={fontColor}
|
||||
/>
|
||||
)}
|
||||
</Cell>
|
||||
<Cell>
|
||||
{(device?.hardwareStatus?.rssi != null ||
|
||||
device?.hardwareStatus?.ping != null) && (
|
||||
<TrackerWifi
|
||||
rssi={device?.hardwareStatus?.rssi}
|
||||
rssiShowNumeric
|
||||
ping={device?.hardwareStatus?.ping}
|
||||
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
|
||||
textColor={fontColor}
|
||||
/>
|
||||
)}
|
||||
</Cell>
|
||||
<Cell>
|
||||
{tracker.tps && (
|
||||
<Typography color={fontColor}>{tracker.tps}</Typography>
|
||||
)}
|
||||
</Cell>
|
||||
<Cell>
|
||||
<TrackerRotCell
|
||||
tracker={tracker}
|
||||
precise={config?.devSettings?.preciseRotation}
|
||||
referenceAdjusted={!config?.devSettings?.rawSlimeRotation}
|
||||
color={fontColor}
|
||||
/>
|
||||
</Cell>
|
||||
<Cell last={!moreInfo}>
|
||||
{tracker?.temp && tracker?.temp?.temp != 0 && (
|
||||
<Typography color={fontColor} whitespace="whitespace-nowrap">
|
||||
{tracker.temp.temp.toFixed(2)}
|
||||
</Typography>
|
||||
)}
|
||||
</Cell>
|
||||
<Cell show={moreInfo}>
|
||||
{tracker.linearAcceleration && (
|
||||
<Typography color={fontColor} whitespace="whitespace-nowrap">
|
||||
{formatVector3(tracker.linearAcceleration, 1)}
|
||||
</Typography>
|
||||
)}
|
||||
</Cell>
|
||||
<Cell show={moreInfo}>
|
||||
{tracker.position && (
|
||||
<Typography color={fontColor} whitespace="whitespace-nowrap">
|
||||
{formatVector3(tracker.position, 2)}
|
||||
</Typography>
|
||||
)}
|
||||
</Cell>
|
||||
<Cell show={moreInfo}>
|
||||
<StayAlignedInfo color={fontColor} tracker={tracker} />
|
||||
</Cell>
|
||||
<Cell last={moreInfo} show={moreInfo}>
|
||||
<Typography color={fontColor} whitespace="whitespace-nowrap">
|
||||
udp://
|
||||
{IPv4.fromNumber(
|
||||
device?.hardwareInfo?.ipAddress?.addr || 0
|
||||
).toString()}
|
||||
</Typography>
|
||||
</Cell>
|
||||
</tr>
|
||||
</>
|
||||
</Tooltip>
|
||||
</TrackerRowProvider.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -167,14 +301,8 @@ export function TrackersTable({
|
||||
clickedTracker: (tracker: TrackerDataT) => void;
|
||||
flatTrackers: FlatDeviceTracker[];
|
||||
}) {
|
||||
const { l10n } = useLocalization();
|
||||
const [hoverTracker, setHoverTracker] = useState<TrackerIdT | null>(null);
|
||||
const { config } = useConfig();
|
||||
const { statuses } = useStatusContext();
|
||||
|
||||
const trackerEqual = (id: TrackerIdT | null) =>
|
||||
id?.trackerNum == hoverTracker?.trackerNum &&
|
||||
(!id?.deviceId || id.deviceId.id == hoverTracker?.deviceId?.id);
|
||||
const { highlightedTrackers } = useTrackingChecklist();
|
||||
|
||||
const filteringEnabled =
|
||||
config?.debug && config?.devSettings?.filterSlimesAndHMD;
|
||||
@@ -191,201 +319,51 @@ export function TrackersTable({
|
||||
return list;
|
||||
}, [flatTrackers, filteringEnabled, sortingEnabled]);
|
||||
|
||||
const fontColor = config?.devSettings?.highContrast ? 'primary' : 'secondary';
|
||||
const moreInfo = config?.devSettings?.moreInfo;
|
||||
|
||||
const hasTemperature = !!filteredSortedTrackers.find(
|
||||
({ tracker }) => tracker?.temp && tracker?.temp?.temp != 0
|
||||
);
|
||||
displayColumns[DisplayColumn.TEMPERATURE] = hasTemperature || false;
|
||||
displayColumns[DisplayColumn.POSITION] = moreInfo || false;
|
||||
displayColumns[DisplayColumn.LINEAR_ACCELERATION] = moreInfo || false;
|
||||
displayColumns[DisplayColumn.STAY_ALIGNED] = moreInfo || false;
|
||||
displayColumns[DisplayColumn.URL] = moreInfo || false;
|
||||
const displayColumnsKeys = Object.keys(displayColumns).filter(
|
||||
(k) => displayColumns[k]
|
||||
);
|
||||
const firstColumnId = +displayColumnsKeys[0];
|
||||
const lastColumnId = +displayColumnsKeys[displayColumnsKeys.length - 1];
|
||||
|
||||
function column({
|
||||
id,
|
||||
label,
|
||||
labelClassName,
|
||||
row,
|
||||
}: {
|
||||
id: DisplayColumn;
|
||||
label: string;
|
||||
labelClassName?: string;
|
||||
row: (data: FlatDeviceTracker) => ReactNode | null;
|
||||
}) {
|
||||
let rounded: 'left' | 'right' | 'none' = 'none';
|
||||
if (firstColumnId === id) rounded = 'left';
|
||||
else if (lastColumnId === id) rounded = 'right';
|
||||
|
||||
if (!displayColumns[id]) return <></>;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('flex flex-col gap-1', {
|
||||
'flex-grow': lastColumnId === id,
|
||||
})}
|
||||
>
|
||||
<div className={`flex px-3 whitespace-nowrap ${labelClassName}`}>
|
||||
{label}
|
||||
</div>
|
||||
{filteredSortedTrackers.map((data, index) => (
|
||||
<RowContainer
|
||||
rounded={rounded}
|
||||
key={index}
|
||||
tracker={data.tracker}
|
||||
onClick={() => clickedTracker(data.tracker)}
|
||||
hover={trackerEqual(data.tracker.trackerId)}
|
||||
onMouseOver={() => setHoverTracker(data.tracker.trackerId)}
|
||||
onMouseOut={() => setHoverTracker(null)}
|
||||
warning={Object.values(statuses).some((status) =>
|
||||
trackerStatusRelated(data.tracker, status)
|
||||
)}
|
||||
>
|
||||
{row(data) || <></>}
|
||||
</RowContainer>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full overflow-x-auto py-2">
|
||||
{column({
|
||||
id: DisplayColumn.NAME,
|
||||
label: l10n.getString('tracker-table-column-name'),
|
||||
row: ({ tracker }) => <TrackerNameCell tracker={tracker} />,
|
||||
})}
|
||||
|
||||
{column({
|
||||
id: DisplayColumn.TYPE,
|
||||
label: l10n.getString('tracker-table-column-type'),
|
||||
row: ({ device }) => (
|
||||
<Typography color={fontColor}>
|
||||
{device?.hardwareInfo?.manufacturer || '--'}
|
||||
</Typography>
|
||||
),
|
||||
})}
|
||||
|
||||
{column({
|
||||
id: DisplayColumn.BATTERY,
|
||||
label: l10n.getString('tracker-table-column-battery'),
|
||||
row: ({ device, tracker }) =>
|
||||
device?.hardwareStatus?.batteryPctEstimate != null && (
|
||||
<TrackerBattery
|
||||
value={device.hardwareStatus.batteryPctEstimate / 100}
|
||||
voltage={device.hardwareStatus.batteryVoltage}
|
||||
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
|
||||
textColor={fontColor}
|
||||
/>
|
||||
),
|
||||
})}
|
||||
|
||||
{column({
|
||||
id: DisplayColumn.PING,
|
||||
label: l10n.getString('tracker-table-column-ping'),
|
||||
row: ({ device, tracker }) =>
|
||||
(device?.hardwareStatus?.rssi != null ||
|
||||
device?.hardwareStatus?.ping != null) && (
|
||||
<TrackerWifi
|
||||
rssi={device?.hardwareStatus?.rssi}
|
||||
rssiShowNumeric
|
||||
ping={device?.hardwareStatus?.ping}
|
||||
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
|
||||
textColor={fontColor}
|
||||
/>
|
||||
),
|
||||
})}
|
||||
|
||||
{column({
|
||||
id: DisplayColumn.TPS,
|
||||
label: l10n.getString('tracker-table-column-tps'),
|
||||
row: ({ tracker }) => (
|
||||
<Typography color={fontColor}>
|
||||
{tracker?.tps != null ? <>{tracker.tps}</> : <></>}
|
||||
</Typography>
|
||||
),
|
||||
})}
|
||||
|
||||
{column({
|
||||
id: DisplayColumn.ROTATION,
|
||||
label: l10n.getString('tracker-table-column-rotation'),
|
||||
labelClassName: classNames({
|
||||
<div className="w-full overflow-x-auto py-2 px-2">
|
||||
<table className="w-full" cellPadding={0} cellSpacing={0}>
|
||||
<tr>
|
||||
<Header name={'tracker-table-column-name'} first />
|
||||
<Header name={'tracker-table-column-type'} />
|
||||
<Header name={'tracker-table-column-battery'} />
|
||||
<Header name={'tracker-table-column-ping'} className="w-24" />
|
||||
<Header name={'tracker-table-column-tps'} />
|
||||
<Header
|
||||
name={'tracker-table-column-rotation'}
|
||||
className={classNames({
|
||||
'w-44': config?.devSettings?.preciseRotation,
|
||||
'w-32': !config?.devSettings?.preciseRotation,
|
||||
}),
|
||||
row: ({ tracker }) => (
|
||||
<TrackerRotCell
|
||||
tracker={tracker}
|
||||
precise={config?.devSettings?.preciseRotation}
|
||||
referenceAdjusted={!config?.devSettings?.rawSlimeRotation}
|
||||
color={fontColor}
|
||||
})}
|
||||
/>
|
||||
),
|
||||
})}
|
||||
|
||||
{column({
|
||||
id: DisplayColumn.TEMPERATURE,
|
||||
label: l10n.getString('tracker-table-column-temperature'),
|
||||
row: ({ tracker }) =>
|
||||
tracker?.temp &&
|
||||
tracker?.temp?.temp != 0 && (
|
||||
<Typography color={fontColor} whitespace="whitespace-nowrap">
|
||||
{`${tracker.temp.temp.toFixed(2)}`}
|
||||
</Typography>
|
||||
),
|
||||
})}
|
||||
|
||||
{column({
|
||||
id: DisplayColumn.LINEAR_ACCELERATION,
|
||||
label: l10n.getString('tracker-table-column-linear-acceleration'),
|
||||
labelClassName: 'w-36',
|
||||
row: ({ tracker }) =>
|
||||
tracker.linearAcceleration && (
|
||||
<Typography color={fontColor} whitespace="whitespace-nowrap">
|
||||
{formatVector3(tracker.linearAcceleration, 1)}
|
||||
</Typography>
|
||||
),
|
||||
})}
|
||||
|
||||
{column({
|
||||
id: DisplayColumn.POSITION,
|
||||
label: l10n.getString('tracker-table-column-position'),
|
||||
labelClassName: 'w-36',
|
||||
row: ({ tracker }) =>
|
||||
tracker.position && (
|
||||
<Typography color={fontColor} whitespace="whitespace-nowrap">
|
||||
{formatVector3(tracker.position, 2)}
|
||||
</Typography>
|
||||
),
|
||||
})}
|
||||
|
||||
{column({
|
||||
id: DisplayColumn.STAY_ALIGNED,
|
||||
label: l10n.getString('tracker-table-column-stay_aligned'),
|
||||
labelClassName: 'w-36',
|
||||
row: ({ tracker }) => (
|
||||
<StayAlignedInfo color={fontColor} tracker={tracker} />
|
||||
),
|
||||
})}
|
||||
|
||||
{column({
|
||||
id: DisplayColumn.URL,
|
||||
label: l10n.getString('tracker-table-column-url'),
|
||||
row: ({ device }) => (
|
||||
<Typography color={fontColor} whitespace="whitespace-nowrap">
|
||||
udp://
|
||||
{IPv4.fromNumber(
|
||||
device?.hardwareInfo?.ipAddress?.addr || 0
|
||||
).toString()}
|
||||
</Typography>
|
||||
),
|
||||
})}
|
||||
<Header name={'tracker-table-column-temperature'} last={!moreInfo} />
|
||||
<Header
|
||||
name={'tracker-table-column-linear-acceleration'}
|
||||
className="w-36"
|
||||
show={moreInfo}
|
||||
/>
|
||||
<Header
|
||||
name={'tracker-table-column-position'}
|
||||
className="w-36"
|
||||
show={moreInfo}
|
||||
/>
|
||||
<Header
|
||||
name={'tracker-table-column-stay_aligned'}
|
||||
className="w-36"
|
||||
show={moreInfo}
|
||||
last={moreInfo}
|
||||
/>
|
||||
<Header name={'tracker-table-column-url'} show={moreInfo} />
|
||||
</tr>
|
||||
{filteredSortedTrackers.map((data) => (
|
||||
<Row
|
||||
clickedTracker={clickedTracker}
|
||||
data={data}
|
||||
highlightedTrackers={highlightedTrackers}
|
||||
/>
|
||||
))}
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
547
gui/src/components/tracking-checklist/TrackingChecklist.tsx
Normal file
547
gui/src/components/tracking-checklist/TrackingChecklist.tsx
Normal file
@@ -0,0 +1,547 @@
|
||||
import {
|
||||
TrackingChecklistStep,
|
||||
TrackingChecklistContext,
|
||||
useTrackingChecklist,
|
||||
trackingchecklistIdtoLabel,
|
||||
} from '@/hooks/tracking-checklist';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
ResetType,
|
||||
TrackingChecklistPublicNetworksT,
|
||||
TrackingChecklistStepId,
|
||||
} from 'solarxr-protocol';
|
||||
import { ReactNode, useEffect, useMemo, useState } from 'react';
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import { CheckIcon } from '@/components/commons/icon/CheckIcon';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { ResetButton } from '@/components/home/ResetButton';
|
||||
import { A } from '@/components/commons/A';
|
||||
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
|
||||
import { ProgressBar } from '@/components/commons/ProgressBar';
|
||||
import { CrossIcon } from '@/components/commons/icon/CrossIcon';
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowRightIcon,
|
||||
} from '@/components/commons/icon/ArrowIcons';
|
||||
import { Localized } from '@fluent/react';
|
||||
import { WrenchIcon } from '@/components/commons/icon/WrenchIcons';
|
||||
import { TrackingChecklistModal } from './TrackingChecklistModal';
|
||||
import { NavLink, useNavigate } from 'react-router-dom';
|
||||
import { useBreakpoint } from '@/hooks/breakpoint';
|
||||
|
||||
function Step({
|
||||
step: { status, id, optional, firstRequired },
|
||||
children,
|
||||
}: {
|
||||
step: TrackingChecklistStep;
|
||||
index: number;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [open, setOpen] = useState(firstRequired);
|
||||
|
||||
const canBeOpened =
|
||||
(status === 'skipped' || status === 'invalid') && !firstRequired;
|
||||
|
||||
useEffect(() => {
|
||||
if (!canBeOpened) setOpen(false);
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col pr-2 ml-6 last:pb-0 pb-3 border-l-[2px] border-background-50',
|
||||
status !== 'complete' || (firstRequired && 'border-dashed')
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex w-full gap-2 ',
|
||||
canBeOpened && 'group cursor-pointer'
|
||||
)}
|
||||
onClick={() => {
|
||||
if (canBeOpened) setOpen((open) => !open);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'p-1 rounded-full fill-background-10 flex items-center justify-center z-10 h-[25px] w-[25px] -ml-[13px]',
|
||||
status === 'complete' && 'bg-accent-background-20',
|
||||
status === 'blocked' && 'bg-background-50',
|
||||
status === 'skipped' && 'bg-background-50 fill-background-30',
|
||||
status === 'invalid' && !optional && 'bg-background-50',
|
||||
status === 'invalid' && optional && 'bg-background-50'
|
||||
)}
|
||||
>
|
||||
{status === 'skipped' && <CheckIcon size={10} />}
|
||||
{status === 'complete' && <CheckIcon size={10} />}
|
||||
{(status === 'invalid' || status === 'blocked') && (
|
||||
<div
|
||||
className={classNames(
|
||||
'h-[12px] w-[12px] rounded-full',
|
||||
optional && 'bg-background-40',
|
||||
!optional &&
|
||||
'bg-accent-background-10 animate-pulse brightness-75'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between w-full group-hover:text-background-20 text-section-title">
|
||||
<Localized id={trackingchecklistIdtoLabel[id]} />
|
||||
{canBeOpened && (
|
||||
<div className="fill-background-30 group-hover:scale-125 group-hover:fill-background-20 transition-transform">
|
||||
<ArrowDownIcon size={20} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{(firstRequired || open) && children && (
|
||||
<div className="pt-2 pl-5">{children}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const stepContentLookup: Record<
|
||||
number,
|
||||
(
|
||||
step: TrackingChecklistStep,
|
||||
context: TrackingChecklistContext
|
||||
) => JSX.Element
|
||||
> = {
|
||||
[TrackingChecklistStepId.TRACKERS_REST_CALIBRATION]: (step, { toggle }) => {
|
||||
return (
|
||||
<div className="space-y-2.5">
|
||||
<Typography id="tracking_checklist-TRACKERS_REST_CALIBRATION-desc" />
|
||||
<div className="flex justify-end">
|
||||
{step.ignorable && (
|
||||
<Button
|
||||
id="tracking_checklist-ignore"
|
||||
variant="secondary"
|
||||
onClick={() => toggle(step.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[TrackingChecklistStepId.FULL_RESET]: () => {
|
||||
return (
|
||||
<div className="space-y-2.5">
|
||||
<Typography id="tracking_checklist-FULL_RESET-desc" />
|
||||
<div>
|
||||
<Typography id="onboarding-automatic_mounting-preparation-v2-step-0" />
|
||||
<Typography id="onboarding-automatic_mounting-preparation-v2-step-1" />
|
||||
<Typography id="onboarding-automatic_mounting-preparation-v2-step-2" />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 py-1.5 gap-2">
|
||||
<div className="flex flex-col bg-background-80 rounded-md relative max-h-52">
|
||||
<CheckIcon className="md:w-9 sm:w-8 w-6 h-auto absolute top-2 right-2 fill-status-success" />
|
||||
<img
|
||||
src="/images/reset/FullResetPose.webp"
|
||||
className="h-full object-contain scale-110"
|
||||
alt="Reset position"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col bg-background-80 rounded-md relative max-h-52">
|
||||
<CheckIcon className="md:w-9 sm:w-8 w-6 h-auto absolute top-2 right-2 fill-status-success" />
|
||||
<img
|
||||
src="/images/reset/FullResetPoseSide.webp"
|
||||
className="h-full object-contain scale-110"
|
||||
alt="Reset position side"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col bg-background-80 rounded-md relative max-h-52">
|
||||
<CrossIcon className="md:w-9 sm:w-8 w-6 h-auto absolute top-2 right-2 fill-status-critical" />
|
||||
<img
|
||||
src="/images/reset/FullResetPoseWrong.webp"
|
||||
className="h-full object-contain scale-110"
|
||||
alt="Reset position wrong"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<ResetButton type={ResetType.Full} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[TrackingChecklistStepId.STEAMVR_DISCONNECTED]: (step, { toggle }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2.5">
|
||||
<Typography id="tracking_checklist-STEAMVR_DISCONNECTED-desc" />
|
||||
<div className="flex justify-between sm:items-center gap-1 flex-col sm:flex-row">
|
||||
<Button
|
||||
id="tracking_checklist-STEAMVR_DISCONNECTED-open"
|
||||
variant="primary"
|
||||
onClick={() => openUrl('steam://run/250820')}
|
||||
/>
|
||||
{step.ignorable && (
|
||||
<Button
|
||||
id="tracking_checklist-ignore"
|
||||
variant="secondary"
|
||||
onClick={() => toggle(step.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
[TrackingChecklistStepId.TRACKER_ERROR]: () => {
|
||||
return <Typography id="tracking_checklist-TRACKER_ERROR-desc" />;
|
||||
},
|
||||
[TrackingChecklistStepId.UNASSIGNED_HMD]: () => {
|
||||
return <Typography id="tracking_checklist-UNASSIGNED_HMD-desc" />;
|
||||
},
|
||||
[TrackingChecklistStepId.NETWORK_PROFILE_PUBLIC]: (step, { toggle }) => {
|
||||
const data = step.extraData as TrackingChecklistPublicNetworksT | null;
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2.5">
|
||||
<Typography
|
||||
id="tracking_checklist-NETWORK_PROFILE_PUBLIC-desc"
|
||||
vars={{
|
||||
count: data?.adapters?.length ?? 0,
|
||||
adapters: data?.adapters?.join(', ') ?? '',
|
||||
}}
|
||||
elems={{
|
||||
PublicFixLink: (
|
||||
<A
|
||||
className="text-background-20"
|
||||
href="https://docs.slimevr.dev/common-issues.html#network-profile-is-currently-set-to-public"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
whitespace="whitespace-pre-wrap"
|
||||
/>
|
||||
<div className="flex justify-between sm:items-center gap-1 flex-col sm:flex-row">
|
||||
<Button
|
||||
id="tracking_checklist-NETWORK_PROFILE_PUBLIC-open"
|
||||
variant="primary"
|
||||
onClick={() => openUrl('ms-settings:network')}
|
||||
/>
|
||||
{step.ignorable && (
|
||||
<Button
|
||||
id="tracking_checklist-ignore"
|
||||
variant="secondary"
|
||||
onClick={() => toggle(step.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
[TrackingChecklistStepId.VRCHAT_SETTINGS]: (step, { toggle }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2.5">
|
||||
<Typography id="tracking_checklist-VRCHAT_SETTINGS-desc" />
|
||||
<div className="flex justify-between sm:items-center gap-1 flex-col sm:flex-row flex-wrap">
|
||||
<Button
|
||||
variant="primary"
|
||||
to="/vrc-warnings"
|
||||
id="tracking_checklist-VRCHAT_SETTINGS-open"
|
||||
/>
|
||||
{step.ignorable && (
|
||||
<Button
|
||||
id="tracking_checklist-ignore"
|
||||
variant="secondary"
|
||||
onClick={() => toggle(step.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
[TrackingChecklistStepId.MOUNTING_CALIBRATION]: (step, { toggle }) => {
|
||||
return (
|
||||
<div className="space-y-2.5">
|
||||
<Typography id="onboarding-automatic_mounting-mounting_reset-step-0" />
|
||||
<Typography id="onboarding-automatic_mounting-mounting_reset-step-1" />
|
||||
<div className="flex w-full justify-center">
|
||||
<img
|
||||
src="/images/mounting-reset-pose.webp"
|
||||
className="h-44"
|
||||
alt="mounting reset ski pose"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between sm:items-center gap-1 flex-col sm:flex-row">
|
||||
<ResetButton type={ResetType.Mounting} group="default" />
|
||||
{step.ignorable && (
|
||||
<Button
|
||||
id="tracking_checklist-ignore"
|
||||
variant="secondary"
|
||||
onClick={() => toggle(step.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[TrackingChecklistStepId.FEET_MOUNTING_CALIBRATION]: (step, { toggle }) => {
|
||||
return (
|
||||
<div className="space-y-2.5">
|
||||
<Typography id="onboarding-automatic_mounting-mounting_reset-feet-step-0" />
|
||||
<Typography id="onboarding-automatic_mounting-mounting_reset-feet-step-1" />
|
||||
<div className="flex w-full gap-2">
|
||||
<div className="flex flex-col bg-background-80 rounded-md w-full">
|
||||
<img
|
||||
src="/images/mounting/MountingFeets.webp"
|
||||
className="h-44 object-contain"
|
||||
alt="mounting reset ski pose"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col bg-background-80 rounded-md w-full">
|
||||
<img
|
||||
src="/images/mounting/MountingFeetsSide.webp"
|
||||
className="h-44 object-contain"
|
||||
alt="mounting reset ski pose"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between sm:items-center gap-1 flex-col sm:flex-row">
|
||||
<ResetButton type={ResetType.Mounting} group="feet" />
|
||||
{step.ignorable && (
|
||||
<Button
|
||||
id="tracking_checklist-ignore"
|
||||
variant="secondary"
|
||||
onClick={() => toggle(step.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[TrackingChecklistStepId.STAY_ALIGNED_CONFIGURED]: (step, { toggle }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2.5">
|
||||
<Typography id="tracking_checklist-STAY_ALIGNED_CONFIGURED-desc" />
|
||||
<div className="flex justify-between sm:items-center gap-1 flex-col sm:flex-row">
|
||||
<Button
|
||||
id="tracking_checklist-STAY_ALIGNED_CONFIGURED-open"
|
||||
variant="primary"
|
||||
to="/onboarding/stay-aligned"
|
||||
state={{ alonePage: true }}
|
||||
/>
|
||||
{step.ignorable && (
|
||||
<Button
|
||||
id="tracking_checklist-ignore"
|
||||
variant="secondary"
|
||||
onClick={() => toggle(step.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export function TrackingChecklistMobile() {
|
||||
const context = useTrackingChecklist();
|
||||
const { completion, firstRequired, warnings } = context;
|
||||
|
||||
return (
|
||||
<div style={{ gridArea: 'l' }}>
|
||||
<NavLink
|
||||
to="/checklist"
|
||||
className={classNames(
|
||||
'bg-accent-background-30 h-full flex items-center justify-between px-2 fill-background-10 no-underline',
|
||||
{
|
||||
'bg-status-critical': completion === 'incomplete',
|
||||
'bg-status-warning text-background-90 fill-background-90':
|
||||
completion === 'partial',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className={'flex flex-col justify-center'}>
|
||||
{completion === 'incomplete' ? 'Required:' : 'Warning:'}{' '}
|
||||
<Localized
|
||||
id={
|
||||
trackingchecklistIdtoLabel[
|
||||
firstRequired?.id ?? warnings[0]?.id ?? 0
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<ArrowRightIcon />
|
||||
</NavLink>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TrackingChecklist({
|
||||
closable = true,
|
||||
closed,
|
||||
closing,
|
||||
toggleClosed,
|
||||
}: {
|
||||
closable?: boolean;
|
||||
closed: boolean;
|
||||
closing: boolean;
|
||||
toggleClosed: () => void;
|
||||
}) {
|
||||
const context = useTrackingChecklist();
|
||||
const { visibleSteps, progress, completion, warnings } = context;
|
||||
|
||||
const slimeState = useMemo(() => {
|
||||
if (completion === 'complete') return SlimeState.HAPPY;
|
||||
if (completion === 'incomplete') return SlimeState.CURIOUS;
|
||||
if (completion === 'partial') return SlimeState.SAD;
|
||||
return SlimeState.HAPPY;
|
||||
}, [completion]);
|
||||
|
||||
const settingsOpenState = useState(false);
|
||||
const [, setSettingsOpen] = settingsOpenState;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
{
|
||||
'overflow-y-auto': !closing && !closed,
|
||||
},
|
||||
'h-full w-full flex flex-col overflow-x-clip pt-1'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex pl-3 pr-2 pb-2 pt-1 justify-between items-center'
|
||||
)}
|
||||
>
|
||||
<div className="gap-2 flex fill-background-40">
|
||||
<Typography variant="section-title" id="tracking_checklist" />
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<div
|
||||
className="flex gap-1 items-center justify-center fill-background-40 hover:fill-background-30 cursor-pointer rounded-full w-8 h-8 hover:bg-background-50"
|
||||
onClick={() => setSettingsOpen(true)}
|
||||
>
|
||||
<WrenchIcon width={15} />
|
||||
</div>
|
||||
{closable && (
|
||||
<div
|
||||
className="flex gap-1 items-center justify-center fill-background-40 hover:fill-background-30 cursor-pointer rounded-full w-8 h-8 hover:bg-background-50"
|
||||
onClick={() => toggleClosed()}
|
||||
>
|
||||
{closed && <ArrowDownIcon size={25} />}
|
||||
{!closed && <CrossIcon size={25} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames('transition-all duration-500 delay-100', {
|
||||
'opacity-0 h-0': closed,
|
||||
})}
|
||||
>
|
||||
{visibleSteps.map((step, index) => (
|
||||
<Step step={step} index={index + 1} key={step.id}>
|
||||
{stepContentLookup[step.id]?.(step, context) || undefined}
|
||||
</Step>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col flex-grow border-l-[2px] justify-end ml-6 transition-all duration-500 delay-100',
|
||||
{
|
||||
'pt-3 border-background-50': !closed,
|
||||
'border-transparent': closed,
|
||||
'border-dashed': completion === 'incomplete',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames('flex w-full gap-2 z-10', {
|
||||
'cursor-pointer': closed,
|
||||
'pointer-events-none': !closed,
|
||||
})}
|
||||
onClick={() => toggleClosed()}
|
||||
>
|
||||
<div className="rounded-full bg-background-50 flex items-center justify-center h-[25px] w-[25px] -ml-[13px] relative">
|
||||
<div
|
||||
className={classNames('h-[12px] w-[12px] rounded-full', {
|
||||
'bg-status-success': completion === 'complete',
|
||||
'bg-status-critical animate-pulse':
|
||||
completion === 'incomplete',
|
||||
'bg-status-warning animate-pulse': completion === 'partial',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className={'flex flex-col justify-center'}>
|
||||
{completion === 'incomplete' && (
|
||||
<Typography
|
||||
variant="section-title"
|
||||
id="tracking_checklist-status-incomplete"
|
||||
/>
|
||||
)}
|
||||
{completion === 'partial' && (
|
||||
<Typography
|
||||
variant="section-title"
|
||||
id="tracking_checklist-status-partial"
|
||||
vars={{ count: warnings.length }}
|
||||
/>
|
||||
)}
|
||||
{completion == 'complete' && (
|
||||
<Typography
|
||||
variant="section-title"
|
||||
id="tracking_checklist-status-complete"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames('w-full flex relative p-3 pr-12', {
|
||||
'pt-0': closed,
|
||||
})}
|
||||
>
|
||||
{!closed && (
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
colorClass={
|
||||
completion === 'incomplete'
|
||||
? 'bg-accent-background-20'
|
||||
: completion === 'partial'
|
||||
? 'bg-status-warning'
|
||||
: 'bg-status-success'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="absolute bottom-0 right-0 w-20 h-20 overflow-clip pointer-events-none">
|
||||
<div className="-rotate-45 translate-x-3.5 translate-y-3.5">
|
||||
<LoaderIcon slimeState={slimeState} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TrackingChecklistModal open={settingsOpenState} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChecklistPage() {
|
||||
const nav = useNavigate();
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMobile) nav('/');
|
||||
}, [isMobile]);
|
||||
|
||||
return (
|
||||
<div className="rounded-t-lg h-full">
|
||||
<TrackingChecklist
|
||||
closable={false}
|
||||
closed={false}
|
||||
closing={false}
|
||||
toggleClosed={() => {}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { BaseModal } from '@/components/commons/BaseModal';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { TrackingChecklistSettings } from '@/components/settings/pages/HomeScreenSettings';
|
||||
|
||||
export function TrackingChecklistModal({
|
||||
open,
|
||||
}: {
|
||||
open: [boolean, Dispatch<SetStateAction<boolean>>];
|
||||
}) {
|
||||
return (
|
||||
<BaseModal
|
||||
isOpen={open[0]}
|
||||
appendClasses={
|
||||
'max-w-xl md:w-full mobile:max-h-[400px] mobile:overflow-y-auto'
|
||||
}
|
||||
closeable
|
||||
onRequestClose={() => {
|
||||
open[1](false);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Typography variant="main-title" id="tracking_checklist-settings" />
|
||||
<TrackingChecklistSettings variant="modal" />
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={() => open[1](false)}
|
||||
id="tracking_checklist-settings-close"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
TrackingChecklistContectC,
|
||||
provideTrackingChecklist,
|
||||
} from '@/hooks/tracking-checklist';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export function TrackingChecklistProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const context = provideTrackingChecklist();
|
||||
|
||||
return (
|
||||
<TrackingChecklistContectC.Provider value={context}>
|
||||
{children}
|
||||
</TrackingChecklistContectC.Provider>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useBreakpoint } from '@/hooks/breakpoint';
|
||||
import { WidgetsComponent } from '@/components/WidgetsComponent';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { NavLink, useNavigate } from 'react-router-dom';
|
||||
import { SkeletonVisualizerWidget } from '@/components/widgets/SkeletonVisualizerWidget';
|
||||
import { Checklist } from '@/components/commons/icon/ChecklistIcon';
|
||||
import { PreviewControls } from '@/components/Sidebar';
|
||||
import { Vector3 } from 'three';
|
||||
|
||||
export function VRModePage() {
|
||||
const nav = useNavigate();
|
||||
@@ -12,8 +15,30 @@ export function VRModePage() {
|
||||
}, [isMobile]);
|
||||
|
||||
return (
|
||||
<div className="p-2 flex flex-col gap-2 h-full">
|
||||
<WidgetsComponent />
|
||||
<div className="flex flex-col gap-2 h-full rounded-t-lg relative">
|
||||
<SkeletonVisualizerWidget
|
||||
onInit={(context) => {
|
||||
context.addView({
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
position: new Vector3(3, 2.5, -3),
|
||||
onHeightChange(v, newHeight) {
|
||||
v.controls.target.set(0, newHeight / 2.4, 0.1);
|
||||
const scale = Math.max(1, newHeight) / 1;
|
||||
v.camera.zoom = 1 / scale;
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<NavLink
|
||||
to="/checklist"
|
||||
className="xs:hidden absolute z-50 h-12 w-12 rounded-full bg-accent-background-30 bottom-3 right-3 flex justify-center items-center fill-background-10"
|
||||
>
|
||||
<Checklist />
|
||||
</NavLink>
|
||||
<PreviewControls open />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ const onOffKey = (value: boolean) =>
|
||||
|
||||
export function VRCWarningsPage() {
|
||||
const { l10n } = useLocalization();
|
||||
const { state, toggleMutedSettings, mutedSettings } = useVRCConfig();
|
||||
const { state, toggleMutedSettings } = useVRCConfig();
|
||||
const { currentLocales } = useLocaleConfig();
|
||||
|
||||
const meterFormat = Intl.NumberFormat(currentLocales, {
|
||||
@@ -115,7 +115,7 @@ export function VRCWarningsPage() {
|
||||
|
||||
const settingRowProps = (key: keyof VRCConfigStateSupported['validity']) => ({
|
||||
mute: () => toggleMutedSettings(key),
|
||||
muted: mutedSettings.includes(key),
|
||||
muted: state.muted.includes(key),
|
||||
valid: state.validity[key] == true,
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useConfig } from '@/hooks/config';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import { CheckBox } from '@/components/commons/Checkbox';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
|
||||
export interface DeveloperModeWidgetForm {
|
||||
highContrast: boolean;
|
||||
@@ -57,6 +56,7 @@ export function DeveloperModeWidget() {
|
||||
key={index}
|
||||
control={control}
|
||||
variant="toggle"
|
||||
outlined
|
||||
name={name}
|
||||
label={l10n.getString(`widget-developer_mode-${label}`)}
|
||||
/>
|
||||
@@ -73,10 +73,7 @@ export function DeveloperModeWidget() {
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="bg-background-60 flex flex-col w-full rounded-md px-2">
|
||||
<div className="mt-2 px-1">
|
||||
<Typography>{l10n.getString('widget-developer_mode')}</Typography>
|
||||
</div>
|
||||
<form className="grid grid-cols-2 w-full rounded-md gap-2">
|
||||
{Object.entries(toggles).map(makeToggle)}
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,6 @@ import * as THREE from 'three';
|
||||
import { BodyPart, BoneT } from 'solarxr-protocol';
|
||||
import { QuaternionFromQuatT, isIdentity } from '@/maths/quaternion';
|
||||
import classNames from 'classnames';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
@@ -18,8 +17,9 @@ import { useAtomValue } from 'jotai';
|
||||
import { bonesAtom } from '@/store/app-store';
|
||||
import { useConfig } from '@/hooks/config';
|
||||
import { Tween } from '@tweenjs/tween.js';
|
||||
import { EyeIcon } from '@/components/commons/icon/EyeIcon';
|
||||
|
||||
const GROUND_COLOR = '#4444aa';
|
||||
const GROUND_COLOR = '#2c2c6b';
|
||||
|
||||
// Just need to know the length of the total body, so don't need right legs
|
||||
const Y_PARTS = [
|
||||
@@ -32,61 +32,6 @@ const Y_PARTS = [
|
||||
BodyPart.LEFT_LOWER_LEG,
|
||||
];
|
||||
|
||||
interface SkeletonVisualizerWidgetProps {
|
||||
height?: number | string;
|
||||
maxHeight?: number | string;
|
||||
}
|
||||
|
||||
export function ToggleableSkeletonVisualizerWidget({
|
||||
height,
|
||||
maxHeight,
|
||||
}: SkeletonVisualizerWidgetProps) {
|
||||
const { l10n } = useLocalization();
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const state = localStorage.getItem('skeletonModelPreview');
|
||||
if (state) setEnabled(state === 'true');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!enabled && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setEnabled(true);
|
||||
localStorage.setItem('skeletonModelPreview', 'true');
|
||||
}}
|
||||
>
|
||||
{l10n.getString('widget-skeleton_visualizer-preview')}
|
||||
</Button>
|
||||
)}
|
||||
{enabled && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setEnabled(false);
|
||||
localStorage.setItem('skeletonModelPreview', 'false');
|
||||
}}
|
||||
>
|
||||
{l10n.getString('widget-skeleton_visualizer-hide')}
|
||||
</Button>
|
||||
<div
|
||||
style={{ height, maxHeight }}
|
||||
className="bg-background-60 p-1 rounded-md"
|
||||
>
|
||||
<SkeletonVisualizerWidget />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export type SkeletonPreviewView = {
|
||||
left: number;
|
||||
bottom: number;
|
||||
@@ -110,7 +55,7 @@ function initializePreview(
|
||||
|
||||
const resolution = new THREE.Vector2(canvas.clientWidth, canvas.clientHeight);
|
||||
const scene = new THREE.Scene();
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
let renderer: THREE.WebGLRenderer | null = new THREE.WebGLRenderer({
|
||||
canvas,
|
||||
alpha: true,
|
||||
antialias: true,
|
||||
@@ -195,7 +140,7 @@ function initializePreview(
|
||||
|
||||
const render = (delta: number) => {
|
||||
views.forEach((v) => {
|
||||
if (v.hidden) return;
|
||||
if (v.hidden || !renderer) return;
|
||||
v.controls.update(delta);
|
||||
|
||||
const left = Math.floor(resolution.x * v.left);
|
||||
@@ -251,6 +196,7 @@ function initializePreview(
|
||||
resize: (width: number, height: number) => {
|
||||
resolution.set(width, height);
|
||||
skeletonHelper.resolution.copy(resolution);
|
||||
if (!renderer) return;
|
||||
renderer.setSize(width, height);
|
||||
},
|
||||
setFrameInterval: (interval: number) => {
|
||||
@@ -276,9 +222,11 @@ function initializePreview(
|
||||
}
|
||||
},
|
||||
destroy: () => {
|
||||
skeletonHelper.dispose();
|
||||
renderer.dispose();
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
skeletonHelper.dispose();
|
||||
if (!renderer) return;
|
||||
renderer.dispose();
|
||||
renderer = null; // Very important for js to free the WebGL context. dispose does not to it alone
|
||||
},
|
||||
addView: ({
|
||||
left,
|
||||
@@ -297,6 +245,8 @@ function initializePreview(
|
||||
hidden?: boolean;
|
||||
onHeightChange: (view: SkeletonPreviewView, newHeight: number) => void;
|
||||
}) => {
|
||||
if (!renderer) return;
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(
|
||||
20,
|
||||
resolution.width / resolution.height,
|
||||
@@ -344,8 +294,10 @@ type PreviewContext = ReturnType<typeof initializePreview>;
|
||||
|
||||
function SkeletonVisualizer({
|
||||
onInit,
|
||||
disabled = false,
|
||||
}: {
|
||||
onInit: (context: PreviewContext) => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const { config } = useConfig();
|
||||
|
||||
@@ -362,15 +314,15 @@ function SkeletonVisualizer({
|
||||
useEffect(() => {
|
||||
if (bones.size === 0) return;
|
||||
const context = previewContext.current;
|
||||
if (!context) return;
|
||||
if (!context || disabled) return;
|
||||
context.rebuildSkeleton(createChildren(bones, BoneKind.root), bones);
|
||||
}, [bones.size]);
|
||||
}, [bones.size, disabled]);
|
||||
|
||||
useEffect(() => {
|
||||
const context = previewContext.current;
|
||||
if (!context) return;
|
||||
if (!context || disabled) return;
|
||||
context.updatesBones(bones);
|
||||
}, [bones]);
|
||||
}, [bones, disabled]);
|
||||
|
||||
const onResize = (e: ResizeObserverEntry) => {
|
||||
const context = previewContext.current;
|
||||
@@ -393,6 +345,7 @@ function SkeletonVisualizer({
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (disabled) return;
|
||||
if (!canvasRef.current || !containerRef.current)
|
||||
throw 'invalid state - no canvas or container';
|
||||
resizeObserver.current.observe(containerRef.current);
|
||||
@@ -416,11 +369,12 @@ function SkeletonVisualizer({
|
||||
if (!previewContext.current || !containerRef.current) return;
|
||||
resizeObserver.current.unobserve(containerRef.current);
|
||||
previewContext.current.destroy();
|
||||
previewContext.current = null;
|
||||
|
||||
containerRef.current.removeEventListener('mouseenter', onEnter);
|
||||
containerRef.current.removeEventListener('mouseleave', onLeave);
|
||||
};
|
||||
}, []);
|
||||
}, [disabled]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={classNames('w-full h-full')}>
|
||||
@@ -444,20 +398,53 @@ export function SkeletonVisualizerWidget({
|
||||
},
|
||||
});
|
||||
},
|
||||
disabled = false,
|
||||
toggleDisabled,
|
||||
}: {
|
||||
onInit?: (context: PreviewContext) => void;
|
||||
disabled?: boolean;
|
||||
toggleDisabled?: () => void;
|
||||
}) {
|
||||
const { l10n } = useLocalization();
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<div className={classNames('w-full h-full relative')}>
|
||||
<div
|
||||
className={classNames('w-full h-full transition-all', {
|
||||
blur: disabled,
|
||||
})}
|
||||
>
|
||||
<ErrorBoundary onError={() => setError(true)} fallback={<></>}>
|
||||
<SkeletonVisualizer onInit={onInit} disabled={disabled} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute h-full w-full top-0 flex items-center justify-center transition-opacity duration-300',
|
||||
{ 'opacity-0 pointer-events-none': !disabled || error }
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="bg-background-90 rounded-lg p-2 px-3 flex gap-2 items-center hover:bg-background-60 cursor-pointer"
|
||||
onClick={() => toggleDisabled?.()}
|
||||
>
|
||||
<EyeIcon closed width={20} />
|
||||
<Typography id="preview-disabled_render" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute h-full w-full top-0 flex items-center justify-center transition-opacity duration-300',
|
||||
{ 'opacity-0 pointer-events-none': !error }
|
||||
)}
|
||||
>
|
||||
<div className="bg-background-90 rounded-lg p-2 px-3 flex gap-2 items-center">
|
||||
<Typography color="primary" textAlign="text-center">
|
||||
{l10n.getString('tips-failed_webgl')}
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<SkeletonVisualizer onInit={onInit} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,16 +3,13 @@ import {
|
||||
DataFeedMessage,
|
||||
DataFeedUpdateT,
|
||||
ResetResponseT,
|
||||
ResetStatus,
|
||||
ResetType,
|
||||
RpcMessage,
|
||||
StartDataFeedT,
|
||||
} from 'solarxr-protocol';
|
||||
import { playSoundOnResetEnded, playSoundOnResetStarted } from '@/sounds/sounds';
|
||||
import { handleResetSounds } from '@/sounds/sounds';
|
||||
import { useConfig } from './config';
|
||||
import { useBonesDataFeedConfig, useDataFeedConfig } from './datafeed-config';
|
||||
import { useWebsocketAPI } from './websocket-api';
|
||||
import { error } from '@/utils/logging';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { bonesAtom, datafeedAtom, devicesAtom } from '@/store/app-store';
|
||||
import { updateSentryContext } from '@/utils/sentry';
|
||||
@@ -55,23 +52,9 @@ export function useProvideAppContext(): AppContext {
|
||||
updateSentryContext(devices);
|
||||
}, [devices]);
|
||||
|
||||
useRPCPacket(RpcMessage.ResetResponse, ({ status, resetType }: ResetResponseT) => {
|
||||
useRPCPacket(RpcMessage.ResetResponse, (resetResponse: ResetResponseT) => {
|
||||
if (!config?.feedbackSound) return;
|
||||
try {
|
||||
switch (status) {
|
||||
case ResetStatus.STARTED: {
|
||||
if (resetType !== ResetType.Yaw)
|
||||
playSoundOnResetStarted(config?.feedbackSoundVolume);
|
||||
break;
|
||||
}
|
||||
case ResetStatus.FINISHED: {
|
||||
playSoundOnResetEnded(resetType, config?.feedbackSoundVolume);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
error(e);
|
||||
}
|
||||
handleResetSounds(config?.feedbackSoundVolume ?? 1, resetResponse);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
90
gui/src/hooks/body-parts.ts
Normal file
90
gui/src/hooks/body-parts.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { BodyPart } from 'solarxr-protocol';
|
||||
|
||||
export const ALL_BODY_PARTS = [
|
||||
BodyPart.NONE,
|
||||
BodyPart.HEAD,
|
||||
BodyPart.NECK,
|
||||
BodyPart.CHEST,
|
||||
BodyPart.WAIST,
|
||||
BodyPart.HIP,
|
||||
BodyPart.LEFT_UPPER_LEG,
|
||||
BodyPart.RIGHT_UPPER_LEG,
|
||||
BodyPart.LEFT_LOWER_LEG,
|
||||
BodyPart.RIGHT_LOWER_LEG,
|
||||
BodyPart.LEFT_FOOT,
|
||||
BodyPart.RIGHT_FOOT,
|
||||
BodyPart.LEFT_LOWER_ARM,
|
||||
BodyPart.RIGHT_LOWER_ARM,
|
||||
BodyPart.LEFT_UPPER_ARM,
|
||||
BodyPart.RIGHT_UPPER_ARM,
|
||||
BodyPart.LEFT_HAND,
|
||||
BodyPart.RIGHT_HAND,
|
||||
BodyPart.LEFT_SHOULDER,
|
||||
BodyPart.RIGHT_SHOULDER,
|
||||
BodyPart.UPPER_CHEST,
|
||||
BodyPart.LEFT_HIP,
|
||||
BodyPart.RIGHT_HIP,
|
||||
BodyPart.LEFT_THUMB_METACARPAL,
|
||||
BodyPart.LEFT_THUMB_PROXIMAL,
|
||||
BodyPart.LEFT_THUMB_DISTAL,
|
||||
BodyPart.LEFT_INDEX_PROXIMAL,
|
||||
BodyPart.LEFT_INDEX_INTERMEDIATE,
|
||||
BodyPart.LEFT_INDEX_DISTAL,
|
||||
BodyPart.LEFT_MIDDLE_PROXIMAL,
|
||||
BodyPart.LEFT_MIDDLE_INTERMEDIATE,
|
||||
BodyPart.LEFT_MIDDLE_DISTAL,
|
||||
BodyPart.LEFT_RING_PROXIMAL,
|
||||
BodyPart.LEFT_RING_INTERMEDIATE,
|
||||
BodyPart.LEFT_RING_DISTAL,
|
||||
BodyPart.LEFT_LITTLE_PROXIMAL,
|
||||
BodyPart.LEFT_LITTLE_INTERMEDIATE,
|
||||
BodyPart.LEFT_LITTLE_DISTAL,
|
||||
BodyPart.RIGHT_THUMB_METACARPAL,
|
||||
BodyPart.RIGHT_THUMB_PROXIMAL,
|
||||
BodyPart.RIGHT_THUMB_DISTAL,
|
||||
BodyPart.RIGHT_INDEX_PROXIMAL,
|
||||
BodyPart.RIGHT_INDEX_INTERMEDIATE,
|
||||
BodyPart.RIGHT_INDEX_DISTAL,
|
||||
BodyPart.RIGHT_MIDDLE_PROXIMAL,
|
||||
BodyPart.RIGHT_MIDDLE_INTERMEDIATE,
|
||||
BodyPart.RIGHT_MIDDLE_DISTAL,
|
||||
BodyPart.RIGHT_RING_PROXIMAL,
|
||||
BodyPart.RIGHT_RING_INTERMEDIATE,
|
||||
BodyPart.RIGHT_RING_DISTAL,
|
||||
BodyPart.RIGHT_LITTLE_PROXIMAL,
|
||||
BodyPart.RIGHT_LITTLE_INTERMEDIATE,
|
||||
BodyPart.RIGHT_LITTLE_DISTAL,
|
||||
];
|
||||
export const FEET_BODY_PARTS = [BodyPart.LEFT_FOOT, BodyPart.RIGHT_FOOT];
|
||||
export const FINGER_BODY_PARTS = [
|
||||
BodyPart.LEFT_THUMB_METACARPAL,
|
||||
BodyPart.LEFT_THUMB_PROXIMAL,
|
||||
BodyPart.LEFT_THUMB_DISTAL,
|
||||
BodyPart.LEFT_INDEX_PROXIMAL,
|
||||
BodyPart.LEFT_INDEX_INTERMEDIATE,
|
||||
BodyPart.LEFT_INDEX_DISTAL,
|
||||
BodyPart.LEFT_MIDDLE_PROXIMAL,
|
||||
BodyPart.LEFT_MIDDLE_INTERMEDIATE,
|
||||
BodyPart.LEFT_MIDDLE_DISTAL,
|
||||
BodyPart.LEFT_RING_PROXIMAL,
|
||||
BodyPart.LEFT_RING_INTERMEDIATE,
|
||||
BodyPart.LEFT_RING_DISTAL,
|
||||
BodyPart.LEFT_LITTLE_PROXIMAL,
|
||||
BodyPart.LEFT_LITTLE_INTERMEDIATE,
|
||||
BodyPart.LEFT_LITTLE_DISTAL,
|
||||
BodyPart.RIGHT_THUMB_METACARPAL,
|
||||
BodyPart.RIGHT_THUMB_PROXIMAL,
|
||||
BodyPart.RIGHT_THUMB_DISTAL,
|
||||
BodyPart.RIGHT_INDEX_PROXIMAL,
|
||||
BodyPart.RIGHT_INDEX_INTERMEDIATE,
|
||||
BodyPart.RIGHT_INDEX_DISTAL,
|
||||
BodyPart.RIGHT_MIDDLE_PROXIMAL,
|
||||
BodyPart.RIGHT_MIDDLE_INTERMEDIATE,
|
||||
BodyPart.RIGHT_MIDDLE_DISTAL,
|
||||
BodyPart.RIGHT_RING_PROXIMAL,
|
||||
BodyPart.RIGHT_RING_INTERMEDIATE,
|
||||
BodyPart.RIGHT_RING_DISTAL,
|
||||
BodyPart.RIGHT_LITTLE_PROXIMAL,
|
||||
BodyPart.RIGHT_LITTLE_INTERMEDIATE,
|
||||
BodyPart.RIGHT_LITTLE_DISTAL,
|
||||
];
|
||||
54
gui/src/hooks/bvh.ts
Normal file
54
gui/src/hooks/bvh.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { isTauri } from '@tauri-apps/api/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { RecordBVHRequestT, RecordBVHStatusT, RpcMessage } from 'solarxr-protocol';
|
||||
import { useWebsocketAPI } from './websocket-api';
|
||||
import { useConfig } from './config';
|
||||
import { save } from '@tauri-apps/plugin-dialog';
|
||||
|
||||
export function useBHV() {
|
||||
const { config } = useConfig();
|
||||
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
|
||||
const [state, setState] = useState<'idle' | 'recording' | 'saving'>('idle');
|
||||
const { l10n } = useLocalization();
|
||||
|
||||
useEffect(() => {
|
||||
sendRPCPacket(RpcMessage.RecordBVHStatusRequest, new RecordBVHRequestT());
|
||||
}, []);
|
||||
|
||||
const toggle = async () => {
|
||||
const record = new RecordBVHRequestT(state === 'recording');
|
||||
|
||||
if (isTauri() && state === 'idle') {
|
||||
if (config?.bvhDirectory) {
|
||||
record.path = config.bvhDirectory;
|
||||
} else {
|
||||
setState('saving');
|
||||
record.path = await save({
|
||||
title: l10n.getString('bvh-save_title'),
|
||||
filters: [
|
||||
{
|
||||
name: 'BVH',
|
||||
extensions: ['bvh'],
|
||||
},
|
||||
],
|
||||
defaultPath: 'bvh-recording.bvh',
|
||||
});
|
||||
setState('idle');
|
||||
}
|
||||
}
|
||||
|
||||
sendRPCPacket(RpcMessage.RecordBVHRequest, record);
|
||||
};
|
||||
|
||||
useRPCPacket(RpcMessage.RecordBVHStatus, (data: RecordBVHStatusT) => {
|
||||
setState(data.recording ? 'recording' : 'idle');
|
||||
});
|
||||
|
||||
return {
|
||||
available:
|
||||
typeof window.__ANDROID__ === 'undefined' || !window.__ANDROID__?.isThere(),
|
||||
state,
|
||||
toggle,
|
||||
};
|
||||
}
|
||||
@@ -46,6 +46,8 @@ export interface Config {
|
||||
showNavbarOnboarding: boolean;
|
||||
vrcMutedWarnings: string[];
|
||||
bvhDirectory: string | null;
|
||||
homeLayout: 'default' | 'table';
|
||||
skeletonPreview: boolean;
|
||||
}
|
||||
|
||||
export interface ConfigContext {
|
||||
@@ -75,6 +77,8 @@ export const defaultConfig: Config = {
|
||||
vrcMutedWarnings: [],
|
||||
devSettings: defaultDevSettings,
|
||||
bvhDirectory: null,
|
||||
homeLayout: 'default',
|
||||
skeletonPreview: true,
|
||||
};
|
||||
|
||||
interface CrossStorage {
|
||||
|
||||
@@ -17,13 +17,16 @@ export function useCountdown({
|
||||
const startCountdown = () => {
|
||||
setIsCounting(true);
|
||||
setDisplayTimer(duration);
|
||||
if (countdownTimer.current) {
|
||||
clearTimer();
|
||||
}
|
||||
|
||||
counter.current = 0;
|
||||
countdownTimer.current = setInterval(
|
||||
() => {
|
||||
counter.current++;
|
||||
setDisplayTimer(duration - counter.current);
|
||||
if (counter.current >= duration) {
|
||||
clearInterval(countdownTimer.current);
|
||||
resetEnd();
|
||||
}
|
||||
},
|
||||
@@ -31,15 +34,20 @@ export function useCountdown({
|
||||
);
|
||||
};
|
||||
|
||||
const clearTimer = () => {
|
||||
clearInterval(countdownTimer.current);
|
||||
countdownTimer.current = undefined;
|
||||
};
|
||||
|
||||
const resetEnd = () => {
|
||||
setIsCounting(false);
|
||||
clearInterval(countdownTimer.current);
|
||||
clearTimer();
|
||||
onCountdownEnd();
|
||||
};
|
||||
|
||||
const abortCountdown = () => {
|
||||
setIsCounting(false);
|
||||
clearInterval(countdownTimer.current);
|
||||
clearTimer();
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
47
gui/src/hooks/pause-tracking.ts
Normal file
47
gui/src/hooks/pause-tracking.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useWebsocketAPI } from './websocket-api';
|
||||
import {
|
||||
RpcMessage,
|
||||
SetPauseTrackingRequestT,
|
||||
TrackingPauseStateRequestT,
|
||||
TrackingPauseStateResponseT,
|
||||
} from 'solarxr-protocol';
|
||||
import { restartAndPlay, trackingPauseSound, trackingPlaySound } from '@/sounds/sounds';
|
||||
import { useConfig } from './config';
|
||||
|
||||
export function usePauseTracking() {
|
||||
const { config } = useConfig();
|
||||
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
|
||||
const [paused, setPaused] = useState(false);
|
||||
|
||||
const toggle = () => {
|
||||
const pause = new SetPauseTrackingRequestT(!paused);
|
||||
sendRPCPacket(RpcMessage.SetPauseTrackingRequest, pause);
|
||||
|
||||
if (!config) return;
|
||||
if (pause.pauseTracking) {
|
||||
restartAndPlay(trackingPauseSound, config.feedbackSoundVolume);
|
||||
} else {
|
||||
restartAndPlay(trackingPlaySound, config.feedbackSoundVolume);
|
||||
}
|
||||
};
|
||||
|
||||
useRPCPacket(
|
||||
RpcMessage.TrackingPauseStateResponse,
|
||||
(data: TrackingPauseStateResponseT) => {
|
||||
setPaused(data.trackingPaused);
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
sendRPCPacket(
|
||||
RpcMessage.TrackingPauseStateRequest,
|
||||
new TrackingPauseStateRequestT()
|
||||
);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
toggle,
|
||||
paused,
|
||||
};
|
||||
}
|
||||
58
gui/src/hooks/reset-settings.ts
Normal file
58
gui/src/hooks/reset-settings.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
ChangeSettingsRequestT,
|
||||
ResetsSettingsT,
|
||||
RpcMessage,
|
||||
SettingsResetRequestT,
|
||||
SettingsResponseT,
|
||||
} from 'solarxr-protocol';
|
||||
import { useWebsocketAPI } from './websocket-api';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export interface ResetSettingsForm {
|
||||
resetMountingFeet: boolean;
|
||||
armsMountingResetMode: number;
|
||||
yawResetSmoothTime: number;
|
||||
saveMountingReset: boolean;
|
||||
resetHmdPitch: boolean;
|
||||
}
|
||||
|
||||
export const defaultResetSettings = {
|
||||
resetMountingFeet: false,
|
||||
armsMountingResetMode: 0,
|
||||
yawResetSmoothTime: 0.0,
|
||||
saveMountingReset: false,
|
||||
resetHmdPitch: false,
|
||||
};
|
||||
|
||||
export function loadResetSettings(resetSettingsForm: ResetSettingsForm) {
|
||||
const resetsSettings = new ResetsSettingsT();
|
||||
resetsSettings.resetMountingFeet = resetSettingsForm.resetMountingFeet;
|
||||
resetsSettings.armsMountingResetMode = resetSettingsForm.armsMountingResetMode;
|
||||
resetsSettings.yawResetSmoothTime = resetSettingsForm.yawResetSmoothTime;
|
||||
resetsSettings.saveMountingReset = resetSettingsForm.saveMountingReset;
|
||||
resetsSettings.resetHmdPitch = resetSettingsForm.resetHmdPitch;
|
||||
|
||||
return resetsSettings;
|
||||
}
|
||||
|
||||
export function useResetSettings() {
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
const [settings, setSettings] = useState<ResetSettingsForm>(defaultResetSettings);
|
||||
|
||||
useEffect(() =>
|
||||
sendRPCPacket(RpcMessage.SettingsRequest, new SettingsResetRequestT())
|
||||
);
|
||||
|
||||
useRPCPacket(RpcMessage.SettingsResponse, (settings: SettingsResponseT) => {
|
||||
if (settings.resetsSettings) setSettings(settings.resetsSettings);
|
||||
});
|
||||
|
||||
return {
|
||||
update: (resetSettingsForm: Partial<ResetSettingsForm>) => {
|
||||
const req = new ChangeSettingsRequestT();
|
||||
const res = loadResetSettings({ ...settings, ...resetSettingsForm });
|
||||
req.resetsSettings = res;
|
||||
sendRPCPacket(RpcMessage.ChangeSettingsRequest, req);
|
||||
},
|
||||
};
|
||||
}
|
||||
151
gui/src/hooks/reset.ts
Normal file
151
gui/src/hooks/reset.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
BodyPart,
|
||||
ResetRequestT,
|
||||
ResetResponseT,
|
||||
ResetStatus,
|
||||
ResetType,
|
||||
RpcMessage,
|
||||
} from 'solarxr-protocol';
|
||||
import { useWebsocketAPI } from './websocket-api';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { assignedTrackersAtom } from '@/store/app-store';
|
||||
import { FEET_BODY_PARTS, FINGER_BODY_PARTS } from './body-parts';
|
||||
import { useLocaleConfig } from '@/i18n/config';
|
||||
|
||||
export type ResetBtnStatus = 'idle' | 'counting' | 'finished';
|
||||
|
||||
export type MountingResetGroup = 'default' | 'feet' | 'fingers';
|
||||
export type UseResetOptions =
|
||||
| { type: ResetType.Full | ResetType.Yaw }
|
||||
| { type: ResetType.Mounting; group: MountingResetGroup };
|
||||
|
||||
export const BODY_PARTS_GROUPS: Record<MountingResetGroup, BodyPart[]> = {
|
||||
default: [],
|
||||
feet: FEET_BODY_PARTS,
|
||||
fingers: FINGER_BODY_PARTS,
|
||||
};
|
||||
|
||||
export function useReset(options: UseResetOptions, onReseted?: () => void) {
|
||||
if (options.type === ResetType.Mounting && !options.group) options.group = 'default';
|
||||
|
||||
const { currentLocales } = useLocaleConfig();
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
|
||||
const finishedTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
const [status, setStatus] = useState<ResetBtnStatus>('idle');
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [duration, setDuration] = useState(0);
|
||||
|
||||
const parts = BODY_PARTS_GROUPS['group' in options ? options.group : 'default'];
|
||||
|
||||
const triggerReset = () => {
|
||||
const req = new ResetRequestT();
|
||||
req.resetType = options.type;
|
||||
req.bodyParts = parts;
|
||||
sendRPCPacket(RpcMessage.ResetRequest, req);
|
||||
};
|
||||
|
||||
const onResetFinished = () => {
|
||||
setStatus('finished');
|
||||
if (onReseted) onReseted();
|
||||
};
|
||||
|
||||
const onResetCanceled = () => {
|
||||
if (status !== 'finished') setStatus('idle');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'finished') {
|
||||
finishedTimeoutRef.current = setTimeout(() => {
|
||||
setStatus('idle'); // only do that if we were on finished status. Allows to reset the outlined border
|
||||
}, 2000);
|
||||
} else {
|
||||
clearTimeout(finishedTimeoutRef.current);
|
||||
}
|
||||
return () => {
|
||||
clearTimeout(finishedTimeoutRef.current);
|
||||
};
|
||||
}, [status]);
|
||||
|
||||
const onResetProgress = (progress: number, duration: number) => {
|
||||
setProgress(progress / 1000);
|
||||
setDuration(duration / 1000);
|
||||
};
|
||||
|
||||
useRPCPacket(
|
||||
RpcMessage.ResetResponse,
|
||||
({ status, resetType, progress, duration, bodyParts }: ResetResponseT) => {
|
||||
if (
|
||||
resetType !== options.type ||
|
||||
(resetType == ResetType.Mounting &&
|
||||
JSON.stringify(parts) !== JSON.stringify(bodyParts))
|
||||
) {
|
||||
onResetCanceled();
|
||||
return;
|
||||
}
|
||||
onResetProgress(progress, duration);
|
||||
switch (status) {
|
||||
case ResetStatus.FINISHED: {
|
||||
onResetFinished();
|
||||
break;
|
||||
}
|
||||
case ResetStatus.STARTED: {
|
||||
setStatus('counting');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const name = useMemo(() => {
|
||||
switch (options.type) {
|
||||
case ResetType.Yaw:
|
||||
return 'reset-yaw';
|
||||
case ResetType.Full:
|
||||
return 'reset-full';
|
||||
case ResetType.Mounting:
|
||||
if (options.group !== 'default') return `reset-mounting-${options.group}`;
|
||||
return 'reset-mounting';
|
||||
default:
|
||||
return 'unhandled';
|
||||
}
|
||||
}, [options.type]);
|
||||
|
||||
let disabled = status === 'counting';
|
||||
if (options.type === ResetType.Mounting && options.group !== 'default') {
|
||||
const assignedTrackers = useAtomValue(assignedTrackersAtom);
|
||||
|
||||
if (
|
||||
!assignedTrackers.some(
|
||||
({ tracker }) =>
|
||||
tracker.info?.bodyPart &&
|
||||
BODY_PARTS_GROUPS[options.group].includes(tracker.info?.bodyPart)
|
||||
)
|
||||
)
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
const localized = useMemo(
|
||||
() =>
|
||||
Intl.NumberFormat('en-US', {
|
||||
maximumFractionDigits: 1,
|
||||
unit: 'second',
|
||||
unitDisplay: 'narrow',
|
||||
style: 'unit',
|
||||
}),
|
||||
[currentLocales]
|
||||
);
|
||||
|
||||
return {
|
||||
triggerReset,
|
||||
progress,
|
||||
duration,
|
||||
status,
|
||||
disabled,
|
||||
name,
|
||||
timer: localized.format(duration - progress),
|
||||
};
|
||||
}
|
||||
|
||||
export function useMountingReset() {}
|
||||
@@ -1,207 +0,0 @@
|
||||
import { createContext, useEffect, useReducer, useContext } from 'react';
|
||||
import {
|
||||
BodyPart,
|
||||
RpcMessage,
|
||||
StatusData,
|
||||
StatusMessageT,
|
||||
StatusPublicNetworkT,
|
||||
StatusSteamVRDisconnectedT,
|
||||
StatusSystemFixedT,
|
||||
StatusSystemRequestT,
|
||||
StatusSystemResponseT,
|
||||
StatusSystemUpdateT,
|
||||
StatusTrackerErrorT,
|
||||
StatusTrackerResetT,
|
||||
StatusUnassignedHMDT,
|
||||
TrackerDataT,
|
||||
} from 'solarxr-protocol';
|
||||
import { useWebsocketAPI } from './websocket-api';
|
||||
import { FluentVariable } from '@fluent/bundle';
|
||||
import { ReactLocalization } from '@fluent/react';
|
||||
import { FlatDeviceTracker } from '@/store/app-store';
|
||||
|
||||
type StatusSystemStateAction =
|
||||
| StatusSystemStateFixedAction
|
||||
| StatusSystemStateNewAction
|
||||
| StatusSystemStateUpdateAction;
|
||||
|
||||
interface StatusSystemStateFixedAction {
|
||||
type: RpcMessage.StatusSystemFixed;
|
||||
data: number;
|
||||
}
|
||||
|
||||
interface StatusSystemStateUpdateAction {
|
||||
type: RpcMessage.StatusSystemUpdate;
|
||||
data: StatusMessageT;
|
||||
}
|
||||
|
||||
interface StatusSystemStateNewAction {
|
||||
type: RpcMessage.StatusSystemResponse;
|
||||
data: StatusMessageT[];
|
||||
}
|
||||
|
||||
interface StatusSystemState {
|
||||
statuses: {
|
||||
[id: number]: StatusMessageT;
|
||||
};
|
||||
}
|
||||
|
||||
export interface StatusSystemContext {
|
||||
statuses: {
|
||||
[id: number]: StatusMessageT;
|
||||
};
|
||||
}
|
||||
|
||||
function reducer(
|
||||
state: StatusSystemState,
|
||||
action: StatusSystemStateAction
|
||||
): StatusSystemState {
|
||||
switch (action.type) {
|
||||
case RpcMessage.StatusSystemFixed: {
|
||||
const newState = {
|
||||
statuses: { ...state.statuses },
|
||||
};
|
||||
delete newState.statuses[action.data];
|
||||
return newState;
|
||||
}
|
||||
case RpcMessage.StatusSystemUpdate: {
|
||||
return {
|
||||
statuses: { ...state.statuses, [action.data.id]: action.data },
|
||||
};
|
||||
}
|
||||
case RpcMessage.StatusSystemResponse: {
|
||||
return {
|
||||
// Convert the array into an object, we dont want to have an array on our map!
|
||||
statuses: action.data.reduce((prev, cur) => ({ ...prev, [cur.id]: cur }), {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useProvideStatusContext(): StatusSystemContext {
|
||||
const { useRPCPacket, sendRPCPacket, isConnected } = useWebsocketAPI();
|
||||
const [state, dispatch] = useReducer(reducer, { statuses: {} });
|
||||
|
||||
useRPCPacket(
|
||||
RpcMessage.StatusSystemResponse,
|
||||
({ currentStatuses }: StatusSystemResponseT) =>
|
||||
dispatch({ type: RpcMessage.StatusSystemResponse, data: currentStatuses })
|
||||
);
|
||||
|
||||
useRPCPacket(RpcMessage.StatusSystemFixed, ({ fixedStatusId }: StatusSystemFixedT) =>
|
||||
dispatch({ type: RpcMessage.StatusSystemFixed, data: fixedStatusId })
|
||||
);
|
||||
|
||||
useRPCPacket(
|
||||
RpcMessage.StatusSystemUpdate,
|
||||
({ newStatus }: StatusSystemUpdateT) =>
|
||||
newStatus && dispatch({ type: RpcMessage.StatusSystemUpdate, data: newStatus })
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isConnected) return;
|
||||
sendRPCPacket(RpcMessage.StatusSystemRequest, new StatusSystemRequestT());
|
||||
}, [isConnected]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export const StatusSystemC = createContext<StatusSystemContext>(undefined as never);
|
||||
|
||||
export function useStatusContext() {
|
||||
const context = useContext<StatusSystemContext>(StatusSystemC);
|
||||
if (!context) {
|
||||
throw new Error('useStatusContext must be within a StatusSystemContext Provider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function parseStatusToLocale(
|
||||
status: StatusMessageT,
|
||||
trackers: FlatDeviceTracker[] | null,
|
||||
l10n: ReactLocalization
|
||||
): Record<string, FluentVariable> {
|
||||
switch (status.dataType) {
|
||||
case StatusData.NONE:
|
||||
case StatusData.StatusTrackerReset:
|
||||
case StatusData.StatusUnassignedHMD:
|
||||
return {};
|
||||
case StatusData.StatusPublicNetwork: {
|
||||
const data = status.data as StatusPublicNetworkT;
|
||||
return {
|
||||
adapters: data.adapters.join(', '),
|
||||
count: data.adapters.length,
|
||||
};
|
||||
}
|
||||
case StatusData.StatusSteamVRDisconnected: {
|
||||
const data = status.data as StatusSteamVRDisconnectedT;
|
||||
if (typeof data.bridgeSettingsName === 'string') {
|
||||
return { type: data.bridgeSettingsName };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
case StatusData.StatusTrackerError: {
|
||||
const data = status.data as StatusTrackerErrorT;
|
||||
if (data.trackerId?.trackerNum === undefined || !trackers) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const tracker = trackers.find(
|
||||
({ tracker }) =>
|
||||
tracker?.trackerId?.trackerNum == data.trackerId?.trackerNum &&
|
||||
tracker?.trackerId?.deviceId?.id == data.trackerId?.deviceId?.id
|
||||
);
|
||||
if (!tracker)
|
||||
return {
|
||||
trackerName: 'unknown',
|
||||
};
|
||||
const name = tracker.tracker.info?.customName
|
||||
? tracker.tracker.info?.customName
|
||||
: tracker.tracker.info?.bodyPart
|
||||
? l10n.getString('body_part-' + BodyPart[tracker.tracker.info?.bodyPart])
|
||||
: tracker.tracker.info?.displayName || 'unknown';
|
||||
if (typeof name !== 'string') {
|
||||
return {
|
||||
trackerName: new TextDecoder().decode(name),
|
||||
};
|
||||
}
|
||||
return { trackerName: name };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const doesntContainTrackerInfo: readonly StatusData[] = [StatusData.NONE];
|
||||
export function trackerStatusRelated(
|
||||
tracker: TrackerDataT,
|
||||
status: StatusMessageT
|
||||
): boolean {
|
||||
if (doesntContainTrackerInfo.includes(status.dataType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (status.dataType) {
|
||||
case StatusData.StatusTrackerReset: {
|
||||
const data = status.data as StatusTrackerResetT;
|
||||
return (
|
||||
data.trackerId?.trackerNum == tracker.trackerId?.trackerNum &&
|
||||
data.trackerId?.deviceId?.id === tracker.trackerId?.deviceId?.id
|
||||
);
|
||||
}
|
||||
case StatusData.StatusTrackerError: {
|
||||
const data = status.data as StatusTrackerErrorT;
|
||||
return (
|
||||
data.trackerId?.trackerNum == tracker.trackerId?.trackerNum &&
|
||||
data.trackerId?.deviceId?.id === tracker.trackerId?.deviceId?.id
|
||||
);
|
||||
}
|
||||
case StatusData.StatusUnassignedHMD: {
|
||||
const data = status.data as StatusUnassignedHMDT;
|
||||
return (
|
||||
data.trackerId?.trackerNum == tracker.trackerId?.trackerNum &&
|
||||
data.trackerId?.deviceId?.id === tracker.trackerId?.deviceId?.id
|
||||
);
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
193
gui/src/hooks/tracking-checklist.ts
Normal file
193
gui/src/hooks/tracking-checklist.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import {
|
||||
TrackingChecklistRequestT,
|
||||
TrackingChecklistResponseT,
|
||||
TrackingChecklistStepId,
|
||||
TrackingChecklistStepT,
|
||||
TrackingChecklistStepVisibility,
|
||||
IgnoreTrackingChecklistStepRequestT,
|
||||
RpcMessage,
|
||||
TrackerIdT,
|
||||
} from 'solarxr-protocol';
|
||||
import { useWebsocketAPI } from './websocket-api';
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export const trackingchecklistIdtoLabel: Record<TrackingChecklistStepId, string> = {
|
||||
[TrackingChecklistStepId.UNKNOWN]: '',
|
||||
[TrackingChecklistStepId.TRACKERS_REST_CALIBRATION]:
|
||||
'tracking_checklist-TRACKERS_REST_CALIBRATION',
|
||||
[TrackingChecklistStepId.FULL_RESET]: 'tracking_checklist-FULL_RESET',
|
||||
[TrackingChecklistStepId.VRCHAT_SETTINGS]: 'tracking_checklist-VRCHAT_SETTINGS',
|
||||
[TrackingChecklistStepId.STEAMVR_DISCONNECTED]:
|
||||
'tracking_checklist-STEAMVR_DISCONNECTED',
|
||||
[TrackingChecklistStepId.UNASSIGNED_HMD]: 'tracking_checklist-UNASSIGNED_HMD',
|
||||
[TrackingChecklistStepId.TRACKER_ERROR]: 'tracking_checklist-TRACKER_ERROR',
|
||||
[TrackingChecklistStepId.NETWORK_PROFILE_PUBLIC]:
|
||||
'tracking_checklist-NETWORK_PROFILE_PUBLIC',
|
||||
[TrackingChecklistStepId.MOUNTING_CALIBRATION]:
|
||||
'tracking_checklist-MOUNTING_CALIBRATION',
|
||||
[TrackingChecklistStepId.FEET_MOUNTING_CALIBRATION]:
|
||||
'tracking_checklist-FEET_MOUNTING_CALIBRATION',
|
||||
[TrackingChecklistStepId.STAY_ALIGNED_CONFIGURED]:
|
||||
'tracking_checklist-STAY_ALIGNED_CONFIGURED',
|
||||
};
|
||||
|
||||
export type TrackingChecklistStepStatus =
|
||||
| 'complete'
|
||||
| 'skipped'
|
||||
| 'blocked'
|
||||
| 'invalid';
|
||||
export type TrackingChecklistStep = TrackingChecklistStepT & {
|
||||
status: TrackingChecklistStepStatus;
|
||||
firstRequired: boolean;
|
||||
};
|
||||
export type highlightedTrackers = {
|
||||
step: TrackingChecklistStep;
|
||||
trackers: Array<TrackerIdT>;
|
||||
};
|
||||
|
||||
const stepVisibility = ({ visibility, status, firstRequired }: TrackingChecklistStep) =>
|
||||
firstRequired ||
|
||||
visibility === TrackingChecklistStepVisibility.ALWAYS ||
|
||||
(visibility === TrackingChecklistStepVisibility.WHEN_INVALID && status != 'complete');
|
||||
|
||||
const createStep = (
|
||||
steps: TrackingChecklistStepT[],
|
||||
step: TrackingChecklistStepT,
|
||||
index: number
|
||||
): TrackingChecklistStep => {
|
||||
const previousSteps = steps.slice(0, index);
|
||||
const previousBlocked = previousSteps.some(
|
||||
({ valid, optional }) => !valid && !optional
|
||||
);
|
||||
|
||||
let status: TrackingChecklistStepStatus = 'complete';
|
||||
if (previousBlocked && !step.valid) status = 'blocked';
|
||||
if (!previousBlocked && !step.valid) status = 'invalid';
|
||||
|
||||
const firstRequiredIndex = steps.findIndex(
|
||||
(s, index) => !s.valid || (index === steps.length - 1 && !s.valid)
|
||||
);
|
||||
|
||||
const skipped =
|
||||
steps.find(
|
||||
(s, sIndex) =>
|
||||
(sIndex > index && s.valid && !s.optional) || sIndex === steps.length - 1
|
||||
) || index === steps.length - 1;
|
||||
if (!step.valid && step.optional && skipped) status = 'skipped';
|
||||
|
||||
return {
|
||||
...step,
|
||||
status,
|
||||
firstRequired:
|
||||
firstRequiredIndex === index ||
|
||||
(firstRequiredIndex === -1 && index === steps.length - 1 && !step.valid),
|
||||
pack: () => 0,
|
||||
};
|
||||
};
|
||||
|
||||
export type TrackingChecklistContext = ReturnType<typeof provideTrackingChecklist>;
|
||||
export type Steps = {
|
||||
steps: TrackingChecklistStepT[];
|
||||
visibleSteps: TrackingChecklistStep[];
|
||||
ignoredSteps: TrackingChecklistStepId[];
|
||||
};
|
||||
export function provideTrackingChecklist() {
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
const [steps, setSteps] = useState<Steps>({
|
||||
steps: [],
|
||||
visibleSteps: [],
|
||||
ignoredSteps: [],
|
||||
});
|
||||
|
||||
useRPCPacket(
|
||||
RpcMessage.TrackingChecklistResponse,
|
||||
(data: TrackingChecklistResponseT) => {
|
||||
const activeSteps = data.steps.filter(
|
||||
(step) => !data.ignoredSteps.includes(step.id) && step.enabled
|
||||
);
|
||||
setSteps({
|
||||
steps: data.steps,
|
||||
visibleSteps: activeSteps
|
||||
.map((step: TrackingChecklistStepT, index) =>
|
||||
createStep(activeSteps, step, index)
|
||||
)
|
||||
.filter(stepVisibility),
|
||||
ignoredSteps: data.ignoredSteps,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
sendRPCPacket(RpcMessage.TrackingChecklistRequest, new TrackingChecklistRequestT());
|
||||
}, []);
|
||||
|
||||
const firstRequired = useMemo(
|
||||
() =>
|
||||
steps.visibleSteps.find(
|
||||
(step) => !step.valid && step.status != 'blocked' && !step.optional
|
||||
),
|
||||
[steps]
|
||||
);
|
||||
|
||||
const highlightedTrackers: highlightedTrackers | undefined = useMemo(() => {
|
||||
if (!firstRequired || !firstRequired.extraData) return undefined;
|
||||
if ('trackersId' in firstRequired.extraData) {
|
||||
return { step: firstRequired, trackers: firstRequired.extraData.trackersId };
|
||||
}
|
||||
if ('trackerId' in firstRequired.extraData && firstRequired.extraData.trackerId) {
|
||||
return { step: firstRequired, trackers: [firstRequired.extraData.trackerId] };
|
||||
}
|
||||
return { step: firstRequired, trackers: [] };
|
||||
}, [firstRequired]);
|
||||
|
||||
const progress = useMemo(() => {
|
||||
const completeSteps = steps.visibleSteps.filter(
|
||||
(step) => step.status === 'complete' || step.status === 'skipped'
|
||||
);
|
||||
return Math.min(1, completeSteps.length / steps.visibleSteps.length);
|
||||
}, [steps]);
|
||||
|
||||
const completion: 'complete' | 'partial' | 'incomplete' = useMemo(() => {
|
||||
if (progress === 1 && steps.visibleSteps.find((step) => step.status === 'skipped'))
|
||||
return 'partial';
|
||||
return progress === 1 || steps.visibleSteps.length === 0
|
||||
? 'complete'
|
||||
: 'incomplete';
|
||||
}, [progress, steps]);
|
||||
|
||||
const warnings = useMemo(
|
||||
() => steps.visibleSteps.filter((step) => !step.valid),
|
||||
[steps]
|
||||
);
|
||||
|
||||
const ignoreStep = (step: TrackingChecklistStepId, ignore: boolean) => {
|
||||
const res = new IgnoreTrackingChecklistStepRequestT();
|
||||
res.stepId = step;
|
||||
res.ignore = ignore;
|
||||
sendRPCPacket(RpcMessage.IgnoreTrackingChecklistStepRequest, res);
|
||||
};
|
||||
|
||||
return {
|
||||
...steps,
|
||||
firstRequired,
|
||||
highlightedTrackers,
|
||||
progress,
|
||||
completion,
|
||||
warnings,
|
||||
ignoreStep,
|
||||
toggle: (step: TrackingChecklistStepId) =>
|
||||
ignoreStep(step, !steps.ignoredSteps.includes(step)),
|
||||
};
|
||||
}
|
||||
|
||||
export const TrackingChecklistContectC = createContext<TrackingChecklistContext>(
|
||||
undefined as never
|
||||
);
|
||||
|
||||
export function useTrackingChecklist() {
|
||||
const context = useContext(TrackingChecklistContectC);
|
||||
if (!context) {
|
||||
throw new Error('useTrackingChecklist must be within a TrackingChecklistProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -3,19 +3,19 @@ import { useWebsocketAPI } from './websocket-api';
|
||||
import {
|
||||
RpcMessage,
|
||||
VRCAvatarMeasurementType,
|
||||
VRCConfigSettingToggleMuteT,
|
||||
VRCConfigStateChangeResponseT,
|
||||
VRCConfigStateRequestT,
|
||||
VRCSpineMode,
|
||||
VRCTrackerModel,
|
||||
} from 'solarxr-protocol';
|
||||
import { useConfig } from './config';
|
||||
|
||||
type NonNull<T> = {
|
||||
[P in keyof T]: NonNullable<T[P]>;
|
||||
};
|
||||
|
||||
export type VRCConfigStateSupported = { isSupported: true } & NonNull<
|
||||
Pick<VRCConfigStateChangeResponseT, 'recommended' | 'state' | 'validity'>
|
||||
Pick<VRCConfigStateChangeResponseT, 'recommended' | 'state' | 'validity' | 'muted'>
|
||||
>;
|
||||
export type VRCConfigState = { isSupported: false } | VRCConfigStateSupported;
|
||||
|
||||
@@ -45,7 +45,6 @@ export const avatarMeasurementTypeTranslationMap: Record<
|
||||
|
||||
export function useVRCConfig() {
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
const { config, setConfig } = useConfig();
|
||||
const [state, setState] = useState<VRCConfigState | null>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -59,31 +58,20 @@ export function useVRCConfig() {
|
||||
}
|
||||
);
|
||||
|
||||
const mutedSettings = useMemo(() => {
|
||||
if (!state?.isSupported) return [];
|
||||
return Object.keys(state.validity).filter((k) =>
|
||||
config?.vrcMutedWarnings.includes(k)
|
||||
);
|
||||
}, [state, config]);
|
||||
|
||||
const invalidConfig = useMemo(() => {
|
||||
if (!state?.isSupported) return false;
|
||||
return Object.entries(state.validity)
|
||||
.filter(([k]) => !config?.vrcMutedWarnings.includes(k))
|
||||
.filter(([k]) => !state.muted.includes(k))
|
||||
.some(([, v]) => !v);
|
||||
}, [state, config]);
|
||||
}, [state]);
|
||||
|
||||
return {
|
||||
state,
|
||||
invalidConfig,
|
||||
mutedSettings,
|
||||
toggleMutedSettings: async (key: keyof VRCConfigStateSupported['validity']) => {
|
||||
if (!config) return;
|
||||
const index = config.vrcMutedWarnings.findIndex((v) => v === key);
|
||||
if (index === -1) config.vrcMutedWarnings.push(key);
|
||||
else config?.vrcMutedWarnings.splice(index, 1);
|
||||
await setConfig(config);
|
||||
console.log(config.vrcMutedWarnings);
|
||||
const req = new VRCConfigSettingToggleMuteT();
|
||||
req.key = key;
|
||||
sendRPCPacket(RpcMessage.VRCConfigSettingToggleMute, req);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,12 +63,11 @@ body {
|
||||
background: theme('colors.background.20');
|
||||
|
||||
--navbar-w: 101px;
|
||||
--widget-w: 274px;
|
||||
--topbar-h: 38px;
|
||||
|
||||
@screen mobile {
|
||||
--topbar-h: 44px;
|
||||
--navbar-h: 90px;
|
||||
--navbar-h: 73px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +90,7 @@ body {
|
||||
--accent-background-50: 46, 33, 69;
|
||||
|
||||
--success: 80, 232, 151;
|
||||
--warning: 216, 205, 55;
|
||||
--warning: 255, 225, 53;
|
||||
--critical: 223, 109, 140;
|
||||
--special: 164, 79, 237;
|
||||
--window-icon-stroke: 192, 161, 216;
|
||||
@@ -153,7 +152,7 @@ body {
|
||||
--accent-background-50: 19, 57, 19;
|
||||
|
||||
--success: 80, 232, 151;
|
||||
--warning: 216, 205, 55;
|
||||
--warning: 255, 225, 53;
|
||||
--critical: 223, 109, 140;
|
||||
--special: 54, 161, 54;
|
||||
--window-icon-stroke: 129, 213, 130;
|
||||
@@ -179,7 +178,7 @@ body {
|
||||
--accent-background-50: 57, 57, 19;
|
||||
|
||||
--success: 80, 232, 151;
|
||||
--warning: 216, 205, 55;
|
||||
--warning: 255, 225, 53;
|
||||
--critical: 223, 109, 140;
|
||||
--special: 161, 160, 54;
|
||||
--window-icon-stroke: 213, 212, 129;
|
||||
@@ -205,7 +204,7 @@ body {
|
||||
--accent-background-50: 57, 34, 19;
|
||||
|
||||
--success: 80, 232, 151;
|
||||
--warning: 216, 205, 55;
|
||||
--warning: 255, 225, 53;
|
||||
--critical: 223, 109, 140;
|
||||
--special: 161, 95, 54;
|
||||
--window-icon-stroke: 213, 162, 129;
|
||||
@@ -231,7 +230,7 @@ body {
|
||||
--accent-background-50: 57, 19, 19;
|
||||
|
||||
--success: 80, 232, 151;
|
||||
--warning: 216, 205, 55;
|
||||
--warning: 255, 225, 53;
|
||||
--critical: 223, 109, 140;
|
||||
--special: 161, 54, 54;
|
||||
--window-icon-stroke: 213, 129, 129;
|
||||
@@ -257,7 +256,7 @@ body {
|
||||
--accent-background-50: 39, 39, 39;
|
||||
|
||||
--success: 80, 232, 151;
|
||||
--warning: 216, 205, 55;
|
||||
--warning: 255, 225, 53;
|
||||
--critical: 223, 109, 140;
|
||||
--special: 121, 121, 121;
|
||||
--window-icon-stroke: 172, 172, 172;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ResetType } from 'solarxr-protocol';
|
||||
import { ResetResponseT, ResetStatus, ResetType } from 'solarxr-protocol';
|
||||
import Xylophone, { ValidNote } from './xylophone';
|
||||
|
||||
const tones: ValidNote[][] = [
|
||||
@@ -10,47 +10,46 @@ const tones: ValidNote[][] = [
|
||||
];
|
||||
|
||||
const xylophone = new Xylophone();
|
||||
const mew = createAudio('/sounds/mew.ogg');
|
||||
const resetSounds: Record<
|
||||
ResetType,
|
||||
{
|
||||
initial: HTMLAudioElement | null;
|
||||
tick: HTMLAudioElement[] | null;
|
||||
end: HTMLAudioElement;
|
||||
mew: HTMLAudioElement | null;
|
||||
}
|
||||
> = {
|
||||
[ResetType.Full]: {
|
||||
initial: createAudio('/sounds/full-reset/init-full-reset-with-tail.ogg'),
|
||||
tick: [
|
||||
createAudio('/sounds/full-reset/full-click-1.ogg'),
|
||||
createAudio('/sounds/full-reset/full-click-2.ogg'),
|
||||
createAudio('/sounds/full-reset/full-click-3.ogg'),
|
||||
],
|
||||
end: createAudio('/sounds/full-reset/end-full-reset-with-tail.ogg'),
|
||||
mew,
|
||||
},
|
||||
[ResetType.Yaw]: {
|
||||
initial: null,
|
||||
tick: null,
|
||||
end: createAudio('/sounds/yaw-reset/yaw-reset.ogg'),
|
||||
mew: null,
|
||||
},
|
||||
[ResetType.Mounting]: {
|
||||
initial: createAudio('/sounds/mounting-reset/init-mounting-reset-with-tail.ogg'),
|
||||
tick: [
|
||||
createAudio('/sounds/mounting-reset/mount-click-1.ogg'),
|
||||
createAudio('/sounds/mounting-reset/mount-click-2.ogg'),
|
||||
createAudio('/sounds/mounting-reset/mount-click-3.ogg'),
|
||||
],
|
||||
end: createAudio('/sounds/mounting-reset/end-mounting-reset-with-tail.ogg'),
|
||||
mew,
|
||||
},
|
||||
};
|
||||
|
||||
export async function playSoundOnResetEnded(resetType: ResetType, volume = 1) {
|
||||
switch (resetType) {
|
||||
case ResetType.Yaw: {
|
||||
xylophone.play({
|
||||
notes: ['C4'],
|
||||
offset: 0.15,
|
||||
type: 'custom',
|
||||
volume,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ResetType.Full: {
|
||||
xylophone.play({
|
||||
notes: ['E3', 'G3'],
|
||||
offset: 0.15,
|
||||
type: 'custom',
|
||||
volume,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ResetType.Mounting: {
|
||||
xylophone.play({
|
||||
notes: ['G3', 'B3', 'D4'],
|
||||
offset: 0.15,
|
||||
type: 'custom',
|
||||
volume,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function playSoundOnResetStarted(volume = 1) {
|
||||
await xylophone.play({
|
||||
notes: ['A4'],
|
||||
offset: 0.4,
|
||||
type: 'custom',
|
||||
volume,
|
||||
});
|
||||
}
|
||||
export const trackingPauseSound = createAudio('/sounds/tracking/pause.ogg');
|
||||
export const trackingPlaySound = createAudio('/sounds/tracking/play.ogg');
|
||||
|
||||
let lastTap = 0;
|
||||
export async function playTapSetupSound(volume = 1) {
|
||||
@@ -84,3 +83,58 @@ export async function playTapSetupSound(volume = 1) {
|
||||
lastTap = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function createAudio(path: string): HTMLAudioElement {
|
||||
const audio = new Audio(path);
|
||||
audio.preload = 'auto';
|
||||
audio.load();
|
||||
return audio;
|
||||
}
|
||||
|
||||
export function restartAndPlay(audio: HTMLAudioElement | null, volume: number) {
|
||||
if (!audio) return;
|
||||
try {
|
||||
audio.load(); // LINUX: Solves wierd bug where webkit would unload sounds wierdly and make the sounds not play anymore
|
||||
|
||||
audio.volume = Math.min(1, Math.pow(volume, Math.E) + 0.05);
|
||||
audio.currentTime = 0;
|
||||
const playPromise = audio.play();
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.catch((error) => {
|
||||
console.error('Audio playback failed:', error);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Audio error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export function handleResetSounds(
|
||||
volume: number,
|
||||
{ progress, status, resetType }: ResetResponseT
|
||||
) {
|
||||
if (!resetSounds) throw 'sounds not loaded';
|
||||
const sounds = resetSounds[resetType];
|
||||
|
||||
if (status === ResetStatus.STARTED) {
|
||||
if (progress === 0) {
|
||||
performance.mark('sound_start');
|
||||
restartAndPlay(sounds.initial, volume);
|
||||
}
|
||||
|
||||
if (sounds.tick) {
|
||||
const tickIndex = (progress / 1000) % sounds.tick.length;
|
||||
if (progress >= 1000 && sounds.tick[tickIndex]) {
|
||||
restartAndPlay(sounds.tick[tickIndex], volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (status === ResetStatus.FINISHED) {
|
||||
performance.mark('sound_end');
|
||||
console.log(performance.measure('sound', 'sound_start', 'sound_end'));
|
||||
|
||||
restartAndPlay(sounds.end, volume);
|
||||
restartAndPlay(sounds.mew, volume);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from 'solarxr-protocol';
|
||||
import { selectAtom } from 'jotai/utils';
|
||||
import { isEqual } from '@react-hookz/deep-equal';
|
||||
import { FEET_BODY_PARTS, FINGER_BODY_PARTS } from '@/hooks/body-parts';
|
||||
|
||||
export interface FlatDeviceTracker {
|
||||
device?: DeviceDataT;
|
||||
@@ -45,14 +46,18 @@ export const unassignedTrackersAtom = atom((get) => {
|
||||
return trackers.filter(({ tracker }) => tracker.info?.bodyPart === BodyPart.NONE);
|
||||
});
|
||||
|
||||
export const connectedIMUTrackersAtom = atom((get) => {
|
||||
export const connectedTrackersAtom = atom((get) => {
|
||||
const trackers = get(flatTrackersAtom);
|
||||
return trackers.filter(
|
||||
({ tracker }) =>
|
||||
tracker.status !== TrackerStatus.DISCONNECTED && tracker.info?.isImu
|
||||
({ tracker }) => tracker.status !== TrackerStatus.DISCONNECTED
|
||||
);
|
||||
});
|
||||
|
||||
export const connectedIMUTrackersAtom = atom((get) => {
|
||||
const trackers = get(connectedTrackersAtom);
|
||||
return trackers.filter(({ tracker }) => tracker.info?.isImu);
|
||||
});
|
||||
|
||||
export const computedTrackersAtom = selectAtom(
|
||||
datafeedAtom,
|
||||
(datafeed) => datafeed.syntheticTrackers.map((tracker) => ({ tracker })),
|
||||
@@ -95,3 +100,16 @@ export const trackerFromIdAtom = ({
|
||||
(a) => a,
|
||||
isEqual
|
||||
);
|
||||
|
||||
export const feetAssignedTrackers = atom((get) =>
|
||||
get(assignedTrackersAtom).some(
|
||||
(t) => t.tracker.info?.bodyPart && FEET_BODY_PARTS.includes(t.tracker.info.bodyPart)
|
||||
)
|
||||
);
|
||||
|
||||
export const fingerAssignedTrackers = atom((get) =>
|
||||
get(assignedTrackersAtom).some(
|
||||
(t) =>
|
||||
t.tracker.info?.bodyPart && FINGER_BODY_PARTS.includes(t.tracker.info.bodyPart)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -188,11 +188,11 @@ export class BoneKind extends Bone {
|
||||
case BodyPart.NONE:
|
||||
throw 'Unexpected body part';
|
||||
case BodyPart.HEAD:
|
||||
return new Color('black');
|
||||
return new Color('gold');
|
||||
case BodyPart.NECK:
|
||||
return new Color('silver');
|
||||
case BodyPart.UPPER_CHEST:
|
||||
return new Color('blue');
|
||||
return new Color('chartreuse');
|
||||
case BodyPart.CHEST:
|
||||
return new Color('purple');
|
||||
case BodyPart.WAIST:
|
||||
@@ -201,13 +201,13 @@ export class BoneKind extends Bone {
|
||||
return new Color('orange');
|
||||
case BodyPart.LEFT_UPPER_LEG:
|
||||
case BodyPart.RIGHT_UPPER_LEG:
|
||||
return new Color('blue');
|
||||
return new Color('chartreuse');
|
||||
case BodyPart.LEFT_LOWER_LEG:
|
||||
case BodyPart.RIGHT_LOWER_LEG:
|
||||
return new Color('teal');
|
||||
case BodyPart.LEFT_FOOT:
|
||||
case BodyPart.RIGHT_FOOT:
|
||||
return new Color('#00ffcc');
|
||||
return new Color('gold');
|
||||
case BodyPart.LEFT_LOWER_ARM:
|
||||
case BodyPart.RIGHT_LOWER_ARM:
|
||||
return new Color('red');
|
||||
|
||||
@@ -3,6 +3,7 @@ import forms from '@tailwindcss/forms';
|
||||
import typography from '@tailwindcss/typography';
|
||||
import gradient from 'tailwind-gradient-mask-image';
|
||||
import type { Config } from 'tailwindcss';
|
||||
import { transform } from 'typescript';
|
||||
|
||||
const colors = {
|
||||
'blue-gray': {
|
||||
@@ -172,6 +173,7 @@ const config = {
|
||||
nsm: { raw: 'not (min-width: 900px)' },
|
||||
sm: '900px',
|
||||
md: '1100px',
|
||||
nmd: { raw: 'not (min-width: 1100px)' },
|
||||
'md-max': { raw: 'not (min-width: 1100px)' },
|
||||
lg: '1300px',
|
||||
xl: '1600px',
|
||||
@@ -228,6 +230,54 @@ const config = {
|
||||
'animation-timing-function': 'cubic-bezier(0.8, 0, 1, 1)',
|
||||
},
|
||||
},
|
||||
'timer-tick': {
|
||||
"0%, 40%": {
|
||||
transform: 'scale(1)',
|
||||
},
|
||||
"20%": {
|
||||
transform: 'scale(1.3)',
|
||||
},
|
||||
},
|
||||
'spin-ccw': {
|
||||
'0%': {
|
||||
transform: 'rotate(0deg)',
|
||||
},
|
||||
'100%': {
|
||||
transform: 'rotate(-360deg)',
|
||||
},
|
||||
},
|
||||
skiing: {
|
||||
'0%, 100%': {
|
||||
transform: 'rotate(0deg) translateX(0%) translateY(0%)',
|
||||
},
|
||||
'10%': {
|
||||
transform: 'rotate(12deg) translateX(-5%) translateY(5%)',
|
||||
},
|
||||
'20%': {
|
||||
transform: 'rotate(10deg) translateX(0%) translateY(0%)',
|
||||
},
|
||||
'30%': {
|
||||
transform: 'rotate(12deg) translateX(5%) translateY(-5%)',
|
||||
},
|
||||
'40%': {
|
||||
transform: 'rotate(10deg) translateX(0%) translateY(0%)',
|
||||
},
|
||||
'50%': {
|
||||
transform: 'rotate(12deg) translateX(-5%) translateY(5%)',
|
||||
},
|
||||
'60%': {
|
||||
transform: 'rotate(10deg) translateX(0%) translateY(0%)',
|
||||
},
|
||||
'70%': {
|
||||
transform: 'rotate(12deg) translateX(5%) translateY(-5%)',
|
||||
},
|
||||
'80%': {
|
||||
transform: 'rotate(10deg) translateX(0%) translateY(0%)',
|
||||
},
|
||||
'90%': {
|
||||
transform: 'rotate(10deg) translateX(-5%) translateY(5%)',
|
||||
},
|
||||
},
|
||||
},
|
||||
backgroundImage: {
|
||||
slime: `linear-gradient(135deg, ${colors.purple[100]} 50%, ${colors['blue-gray'][700]} 50% 100%)`,
|
||||
@@ -240,6 +290,11 @@ const config = {
|
||||
'trans-flag': `linear-gradient(135deg, ${colors['trans-blue'][800]} 40%, ${colors['trans-blue'][700]} 40% 70%, ${colors['trans-blue'][600]} 70% 100%)`,
|
||||
'asexual-flag': `linear-gradient(135deg, ${colors['asexual'][100]} 30%, ${colors['asexual'][200]} 30% 50%, ${colors['asexual'][300]} 50% 70%, ${colors['asexual'][400]} 70% 100%)`,
|
||||
},
|
||||
animation: {
|
||||
'spin-ccw': 'spin-ccw 1s linear infinite',
|
||||
'timer-tick': 'timer-tick 1s linear infinite',
|
||||
skiing: 'skiing 1s linear infinite',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
checked: 'checked=true',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"lib": ["dom", "dom.iterable", "ES2023"],
|
||||
"target": "es2023",
|
||||
"lib": ["dom", "dom.iterable", "es2023"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
|
||||
3777
pnpm-lock.yaml
generated
3777
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
||||
package dev.slimevr
|
||||
|
||||
data class NetworkInfo(
|
||||
val name: String?,
|
||||
val description: String?,
|
||||
val category: NetworkCategory?,
|
||||
val connectivity: Set<ConnectivityFlags>?,
|
||||
val connected: Boolean?,
|
||||
)
|
||||
|
||||
/**
|
||||
* @see <a href="https://learn.microsoft.com/en-us/windows/win32/api/netlistmgr/ne-netlistmgr-nlm_network_category">NLM_NETWORK_CATEGORY enumeration (netlistmgr.h)</a>
|
||||
*/
|
||||
enum class NetworkCategory(val value: Int) {
|
||||
PUBLIC(0),
|
||||
PRIVATE(1),
|
||||
DOMAIN_AUTHENTICATED(2),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromInt(value: Int) = values().find { it.value == value }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see <a href="https://learn.microsoft.com/en-us/windows/win32/api/netlistmgr/ne-netlistmgr-nlm_connectivity">NLM_CONNECTIVITY enumeration (netlistmgr.h)</a>
|
||||
*/
|
||||
enum class ConnectivityFlags(val value: Int) {
|
||||
DISCONNECTED(0),
|
||||
IPV4_NOTRAFFIC(0x1),
|
||||
IPV6_NOTRAFFIC(0x2),
|
||||
IPV4_SUBNET(0x10),
|
||||
IPV4_LOCALNETWORK(0x20),
|
||||
IPV4_INTERNET(0x40),
|
||||
IPV6_SUBNET(0x100),
|
||||
IPV6_LOCALNETWORK(0x200),
|
||||
IPV6_INTERNET(0x400),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromInt(value: Int): Set<ConnectivityFlags> = if (value == 0) {
|
||||
setOf(DISCONNECTED)
|
||||
} else {
|
||||
values().filter { it != DISCONNECTED && (value and it.value) != 0 }.toSet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class NetworkProfileChecker {
|
||||
abstract val isSupported: Boolean
|
||||
abstract val publicNetworks: List<NetworkInfo>
|
||||
}
|
||||
|
||||
class NetworkProfileCheckerStub : NetworkProfileChecker() {
|
||||
override val isSupported: Boolean
|
||||
get() = false
|
||||
override val publicNetworks: List<NetworkInfo>
|
||||
get() = listOf()
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import dev.slimevr.posestreamer.BVHRecorder
|
||||
import dev.slimevr.protocol.ProtocolAPI
|
||||
import dev.slimevr.protocol.rpc.settings.RPCSettingsHandler
|
||||
import dev.slimevr.reset.ResetHandler
|
||||
import dev.slimevr.reset.ResetTimerManager
|
||||
import dev.slimevr.reset.resetTimer
|
||||
import dev.slimevr.serial.ProvisioningHandler
|
||||
import dev.slimevr.serial.SerialHandler
|
||||
import dev.slimevr.serial.SerialHandlerStub
|
||||
@@ -28,6 +30,7 @@ import dev.slimevr.tracking.processor.HumanPoseManager
|
||||
import dev.slimevr.tracking.processor.skeleton.HumanSkeleton
|
||||
import dev.slimevr.tracking.trackers.*
|
||||
import dev.slimevr.tracking.trackers.udp.TrackersUDPServer
|
||||
import dev.slimevr.trackingchecklist.TrackingChecklistManager
|
||||
import dev.slimevr.util.ann.VRServerThread
|
||||
import dev.slimevr.websocketapi.WebSocketVRBridge
|
||||
import io.eiren.util.ann.ThreadSafe
|
||||
@@ -55,6 +58,7 @@ class VRServer @JvmOverloads constructor(
|
||||
serialHandlerProvider: (VRServer) -> SerialHandler = { _ -> SerialHandlerStub() },
|
||||
flashingHandlerProvider: (VRServer) -> SerialFlashingHandler? = { _ -> null },
|
||||
vrcConfigHandlerProvider: (VRServer) -> VRCConfigHandler = { _ -> VRCConfigHandlerStub() },
|
||||
networkProfileProvider: (VRServer) -> NetworkProfileChecker = { _ -> NetworkProfileCheckerStub() },
|
||||
acquireMulticastLock: () -> Any? = { null },
|
||||
@JvmField val configManager: ConfigManager,
|
||||
) : Thread("VRServer") {
|
||||
@@ -99,6 +103,7 @@ class VRServer @JvmOverloads constructor(
|
||||
@JvmField
|
||||
val protocolAPI: ProtocolAPI
|
||||
private val timer = Timer()
|
||||
private val resetTimerManager = ResetTimerManager()
|
||||
val fpsTimer = NanoTimer()
|
||||
|
||||
@JvmField
|
||||
@@ -113,6 +118,10 @@ class VRServer @JvmOverloads constructor(
|
||||
@JvmField
|
||||
val handshakeHandler = HandshakeHandler()
|
||||
|
||||
val trackingChecklistManager: TrackingChecklistManager
|
||||
|
||||
val networkProfileChecker: NetworkProfileChecker
|
||||
|
||||
init {
|
||||
// UwU
|
||||
deviceManager = DeviceManager(this)
|
||||
@@ -126,6 +135,8 @@ class VRServer @JvmOverloads constructor(
|
||||
autoBoneHandler = AutoBoneHandler(this)
|
||||
firmwareUpdateHandler = FirmwareUpdateHandler(this)
|
||||
vrcConfigManager = VRChatConfigManager(this, vrcConfigHandlerProvider(this))
|
||||
networkProfileChecker = networkProfileProvider(this)
|
||||
trackingChecklistManager = TrackingChecklistManager(this)
|
||||
protocolAPI = ProtocolAPI(this)
|
||||
val computedTrackers = humanPoseManager.computedTrackers
|
||||
|
||||
@@ -164,6 +175,7 @@ class VRServer @JvmOverloads constructor(
|
||||
for (tracker in computedTrackers) {
|
||||
registerTracker(tracker)
|
||||
}
|
||||
|
||||
instance = this
|
||||
}
|
||||
|
||||
@@ -300,7 +312,7 @@ class VRServer @JvmOverloads constructor(
|
||||
queueTask { humanPoseManager.resetTrackersYaw(resetSourceName, bodyParts) }
|
||||
}
|
||||
|
||||
fun resetTrackersMounting(resetSourceName: String?, bodyParts: List<Int> = TrackerUtils.allBodyPartsButFingers) {
|
||||
fun resetTrackersMounting(resetSourceName: String?, bodyParts: List<Int>? = null) {
|
||||
queueTask { humanPoseManager.resetTrackersMounting(resetSourceName, bodyParts) }
|
||||
}
|
||||
|
||||
@@ -330,40 +342,52 @@ class VRServer @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun scheduleResetTrackersFull(resetSourceName: String?, delay: Long) {
|
||||
if (delay > 0) {
|
||||
resetHandler.sendStarted(ResetType.Full)
|
||||
}
|
||||
timer.schedule(delay) {
|
||||
fun scheduleResetTrackersFull(resetSourceName: String?, delay: Long, bodyParts: List<Int> = ArrayList()) {
|
||||
resetTimer(
|
||||
resetTimerManager,
|
||||
delay,
|
||||
onTick = { progress ->
|
||||
resetHandler.sendStarted(ResetType.Full, bodyParts, progress, delay.toInt())
|
||||
},
|
||||
onComplete = {
|
||||
queueTask {
|
||||
humanPoseManager.resetTrackersFull(resetSourceName)
|
||||
resetHandler.sendFinished(ResetType.Full)
|
||||
}
|
||||
humanPoseManager.resetTrackersFull(resetSourceName, bodyParts)
|
||||
resetHandler.sendFinished(ResetType.Full, bodyParts, delay.toInt())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun scheduleResetTrackersYaw(resetSourceName: String?, delay: Long) {
|
||||
if (delay > 0) {
|
||||
resetHandler.sendStarted(ResetType.Yaw)
|
||||
}
|
||||
timer.schedule(delay) {
|
||||
fun scheduleResetTrackersYaw(resetSourceName: String?, delay: Long, bodyParts: List<Int> = TrackerUtils.allBodyPartsButFingers) {
|
||||
resetTimer(
|
||||
resetTimerManager,
|
||||
delay,
|
||||
onTick = { progress ->
|
||||
resetHandler.sendStarted(ResetType.Yaw, bodyParts, progress, delay.toInt())
|
||||
},
|
||||
onComplete = {
|
||||
queueTask {
|
||||
humanPoseManager.resetTrackersYaw(resetSourceName)
|
||||
resetHandler.sendFinished(ResetType.Yaw)
|
||||
}
|
||||
humanPoseManager.resetTrackersYaw(resetSourceName, bodyParts)
|
||||
resetHandler.sendFinished(ResetType.Yaw, bodyParts, delay.toInt())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun scheduleResetTrackersMounting(resetSourceName: String?, delay: Long) {
|
||||
if (delay > 0) {
|
||||
resetHandler.sendStarted(ResetType.Mounting)
|
||||
}
|
||||
timer.schedule(delay) {
|
||||
fun scheduleResetTrackersMounting(resetSourceName: String?, delay: Long, bodyParts: List<Int>? = null) {
|
||||
resetTimer(
|
||||
resetTimerManager,
|
||||
delay,
|
||||
onTick = { progress ->
|
||||
resetHandler.sendStarted(ResetType.Mounting, bodyParts, progress, delay.toInt())
|
||||
},
|
||||
onComplete = {
|
||||
queueTask {
|
||||
humanPoseManager.resetTrackersMounting(resetSourceName)
|
||||
resetHandler.sendFinished(ResetType.Mounting)
|
||||
}
|
||||
humanPoseManager.resetTrackersMounting(resetSourceName, bodyParts)
|
||||
resetHandler.sendFinished(ResetType.Mounting, bodyParts, delay.toInt())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun scheduleSetPauseTracking(pauseTracking: Boolean, sourceName: String?, delay: Long) {
|
||||
|
||||
@@ -53,4 +53,6 @@ interface ISteamVRBridge : Bridge {
|
||||
|
||||
fun getAutomaticSharedTrackers(): Boolean
|
||||
fun setAutomaticSharedTrackers(value: Boolean)
|
||||
|
||||
fun getBridgeConfigKey(): String
|
||||
}
|
||||
|
||||
@@ -29,6 +29,24 @@ enum class ArmsResetModes(val id: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
enum class MountingMethods(val id: Int) {
|
||||
MANUAL(0),
|
||||
AUTOMATIC(1),
|
||||
;
|
||||
|
||||
companion object {
|
||||
val values = MountingMethods.entries.toTypedArray()
|
||||
|
||||
@JvmStatic
|
||||
fun fromId(id: Int): MountingMethods? {
|
||||
for (filter in values) {
|
||||
if (filter.id == id) return filter
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ResetsConfig {
|
||||
|
||||
// Always reset mounting for feet
|
||||
@@ -46,6 +64,12 @@ class ResetsConfig {
|
||||
// Reset the HMD's pitch upon full reset
|
||||
var resetHmdPitch = false
|
||||
|
||||
var lastMountingMethod = MountingMethods.AUTOMATIC
|
||||
|
||||
var yawResetDelay = 0.0f
|
||||
var fullResetDelay = 3.0f
|
||||
var mountingResetDelay = 3.0f
|
||||
|
||||
fun updateTrackersResetsSettings() {
|
||||
for (t in VRServer.instance.allTrackers) {
|
||||
t.resetsHandler.readResetConfig(this)
|
||||
|
||||
@@ -14,20 +14,16 @@ class TapDetectionConfig {
|
||||
var mountingResetEnabled = true
|
||||
var setupMode = false
|
||||
var yawResetTaps = 2
|
||||
// clamp to 2-3 to prevent errors
|
||||
set(yawResetTaps) {
|
||||
field = FastMath.clamp(yawResetTaps.toFloat(), 2f, 10f).toInt()
|
||||
field = yawResetTaps
|
||||
field = yawResetTaps.coerceIn(2, 10)
|
||||
}
|
||||
var fullResetTaps = 3
|
||||
set(fullResetTaps) {
|
||||
field = FastMath.clamp(fullResetTaps.toFloat(), 2f, 10f).toInt()
|
||||
field = fullResetTaps
|
||||
field = fullResetTaps.coerceIn(2, 10)
|
||||
}
|
||||
var mountingResetTaps = 3
|
||||
set(mountingResetTaps) {
|
||||
field = FastMath.clamp(mountingResetTaps.toFloat(), 2f, 10f).toInt()
|
||||
field = mountingResetTaps
|
||||
field = mountingResetTaps.coerceIn(2, 10)
|
||||
}
|
||||
var numberTrackersOverThreshold = 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package dev.slimevr.config
|
||||
|
||||
class TrackingChecklistConfig {
|
||||
val ignoredStepsIds: MutableList<Int> = mutableListOf()
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package dev.slimevr.config
|
||||
|
||||
class VRCConfig {
|
||||
// List of fields ignored in vrc warnings - @see VRCConfigValidity
|
||||
val mutedWarnings: MutableList<String> = mutableListOf()
|
||||
}
|
||||
@@ -54,6 +54,10 @@ class VRConfig {
|
||||
|
||||
val overlay: OverlayConfig = OverlayConfig()
|
||||
|
||||
val trackingChecklist: TrackingChecklistConfig = TrackingChecklistConfig()
|
||||
|
||||
val vrcConfig: VRCConfig = VRCConfig()
|
||||
|
||||
init {
|
||||
// Initialize default settings for OSC Router
|
||||
oscRouter.portIn = 9002
|
||||
@@ -104,7 +108,7 @@ class VRConfig {
|
||||
tracker.readConfig(config)
|
||||
if (tracker.isImu()) tracker.resetsHandler.readDriftCompensationConfig(driftCompensation)
|
||||
tracker.resetsHandler.readResetConfig(resetsConfig)
|
||||
if (tracker.needsReset) {
|
||||
if (tracker.allowReset) {
|
||||
tracker.saveMountingResetOrientation(config)
|
||||
}
|
||||
if (tracker.allowFiltering) {
|
||||
|
||||
@@ -78,11 +78,11 @@ data class VRCConfigValidity(
|
||||
val shoulderTrackingOk: Boolean,
|
||||
val shoulderWidthCompensationOk: Boolean,
|
||||
val userHeightOk: Boolean,
|
||||
val calibrationOk: Boolean,
|
||||
val calibrationRangeOk: Boolean,
|
||||
val calibrationVisualsOk: Boolean,
|
||||
val tackerModelOk: Boolean,
|
||||
val trackerModelOk: Boolean,
|
||||
val spineModeOk: Boolean,
|
||||
val avatarMeasurementOk: Boolean,
|
||||
val avatarMeasurementTypeOk: Boolean,
|
||||
)
|
||||
|
||||
abstract class VRCConfigHandler {
|
||||
@@ -98,13 +98,14 @@ class VRCConfigHandlerStub : VRCConfigHandler() {
|
||||
}
|
||||
|
||||
interface VRCConfigListener {
|
||||
fun onChange(validity: VRCConfigValidity, values: VRCConfigValues, recommended: VRCConfigRecommendedValues)
|
||||
fun onChange(validity: VRCConfigValidity, values: VRCConfigValues, recommended: VRCConfigRecommendedValues, muted: List<String>)
|
||||
}
|
||||
|
||||
class VRChatConfigManager(val server: VRServer, private val handler: VRCConfigHandler) {
|
||||
|
||||
private val listeners: MutableList<VRCConfigListener> = CopyOnWriteArrayList()
|
||||
var currentValues: VRCConfigValues? = null
|
||||
var currentValidity: VRCConfigValidity? = null
|
||||
|
||||
val isSupported: Boolean
|
||||
get() = handler.isSupported
|
||||
@@ -113,6 +114,31 @@ class VRChatConfigManager(val server: VRServer, private val handler: VRCConfigHa
|
||||
handler.initHandler(::onChange)
|
||||
}
|
||||
|
||||
fun toggleMuteWarning(key: String) {
|
||||
val keys = VRCConfigValidity::class.java.declaredFields.asSequence().map { p -> p.name }
|
||||
if (!keys.contains(key)) return
|
||||
|
||||
if (!server.configManager.vrConfig.vrcConfig.mutedWarnings.contains(key)) {
|
||||
server.configManager.vrConfig.vrcConfig.mutedWarnings.add(key)
|
||||
} else {
|
||||
server.configManager.vrConfig.vrcConfig.mutedWarnings.remove(key)
|
||||
}
|
||||
|
||||
server.configManager.saveConfig()
|
||||
|
||||
val recommended = recommendedValues()
|
||||
val validity = currentValidity ?: return
|
||||
val values = currentValues ?: return
|
||||
listeners.forEach {
|
||||
it.onChange(
|
||||
validity,
|
||||
values,
|
||||
recommended,
|
||||
server.configManager.vrConfig.vrcConfig.mutedWarnings,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* shoulderTrackingDisabled should be true if:
|
||||
* The user isn't tracking their whole arms from their controllers:
|
||||
@@ -160,20 +186,28 @@ class VRChatConfigManager(val server: VRServer, private val handler: VRCConfigHa
|
||||
legacyModeOk = values.legacyMode == recommended.legacyMode,
|
||||
shoulderTrackingOk = values.shoulderTrackingDisabled == recommended.shoulderTrackingDisabled,
|
||||
spineModeOk = recommended.spineMode.contains(values.spineMode),
|
||||
tackerModelOk = values.trackerModel == recommended.trackerModel,
|
||||
calibrationOk = abs(values.calibrationRange - recommended.calibrationRange) < 0.1,
|
||||
trackerModelOk = values.trackerModel == recommended.trackerModel,
|
||||
calibrationRangeOk = abs(values.calibrationRange - recommended.calibrationRange) < 0.1,
|
||||
userHeightOk = abs(server.humanPoseManager.realUserHeight - values.userHeight) < 0.1,
|
||||
calibrationVisualsOk = values.calibrationVisuals == recommended.calibrationVisuals,
|
||||
avatarMeasurementOk = values.avatarMeasurementType == recommended.avatarMeasurementType,
|
||||
avatarMeasurementTypeOk = values.avatarMeasurementType == recommended.avatarMeasurementType,
|
||||
shoulderWidthCompensationOk = values.shoulderWidthCompensation == recommended.shoulderWidthCompensation,
|
||||
)
|
||||
|
||||
fun forceUpdate() {
|
||||
val values = currentValues
|
||||
if (values != null) {
|
||||
this.onChange(values)
|
||||
}
|
||||
}
|
||||
|
||||
fun onChange(values: VRCConfigValues) {
|
||||
val recommended = recommendedValues()
|
||||
val validity = checkValidity(values, recommended)
|
||||
currentValidity = validity
|
||||
currentValues = values
|
||||
listeners.forEach {
|
||||
it.onChange(validity, values, recommended)
|
||||
it.onChange(validity, values, recommended, server.configManager.vrConfig.vrcConfig.mutedWarnings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,7 +290,7 @@ class VMCHandler(
|
||||
userEditable = true,
|
||||
isComputed = position != null,
|
||||
usesTimeout = true,
|
||||
needsReset = position != null,
|
||||
allowReset = position != null,
|
||||
)
|
||||
trackerDevice!!.trackers[trackerDevice!!.trackers.size] = tracker
|
||||
byTrackerNameTracker[name] = tracker
|
||||
|
||||
@@ -275,7 +275,7 @@ class VRCOSCHandler(
|
||||
hasPosition = true,
|
||||
userEditable = true,
|
||||
isComputed = true,
|
||||
needsReset = trackerPosition != TrackerPosition.HEAD,
|
||||
allowReset = trackerPosition != TrackerPosition.HEAD,
|
||||
usesTimeout = true,
|
||||
)
|
||||
vrsystemTrackersDevice!!.trackers[trackerPosition.ordinal] = tracker
|
||||
@@ -368,7 +368,7 @@ class VRCOSCHandler(
|
||||
hasPosition = true,
|
||||
userEditable = true,
|
||||
isComputed = true,
|
||||
needsReset = true,
|
||||
allowReset = true,
|
||||
usesTimeout = true,
|
||||
)
|
||||
oscTrackersDevice!!.trackers[trackerId] = tracker
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user