mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
Compare commits
30 Commits
v19.0.0-rc
...
onboarding
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a088145963 | ||
|
|
75cf328aea | ||
|
|
c2fe9541dc | ||
|
|
9c9c5524a5 | ||
|
|
fa74a748ac | ||
|
|
b90da87602 | ||
|
|
40b2da34b2 | ||
|
|
da133c086f | ||
|
|
92b7ca7bda | ||
|
|
96fa13aea9 | ||
|
|
6a8786b241 | ||
|
|
8bdee03164 | ||
|
|
fa10e2d73a | ||
|
|
1385403817 | ||
|
|
5f60e59e51 | ||
|
|
c341534166 | ||
|
|
957e81c37a | ||
|
|
c0269155db | ||
|
|
945b8b392d | ||
|
|
0c9a016598 | ||
|
|
583e335fc7 | ||
|
|
986ef8d4b4 | ||
|
|
97fb9dd098 | ||
|
|
db80609379 | ||
|
|
bb5b9f3bfe | ||
|
|
085ba25559 | ||
|
|
682300e707 | ||
|
|
af7f13e8bd | ||
|
|
e530cb5327 | ||
|
|
6b8e4c961e |
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, <b>please turn on SteamVR</b> or check <docs>the docs for more info</docs>.
|
||||
onboarding-usage-vr-choose-standalone = I use standalone
|
||||
onboarding-usage-vr-choose-standalone-label = For VRChat Quest/Pico users
|
||||
onboarding-usage-vr-choose-standalone-description =
|
||||
Standalone use connects through OSC instead of SteamVR to provide full body tracking with SlimeVR.
|
||||
Any PC that can run SlimeVR server can function like this, as well as phones, which are the recommended ways for best ergonomics.
|
||||
onboarding-usage-vr-standalone-title = Setting up VRChat
|
||||
onboarding-usage-vr-standalone-next = Done!
|
||||
|
||||
## Mocap head usage choose
|
||||
onboarding-usage-mocap-head_choose = What kind of head tracking do you want?
|
||||
onboarding-usage-mocap-head_choose-description = You can use either a tracker or a headset for the head!
|
||||
|
||||
onboarding-usage-mocap-head_choose-standalone = SlimeVR head tracker
|
||||
onboarding-usage-mocap-head_choose-standalone-label = Use an IMU tracker for tracking position
|
||||
onboarding-usage-mocap-head_choose-standalone-description =
|
||||
This enables head tracking using a head mounted SlimeVR tracker.
|
||||
|
||||
This is much less precise in the way that if you walk and return to your starting point, you won't be on the same place on the recording.
|
||||
onboarding-usage-mocap-head_choose-standalone-button = Use IMU tracker
|
||||
|
||||
onboarding-usage-mocap-head_choose-steamvr = SteamVR head tracking
|
||||
onboarding-usage-mocap-head_choose-steamvr-label = Use an HMD or a positional tracker for precision
|
||||
onboarding-usage-mocap-head_choose-steamvr-description =
|
||||
Most accurate way to track the head, using true positional data as reference.
|
||||
|
||||
This allows for the best quality motion capture recordings as well as movements that require both feet to leave the floor at the same time.
|
||||
onboarding-usage-mocap-head_choose-steamvr-button = Use SteamVR
|
||||
|
||||
## Mocap data mode choose
|
||||
onboarding-usage-mocap-data_choose = What kind of data format to use?
|
||||
onboarding-usage-mocap-data_choose-description = description
|
||||
|
||||
onboarding-usage-mocap-data_choose-option-title = { $mode ->
|
||||
*[BVH] BVH
|
||||
[STEAMVR] SteamVR
|
||||
[VMC] VMC
|
||||
}
|
||||
onboarding-usage-mocap-data_choose-option-label = { $mode ->
|
||||
*[BVH] Natively supported on most animation programs
|
||||
[STEAMVR] For programs that support OpenVR as a source of data
|
||||
[VMC] Popular data protocol for VTubing
|
||||
}
|
||||
|
||||
## Tracker assignment setup
|
||||
onboarding-assign_trackers-back = Go Back to Wi-Fi Credentials
|
||||
onboarding-assign_trackers-title = Assign trackers
|
||||
|
||||
BIN
gui/public/images/nighty-vr-sitting.webp
Normal file
BIN
gui/public/images/nighty-vr-sitting.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
BIN
gui/public/images/usage-mocap.webp
Normal file
BIN
gui/public/images/usage-mocap.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 222 KiB |
BIN
gui/public/images/usage-vr.webp
Normal file
BIN
gui/public/images/usage-vr.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 287 KiB |
BIN
gui/public/images/usage-vtuber.webp
Normal file
BIN
gui/public/images/usage-vtuber.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
BIN
gui/public/images/vrslimes.webp
Normal file
BIN
gui/public/images/vrslimes.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
gui/public/videos/vrchatosc.webm
Normal file
BIN
gui/public/videos/vrchatosc.webm
Normal file
Binary file not shown.
@@ -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={<AssignmentTutorialPage />}
|
||||
/>
|
||||
|
||||
<Route path="usage">
|
||||
<Route path="choose" element={<UsageChoose />} />
|
||||
<Route path="vr/choose" element={<VRUsageChoose />} />
|
||||
<Route path="vr/standalone" element={<StandaloneUsageSetup />} />
|
||||
<Route path="mocap/data/choose" element={<MocapDataChoose />} />
|
||||
<Route
|
||||
path="mocap/head-choose"
|
||||
element={<HeadTrackingChoose />}
|
||||
/>
|
||||
<Route path="mocap/data/vmc" element={<MocapVMCSetup />} />
|
||||
<Route path="mocap/data/bvh" element={<MocapBVHSetup />} />
|
||||
<Route path="mocap/data/steamvr" element={<MocapSteamSetup />} />
|
||||
</Route>
|
||||
|
||||
<Route path="trackers-assign" element={<TrackersAssignPage />} />
|
||||
<Route path="enter-vr" element={<EnterVRPage />} />
|
||||
<Route path="mounting/choose" element={<MountingChoose />}></Route>
|
||||
|
||||
@@ -2,6 +2,7 @@ import classNames from 'classnames';
|
||||
import React, { ReactNode, useMemo } from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { LoaderIcon, SlimeState } from './icon/LoaderIcon';
|
||||
import { XOR } from 'ts-xor';
|
||||
|
||||
function ButtonContent({
|
||||
loading,
|
||||
@@ -36,6 +37,25 @@ function ButtonContent({
|
||||
);
|
||||
}
|
||||
|
||||
type ButtonBaseParams = {
|
||||
children?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
variant: 'primary' | 'secondary' | 'tertiary' | 'quaternary';
|
||||
loading?: boolean;
|
||||
rounded?: boolean;
|
||||
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'>;
|
||||
|
||||
type ButtonNavigateParams = { to: string; state?: any } & ButtonBaseParams;
|
||||
type ButtonScriptParams = {
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
} & ButtonBaseParams;
|
||||
type ButtonSubmitParams = { type: 'submit' } & ButtonBaseParams;
|
||||
type ButtonParams = XOR<
|
||||
ButtonNavigateParams,
|
||||
ButtonScriptParams,
|
||||
ButtonSubmitParams
|
||||
>;
|
||||
|
||||
export function Button({
|
||||
children,
|
||||
variant,
|
||||
@@ -46,15 +66,7 @@ export function Button({
|
||||
icon,
|
||||
rounded = false,
|
||||
...props
|
||||
}: {
|
||||
children?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
variant: 'primary' | 'secondary' | 'tertiary' | 'quaternary';
|
||||
to?: string;
|
||||
loading?: boolean;
|
||||
rounded?: boolean;
|
||||
state?: any;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
}: ButtonParams) {
|
||||
const classes = useMemo(() => {
|
||||
const variantsMap = {
|
||||
primary: classNames({
|
||||
|
||||
70
gui/src/components/commons/PausableVideo.tsx
Normal file
70
gui/src/components/commons/PausableVideo.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { PlayCircleIcon } from './icon/PlayIcon';
|
||||
import { useDebouncedEffect } from '@/hooks/timeout';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export function PausableVideo({
|
||||
src,
|
||||
poster,
|
||||
restartOnPause = false,
|
||||
autoPlay = false,
|
||||
}: {
|
||||
src?: string;
|
||||
poster?: string;
|
||||
restartOnPause?: boolean;
|
||||
autoPlay?: boolean;
|
||||
}) {
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const [paused, setPaused] = useState(!autoPlay);
|
||||
const [atStart, setAtStart] = useState(true);
|
||||
|
||||
function toggleVideo() {
|
||||
if (!videoRef.current) return;
|
||||
if (videoRef.current.paused) {
|
||||
videoRef.current.play();
|
||||
} else {
|
||||
videoRef.current.pause();
|
||||
if (restartOnPause) {
|
||||
videoRef.current.currentTime = 0;
|
||||
}
|
||||
setAtStart(videoRef.current.currentTime === 0);
|
||||
}
|
||||
setPaused(videoRef.current.paused);
|
||||
}
|
||||
|
||||
useDebouncedEffect(
|
||||
() => {
|
||||
if (paused) videoRef.current?.pause();
|
||||
},
|
||||
[paused],
|
||||
250
|
||||
);
|
||||
|
||||
return (
|
||||
<button className="relative appearance-none" onClick={toggleVideo}>
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute w-[100px] h-[100px] top-0 bottom-0 left-0 right-0 m-auto',
|
||||
'fill-background-20',
|
||||
paused && !atStart && 'opacity-50'
|
||||
)}
|
||||
hidden={!paused}
|
||||
>
|
||||
<PlayCircleIcon width={100}></PlayCircleIcon>
|
||||
</div>
|
||||
|
||||
<video
|
||||
preload="auto"
|
||||
ref={videoRef}
|
||||
src={src}
|
||||
poster={poster}
|
||||
className="min-w-[12rem] w-[30rem]"
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
autoPlay={autoPlay}
|
||||
controls={false}
|
||||
></video>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import { Control, Controller } from 'react-hook-form';
|
||||
import { Typography } from './Typography';
|
||||
import { ReactNode } from 'react';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
|
||||
export function Radio({
|
||||
control,
|
||||
@@ -12,6 +12,7 @@ export function Radio({
|
||||
children,
|
||||
// input props
|
||||
disabled,
|
||||
variant = 'secondary',
|
||||
...props
|
||||
}: {
|
||||
control: Control<any>;
|
||||
@@ -20,19 +21,36 @@ export function Radio({
|
||||
value: string;
|
||||
description?: string | null;
|
||||
children?: ReactNode;
|
||||
variant?: 'secondary' | 'none';
|
||||
} & React.HTMLProps<HTMLInputElement>) {
|
||||
const variantClasses = useMemo(() => {
|
||||
const variantsMap = {
|
||||
secondary: classNames({
|
||||
'bg-background-60 hover:bg-background-50': !disabled,
|
||||
'bg-background-80': disabled,
|
||||
}),
|
||||
none: '',
|
||||
};
|
||||
return variantsMap[variant];
|
||||
}, [variant, disabled]);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field: { onChange, ref, name, value: checked } }) => (
|
||||
<label
|
||||
className={classNames('w-full p-3 rounded-md flex gap-3 border-2', {
|
||||
'border-accent-background-30': value == checked,
|
||||
'border-transparent': value != checked,
|
||||
'bg-background-60 cursor-pointer hover:bg-background-50': !disabled,
|
||||
'bg-background-80 cursor-not-allowed': disabled,
|
||||
})}
|
||||
className={classNames(
|
||||
'w-full rounded-md flex gap-3 border-2 group/radio',
|
||||
variantClasses,
|
||||
{
|
||||
'border-accent-background-30': value == checked,
|
||||
'border-transparent': value != checked,
|
||||
'cursor-pointer': !disabled,
|
||||
'cursor-not-allowed': disabled,
|
||||
'p-3': variant !== 'none',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
@@ -48,7 +66,7 @@ export function Radio({
|
||||
checked={value == checked}
|
||||
{...props}
|
||||
/>
|
||||
<div className="flex flex-col gap-2 pointer-events-none">
|
||||
<div className="flex flex-col gap-2 pointer-events-none w-full">
|
||||
{children ? children : <Typography bold>{label}</Typography>}
|
||||
{description && (
|
||||
<Typography variant="standard" color="secondary">
|
||||
|
||||
@@ -54,7 +54,7 @@ export function Typography({
|
||||
tag,
|
||||
{
|
||||
className: classNames([
|
||||
'transition-colors',
|
||||
'transition-colors hyphens-auto',
|
||||
variant === 'mobile-title' &&
|
||||
'xs:text-main-title mobile:text-section-title',
|
||||
variant === 'main-title' && 'text-main-title',
|
||||
|
||||
@@ -33,7 +33,7 @@ export function EnterVRPage() {
|
||||
</div>
|
||||
<div className="w-full py-4 flex flex-row">
|
||||
<div className="flex flex-grow">
|
||||
<Button variant="secondary" to="/" onClick={skipSetup}>
|
||||
<Button variant="secondary" onClick={skipSetup}>
|
||||
{l10n.getString('onboarding-skip')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { useOnboarding } from '@/hooks/onboarding';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { useState } from 'react';
|
||||
import { SkipSetupWarningModal } from '@/components/onboarding/SkipSetupWarningModal';
|
||||
import classNames from 'classnames';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
|
||||
export function MountingChoose() {
|
||||
const { l10n } = useLocalization();
|
||||
const { applyProgress, skipSetup, state } = useOnboarding();
|
||||
const { applyProgress, state } = useOnboarding();
|
||||
const [animated, setAnimated] = useState(false);
|
||||
const [showWarning, setShowWarning] = useState(false);
|
||||
|
||||
applyProgress(0.65);
|
||||
|
||||
@@ -137,11 +135,6 @@ export function MountingChoose() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SkipSetupWarningModal
|
||||
accept={skipSetup}
|
||||
onClose={() => setShowWarning(false)}
|
||||
isOpen={showWarning}
|
||||
></SkipSetupWarningModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ export function TrackersAssignPage() {
|
||||
}
|
||||
);
|
||||
|
||||
applyProgress(0.5);
|
||||
applyProgress(0.55);
|
||||
|
||||
const { closeChokerWarning, tryOpenChokerWarning, shouldShowChokerWarn } =
|
||||
useChokerWarning({
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import { DOCS_SITE } from '@/App';
|
||||
import { A } from '@/components/commons/A';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { WarningBox } from '@/components/commons/TipBox';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { useOnboarding } from '@/hooks/onboarding';
|
||||
import { useStatusContext } from '@/hooks/status-system';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
ChangeSettingsRequestT,
|
||||
ModelSettingsT,
|
||||
ModelTogglesT,
|
||||
RpcMessage,
|
||||
SettingsRequestT,
|
||||
SettingsResponseT,
|
||||
StatusData,
|
||||
StatusSteamVRDisconnectedT,
|
||||
} from 'solarxr-protocol';
|
||||
|
||||
export function HeadTrackingChoose() {
|
||||
const { l10n } = useLocalization();
|
||||
const { applyProgress, state } = useOnboarding();
|
||||
const { statuses } = useStatusContext();
|
||||
const [animated, setAnimated] = useState(false);
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
const fetchedSettings = useRef<ModelTogglesT | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT());
|
||||
}, []);
|
||||
|
||||
const toggleMocap = (bool: boolean) => {
|
||||
const settings = new ChangeSettingsRequestT();
|
||||
const modelSettings = new ModelSettingsT();
|
||||
|
||||
modelSettings.toggles = fetchedSettings.current ?? new ModelTogglesT();
|
||||
modelSettings.toggles.selfLocalization = bool;
|
||||
|
||||
settings.modelSettings = modelSettings;
|
||||
sendRPCPacket(RpcMessage.ChangeSettingsRequest, settings);
|
||||
};
|
||||
|
||||
useRPCPacket(
|
||||
RpcMessage.SettingsResponse,
|
||||
(oldSettings: SettingsResponseT) => {
|
||||
fetchedSettings.current = oldSettings.modelSettings?.toggles ?? null;
|
||||
|
||||
toggleMocap(false);
|
||||
}
|
||||
);
|
||||
|
||||
const missingSteamVr = useMemo(
|
||||
() =>
|
||||
Object.values(statuses).some(
|
||||
(x) =>
|
||||
x.dataType === StatusData.StatusSteamVRDisconnected &&
|
||||
(x.data as StatusSteamVRDisconnectedT).bridgeSettingsName ===
|
||||
'steamvr'
|
||||
),
|
||||
[statuses]
|
||||
);
|
||||
|
||||
applyProgress(0.55);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center relative overflow-y-auto px-4 pb-4">
|
||||
<div className="flex flex-col gap-4 justify-center">
|
||||
<div className="xs:w-10/12 xs:max-w-[666px]">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-usage-mocap-head_choose')}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="standard"
|
||||
color="secondary"
|
||||
whitespace="whitespace-pre-line"
|
||||
>
|
||||
{l10n.getString('onboarding-usage-mocap-head_choose-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'grid xs:grid-cols-2 w-full xs:flex-row mobile:flex-col gap-4 [&>div]:grow'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-lg p-4 flex',
|
||||
!state.alonePage && 'bg-background-70',
|
||||
state.alonePage && 'bg-background-60'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<div>
|
||||
<Typography variant="main-title" bold>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-mocap-head_choose-standalone'
|
||||
)}
|
||||
</Typography>
|
||||
<Typography variant="vr-accessible" italic>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-mocap-head_choose-standalone-label'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography
|
||||
color="secondary"
|
||||
whitespace="whitespace-pre-line"
|
||||
>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-mocap-head_choose-standalone-description'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant={!state.alonePage ? 'secondary' : 'tertiary'}
|
||||
className="self-start mt-auto"
|
||||
onClick={() => {
|
||||
toggleMocap(true);
|
||||
|
||||
navigate('/onboarding/usage/mocap/data/choose', {
|
||||
state: { alonePage: state.alonePage },
|
||||
});
|
||||
}}
|
||||
>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-mocap-head_choose-standalone-button'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-lg p-4 flex flex-row relative',
|
||||
!state.alonePage && 'bg-background-70',
|
||||
state.alonePage && 'bg-background-60'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<div>
|
||||
<img
|
||||
onMouseEnter={() => setAnimated(() => true)}
|
||||
onAnimationEnd={() => setAnimated(() => false)}
|
||||
src="/images/nighty-vr-sitting.webp"
|
||||
className={classNames(
|
||||
'absolute w-[150px] -right-8 -top-36',
|
||||
animated && 'animate-[bounce_1s_1]'
|
||||
)}
|
||||
></img>
|
||||
<Typography variant="main-title" bold>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-mocap-head_choose-steamvr'
|
||||
)}
|
||||
</Typography>
|
||||
<Typography variant="vr-accessible" italic>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-mocap-head_choose-steamvr-label'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Typography
|
||||
color="secondary"
|
||||
whitespace="whitespace-pre-line"
|
||||
>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-mocap-head_choose-steamvr-description'
|
||||
)}
|
||||
</Typography>
|
||||
{
|
||||
// TODO: Add a button to open SteamVR via tauri's open()
|
||||
missingSteamVr && (
|
||||
<Localized
|
||||
id="onboarding-usage-vr-choose-steamvr-warning"
|
||||
elems={{
|
||||
docs: (
|
||||
<A
|
||||
href={`${DOCS_SITE}/common-issues.html#the-trackers-are-connected-to-the-slimevr-server-but-arent-turning-up-on-steam`}
|
||||
></A>
|
||||
),
|
||||
b: <b></b>,
|
||||
}}
|
||||
>
|
||||
<WarningBox>SteamVR driver not connected</WarningBox>
|
||||
</Localized>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant={'primary'}
|
||||
to="/onboarding/usage/mocap/data/choose"
|
||||
className="self-start mt-auto"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
disabled={missingSteamVr}
|
||||
>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-mocap-head_choose-steamvr-button'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="self-start"
|
||||
to="/onboarding/usage/choose"
|
||||
>
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function MocapBVHSetup() {
|
||||
return <div className="bg-background-70 w-[512px] rounded-md"></div>;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { useOnboarding } from '@/hooks/onboarding';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Radio } from '@/components/commons/Radio';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export enum MocapDataType {
|
||||
BVH = 'BVH',
|
||||
STEAMVR = 'STEAMVR',
|
||||
VMC = 'VMC',
|
||||
}
|
||||
|
||||
const TYPE_TO_NAV = {
|
||||
[MocapDataType.BVH]: '/onboarding/usage/mocap/data/bvh',
|
||||
[MocapDataType.STEAMVR]: '/onboarding/usage/mocap/data/steamvr',
|
||||
[MocapDataType.VMC]: '/onboarding/usage/mocap/data/vmc',
|
||||
};
|
||||
|
||||
export function MocapDataChoose() {
|
||||
const { l10n } = useLocalization();
|
||||
const { applyProgress } = useOnboarding();
|
||||
const navigate = useNavigate();
|
||||
const { control, watch } = useForm<{
|
||||
usageReason?: MocapDataType;
|
||||
}>();
|
||||
|
||||
const usageReason = watch('usageReason');
|
||||
|
||||
const ItemContent = ({ mode }: { mode: MocapDataType }) => (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex bg-background-60 py-2 px-4 group-hover/radio:bg-background-50 rounded-t-md'
|
||||
)}
|
||||
>
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-usage-mocap-data_choose-option-title', {
|
||||
mode: MocapDataType[mode],
|
||||
})}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex flex-col bg-background-70 group-hover/radio:bg-background-60 rounded-b-md py-2 px-4">
|
||||
<Typography>
|
||||
{l10n.getString('onboarding-usage-mocap-data_choose-option-label', {
|
||||
mode: MocapDataType[mode],
|
||||
})}
|
||||
</Typography>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const usages = useMemo(
|
||||
() =>
|
||||
Object.values(MocapDataType).map((mode) => (
|
||||
<Radio
|
||||
key={mode}
|
||||
name="usageReason"
|
||||
control={control}
|
||||
value={mode.toString()}
|
||||
variant="none"
|
||||
className="hidden"
|
||||
>
|
||||
<div>
|
||||
<ItemContent mode={mode}></ItemContent>
|
||||
</div>
|
||||
</Radio>
|
||||
)),
|
||||
[control, l10n]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => usageReason && navigate(TYPE_TO_NAV[usageReason]),
|
||||
[usageReason]
|
||||
);
|
||||
|
||||
applyProgress(0.6);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center">
|
||||
<div className="flex flex-col w-full overflow-y-auto px-4 py-4 xs:items-center">
|
||||
<div className="flex mobile:flex-col xs:gap-8 mobile:gap-4 mobile:pb-4 w-full min-h-0 justify-center">
|
||||
<div className="flex flex-col xs:max-w-sm gap-3 justify-center">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-usage-mocap-data_choose')}
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('onboarding-usage-mocap-data_choose-description')}
|
||||
</Typography>
|
||||
{usages}
|
||||
<div className="flex flex-row">
|
||||
<Button
|
||||
variant="secondary"
|
||||
to="/onboarding/usage/mocap/head-choose"
|
||||
>
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function MocapSteamSetup() {
|
||||
return <div className="bg-background-70 w-[512px] rounded-md"></div>;
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import { CheckBox } from '@/components/commons/Checkbox';
|
||||
import { FileInput } from '@/components/commons/FileInput';
|
||||
import { VMCIcon } from '@/components/commons/icon/VMCIcon';
|
||||
import { Input } from '@/components/commons/Input';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import {
|
||||
DEFAULT_VMC_VALUES,
|
||||
parseVRMFile,
|
||||
VMCSettingsForm,
|
||||
} from '@/components/settings/pages/VMCSettings';
|
||||
import { SettingsPagePaneLayout } from '@/components/settings/SettingsPageLayout';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
ChangeSettingsRequestT,
|
||||
OSCSettingsT,
|
||||
RpcMessage,
|
||||
SettingsRequestT,
|
||||
SettingsResponseT,
|
||||
VMCOSCSettingsT,
|
||||
} from 'solarxr-protocol';
|
||||
|
||||
export function MocapVMCSetup() {
|
||||
const { l10n } = useLocalization();
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
const [modelName, setModelName] = useState<string | null>(null);
|
||||
|
||||
const { reset, control, watch, handleSubmit } = useForm<VMCSettingsForm>({
|
||||
defaultValues: {
|
||||
...DEFAULT_VMC_VALUES,
|
||||
vmc: { oscSettings: { enabled: true }, anchorHip: false },
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (values: VMCSettingsForm) => {
|
||||
const settings = new ChangeSettingsRequestT();
|
||||
|
||||
if (values.vmc) {
|
||||
const vmcOsc = new VMCOSCSettingsT();
|
||||
|
||||
vmcOsc.oscSettings = Object.assign(new OSCSettingsT(), {
|
||||
...values.vmc.oscSettings,
|
||||
vmc: { oscSettings: { enabled: true }, anchorHip: false },
|
||||
});
|
||||
|
||||
if (values.vmc.vrmJson !== undefined) {
|
||||
if (values.vmc.vrmJson.length > 0) {
|
||||
vmcOsc.vrmJson = await parseVRMFile(values.vmc.vrmJson[0]);
|
||||
if (vmcOsc.vrmJson) {
|
||||
setModelName(
|
||||
JSON.parse(vmcOsc.vrmJson)?.extensions?.VRM?.meta?.title || ''
|
||||
);
|
||||
}
|
||||
} else {
|
||||
vmcOsc.vrmJson = '';
|
||||
setModelName(null);
|
||||
}
|
||||
}
|
||||
vmcOsc.anchorHip = values.vmc.anchorHip;
|
||||
vmcOsc.mirrorTracking = values.vmc.mirrorTracking;
|
||||
|
||||
settings.vmcOsc = vmcOsc;
|
||||
}
|
||||
sendRPCPacket(RpcMessage.ChangeSettingsRequest, settings);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch(() => handleSubmit(onSubmit)());
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT());
|
||||
}, []);
|
||||
|
||||
useRPCPacket(RpcMessage.SettingsResponse, (settings: SettingsResponseT) => {
|
||||
const formData: VMCSettingsForm = DEFAULT_VMC_VALUES;
|
||||
if (settings.vmcOsc) {
|
||||
if (settings.vmcOsc.oscSettings) {
|
||||
formData.vmc.oscSettings.enabled = settings.vmcOsc.oscSettings.enabled;
|
||||
if (settings.vmcOsc.oscSettings.portIn)
|
||||
formData.vmc.oscSettings.portIn = settings.vmcOsc.oscSettings.portIn;
|
||||
if (settings.vmcOsc.oscSettings.portOut)
|
||||
formData.vmc.oscSettings.portOut =
|
||||
settings.vmcOsc.oscSettings.portOut;
|
||||
if (settings.vmcOsc.oscSettings.address)
|
||||
formData.vmc.oscSettings.address =
|
||||
settings.vmcOsc.oscSettings.address.toString();
|
||||
}
|
||||
const vrmJson = settings.vmcOsc.vrmJson?.toString();
|
||||
if (vrmJson) {
|
||||
setModelName(JSON.parse(vrmJson)?.extensions?.VRM?.meta?.title || '');
|
||||
}
|
||||
|
||||
formData.vmc.anchorHip = settings.vmcOsc.anchorHip;
|
||||
formData.vmc.mirrorTracking = settings.vmcOsc.mirrorTracking;
|
||||
}
|
||||
|
||||
reset(formData);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="bg-background-70 sm:w-[512px] rounded-md overflow-scroll">
|
||||
<SettingsPagePaneLayout icon={<VMCIcon></VMCIcon>} id="vmc">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('settings-osc-vmc')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pt-2 pb-4">
|
||||
<>
|
||||
{l10n
|
||||
.getString('settings-osc-vmc-description')
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
</Typography>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vmc-network')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<>
|
||||
{l10n
|
||||
.getString('settings-osc-vmc-network-description')
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
</Typography>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pb-5">
|
||||
<Localized
|
||||
id="settings-osc-vmc-network-port_in"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
control={control}
|
||||
name="vmc.oscSettings.portIn"
|
||||
rules={{ required: true }}
|
||||
placeholder="9002"
|
||||
label=""
|
||||
></Input>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="settings-osc-vmc-network-port_out"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
control={control}
|
||||
name="vmc.oscSettings.portOut"
|
||||
rules={{ required: true }}
|
||||
placeholder="9000"
|
||||
label=""
|
||||
></Input>
|
||||
</Localized>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vmc-network-address')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('settings-osc-vmc-network-address-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid gap-3 pb-5">
|
||||
<Input
|
||||
type="text"
|
||||
control={control}
|
||||
name="vmc.oscSettings.address"
|
||||
rules={{
|
||||
required: true,
|
||||
pattern: /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/i,
|
||||
}}
|
||||
placeholder={l10n.getString(
|
||||
'settings-osc-vmc-network-address-placeholder'
|
||||
)}
|
||||
label=""
|
||||
></Input>
|
||||
</div>
|
||||
<Typography bold>{l10n.getString('settings-osc-vmc-vrm')}</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('settings-osc-vmc-vrm-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid gap-3 pb-5">
|
||||
<FileInput
|
||||
control={control}
|
||||
name="vmc.vrmJson"
|
||||
rules={{
|
||||
required: false,
|
||||
}}
|
||||
value="help"
|
||||
importedFileName={
|
||||
// if modelname is an empty string, it's an untitled model
|
||||
modelName === ''
|
||||
? l10n.getString('settings-osc-vmc-vrm-untitled_model')
|
||||
: modelName
|
||||
}
|
||||
label="settings-osc-vmc-vrm-file_select"
|
||||
accept="model/gltf-binary, model/gltf+json, model/vrml, .vrm, .glb, .gltf"
|
||||
></FileInput>
|
||||
{/* For some reason, linux (GNOME) is detecting the VRM file is a VRML */}
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vmc-mirror_tracking')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('settings-osc-vmc-mirror_tracking-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pb-5">
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="vmc.mirrorTracking"
|
||||
label={l10n.getString('settings-osc-vmc-mirror_tracking-label')}
|
||||
/>
|
||||
</div>
|
||||
</SettingsPagePaneLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { PausableVideo } from '@/components/commons/PausableVideo';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { useOnboarding } from '@/hooks/onboarding';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import { VRCHAT_OSC_VIDEO } from '@/utils/tauri';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
ChangeSettingsRequestT,
|
||||
OSCSettingsT,
|
||||
RpcMessage,
|
||||
SettingsRequestT,
|
||||
SettingsResponseT,
|
||||
VRCOSCSettingsT,
|
||||
} from 'solarxr-protocol';
|
||||
|
||||
export function StandaloneUsageSetup() {
|
||||
const { applyProgress, state } = useOnboarding();
|
||||
const { l10n } = useLocalization();
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
const fetchedSettings = useRef<OSCSettingsT | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT());
|
||||
}, []);
|
||||
|
||||
const toggleVrc = (bool: boolean) => {
|
||||
const oldOscSettings = fetchedSettings.current;
|
||||
|
||||
const settings = new ChangeSettingsRequestT();
|
||||
const vrcOsc = new VRCOSCSettingsT();
|
||||
vrcOsc.oscSettings = new OSCSettingsT(
|
||||
bool,
|
||||
oldOscSettings?.portIn,
|
||||
oldOscSettings?.portOut,
|
||||
oldOscSettings?.address
|
||||
);
|
||||
|
||||
settings.vrcOsc = vrcOsc;
|
||||
sendRPCPacket(RpcMessage.ChangeSettingsRequest, settings);
|
||||
};
|
||||
|
||||
useRPCPacket(
|
||||
RpcMessage.SettingsResponse,
|
||||
(oldSettings: SettingsResponseT) => {
|
||||
fetchedSettings.current = oldSettings.vrcOsc?.oscSettings ?? null;
|
||||
|
||||
toggleVrc(true);
|
||||
}
|
||||
);
|
||||
|
||||
applyProgress(0.6);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full w-full mobile:pt-10">
|
||||
<div className="flex mobile:flex-col items-center xs:justify-center gap-5">
|
||||
<div className="mb-auto w-[512px] flex flex-col gap-2">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-usage-vr-standalone-title')}
|
||||
</Typography>
|
||||
<Typography color="secondary" whitespace="whitespace-pre-line">
|
||||
{l10n.getString('settings-osc-vrchat-description-guide')}
|
||||
</Typography>
|
||||
<div className="flex pt-2">
|
||||
<Button
|
||||
variant={!state.alonePage ? 'secondary' : 'tertiary'}
|
||||
className="self-start mt-auto"
|
||||
onClick={() => {
|
||||
toggleVrc(false);
|
||||
navigate('/onboarding/usage/vr/choose', {
|
||||
state: { alonePage: state.alonePage },
|
||||
});
|
||||
}}
|
||||
>
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
to=""
|
||||
className="ml-auto"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
>
|
||||
{l10n.getString('onboarding-usage-vr-standalone-next')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex gap-5 w-10/12 max-w-[600px] items-center'
|
||||
)}
|
||||
>
|
||||
<div className="rounded-lg overflow-hidden aspect-square">
|
||||
<PausableVideo src={VRCHAT_OSC_VIDEO} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
140
gui/src/components/onboarding/pages/usage-reason/UsageChoose.tsx
Normal file
140
gui/src/components/onboarding/pages/usage-reason/UsageChoose.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { useOnboarding } from '@/hooks/onboarding';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Radio } from '@/components/commons/Radio';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import classNames from 'classnames';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
enum UsageReason {
|
||||
VR,
|
||||
VTUBING,
|
||||
MOCAP,
|
||||
}
|
||||
|
||||
interface UsageInfo {
|
||||
path: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
const REASON_TO_PATH: Record<UsageReason, UsageInfo> = {
|
||||
[UsageReason.MOCAP]: {
|
||||
path: '/onboarding/usage/mocap/head-choose',
|
||||
image: '/images/usage-mocap.webp',
|
||||
},
|
||||
[UsageReason.VR]: {
|
||||
path: '/onboarding/usage/vr/choose',
|
||||
image: '/images/usage-vr.webp',
|
||||
},
|
||||
[UsageReason.VTUBING]: {
|
||||
path: '/onboarding/usage/vtubing/choose',
|
||||
image: '/images/usage-vtuber.webp',
|
||||
},
|
||||
};
|
||||
|
||||
export function UsageChoose() {
|
||||
const { l10n } = useLocalization();
|
||||
const { applyProgress } = useOnboarding();
|
||||
const { control, watch } = useForm<{
|
||||
usageReason: UsageReason;
|
||||
}>({
|
||||
defaultValues: {
|
||||
usageReason: UsageReason.VR,
|
||||
},
|
||||
});
|
||||
|
||||
const usageReason = watch('usageReason');
|
||||
|
||||
const ItemContent = ({ mode }: { mode: UsageReason }) => (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex bg-background-60 py-2 px-4 group-hover/radio:bg-background-50 rounded-t-md'
|
||||
)}
|
||||
>
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-usage-choose-option-title', {
|
||||
mode: UsageReason[mode],
|
||||
})}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex flex-col bg-background-70 group-hover/radio:bg-background-60 rounded-b-md py-2 px-4">
|
||||
<Typography>
|
||||
{l10n.getString('onboarding-usage-choose-option-label', {
|
||||
mode: UsageReason[mode],
|
||||
})}
|
||||
</Typography>
|
||||
<Typography variant="standard" color="secondary">
|
||||
{l10n.getString('onboarding-usage-choose-option-description', {
|
||||
mode: UsageReason[mode],
|
||||
})}
|
||||
</Typography>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const usages = useMemo(
|
||||
() =>
|
||||
Object.values(UsageReason)
|
||||
.filter(checkIfUsageReason)
|
||||
.map((mode) => (
|
||||
<Radio
|
||||
key={mode}
|
||||
name="usageReason"
|
||||
control={control}
|
||||
value={mode.toString()}
|
||||
variant="none"
|
||||
className="hidden"
|
||||
>
|
||||
<div>
|
||||
<ItemContent mode={mode}></ItemContent>
|
||||
</div>
|
||||
</Radio>
|
||||
)),
|
||||
[control, l10n]
|
||||
);
|
||||
|
||||
applyProgress(0.5);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center">
|
||||
<div className="flex flex-col w-full overflow-y-auto px-4 xs:items-center">
|
||||
<div className="flex mobile:flex-col xs:gap-8 mobile:gap-4 mobile:pb-4 w-full justify-center">
|
||||
<div className="flex flex-col xs:max-w-sm gap-3 justify-center">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-usage-choose')}
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('onboarding-usage-choose-description')}
|
||||
</Typography>
|
||||
{usages}
|
||||
<div className="flex flex-row">
|
||||
<Button variant="secondary" to="/onboarding/assign-tutorial">
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
to={REASON_TO_PATH[usageReason].path}
|
||||
className="ml-auto"
|
||||
>
|
||||
{l10n.getString('onboarding-enter_vr-ready')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center">
|
||||
<img
|
||||
className="mobile:hidden rounded-3xl"
|
||||
src={REASON_TO_PATH[usageReason].image}
|
||||
width="496"
|
||||
></img>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function checkIfUsageReason(val: any): val is UsageReason {
|
||||
return typeof val === 'number';
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import { DOCS_SITE } from '@/App';
|
||||
import { A } from '@/components/commons/A';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { WarningBox } from '@/components/commons/TipBox';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { useOnboarding } from '@/hooks/onboarding';
|
||||
import { useStatusContext } from '@/hooks/status-system';
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import classNames from 'classnames';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { StatusData, StatusSteamVRDisconnectedT } from 'solarxr-protocol';
|
||||
|
||||
export function VRUsageChoose() {
|
||||
const { l10n } = useLocalization();
|
||||
const { applyProgress, state } = useOnboarding();
|
||||
const { statuses } = useStatusContext();
|
||||
const [animated, setAnimated] = useState(false);
|
||||
|
||||
const missingSteamVr = useMemo(
|
||||
() =>
|
||||
Object.values(statuses).some(
|
||||
(x) =>
|
||||
x.dataType === StatusData.StatusSteamVRDisconnected &&
|
||||
(x.data as StatusSteamVRDisconnectedT).bridgeSettingsName ===
|
||||
'steamvr'
|
||||
),
|
||||
[statuses]
|
||||
);
|
||||
|
||||
applyProgress(0.55);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center relative overflow-y-auto px-4 pb-4">
|
||||
<div className="flex flex-col gap-4 justify-center">
|
||||
<div className="xs:w-10/12 xs:max-w-[666px]">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-usage-vr-choose')}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="standard"
|
||||
color="secondary"
|
||||
whitespace="whitespace-pre-line"
|
||||
>
|
||||
{l10n.getString('onboarding-usage-vr-choose-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'grid xs:grid-cols-2 w-full xs:flex-row mobile:flex-col gap-4 [&>div]:grow'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-lg p-4 flex',
|
||||
!state.alonePage && 'bg-background-70',
|
||||
state.alonePage && 'bg-background-60'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<div>
|
||||
<Typography variant="main-title" bold>
|
||||
{l10n.getString('onboarding-usage-vr-choose-standalone')}
|
||||
</Typography>
|
||||
<Typography variant="vr-accessible" italic>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-vr-choose-standalone-label'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography
|
||||
color="secondary"
|
||||
whitespace="whitespace-pre-line"
|
||||
>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-vr-choose-standalone-description'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant={!state.alonePage ? 'secondary' : 'tertiary'}
|
||||
to={'/onboarding/usage/vr/standalone'}
|
||||
className="self-start mt-auto"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
>
|
||||
{l10n.getString('onboarding-usage-vr-choose-standalone')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-lg p-4 flex flex-row relative',
|
||||
!state.alonePage && 'bg-background-70',
|
||||
state.alonePage && 'bg-background-60'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<div>
|
||||
<img
|
||||
onMouseEnter={() => setAnimated(() => true)}
|
||||
onAnimationEnd={() => setAnimated(() => false)}
|
||||
src="/images/vrslimes.webp"
|
||||
className={classNames(
|
||||
'absolute w-[150px] -right-8 -top-16',
|
||||
animated && 'animate-[bounce_1s_1]'
|
||||
)}
|
||||
></img>
|
||||
<Typography variant="main-title" bold>
|
||||
{l10n.getString('onboarding-usage-vr-choose-steamvr')}
|
||||
</Typography>
|
||||
<Typography variant="vr-accessible" italic>
|
||||
{l10n.getString('onboarding-usage-vr-choose-steamvr-label')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Typography
|
||||
color="secondary"
|
||||
whitespace="whitespace-pre-line"
|
||||
>
|
||||
{l10n.getString(
|
||||
'onboarding-usage-vr-choose-steamvr-description'
|
||||
)}
|
||||
</Typography>
|
||||
{
|
||||
// TODO: Add a button to open SteamVR via tauri's open()
|
||||
missingSteamVr && (
|
||||
<Localized
|
||||
id="onboarding-usage-vr-choose-steamvr-warning"
|
||||
elems={{
|
||||
docs: (
|
||||
<A
|
||||
href={`${DOCS_SITE}/common-issues.html#the-trackers-are-connected-to-the-slimevr-server-but-arent-turning-up-on-steam`}
|
||||
></A>
|
||||
),
|
||||
b: <b></b>,
|
||||
}}
|
||||
>
|
||||
<WarningBox>SteamVR driver not connected</WarningBox>
|
||||
</Localized>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant={'primary'}
|
||||
to="/onboarding/mounting/manual"
|
||||
className="self-start mt-auto"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
disabled={missingSteamVr}
|
||||
>
|
||||
{l10n.getString('onboarding-usage-vr-choose-steamvr')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="self-start"
|
||||
to="/onboarding/usage/choose"
|
||||
>
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from '@/components/settings/SettingsPageLayout';
|
||||
import { error } from '@/utils/logging';
|
||||
|
||||
interface VMCSettingsForm {
|
||||
export interface VMCSettingsForm {
|
||||
vmc: {
|
||||
oscSettings: {
|
||||
enabled: boolean;
|
||||
@@ -36,7 +36,7 @@ interface VMCSettingsForm {
|
||||
};
|
||||
}
|
||||
|
||||
const defaultValues = {
|
||||
export const DEFAULT_VMC_VALUES = {
|
||||
vmc: {
|
||||
oscSettings: {
|
||||
enabled: false,
|
||||
@@ -55,7 +55,7 @@ export function VMCSettings() {
|
||||
const [modelName, setModelName] = useState<string | null>(null);
|
||||
|
||||
const { reset, control, watch, handleSubmit } = useForm<VMCSettingsForm>({
|
||||
defaultValues,
|
||||
defaultValues: DEFAULT_VMC_VALUES,
|
||||
});
|
||||
|
||||
const onSubmit = async (values: VMCSettingsForm) => {
|
||||
@@ -99,7 +99,7 @@ export function VMCSettings() {
|
||||
}, []);
|
||||
|
||||
useRPCPacket(RpcMessage.SettingsResponse, (settings: SettingsResponseT) => {
|
||||
const formData: VMCSettingsForm = defaultValues;
|
||||
const formData: VMCSettingsForm = DEFAULT_VMC_VALUES;
|
||||
if (settings.vmcOsc) {
|
||||
if (settings.vmcOsc.oscSettings) {
|
||||
formData.vmc.oscSettings.enabled = settings.vmcOsc.oscSettings.enabled;
|
||||
@@ -299,7 +299,7 @@ export function VMCSettings() {
|
||||
const gltfHeaderStart = 0;
|
||||
const gltfHeaderEnd = 20;
|
||||
|
||||
async function parseVRMFile(vrm: File): Promise<string | null> {
|
||||
export async function parseVRMFile(vrm: File): Promise<string | null> {
|
||||
const headerView = new DataView(
|
||||
await vrm.slice(gltfHeaderStart, gltfHeaderEnd).arrayBuffer()
|
||||
);
|
||||
|
||||
@@ -131,8 +131,13 @@ export function VRCOSCSettings() {
|
||||
<div className="flex flex-col pt-2 pb-4">
|
||||
<>
|
||||
{l10n
|
||||
.getString('settings-osc-vrchat-description-v1')
|
||||
.getString('settings-osc-vrchat-description-v2')
|
||||
.split('\n')
|
||||
.concat(
|
||||
l10n
|
||||
.getString('settings-osc-vrchat-description-guide')
|
||||
.split('\n')
|
||||
)
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
|
||||
@@ -45,6 +45,7 @@ function TrackerBig({
|
||||
<>
|
||||
{device.hardwareStatus.batteryPctEstimate && (
|
||||
<TrackerBattery
|
||||
voltage={device.hardwareStatus.batteryVoltage}
|
||||
value={device.hardwareStatus.batteryPctEstimate / 100}
|
||||
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
|
||||
/>
|
||||
@@ -93,6 +94,7 @@ function TrackerSmol({
|
||||
<div className="flex flex-col justify-center items-center">
|
||||
{device.hardwareStatus.batteryPctEstimate && (
|
||||
<TrackerBattery
|
||||
voltage={device.hardwareStatus.batteryVoltage}
|
||||
value={device.hardwareStatus.batteryPctEstimate / 100}
|
||||
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
|
||||
/>
|
||||
|
||||
@@ -17,6 +17,12 @@ body {
|
||||
}
|
||||
|
||||
@media (-webkit-animation) {
|
||||
img.uncrisp {
|
||||
// Webkit moment https://stackoverflow.com/questions/7908168/image-resize-gives-slight-brief-pixelation-in-webkit-browsers
|
||||
// doesn't happen in newer versions, weird.
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-name), 'Noto Sans CJK', sans-serif, 'Twemoji Webkit',
|
||||
emoji;
|
||||
|
||||
@@ -14,6 +14,7 @@ export async function fetchResourceUrl(url: string) {
|
||||
// FIXME: For some fucking reason, you can't top-level await on a react component file
|
||||
// on Chromium on developments builds specifically -Uriel
|
||||
export const AUTOBONE_VIDEO = await fetchResourceUrl('/videos/autobone.webm');
|
||||
export const VRCHAT_OSC_VIDEO = await fetchResourceUrl('/videos/vrchatosc.webm');
|
||||
|
||||
export const isTrayAvailable =
|
||||
isTauri() && (await invoke<boolean>('is_tray_available'));
|
||||
|
||||
41
pnpm-lock.yaml
generated
41
pnpm-lock.yaml
generated
@@ -228,6 +228,9 @@ importers:
|
||||
tailwindcss:
|
||||
specifier: ^3.4.13
|
||||
version: 3.4.14(ts-node@9.1.1(typescript@5.6.3))
|
||||
ts-xor:
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
typescript-eslint:
|
||||
specifier: ^8.8.0
|
||||
version: 8.8.0(eslint@8.57.1)(typescript@5.6.3)
|
||||
@@ -2052,27 +2055,6 @@ packages:
|
||||
eslint-import-resolver-webpack:
|
||||
optional: true
|
||||
|
||||
eslint-module-utils@2.8.1:
|
||||
resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==}
|
||||
engines: {node: '>=4'}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/parser': '*'
|
||||
eslint: '*'
|
||||
eslint-import-resolver-node: '*'
|
||||
eslint-import-resolver-typescript: '*'
|
||||
eslint-import-resolver-webpack: '*'
|
||||
peerDependenciesMeta:
|
||||
'@typescript-eslint/parser':
|
||||
optional: true
|
||||
eslint:
|
||||
optional: true
|
||||
eslint-import-resolver-node:
|
||||
optional: true
|
||||
eslint-import-resolver-typescript:
|
||||
optional: true
|
||||
eslint-import-resolver-webpack:
|
||||
optional: true
|
||||
|
||||
eslint-plugin-import@2.31.0:
|
||||
resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -3965,6 +3947,9 @@ packages:
|
||||
ts-pattern@5.5.0:
|
||||
resolution: {integrity: sha512-jqbIpTsa/KKTJYWgPNsFNbLVpwCgzXfFJ1ukNn4I8hMwyQzHMJnk/BqWzggB0xpkILuKzaO/aMYhS0SkaJyKXg==}
|
||||
|
||||
ts-xor@1.3.0:
|
||||
resolution: {integrity: sha512-RLXVjliCzc1gfKQFLRpfeD0rrWmjnSTgj7+RFhoq3KRkUYa8LE/TIidYOzM5h+IdFBDSjjSgk9Lto9sdMfDFEA==}
|
||||
|
||||
tsconfig-paths@3.15.0:
|
||||
resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
|
||||
|
||||
@@ -6229,7 +6214,7 @@ snapshots:
|
||||
debug: 4.3.5
|
||||
enhanced-resolve: 5.17.0
|
||||
eslint: 8.57.1
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
|
||||
eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
|
||||
fast-glob: 3.3.2
|
||||
get-tsconfig: 4.7.5
|
||||
is-bun-module: 1.2.1
|
||||
@@ -6253,16 +6238,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.6.3)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@8.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
@@ -8558,6 +8533,8 @@ snapshots:
|
||||
|
||||
ts-pattern@5.5.0: {}
|
||||
|
||||
ts-xor@1.3.0: {}
|
||||
|
||||
tsconfig-paths@3.15.0:
|
||||
dependencies:
|
||||
'@types/json5': 0.0.29
|
||||
|
||||
@@ -2,13 +2,14 @@ package dev.slimevr.status
|
||||
|
||||
import solarxr_protocol.rpc.StatusDataUnion
|
||||
import solarxr_protocol.rpc.StatusMessageT
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class StatusSystem {
|
||||
private val listeners: MutableList<StatusListener> = CopyOnWriteArrayList()
|
||||
private val statuses: MutableMap<Int, StatusDataUnion> = HashMap()
|
||||
private val prioritizedStatuses: MutableSet<Int> = HashSet()
|
||||
private val statuses: MutableMap<Int, StatusDataUnion> = ConcurrentHashMap()
|
||||
private val prioritizedStatuses: MutableSet<Int> = ConcurrentHashMap.newKeySet()
|
||||
private val idCounter = AtomicInteger(1)
|
||||
|
||||
fun addListener(listener: StatusListener) {
|
||||
|
||||
Reference in New Issue
Block a user