diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index 7158573da2..742821b0eb 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -934,6 +934,38 @@ onboarding-setup_warning = onboarding-setup_warning-skip = Skip setup onboarding-setup_warning-cancel = Continue setup +## Quiz +onboarding-quiz_continue= Continue + +onboarding-quiz-slimeset-title = Tracker Type +onboarding-quiz-slimeset-description = Which tracker type are you using? + (If you have third party trackers and are not sure, ask your seller) +onboarding-quiz-slimeset-answer-regular = Regular Slime +onboarding-quiz-slimeset-answer-butterfly = Butterfly +onboarding-quiz-slimeset-answer-wifi = Wifi Slime +onboarding-quiz-slimeset-answer-dongle = Dongle Slime + +onboarding-quiz-usage-title = Use Case +onboarding-quiz-usage-description = What are you using your trackers for? If you plan on using SlimeVR for multiple purposes, you can change the affected settings later. +onboarding-quiz-usage-answer-VRC = VR Gaming (e.g. VRChat) +onboarding-quiz-usage-answer-mocap = Mocap (Motion Capture) +onboarding-quiz-usage-answer-vtubing = VTubing + +onboarding-quiz-runtime-title = Runtime +onboarding-quiz-runtime-description = Do you play VRChat on a PC via SteamVR (including with Virtual Desktop, Steam Link, ALVR, etc.), or on a standalone headset such as a Meta Quest or Pico? +onboarding-quiz-runtime-answer-steamvr = SteamVR +onboarding-quiz-runtime-answer-standalone = Standalone + +onboarding-quiz-mocap-pos-title = Head Tracker Position +onboarding-quiz-mocap-pos-description = Where is the head tracker positioned on your head? +onboarding-quiz-mocap-pos-answer-forehead = Forehead +onboarding-quiz-mocap-pos-answer-face = Face + +onboarding-quiz-update-title = Tracker Update +onboarding-quiz-update-description = Would you like to update your trackers to make sure they have the most up to date firmware? +onboarding-quiz-update-answer-yes = Yes +onboarding-quiz-update-answer-no = No + ## Wi-Fi setup onboarding-wifi_creds-back = Go back to introduction onboarding-wifi_creds-v2 = Trackers using Wi-Fi @@ -1468,6 +1500,7 @@ firmware_tool-flashing_step-description = firmware_tool-flashing_step-warning-v2 = Do not unplug or turn off the tracker during the upload process unless told to, it may make your board unusable firmware_tool-flashing_step-flash_more = Flash more trackers firmware_tool-flashing_step-exit = Exit +firmware_tool-flashing_step-onboarding_continue = Continue ## firmware tool build status firmware_tool-build-QUEUED = Waiting to build.... diff --git a/gui/src/App.tsx b/gui/src/App.tsx index f7a79770e3..418c7222d7 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -27,6 +27,12 @@ import { AutomaticMountingPage } from './components/onboarding/pages/mounting/Au import { ManualMountingPage } from './components/onboarding/pages/mounting/ManualMounting'; import { TrackersAssignPage } from './components/onboarding/pages/trackers-assign/TrackerAssignment'; import { WifiCredsPage } from './components/onboarding/pages/WifiCreds'; +import { QuizSlimeSetQuestion } from './components/onboarding/pages/Quiz/QuizSteps/SlimeSetQuestion'; +import { QuizUsageQuestion } from './components/onboarding/pages/Quiz/QuizSteps/UsageQuestion'; +import { QuizRuntimeQuestion } from './components/onboarding/pages/Quiz/QuizSteps/RuntimeQuestionLast'; +import { QuizMocapPosQuestion } from './components/onboarding/pages/Quiz/QuizSteps/MocapPosQuestionLast'; +import { UpdateQuestion } from './components/onboarding/pages/Quiz/QuizSteps/UpdateQuestion'; +import { DonglePage } from './components/onboarding/pages/Dongle'; import { ConfigContextProvider } from './components/providers/ConfigContext'; import { SerialDetectionModal } from './components/SerialDetectionModal'; import { VRCOSCSettings } from './components/settings/pages/VRCOSCSettings'; @@ -153,6 +159,13 @@ function Layout() { > } /> } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/gui/src/components/firmware-tool/steps/FlashingStep.tsx b/gui/src/components/firmware-tool/steps/FlashingStep.tsx index 1a19f517bf..93eecc0c90 100644 --- a/gui/src/components/firmware-tool/steps/FlashingStep.tsx +++ b/gui/src/components/firmware-tool/steps/FlashingStep.tsx @@ -36,7 +36,7 @@ export function FlashingStep({ const { l10n } = useLocalization(); const { selectedDevices, selectDevices, files, selectedDefault } = useFirmwareTool(); - const { state: onboardingState } = useOnboarding(); + const { state: onboardingState, state } = useOnboarding(); const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); const [status, setStatus] = useState<{ [key: string]: { @@ -203,15 +203,28 @@ export function FlashingStep({ onClick={() => goTo('FlashingMethod')} /> - - diff --git a/gui/src/components/onboarding/pages/Quiz/QuizSteps/MocapPosQuestionLast.tsx b/gui/src/components/onboarding/pages/Quiz/QuizSteps/MocapPosQuestionLast.tsx new file mode 100644 index 0000000000..65db77c1f7 --- /dev/null +++ b/gui/src/components/onboarding/pages/Quiz/QuizSteps/MocapPosQuestionLast.tsx @@ -0,0 +1,142 @@ +import { useOnboarding } from '@/hooks/onboarding'; +import classNames from 'classnames'; +import { useState, useEffect, createContext } from 'react'; +import { Typography } from '@/components/commons/Typography'; +import { Button } from '@/components/commons/Button'; +import { useWebsocketAPI } from '@/hooks/websocket-api'; +import { Localized } from '@fluent/react'; +import { + RpcMessage, + SettingsResponseT, + ChangeSettingsRequestT, + ModelSettingsT, + SettingsRequestT, + ResetsSettingsT, + ModelTogglesT, +} from 'solarxr-protocol'; +export const LevelContext = createContext(1); + +export function QuizMocapPosQuestion() { + const { applyProgress, setMocapPos, usage, slimeSet, mocapPos } = + useOnboarding(); + const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); + const [settings, setSettings] = useState(); + const [to, setTo] = useState(''); + const [disabled, setDisabled] = useState(true); + + applyProgress(0.4); + + useEffect(() => { + sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT()); + }, []); + + useRPCPacket(RpcMessage.SettingsResponse, (settings: SettingsResponseT) => { + setSettings(settings); + }); + + const updateTo = () => { + if (slimeSet === 'butterfly') { + setTo('/onboarding/dongle'); + } else { + setTo('/onboarding/wifi-creds'); + } + }; + + const applySettings = () => { + if (!settings?.modelSettings || !settings?.vrcOsc) + throw 'settings should be set'; + const req = new ChangeSettingsRequestT(); + const modelSettings = new ModelSettingsT(); + const resetSettings = new ResetsSettingsT(); + + if (usage === 'mocap') { + const toggles = Object.assign( + new ModelTogglesT(), + settings.modelSettings.toggles + ); + toggles.selfLocalization = true; + modelSettings.toggles = toggles; + req.modelSettings = modelSettings; + + if (mocapPos === 'forehead') { + const resets = Object.assign(resetSettings, settings.resetsSettings); + resets.resetHmdPitch = true; + req.resetsSettings = resets; + } + } + + sendRPCPacket(RpcMessage.ChangeSettingsRequest, req); + }; + + applyProgress(0.2); + + return ( +
+
+
+ +
+
+
+ +
+
+
{ + setMocapPos('forehead'); + updateTo(); + setDisabled(false); + }} + className={classNames( + 'rounded-lg overflow-hidden transition-[box-shadow] duration-200 ease-linear hover:bg-background-50 cursor-pointer bg-background-60', + mocapPos === 'forehead' && + 'outline outline-3 outline-accent-background-40' + )} + > +
+
+ +
+
+
+
{ + setMocapPos('face'); + updateTo(); + setDisabled(false); + }} + className={classNames( + 'rounded-lg overflow-hidden transition-[box-shadow] duration-200 ease-linear hover:bg-background-50 cursor-pointer bg-background-60', + mocapPos === 'face' && + 'outline outline-3 outline-accent-background-40' + )} + > +
+
+ +
+
+
+
+
+
+ +
+
+
+ ); +} diff --git a/gui/src/components/onboarding/pages/Quiz/QuizSteps/RuntimeQuestionLast.tsx b/gui/src/components/onboarding/pages/Quiz/QuizSteps/RuntimeQuestionLast.tsx new file mode 100644 index 0000000000..0a608bedda --- /dev/null +++ b/gui/src/components/onboarding/pages/Quiz/QuizSteps/RuntimeQuestionLast.tsx @@ -0,0 +1,123 @@ +import { useOnboarding } from '@/hooks/onboarding'; +import classNames from 'classnames'; +import { useState, useEffect } from 'react'; +import { Typography } from '@/components/commons/Typography'; +import { Button } from '@/components/commons/Button'; +import { useWebsocketAPI } from '@/hooks/websocket-api'; +import { Localized } from '@fluent/react'; +import { + RpcMessage, + SettingsResponseT, + ChangeSettingsRequestT, + SettingsRequestT, + VRCOSCSettingsT, +} from 'solarxr-protocol'; + +export function QuizRuntimeQuestion() { + const { applyProgress, setRuntime, slimeSet, runtime } = useOnboarding(); + const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); + const [settings, setSettings] = useState(); + const [to, setTo] = useState(''); + const [disabled, setDisabled] = useState(true); + + applyProgress(0.4); + + useEffect(() => { + sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT()); + }, []); + + useRPCPacket(RpcMessage.SettingsResponse, (settings: SettingsResponseT) => { + setSettings(settings); + }); + + const updateTo = () => { + if (slimeSet === 'butterfly') { + setTo('/onboarding/dongle'); + } else { + setTo('/onboarding/wifi-creds'); + } + }; + + const applySettings = () => { + if (!settings?.modelSettings || !settings?.vrcOsc) + throw 'settings should be set'; + const req = new ChangeSettingsRequestT(); + const oscSettings = new VRCOSCSettingsT(); + + if (runtime === 'standalone') { + const osc = Object.assign(oscSettings, settings.vrcOsc.oscSettings); + osc.enabled = true; + oscSettings.oscSettings = osc; + req.vrcOsc = oscSettings; + } + + sendRPCPacket(RpcMessage.ChangeSettingsRequest, req); + }; + + return ( +
+
+
+ +
+
+
+ +
+
+
{ + setRuntime('steamvr'); + updateTo(); + setDisabled(false); + }} + className={classNames( + 'rounded-lg overflow-hidden transition-[box-shadow] duration-200 ease-linear hover:bg-background-50 cursor-pointer bg-background-60', + runtime === 'steamvr' && + 'outline outline-3 outline-accent-background-40' + )} + > +
+
+ +
+
+
+
{ + setRuntime('standalone'); + updateTo(); + setDisabled(false); + }} + className={classNames( + 'rounded-lg overflow-hidden transition-[box-shadow] duration-200 ease-linear hover:bg-background-50 cursor-pointer bg-background-60', + runtime === 'standalone' && + 'outline outline-3 outline-accent-background-40' + )} + > +
+
+ +
+
+
+
+
+
+ +
+
+
+ ); +} diff --git a/gui/src/components/onboarding/pages/Quiz/QuizSteps/SlimeSetQuestion.tsx b/gui/src/components/onboarding/pages/Quiz/QuizSteps/SlimeSetQuestion.tsx new file mode 100644 index 0000000000..f12121cec7 --- /dev/null +++ b/gui/src/components/onboarding/pages/Quiz/QuizSteps/SlimeSetQuestion.tsx @@ -0,0 +1,119 @@ +import { useOnboarding } from '@/hooks/onboarding'; +import classNames from 'classnames'; +import { useState } from 'react'; +import { Typography } from '@/components/commons/Typography'; +import { Button } from '@/components/commons/Button'; +import { Localized } from '@fluent/react'; + +export function QuizSlimeSetQuestion() { + const { applyProgress, setSlimeSet, slimeSet } = useOnboarding(); + const [to, setTo] = useState(''); + const [disabled, setDisabled] = useState(true); + + applyProgress(0.2); + + return ( +
+
+
+ +
+
+
+ +
+
+
{ + setSlimeSet('regular-slime'); + setTo('/onboarding/quiz/Update?'); + setDisabled(false); + }} + className={classNames( + 'rounded-lg overflow-hidden transition-[box-shadow] duration-200 ease-linear hover:bg-background-50 cursor-pointer bg-background-60', + slimeSet === 'regular-slime' && + 'outline outline-3 outline-accent-background-40' + )} + > +
+
+ +
+
+
+
{ + setSlimeSet('butterfly'); + setTo('/onboarding/quiz/usage'); + setDisabled(false); + }} + className={classNames( + 'rounded-lg overflow-hidden transition-[box-shadow] duration-200 ease-linear hover:bg-background-50 cursor-pointer bg-background-60', + slimeSet === 'butterfly' && + 'outline outline-3 outline-accent-background-40' + )} + > +
+
+ +
+
+
+
{ + setSlimeSet('wifi-slime'); + setTo('/onboarding/quiz/Update?'); + setDisabled(false); + }} + className={classNames( + 'rounded-lg overflow-hidden transition-[box-shadow] duration-200 ease-linear hover:bg-background-50 cursor-pointer bg-background-60', + slimeSet === 'wifi-slime' && + 'outline outline-3 outline-accent-background-40' + )} + > +
+
+ +
+
+
+
{ + setSlimeSet('dongle-slime'); + setTo('/onboarding/quiz/usage'); + setDisabled(false); + }} + className={classNames( + 'rounded-lg overflow-hidden transition-[box-shadow] duration-200 ease-linear hover:bg-background-50 cursor-pointer bg-background-60', + slimeSet === 'dongle-slime' && + 'outline outline-3 outline-accent-background-40' + )} + > +
+
+ +
+
+
+
+
+
+ +
+
+
+ ); +} diff --git a/gui/src/components/onboarding/pages/Quiz/QuizSteps/UpdateQuestion.tsx b/gui/src/components/onboarding/pages/Quiz/QuizSteps/UpdateQuestion.tsx new file mode 100644 index 0000000000..2b7364913e --- /dev/null +++ b/gui/src/components/onboarding/pages/Quiz/QuizSteps/UpdateQuestion.tsx @@ -0,0 +1,76 @@ +import { useOnboarding } from '@/hooks/onboarding'; +import classNames from 'classnames'; +import { useState } from 'react'; +import { Typography } from '@/components/commons/Typography'; +import { Button } from '@/components/commons/Button'; +import { Localized } from '@fluent/react'; + +export function UpdateQuestion() { + const { applyProgress, setUpdate, update } = useOnboarding(); + const [disabled, setDisabled] = useState(true); + + applyProgress(0.2); + + return ( +
+
+
+ +
+
+
+ +
+
+
{ + setUpdate(true); + setDisabled(false); + }} + className={classNames( + 'rounded-lg overflow-hidden transition-[box-shadow] duration-200 ease-linear hover:bg-background-50 cursor-pointer bg-background-60', + update === true && + 'outline outline-3 outline-accent-background-40' + )} + > +
+
+ +
+
+
+
{ + setUpdate(false); + setDisabled(false); + }} + className={classNames( + 'rounded-lg overflow-hidden transition-[box-shadow] duration-200 ease-linear hover:bg-background-50 cursor-pointer bg-background-60', + update === false && + 'outline outline-3 outline-accent-background-40' + )} + > +
+
+ +
+
+
+
+
+
+ +
+
+
+ ); +} diff --git a/gui/src/components/onboarding/pages/Quiz/QuizSteps/UsageQuestion.tsx b/gui/src/components/onboarding/pages/Quiz/QuizSteps/UsageQuestion.tsx new file mode 100644 index 0000000000..952db70242 --- /dev/null +++ b/gui/src/components/onboarding/pages/Quiz/QuizSteps/UsageQuestion.tsx @@ -0,0 +1,98 @@ +import { useOnboarding } from '@/hooks/onboarding'; +import classNames from 'classnames'; +import { useState } from 'react'; +import { Typography } from '@/components/commons/Typography'; +import { Button } from '@/components/commons/Button'; +import { Localized } from '@fluent/react'; + +export function QuizUsageQuestion() { + const { applyProgress, setUsage, usage } = useOnboarding(); + const [to, setTo] = useState(''); + const [disabled, setDisabled] = useState(true); + + applyProgress(0.3); + + return ( +
+
+
+ +
+
+
+ +
+
+
{ + setUsage('vrchat'); + setTo('/onboarding/quiz/runtime'); + setDisabled(false); + }} + className={classNames( + 'rounded-lg overflow-hidden transition-[box-shadow] duration-200 ease-linear hover:bg-background-50 cursor-pointer bg-background-60', + usage === 'vrchat' && + 'outline outline-3 outline-accent-background-40' + )} + > +
+
+ +
+
+
+
{ + setUsage('mocap'); + setTo('/onboarding/quiz/mocap-pos'); + setDisabled(false); + }} + className={classNames( + 'rounded-lg overflow-hidden transition-[box-shadow] duration-200 ease-linear hover:bg-background-50 cursor-pointer bg-background-60', + usage === 'mocap' && + 'outline outline-3 outline-accent-background-40' + )} + > +
+
+ +
+
+
+
{ + setUsage('vtubing'); + setTo('/onboarding/quiz/mocap-pos'); + setDisabled(false); + }} + className={classNames( + 'rounded-lg overflow-hidden transition-[box-shadow] duration-200 ease-linear hover:bg-background-50 cursor-pointer bg-background-60', + usage === 'vtubing' && + 'outline outline-3 outline-accent-background-40' + )} + > +
+
+ +
+
+
+
+
+
+ +
+
+
+ ); +} diff --git a/gui/src/components/onboarding/pages/WifiCreds.tsx b/gui/src/components/onboarding/pages/WifiCreds.tsx index 992ec77df7..2bced566d5 100644 --- a/gui/src/components/onboarding/pages/WifiCreds.tsx +++ b/gui/src/components/onboarding/pages/WifiCreds.tsx @@ -5,47 +5,22 @@ import { Button } from '@/components/commons/Button'; import { Input } from '@/components/commons/Input'; import { Typography } from '@/components/commons/Typography'; import classNames from 'classnames'; -import { USBIcon } from '@/components/commons/icon/UsbIcon'; import { WifiIcon } from '@/components/commons/icon/WifiIcon'; -import { WarningBox } from '@/components/commons/TipBox'; export function WifiCredsPage() { const { l10n } = useLocalization(); const { applyProgress, state } = useOnboarding(); - const { control, handleSubmit, submitWifiCreds, formState } = useWifiForm(); + const { control, handleSubmit, submitWifiCreds, formState } = useWifiForm(''); - applyProgress(0.2); + applyProgress(0.5); return (
-
-
-
-
- -
- -
-
- - - WARNING - -
-
-
-
+
- -
- - {l10n.getString( - 'settings-general-fk_settings-self_localization-title' - )} - - - {l10n.getString( - 'settings-general-fk_settings-self_localization-description' - )} - -
-
- -
)} +
+ + {l10n.getString( + 'settings-general-fk_settings-self_localization-title' + )} + + + {l10n.getString( + 'settings-general-fk_settings-self_localization-description' + )} + +
+
+ +
diff --git a/gui/src/hooks/onboarding.ts b/gui/src/hooks/onboarding.ts index 0ee82895fd..ca84190f1a 100644 --- a/gui/src/hooks/onboarding.ts +++ b/gui/src/hooks/onboarding.ts @@ -1,4 +1,11 @@ -import { createContext, Reducer, useContext, useLayoutEffect, useReducer } from 'react'; +import { + createContext, + Reducer, + useContext, + useLayoutEffect, + useReducer, + useState, +} from 'react'; import { useLocation } from 'react-router-dom'; import { useConfig } from './config'; @@ -15,9 +22,19 @@ interface OnboardingState { export interface OnboardingContext { state: OnboardingState; + slimeSet: 'butterfly' | 'regular-slime' | 'dongle-slime' | 'wifi-slime' | undefined; + usage: 'vrchat' | 'mocap' | 'vtubing' | undefined; + update: true | false | undefined; + runtime: 'steamvr' | 'standalone' | undefined; + mocapPos: 'forehead' | 'face' | undefined; applyProgress: (value: number) => void; setWifiCredentials: (ssid: string, password?: string) => void; skipSetup: () => void; + setSlimeSet: React.Dispatch>; + setUsage: React.Dispatch>; + setUpdate: React.Dispatch>; + setRuntime: React.Dispatch>; + setMocapPos: React.Dispatch>; } export function reducer(state: OnboardingState, action: OnboardingAction) { @@ -44,6 +61,11 @@ export function reducer(state: OnboardingState, action: OnboardingAction) { export function useProvideOnboarding(): OnboardingContext { const { setConfig } = useConfig(); + const [slimeSet, setSlimeSet] = useState(undefined); + const [usage, setUsage] = useState(undefined); + const [update, setUpdate] = useState(undefined); + const [runtime, setRuntime] = useState(undefined); + const [mocapPos, setMocapPos] = useState(undefined); const [state, dispatch] = useReducer>( reducer, { @@ -63,6 +85,11 @@ export function useProvideOnboarding(): OnboardingContext { return { state, + slimeSet, + usage, + runtime, + mocapPos, + update, applyProgress: (value: number) => { useLayoutEffect(() => { dispatch({ type: 'progress', value }); @@ -74,6 +101,11 @@ export function useProvideOnboarding(): OnboardingContext { skipSetup: () => { setConfig({ doneOnboarding: true }); }, + setSlimeSet, + setUsage, + setUpdate, + setRuntime, + setMocapPos, }; } diff --git a/gui/src/hooks/wifi-form.tsx b/gui/src/hooks/wifi-form.tsx index 28a24f9426..4feb75cba9 100644 --- a/gui/src/hooks/wifi-form.tsx +++ b/gui/src/hooks/wifi-form.tsx @@ -8,7 +8,7 @@ export interface WifiFormData { password?: string; } -export function useWifiForm() { +export function useWifiForm(nextPage?: string) { const navigate = useNavigate(); const { state, setWifiCredentials } = useOnboarding(); const { register, reset, handleSubmit, formState, control } = @@ -28,7 +28,7 @@ export function useWifiForm() { const submitWifiCreds = (value: WifiFormData) => { setWifiCredentials(value.ssid, value.password ?? ''); - navigate('/onboarding/connect-trackers', { + navigate(nextPage ? nextPage : '/onboarding/connect-trackers', { state: { alonePage: state.alonePage }, }); };