diff --git a/gui/package.json b/gui/package.json index 00bd7da5ca..f3a0c160d4 100644 --- a/gui/package.json +++ b/gui/package.json @@ -90,7 +90,8 @@ "spdx-satisfies": "^5.0.1", "tailwind-gradient-mask-image": "^1.2.0", "tailwindcss": "^3.4.13", + "ts-xor": "^1.3.0", "vite": "^5.4.8", "typescript-eslint": "^8.8.0" } -} \ No newline at end of file +} diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index b9a1816a34..9badaaf3c0 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -610,12 +610,14 @@ settings-osc-router-network-address-placeholder = IPV4 address ## OSC VRChat settings settings-osc-vrchat = VRChat OSC Trackers # This cares about multilines -settings-osc-vrchat-description-v1 = +settings-osc-vrchat-description-v2 = Change settings specific to the OSC Trackers standard used for sending tracking data to applications without SteamVR (ex. Quest standalone). +# This cares about multilines +settings-osc-vrchat-description-guide = Make sure to enable OSC in VRChat via the Action Menu under OSC > Enabled. - To allow receiving HMD and controller data from VRChat, go in your main menu's - settings under Tracking & IK > Allow Sending Head and Wrist VR Tracking OSC Data. + + To allow receiving HMD and controller data from VRChat, go in your main menu's settings under Tracking & IK > Allow Sending Head and Wrist VR Tracking OSC Data. settings-osc-vrchat-enable = Enable settings-osc-vrchat-enable-description = Toggle the sending and receiving of data. settings-osc-vrchat-enable-label = Enable @@ -807,6 +809,79 @@ onboarding-assignment_tutorial-second_step-v2 = 2. Attach the strap to your trac onboarding-assignment_tutorial-second_step-continuation-v2 = The velcro side for the extension should be facing up like the following image: onboarding-assignment_tutorial-done = I put stickers and straps! +## Usage reason choose +onboarding-usage-choose = What are you gonna use SlimeVR for? +onboarding-usage-choose-description = What are you gonna use SlimeVR for? +onboarding-usage-choose-option-title = { $mode -> + *[VR] Virtual Reality + [VTUBING] VTuber + [MOCAP] Motion Capture +} +onboarding-usage-choose-option-label = { $mode -> + *[VR] For use with games and applications that use a headset + [VTUBING] For use with VTubing programs that use the VMC protocol + [MOCAP] For recording a whole body with precise tracking. +} +onboarding-usage-choose-option-description = { $mode -> + *[VR] Users of SteamVR or VR programs that use OSC can select this option to get right to it. + [VTUBING] VTubing programs work with SlimeVR using VMC (Virtual Motion Capture), this is what to pick for that! + [MOCAP] Many 3D programs can record live mocap for use in animation, and BVH recording is also supported directly in the app. +} + +## VR usage choose +onboarding-usage-vr-choose = Choose your VR setup +onboarding-usage-vr-choose-description = There are different ways to connect SlimeVR to your virtual reality setup! You can decide which you will use here. +onboarding-usage-vr-choose-steamvr = I use SteamVR +onboarding-usage-vr-choose-steamvr-label = For PCVR +# uses multiline +onboarding-usage-vr-choose-steamvr-description = + SlimeVR emulates SteamVR trackers using the rotational data of the trackers and a human skeleton model, so SteamVR games and programs can use it for full body tracking. + + SteamVR must be installed and a headset or positional head tracker connected to the SlimeVR server to use this method. +onboarding-usage-vr-choose-steamvr-warning = The SteamVR driver is currently not connected, please turn on SteamVR or check the docs for more info. +onboarding-usage-vr-choose-standalone = I use standalone +onboarding-usage-vr-choose-standalone-label = For VRChat Quest/Pico users +onboarding-usage-vr-choose-standalone-description = + Standalone use connects through OSC instead of SteamVR to provide full body tracking with SlimeVR. + Any PC that can run SlimeVR server can function like this, as well as phones, which are the recommended ways for best ergonomics. +onboarding-usage-vr-standalone-title = Setting up VRChat +onboarding-usage-vr-standalone-next = Done! + +## Mocap head usage choose +onboarding-usage-mocap-head_choose = What kind of head tracking do you want? +onboarding-usage-mocap-head_choose-description = You can use either a tracker or a headset for the head! + +onboarding-usage-mocap-head_choose-standalone = SlimeVR head tracker +onboarding-usage-mocap-head_choose-standalone-label = Use an IMU tracker for tracking position +onboarding-usage-mocap-head_choose-standalone-description = + This enables head tracking using a head mounted SlimeVR tracker. + + This is much less precise in the way that if you walk and return to your starting point, you won't be on the same place on the recording. +onboarding-usage-mocap-head_choose-standalone-button = Use IMU tracker + +onboarding-usage-mocap-head_choose-steamvr = SteamVR head tracking +onboarding-usage-mocap-head_choose-steamvr-label = Use an HMD or a positional tracker for precision +onboarding-usage-mocap-head_choose-steamvr-description = + Most accurate way to track the head, using true positional data as reference. + + This allows for the best quality motion capture recordings as well as movements that require both feet to leave the floor at the same time. +onboarding-usage-mocap-head_choose-steamvr-button = Use SteamVR + +## Mocap data mode choose +onboarding-usage-mocap-data_choose = What kind of data format to use? +onboarding-usage-mocap-data_choose-description = description + +onboarding-usage-mocap-data_choose-option-title = { $mode -> + *[BVH] BVH + [STEAMVR] SteamVR + [VMC] VMC +} +onboarding-usage-mocap-data_choose-option-label = { $mode -> + *[BVH] Natively supported on most animation programs + [STEAMVR] For programs that support OpenVR as a source of data + [VMC] Popular data protocol for VTubing +} + ## Tracker assignment setup onboarding-assign_trackers-back = Go Back to Wi-Fi Credentials onboarding-assign_trackers-title = Assign trackers diff --git a/gui/public/images/nighty-vr-sitting.webp b/gui/public/images/nighty-vr-sitting.webp new file mode 100644 index 0000000000..db7bb3c2c5 Binary files /dev/null and b/gui/public/images/nighty-vr-sitting.webp differ diff --git a/gui/public/images/usage-mocap.webp b/gui/public/images/usage-mocap.webp new file mode 100644 index 0000000000..baa0b2afbd Binary files /dev/null and b/gui/public/images/usage-mocap.webp differ diff --git a/gui/public/images/usage-vr.webp b/gui/public/images/usage-vr.webp new file mode 100644 index 0000000000..e85500c623 Binary files /dev/null and b/gui/public/images/usage-vr.webp differ diff --git a/gui/public/images/usage-vtuber.webp b/gui/public/images/usage-vtuber.webp new file mode 100644 index 0000000000..73456f597f Binary files /dev/null and b/gui/public/images/usage-vtuber.webp differ diff --git a/gui/public/images/vrslimes.webp b/gui/public/images/vrslimes.webp new file mode 100644 index 0000000000..e7ee9e94ad Binary files /dev/null and b/gui/public/images/vrslimes.webp differ diff --git a/gui/public/videos/vrchatosc.webm b/gui/public/videos/vrchatosc.webm new file mode 100644 index 0000000000..8a989e3406 Binary files /dev/null and b/gui/public/videos/vrchatosc.webm differ diff --git a/gui/src/App.tsx b/gui/src/App.tsx index e2bb4c4a6f..70fef69860 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -59,6 +59,14 @@ import { useDiscordPresence } from './hooks/discord-presence'; import { EmptyLayout } from './components/EmptyLayout'; import { AdvancedSettings } from './components/settings/pages/AdvancedSettings'; import { FirmwareUpdate } from './components/firmware-update/FirmwareUpdate'; +import { UsageChoose } from './components/onboarding/pages/usage-reason/UsageChoose'; +import { VRUsageChoose } from './components/onboarding/pages/usage-reason/VRUsageChoose'; +import { StandaloneUsageSetup } from './components/onboarding/pages/usage-reason/StandaloneUsageSetup'; +import { HeadTrackingChoose } from './components/onboarding/pages/usage-reason/HeadTrackingChoose'; +import { MocapDataChoose } from './components/onboarding/pages/usage-reason/MocapDataChoose'; +import { MocapVMCSetup } from './components/onboarding/pages/usage-reason/MocapVMCSetup'; +import { MocapBVHSetup } from './components/onboarding/pages/usage-reason/MocapBVHSetup'; +import { MocapSteamSetup } from './components/onboarding/pages/usage-reason/MocapSteamSetup'; export const GH_REPO = 'SlimeVR/SlimeVR-Server'; export const VersionContext = createContext(''); @@ -144,6 +152,21 @@ function Layout() { path="assign-tutorial" element={} /> + + + } /> + } /> + } /> + } /> + } + /> + } /> + } /> + } /> + + } /> } /> }> diff --git a/gui/src/components/commons/Button.tsx b/gui/src/components/commons/Button.tsx index eb66eaa7cc..18403efe0a 100644 --- a/gui/src/components/commons/Button.tsx +++ b/gui/src/components/commons/Button.tsx @@ -2,6 +2,7 @@ import classNames from 'classnames'; import React, { ReactNode, useMemo } from 'react'; import { NavLink } from 'react-router-dom'; import { LoaderIcon, SlimeState } from './icon/LoaderIcon'; +import { XOR } from 'ts-xor'; function ButtonContent({ loading, @@ -36,6 +37,25 @@ function ButtonContent({ ); } +type ButtonBaseParams = { + children?: ReactNode; + icon?: ReactNode; + variant: 'primary' | 'secondary' | 'tertiary' | 'quaternary'; + loading?: boolean; + rounded?: boolean; +} & Omit, 'onClick'>; + +type ButtonNavigateParams = { to: string; state?: any } & ButtonBaseParams; +type ButtonScriptParams = { + onClick?: React.MouseEventHandler; +} & ButtonBaseParams; +type ButtonSubmitParams = { type: 'submit' } & ButtonBaseParams; +type ButtonParams = XOR< + ButtonNavigateParams, + ButtonScriptParams, + ButtonSubmitParams +>; + export function Button({ children, variant, @@ -46,15 +66,7 @@ export function Button({ icon, rounded = false, ...props -}: { - children?: ReactNode; - icon?: ReactNode; - variant: 'primary' | 'secondary' | 'tertiary' | 'quaternary'; - to?: string; - loading?: boolean; - rounded?: boolean; - state?: any; -} & React.ButtonHTMLAttributes) { +}: ButtonParams) { const classes = useMemo(() => { const variantsMap = { primary: classNames({ diff --git a/gui/src/components/commons/PausableVideo.tsx b/gui/src/components/commons/PausableVideo.tsx new file mode 100644 index 0000000000..7ffac23a87 --- /dev/null +++ b/gui/src/components/commons/PausableVideo.tsx @@ -0,0 +1,70 @@ +import { useRef, useState } from 'react'; +import { PlayCircleIcon } from './icon/PlayIcon'; +import { useDebouncedEffect } from '@/hooks/timeout'; +import classNames from 'classnames'; + +export function PausableVideo({ + src, + poster, + restartOnPause = false, + autoPlay = false, +}: { + src?: string; + poster?: string; + restartOnPause?: boolean; + autoPlay?: boolean; +}) { + const videoRef = useRef(null); + const [paused, setPaused] = useState(!autoPlay); + const [atStart, setAtStart] = useState(true); + + function toggleVideo() { + if (!videoRef.current) return; + if (videoRef.current.paused) { + videoRef.current.play(); + } else { + videoRef.current.pause(); + if (restartOnPause) { + videoRef.current.currentTime = 0; + } + setAtStart(videoRef.current.currentTime === 0); + } + setPaused(videoRef.current.paused); + } + + useDebouncedEffect( + () => { + if (paused) videoRef.current?.pause(); + }, + [paused], + 250 + ); + + return ( + + ); +} diff --git a/gui/src/components/commons/Radio.tsx b/gui/src/components/commons/Radio.tsx index c5c2d9f9bc..d5f532a075 100644 --- a/gui/src/components/commons/Radio.tsx +++ b/gui/src/components/commons/Radio.tsx @@ -1,7 +1,7 @@ import classNames from 'classnames'; import { Control, Controller } from 'react-hook-form'; import { Typography } from './Typography'; -import { ReactNode } from 'react'; +import { ReactNode, useMemo } from 'react'; export function Radio({ control, @@ -12,6 +12,7 @@ export function Radio({ children, // input props disabled, + variant = 'secondary', ...props }: { control: Control; @@ -20,19 +21,36 @@ export function Radio({ value: string; description?: string | null; children?: ReactNode; + variant?: 'secondary' | 'none'; } & React.HTMLProps) { + const variantClasses = useMemo(() => { + const variantsMap = { + secondary: classNames({ + 'bg-background-60 hover:bg-background-50': !disabled, + 'bg-background-80': disabled, + }), + none: '', + }; + return variantsMap[variant]; + }, [variant, disabled]); + return ( (