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",
|
||||
]
|
||||
|
||||
64
TRADEMARK.md
64
TRADEMARK.md
@@ -1,33 +1,33 @@
|
||||
## SlimeVR is a trademark or a registered trademark of SlimeVR B.V.
|
||||
|
||||
**Usage of SlimeVR software, hardware, or other intellectual property in this or other repositories does not grant you the right to use SlimeVR trademark as your own.**
|
||||
|
||||
The purpose of a trademark is to remove uncertainty for users and customers regarding the product's manufacturer or endorsement. You're not allowed to market your product using SlimeVR name, and your usage of the name should be only factual and descriptive. For example, calling original SlimeVR products SlimeVR or describing compatibility of other products or derivatives. This applies to all products, including software, and hardware including non-official Full-Body Trackers.
|
||||
|
||||
**Here are a few _acceptable_ uses of SlimeVR name when selling unofficial Slime trackers:**
|
||||
* SlimeVR-compatible trackers
|
||||
* Unofficial SlimeVR trackers / Non-official SlimeVR trackers
|
||||
* DIY SlimeVR trackers
|
||||
* Third-party SlimeVR Trackers
|
||||
* Custom SlimeVR-compatible trackers
|
||||
* < Your Brand > Slime Trackers
|
||||
* Using "SlimeVR" as a search tag
|
||||
|
||||
**_Unacceptable_ uses include, but are not limited to:**
|
||||
* SlimeVR store
|
||||
* Buy SlimeVR
|
||||
* SlimeVR Trackers
|
||||
* Original SlimeVR
|
||||
* Official SlimeVR
|
||||
* SlimeVR BMI270 (or any other IMU model along with SlimeVR name)
|
||||
* < Your brand > SlimeVR / < your brand > SlimeVR Trackers
|
||||
|
||||
Use of the SlimeVR name that can cause confusion is not allowed in any part of the listing, including, but not limited to: product title, product description, product metadata, site title, site name, site metadata, site texts, social media posts, or other advertisement.
|
||||
|
||||
Also, please ensure you use the correct spelling and capitalization: only **"SlimeVR" is acceptable**. Not "Slimevr", "slimevr", or "Slime VR". You're allowed to use the word "slime" as you wish, it's not a trademark.
|
||||
|
||||
Please understand that we have an obligation to reduce confusion for the customers, and we believe that our usage terms are generous compared to many other companies and products. This applies to all sellers or derivative products, we do not make exceptions.
|
||||
|
||||
---
|
||||
|
||||
## SlimeVR is a trademark or a registered trademark of SlimeVR B.V.
|
||||
|
||||
**Usage of SlimeVR software, hardware, or other intellectual property in this or other repositories does not grant you the right to use SlimeVR trademark as your own.**
|
||||
|
||||
The purpose of a trademark is to remove uncertainty for users and customers regarding the product's manufacturer or endorsement. You're not allowed to market your product using SlimeVR name, and your usage of the name should be only factual and descriptive. For example, calling original SlimeVR products SlimeVR or describing compatibility of other products or derivatives. This applies to all products, including software, and hardware including non-official Full-Body Trackers.
|
||||
|
||||
**Here are a few _acceptable_ uses of SlimeVR name when selling unofficial Slime trackers:**
|
||||
* SlimeVR-compatible trackers
|
||||
* Unofficial SlimeVR trackers / Non-official SlimeVR trackers
|
||||
* DIY SlimeVR trackers
|
||||
* Third-party SlimeVR Trackers
|
||||
* Custom SlimeVR-compatible trackers
|
||||
* < Your Brand > Slime Trackers
|
||||
* Using "SlimeVR" as a search tag
|
||||
|
||||
**_Unacceptable_ uses include, but are not limited to:**
|
||||
* SlimeVR store
|
||||
* Buy SlimeVR
|
||||
* SlimeVR Trackers
|
||||
* Original SlimeVR
|
||||
* Official SlimeVR
|
||||
* SlimeVR BMI270 (or any other IMU model along with SlimeVR name)
|
||||
* < Your brand > SlimeVR / < your brand > SlimeVR Trackers
|
||||
|
||||
Use of the SlimeVR name that can cause confusion is not allowed in any part of the listing, including, but not limited to: product title, product description, product metadata, site title, site name, site metadata, site texts, social media posts, or other advertisement.
|
||||
|
||||
Also, please ensure you use the correct spelling and capitalization: only **"SlimeVR" is acceptable**. Not "Slimevr", "slimevr", or "Slime VR". You're allowed to use the word "slime" as you wish, it's not a trademark.
|
||||
|
||||
Please understand that we have an obligation to reduce confusion for the customers, and we believe that our usage terms are generous compared to many other companies and products. This applies to all sellers or derivative products, we do not make exceptions.
|
||||
|
||||
---
|
||||
|
||||
If you have any questions about SlimeVR trademark or copyrighted materials, you can reach out to us at *tm[at]slimevr.dev*.
|
||||
@@ -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": [
|
||||
@@ -52,4 +63,4 @@
|
||||
"opener:default",
|
||||
"log:default"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 style={{ gridArea: 't' }}>
|
||||
<TopBar />
|
||||
</div>
|
||||
<div style={{ gridArea: 's' }} 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'
|
||||
)}
|
||||
>
|
||||
{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 />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={classNames('main-layout w-full h-screen', full && 'full', {
|
||||
'checklist-ok': completion === 'complete',
|
||||
})}
|
||||
>
|
||||
<div style={{ gridArea: 't' }}>
|
||||
<TopBar />
|
||||
</div>
|
||||
<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-md',
|
||||
background && 'bg-background-70',
|
||||
{ 'rounded-t-none': !isMobile && full }
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{full && isMobile && completion !== 'complete' && (
|
||||
<TrackingChecklistMobile />
|
||||
)}
|
||||
{full && (
|
||||
<div style={{ gridArea: 'b' }}>
|
||||
<Toolbar />
|
||||
</div>
|
||||
)}
|
||||
{!isMobile && full && (
|
||||
<div style={{ gridArea: 's' }} className="mr-2">
|
||||
<Sidebar />
|
||||
</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}>
|
||||
{children}
|
||||
{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}>
|
||||
{children}
|
||||
{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>
|
||||
),
|
||||
document.body
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
|
||||
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,95 +31,121 @@ 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'
|
||||
)}
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
<div className="overflow-y-auto flex flex-col gap-3">
|
||||
{trackers.length === 0 && (
|
||||
<div className="flex px-5 pt-5 justify-center">
|
||||
<Typography variant="standard">
|
||||
{l10n.getString('home-no_trackers')}
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
{trackers.length === 0 && (
|
||||
<div className="flex px-5 pt-5 justify-center">
|
||||
<Typography variant="standard">
|
||||
{l10n.getString('home-no_trackers')}
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!config?.debug && 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
|
||||
key={index}
|
||||
tracker={tracker}
|
||||
device={device}
|
||||
onClick={() => sendToSettings(tracker)}
|
||||
smol
|
||||
showUpdates
|
||||
interactable
|
||||
warning={Object.values(statuses).some((status) =>
|
||||
trackerStatusRelated(tracker, status)
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{config?.debug && trackers.length > 0 && (
|
||||
<div className="px-2 pt-5 overflow-y-scroll overflow-x-auto">
|
||||
<TrackersTable
|
||||
flatTrackers={trackers}
|
||||
clickedTracker={(tracker) => sendToSettings(tracker)}
|
||||
{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
|
||||
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' && 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>
|
||||
)}
|
||||
</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;
|
||||
|
||||
@@ -64,7 +64,7 @@ export function SettingsPagePaneLayout({
|
||||
{...props}
|
||||
>
|
||||
<div className="flex mobile:absolute mobile:right-4">
|
||||
<div className=" w-10 h-10 bg-accent-background-40 flex justify-center items-center rounded-full fill-background-10">
|
||||
<div className="w-10 h-10 bg-accent-background-40 flex justify-center items-center rounded-full fill-background-10">
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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,16 +305,19 @@ export function InterfaceSettings() {
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid sm:grid-cols-2 pb-4">
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
control={control}
|
||||
outlined
|
||||
name="behavior.devmode"
|
||||
label={l10n.getString(
|
||||
'settings-general-interface-dev_mode-label'
|
||||
)}
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-2 pb-4 w-full">
|
||||
<div className="">
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
control={control}
|
||||
outlined
|
||||
name="behavior.devmode"
|
||||
label={l10n.getString(
|
||||
'settings-general-interface-dev_mode-label'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{config?.debug && <DeveloperModeWidget />}
|
||||
</div>
|
||||
|
||||
<Typography variant="section-title">
|
||||
|
||||
@@ -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={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">
|
||||
<Typography bold truncate>
|
||||
|
||||
<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,15 +36,36 @@ 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">
|
||||
<BodyPartIcon bodyPart={tracker.info?.bodyPart} />
|
||||
<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">
|
||||
@@ -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 (
|
||||
<div
|
||||
className={classNames(
|
||||
'py-1',
|
||||
rounded === 'left' && 'pl-3',
|
||||
rounded === 'right' && 'pr-3',
|
||||
'overflow-hidden'
|
||||
)}
|
||||
>
|
||||
<td className={classNames('py-2 group overflow-hidden', { hidden: !show })}>
|
||||
<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>
|
||||
</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({
|
||||
'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}
|
||||
<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,
|
||||
})}
|
||||
/>
|
||||
),
|
||||
})}
|
||||
|
||||
{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={
|
||||
<Typography color="primary" textAlign="text-center">
|
||||
{l10n.getString('tips-failed_webgl')}
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<SkeletonVisualizer onInit={onInit} />
|
||||
</ErrorBoundary>
|
||||
<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>
|
||||
</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();
|
||||
|
||||
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;
|
||||
}
|
||||
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 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) {
|
||||
queueTask {
|
||||
humanPoseManager.resetTrackersFull(resetSourceName)
|
||||
resetHandler.sendFinished(ResetType.Full)
|
||||
}
|
||||
}
|
||||
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, bodyParts)
|
||||
resetHandler.sendFinished(ResetType.Full, bodyParts, delay.toInt())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun scheduleResetTrackersYaw(resetSourceName: String?, delay: Long) {
|
||||
if (delay > 0) {
|
||||
resetHandler.sendStarted(ResetType.Yaw)
|
||||
}
|
||||
timer.schedule(delay) {
|
||||
queueTask {
|
||||
humanPoseManager.resetTrackersYaw(resetSourceName)
|
||||
resetHandler.sendFinished(ResetType.Yaw)
|
||||
}
|
||||
}
|
||||
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, bodyParts)
|
||||
resetHandler.sendFinished(ResetType.Yaw, bodyParts, delay.toInt())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun scheduleResetTrackersMounting(resetSourceName: String?, delay: Long) {
|
||||
if (delay > 0) {
|
||||
resetHandler.sendStarted(ResetType.Mounting)
|
||||
}
|
||||
timer.schedule(delay) {
|
||||
queueTask {
|
||||
humanPoseManager.resetTrackersMounting(resetSourceName)
|
||||
resetHandler.sendFinished(ResetType.Mounting)
|
||||
}
|
||||
}
|
||||
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, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user