Add "Setup Stay Aligned" flow

This commit is contained in:
Jabberrock
2025-04-28 18:22:34 -07:00
committed by jabberrock
parent 853d112737
commit 44765d5b6e
11 changed files with 330 additions and 1 deletions

View File

@@ -544,6 +544,8 @@ settings-general-tracker_mechanics-use_mag_on_all_trackers-label = Use magnetome
settings-stay_aligned = Stay Aligned
settings-stay_aligned-description = Stay Aligned reduces drift by gradually adjusting your trackers to match your most common poses.
settings-stay_aligned-setup-label = Setup Stay Aligned
settings-stay_aligned-setup-description = You must complete "Setup Stay Aligned" to enable Stay Aligned.
settings-stay_aligned-warnings-drift_compensation = ⚠ Please turn off Drift Compensation! Drift Compensation will conflict with Stay Aligned.
settings-stay_aligned-enabled-label = Enabled
settings-stay_aligned-extra_yaw_correction-label = Extra correction for low quality IMUs (e.g. BMI160, MPU60XX)
@@ -1247,6 +1249,34 @@ onboarding-scaled_proportions-reset_proportion-description = To set your body pr
onboarding-scaled_proportions-done-title = Body proportions set
onboarding-scaled_proportions-done-description = Your body proportions should now be configured based on your height.
## Stay Aligned setup
onboarding-stay_aligned-title = Stay Aligned
onboarding-stay_aligned-description = Configure Stay Aligned to keep your trackers aligned.
onboarding-stay_aligned-verify_mounting-title = Check your Mounting
onboarding-stay_aligned-verify_mounting-step-0 = Stay Aligned requires good mounting. Otherwise, you won't get a good experience with Stay Aligned.
onboarding-stay_aligned-verify_mounting-step-1 = 1. Move around while standing.
onboarding-stay_aligned-verify_mounting-step-2 = 2. Sit down and move your legs and feet.
onboarding-stay_aligned-verify_mounting-step-3 = 3. If your trackers aren't in the right place, restart the process.
onboarding-stay_aligned-relaxed_poses-standing-title = Relaxed Standing Pose
onboarding-stay_aligned-relaxed_poses-standing-step-0 = 1. Stand in a comfortable position. Relax!
onboarding-stay_aligned-relaxed_poses-standing-step-1 = 2. Check that your trackers match your body. If it does not match, you need to restart this flow.
onboarding-stay_aligned-relaxed_poses-standing-step-2 = 3. Press the "Detect pose" button.
onboarding-stay_aligned-relaxed_poses-sitting-title = Relaxed Sitting in Chair Pose
onboarding-stay_aligned-relaxed_poses-sitting-step-0 = 1. Sit in a comfortable position. Relax!
onboarding-stay_aligned-relaxed_poses-sitting-step-1 = 2. Check that your trackers match your body. If it does not match, you need to restart this flow.
onboarding-stay_aligned-relaxed_poses-sitting-step-2 = 3. Press the "Detect pose" button.
onboarding-stay_aligned-relaxed_poses-flat-title = Relaxed Sitting on Floor Pose
onboarding-stay_aligned-relaxed_poses-flat-step-0 = 1. Sit on the floor with your legs in front. Relax!
onboarding-stay_aligned-relaxed_poses-flat-step-1 = 2. Check that your trackers match your body. If it does not match, you need to restart this flow.
onboarding-stay_aligned-relaxed_poses-flat-step-2 = 3. Press the "Detect pose" button.
onboarding-stay_aligned-relaxed_poses-skip_step = Skip
onboarding-stay_aligned-done-title = Stay Aligned enabled!
onboarding-stay_aligned-done-description = Your Stay Aligned setup is complete!
onboarding-stay_aligned-previous_step = Previous
onboarding-stay_aligned-next_step = Next
onboarding-stay_aligned-restart = Restart
onboarding-stay_aligned-done = Done
## Home
home-no_trackers = No trackers detected or assigned

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -60,6 +60,7 @@ import { AdvancedSettings } from './components/settings/pages/AdvancedSettings';
import { FirmwareUpdate } from './components/firmware-update/FirmwareUpdate';
import { ConnectionLost } from './components/onboarding/pages/ConnectionLost';
import { VRCWarningsPage } from './components/vrc/VRCWarningsPage';
import { StayAlignedSetup } from './components/onboarding/pages/stay-aligned/StayAlignedSetup';
export const GH_REPO = 'SlimeVR/SlimeVR-Server';
export const VersionContext = createContext('');
@@ -173,6 +174,7 @@ function Layout() {
path="body-proportions/scaled"
element={<ScaledProportionsPage />}
/>
<Route path="stay-aligned" element={<StayAlignedSetup />} />
<Route path="done" element={<DonePage />} />
</Route>
<Route path="*" element={<TopBar></TopBar>}></Route>

View File

@@ -7,12 +7,18 @@ import { PreparationStep } from './mounting-steps/Preparation';
import { PutTrackersOnStep } from './mounting-steps/PutTrackersOn';
import { useLocalization } from '@fluent/react';
const steps: Step[] = [
// Auto mounting steps that can be included within other flows
export const autoMountingSteps: Step[] = [
{ type: 'numbered', component: PutTrackersOnStep },
{ type: 'numbered', component: PreparationStep },
{ type: 'numbered', component: MountingResetStep },
];
const steps: Step[] = [
...autoMountingSteps,
{ type: 'fullsize', component: DoneStep },
];
export function AutomaticMountingPage() {
const { l10n } = useLocalization();
const { applyProgress, state } = useOnboarding();

View File

@@ -0,0 +1,65 @@
import { useOnboarding } from '@/hooks/onboarding';
import { Typography } from '@/components/commons/Typography';
import { Step, StepperSlider } from '@/components/onboarding/StepperSlider';
import { DoneStep } from './stay-aligned-steps/Done';
import { useLocalization } from '@fluent/react';
import { autoMountingSteps } from '@/components/onboarding/pages/mounting/AutomaticMounting';
import {
FlatRelaxedPoseStep,
SittingRelaxedPoseStep,
StandingRelaxedPoseStep,
} from './stay-aligned-steps/RelaxedPoseSteps';
import { EnableStayAlignedRequestT, RpcMessage } from 'solarxr-protocol';
import { RPCPacketType, useWebsocketAPI } from '@/hooks/websocket-api';
import { useEffect } from 'react';
import { VerifyMountingStep } from './stay-aligned-steps/VerifyMounting';
export function enableStayAligned(
enable: boolean,
sendRPCPacket: (type: RpcMessage, data: RPCPacketType) => void
) {
const req = new EnableStayAlignedRequestT();
req.enable = enable;
sendRPCPacket(RpcMessage.EnableStayAlignedRequest, req);
}
const steps: Step[] = [
...autoMountingSteps,
{ type: 'numbered', component: VerifyMountingStep },
{ type: 'numbered', component: StandingRelaxedPoseStep },
{ type: 'numbered', component: SittingRelaxedPoseStep },
{ type: 'numbered', component: FlatRelaxedPoseStep },
{ type: 'fullsize', component: DoneStep },
];
export function StayAlignedSetup() {
const { l10n } = useLocalization();
const { state } = useOnboarding();
const { sendRPCPacket } = useWebsocketAPI();
useEffect(() => {
// Disable Stay Aligned as soon as we enter the setup flow so that we don't
// adjust the trackers while trying to set up the feature
enableStayAligned(false, sendRPCPacket);
}, []);
return (
<div className="flex flex-col gap-2 h-full items-center w-full xs:justify-center relative overflow-y-auto overflow-x-hidden px-4 pb-4">
<div className="flex flex-col w-full h-full xs:justify-center xs:max-w-3xl gap-5">
<div className="flex flex-col xs:max-w-lg gap-3">
<Typography variant="main-title">
{l10n.getString('onboarding-stay_aligned-title')}
</Typography>
<Typography color="secondary">
{l10n.getString('onboarding-stay_aligned-description')}
</Typography>
</div>
<div className="flex pb-4">
<StepperSlider
variant={state.alonePage ? 'alone' : 'onboarding'}
steps={steps}
/>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,45 @@
import { Button } from '@/components/commons/Button';
import { Typography } from '@/components/commons/Typography';
import { SkeletonVisualizerWidget } from '@/components/widgets/SkeletonVisualizerWidget';
import { useLocalization } from '@fluent/react';
export function DoneStep({
resetSteps,
variant,
}: {
resetSteps: () => void;
variant: 'onboarding' | 'alone';
}) {
const { l10n } = useLocalization();
return (
<div className="flex flex-col items-center w-full justify-center gap-5">
<div className="flex gap-1 flex-col justify-center items-center">
<Typography variant="section-title">
{l10n.getString('onboarding-stay_aligned-done-title')}
</Typography>
<Typography color="secondary">
{l10n.getString('onboarding-stay_aligned-done-description')}
</Typography>
</div>
<div className="flex gap-3">
<Button
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
onClick={resetSteps}
>
{l10n.getString('onboarding-stay_aligned-restart')}
</Button>
<Button
variant="primary"
to="/settings/trackers"
state={{ scrollTo: 'stayaligned' }}
>
{l10n.getString('onboarding-stay_aligned-done')}
</Button>
</div>
<SkeletonVisualizerWidget />
</div>
);
}

View File

@@ -0,0 +1,119 @@
import { Button } from '@/components/commons/Button';
import { Typography } from '@/components/commons/Typography';
import {
CurrentRelaxedPose,
DetectRelaxedPoseButton,
ResetRelaxedPoseButton,
} from '@/components/stay-aligned/RelaxedPose';
import { useLocalization } from '@fluent/react';
import { StayAlignedRelaxedPose } from 'solarxr-protocol';
import { enableStayAligned } from '@/components/onboarding/pages/stay-aligned/StayAlignedSetup';
import { useWebsocketAPI } from '@/hooks/websocket-api';
function makeRelaxedPoseStep(
titleKey: string,
descriptionKeys: string[],
imageUrl: string,
relaxedPose: StayAlignedRelaxedPose,
lastStep: boolean
) {
return ({
nextStep,
prevStep,
variant,
}: {
nextStep: () => void;
prevStep: () => void;
variant: 'onboarding' | 'alone';
}) => {
const { l10n } = useLocalization();
const { sendRPCPacket } = useWebsocketAPI();
return (
<div className="flex mobile:flex-col">
<div className="flex flex-grow flex-col gap-4 max-w-sm">
<Typography variant="main-title" bold>
{l10n.getString(titleKey)}
</Typography>
<div className="flex flex-col gap-2">
{descriptionKeys.map((descriptionKey) => (
<Typography color="secondary">
{l10n.getString(descriptionKey)}
</Typography>
))}
</div>
<CurrentRelaxedPose />
<div className="flex gap-3 mobile:justify-between">
<Button
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
onClick={prevStep}
>
{l10n.getString('onboarding-stay_aligned-previous_step')}
</Button>
<DetectRelaxedPoseButton
onClick={() => {
if (lastStep) {
enableStayAligned(true, sendRPCPacket);
}
nextStep();
}}
pose={relaxedPose}
/>
<ResetRelaxedPoseButton
onClick={() => {
if (lastStep) {
enableStayAligned(true, sendRPCPacket);
}
nextStep();
}}
pose={relaxedPose}
>
{l10n.getString(
'onboarding-stay_aligned-relaxed_poses-skip_step'
)}
</ResetRelaxedPoseButton>
</div>
</div>
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
<img src={imageUrl} width={200} alt="Reset position" />
</div>
</div>
);
};
}
export const StandingRelaxedPoseStep = makeRelaxedPoseStep(
'onboarding-stay_aligned-relaxed_poses-standing-title',
[
'onboarding-stay_aligned-relaxed_poses-standing-step-0',
'onboarding-stay_aligned-relaxed_poses-standing-step-1',
'onboarding-stay_aligned-relaxed_poses-standing-step-2',
],
'/images/relaxed_pose_standing.webp',
StayAlignedRelaxedPose.STANDING,
false
);
export const SittingRelaxedPoseStep = makeRelaxedPoseStep(
'onboarding-stay_aligned-relaxed_poses-sitting-title',
[
'onboarding-stay_aligned-relaxed_poses-sitting-step-0',
'onboarding-stay_aligned-relaxed_poses-sitting-step-1',
'onboarding-stay_aligned-relaxed_poses-sitting-step-2',
],
'/images/relaxed_pose_sitting.webp',
StayAlignedRelaxedPose.SITTING,
false
);
export const FlatRelaxedPoseStep = makeRelaxedPoseStep(
'onboarding-stay_aligned-relaxed_poses-flat-title',
[
'onboarding-stay_aligned-relaxed_poses-flat-step-0',
'onboarding-stay_aligned-relaxed_poses-flat-step-1',
'onboarding-stay_aligned-relaxed_poses-flat-step-2',
],
'/images/relaxed_pose_flat.webp',
StayAlignedRelaxedPose.FLAT,
true
);

View File

@@ -0,0 +1,50 @@
import { Button } from '@/components/commons/Button';
import { Typography } from '@/components/commons/Typography';
import { useLocalization } from '@fluent/react';
export function VerifyMountingStep({
nextStep,
resetSteps,
variant,
}: {
nextStep: () => void;
resetSteps: () => void;
variant: 'onboarding' | 'alone';
}) {
const { l10n } = useLocalization();
return (
<div className="flex mobile:flex-col">
<div className="flex flex-grow flex-col gap-4 max-w-sm">
<Typography variant="main-title" bold>
{l10n.getString('onboarding-stay_aligned-verify_mounting-title')}
</Typography>
<div className="flex flex-col gap-2">
<Typography color="secondary">
{l10n.getString('onboarding-stay_aligned-verify_mounting-step-0')}
</Typography>
<Typography color="secondary">
{l10n.getString('onboarding-stay_aligned-verify_mounting-step-1')}
</Typography>
<Typography color="secondary">
{l10n.getString('onboarding-stay_aligned-verify_mounting-step-2')}
</Typography>
<Typography color="secondary">
{l10n.getString('onboarding-stay_aligned-verify_mounting-step-3')}
</Typography>
</div>
<div className="flex gap-3 mobile:justify-between">
<Button
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
onClick={resetSteps}
>
{l10n.getString('onboarding-stay_aligned-restart')}
</Button>
<Button variant="primary" onClick={nextStep}>
{l10n.getString('onboarding-stay_aligned-next_step')}
</Button>
</div>
</div>
</div>
);
}

View File

@@ -96,6 +96,18 @@ export function StayAlignedSettings({
<Typography color="secondary">
{l10n.getString('settings-stay_aligned-description')}
</Typography>
<Typography color="secondary">
{l10n.getString('settings-stay_aligned-setup-description')}
</Typography>
<div className="flex mt-2">
<Button
variant="primary"
to="/onboarding/stay-aligned"
state={{ alonePage: true }}
>
{l10n.getString('settings-stay_aligned-setup-label')}
</Button>
</div>
</div>
<div className="mt-4">
<Typography bold>