Compare commits

...

30 Commits

Author SHA1 Message Date
Uriel
a088145963 Merge branch 'main' into onboarding-usage-step 2025-01-22 18:05:32 +01:00
Uriel
75cf328aea keep improving on some pages 2024-12-17 19:08:55 +01:00
Uriel
c2fe9541dc add mocap vmc setting 2024-12-12 15:10:28 +01:00
Uriel
9c9c5524a5 Merge branch 'main' into onboarding-usage-step 2024-12-12 14:26:07 +01:00
Uriel
fa74a748ac Start adding strings of stuff 2024-12-10 19:25:59 +01:00
Uriel
b90da87602 mocap data choose 2024-12-02 19:53:59 +01:00
Uriel
40b2da34b2 continue working in the data choose page 2024-12-02 19:53:58 +01:00
Uriel
da133c086f Merge branch 'main' into onboarding-usage-step 2024-12-02 19:45:26 +01:00
Uriel
92b7ca7bda modify how it looks 2024-11-27 14:53:37 +01:00
Uriel
96fa13aea9 improve buttons 2024-11-21 20:20:43 +01:00
Uriel
6a8786b241 Merge branch 'main' into onboarding-usage-step 2024-11-20 21:14:39 +01:00
Uriel
8bdee03164 add head tracking choose page 2024-11-20 21:10:48 +01:00
Uriel
fa10e2d73a Merge branch 'main' into onboarding-usage-step 2024-11-19 19:42:01 +01:00
Uriel
1385403817 improve translations 2024-11-04 22:43:48 +01:00
Uriel
5f60e59e51 Merge branch 'main' into onboarding-usage-step 2024-11-04 17:23:34 +01:00
Uriel
c341534166 fix images again 2024-10-30 19:33:30 +01:00
Uriel
957e81c37a commit the code that uses it 2024-10-29 22:24:01 +01:00
Uriel
c0269155db add images for radio button 2024-10-29 22:22:07 +01:00
Uriel
945b8b392d use voltage inside tracker settings page for showing charging 2024-10-28 21:49:45 +01:00
Uriel
0c9a016598 Merge branch 'main' into onboarding-usage-step 2024-10-28 21:24:24 +01:00
Uriel
583e335fc7 fix status not working correctly 2024-10-28 18:29:36 +01:00
Uriel
986ef8d4b4 add standalone usage guide step 2024-10-25 23:23:10 +02:00
Uriel
97fb9dd098 fix bugs 2024-10-25 18:13:50 +02:00
Uriel
db80609379 fix progress 2024-10-24 01:01:03 +02:00
Uriel
bb5b9f3bfe add check and warning 2024-10-23 23:48:19 +02:00
Uriel
085ba25559 add image 2024-10-23 20:27:03 +02:00
Uriel
682300e707 add vrusagechoose 2024-10-23 19:50:50 +02:00
Uriel
af7f13e8bd Merge branch 'main' into onboarding-usage-step 2024-10-21 15:25:32 +02:00
Uriel
e530cb5327 Merge branch 'main' into onboarding-usage-step 2024-10-14 18:34:33 +02:00
Uriel
6b8e4c961e start adding usage step on onboarding 2024-10-08 18:12:55 +02:00
31 changed files with 1240 additions and 72 deletions

View File

@@ -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"
}
}
}

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

View File

@@ -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>

View File

@@ -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({

View 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>
);
}

View File

@@ -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">

View File

@@ -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',

View File

@@ -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>

View File

@@ -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>
</>
);
}

View File

@@ -247,7 +247,7 @@ export function TrackersAssignPage() {
}
);
applyProgress(0.5);
applyProgress(0.55);
const { closeChokerWarning, tryOpenChokerWarning, shouldShowChokerWarn } =
useChokerWarning({

View File

@@ -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>
);
}

View File

@@ -0,0 +1,3 @@
export function MocapBVHSetup() {
return <div className="bg-background-70 w-[512px] rounded-md"></div>;
}

View File

@@ -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>
);
}

View File

@@ -0,0 +1,3 @@
export function MocapSteamSetup() {
return <div className="bg-background-70 w-[512px] rounded-md"></div>;
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View 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';
}

View File

@@ -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>
);
}

View File

@@ -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()
);

View File

@@ -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}

View File

@@ -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}
/>

View File

@@ -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;

View File

@@ -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
View File

@@ -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

View File

@@ -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) {