From 63383f8a3264d28129b9eea767024aae4dc4bc21 Mon Sep 17 00:00:00 2001 From: loucass003 Date: Fri, 25 Apr 2025 16:07:19 +0200 Subject: [PATCH 01/34] WIP --- gui/src/App.tsx | 6 +- .../ClearDriftCompensationButton.tsx | 7 +- gui/src/components/ClearMountingButton.tsx | 7 +- gui/src/components/MainLayout.scss | 8 +- gui/src/components/MainLayout.tsx | 72 +++-- gui/src/components/Navbar.tsx | 69 ++--- gui/src/components/SessionFlightList.tsx | 51 ++++ gui/src/components/Toolbar.tsx | 38 +++ gui/src/components/commons/Tooltip.tsx | 43 ++- gui/src/components/commons/icon/CrossIcon.tsx | 2 +- gui/src/components/commons/icon/HomeIcon.tsx | 12 + gui/src/components/commons/icon/SkiIcon.tsx | 12 + .../onboarding/OnboardingLayout.tsx | 4 +- .../widgets/SkeletonVisualizerWidget.tsx | 2 +- gui/src/hooks/session-flightlist.ts | 99 +++++++ gui/src/index.scss | 8 +- gui/src/utils/skeletonHelper.ts | 6 +- .../src/main/java/dev/slimevr/VRServer.kt | 7 + .../main/java/dev/slimevr/bridge/Bridge.kt | 2 + .../dev/slimevr/config/FlightListConfig.kt | 5 + .../main/java/dev/slimevr/config/VRConfig.kt | 4 +- .../slimevr/flightlist/FlightListManager.kt | 246 ++++++++++++++++++ .../slimevr/games/vrchat/VRCConfigHandler.kt | 1 + .../main/java/dev/slimevr/osc/VMCHandler.kt | 2 +- .../java/dev/slimevr/osc/VRCOSCHandler.kt | 4 +- .../protocol/datafeed/DataFeedBuilder.java | 6 +- .../dev/slimevr/protocol/rpc/RPCHandler.kt | 4 +- .../rpc/flightlist/RPCFlightListHandler.kt | 77 ++++++ .../java/dev/slimevr/status/StatusSystem.kt | 31 --- .../tracking/processor/HumanPoseManager.kt | 52 +--- .../processor/skeleton/HumanSkeleton.kt | 17 +- .../dev/slimevr/tracking/trackers/Tracker.kt | 143 +++------- .../tracking/trackers/TrackerResetsHandler.kt | 17 +- .../trackers/udp/TrackersUDPServer.kt | 29 ++- .../java/dev/slimevr/unit/LegTweaksTests.kt | 4 +- .../dev/slimevr/unit/MountingResetTests.kt | 8 +- .../slimevr/unit/ReferenceAdjustmentsTests.kt | 6 +- .../java/dev/slimevr/unit/TestTrackerSet.kt | 4 +- .../slimevr/desktop/platform/SteamVRBridge.kt | 32 +-- .../platform/linux/UnixSocketBridge.java | 9 + .../windows/WindowsNamedPipeBridge.java | 9 + .../tracking/trackers/hid/TrackersHID.kt | 5 +- 42 files changed, 818 insertions(+), 352 deletions(-) create mode 100644 gui/src/components/SessionFlightList.tsx create mode 100644 gui/src/components/Toolbar.tsx create mode 100644 gui/src/components/commons/icon/HomeIcon.tsx create mode 100644 gui/src/components/commons/icon/SkiIcon.tsx create mode 100644 gui/src/hooks/session-flightlist.ts create mode 100644 server/core/src/main/java/dev/slimevr/config/FlightListConfig.kt create mode 100644 server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt create mode 100644 server/core/src/main/java/dev/slimevr/protocol/rpc/flightlist/RPCFlightListHandler.kt diff --git a/gui/src/App.tsx b/gui/src/App.tsx index de9beabacd..c95bffcc99 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -82,7 +82,7 @@ function Layout() { + } @@ -90,7 +90,7 @@ function Layout() { + } @@ -114,7 +114,7 @@ function Layout() { + } diff --git a/gui/src/components/ClearDriftCompensationButton.tsx b/gui/src/components/ClearDriftCompensationButton.tsx index e43d0e8350..cc66dbc4ee 100644 --- a/gui/src/components/ClearDriftCompensationButton.tsx +++ b/gui/src/components/ClearDriftCompensationButton.tsx @@ -1,8 +1,8 @@ import { Localized } from '@fluent/react'; import { ClearDriftCompensationRequestT, RpcMessage } from 'solarxr-protocol'; import { useWebsocketAPI } from '@/hooks/websocket-api'; -import { BigButton } from './commons/BigButton'; import { TrashIcon } from './commons/icon/TrashIcon'; +import { Button } from './commons/Button'; export function ClearDriftCompensationButton({ disabled, @@ -18,11 +18,12 @@ export function ClearDriftCompensationButton({ return ( - } onClick={clearDriftCompensation} disabled={disabled} - > + variant="secondary" + > ); } diff --git a/gui/src/components/ClearMountingButton.tsx b/gui/src/components/ClearMountingButton.tsx index 384ab4aa26..74c8279850 100644 --- a/gui/src/components/ClearMountingButton.tsx +++ b/gui/src/components/ClearMountingButton.tsx @@ -1,11 +1,11 @@ import { Localized } from '@fluent/react'; import { ClearMountingResetRequestT, RpcMessage } from 'solarxr-protocol'; import { useWebsocketAPI } from '@/hooks/websocket-api'; -import { BigButton } from './commons/BigButton'; import { TrashIcon } from './commons/icon/TrashIcon'; import { Quaternion } from 'three'; import { QuaternionFromQuatT, similarQuaternions } from '@/maths/quaternion'; import { useMemo } from 'react'; +import { Button } from './commons/Button'; import { useAtomValue } from 'jotai'; import { assignedTrackersAtom } from '@/store/app-store'; @@ -34,11 +34,12 @@ export function ClearMountingButton() { return ( - } onClick={clearMounting} disabled={!trackerWithMounting} - /> + variant="primary" + > ); } diff --git a/gui/src/components/MainLayout.scss b/gui/src/components/MainLayout.scss index 7ab819adbd..9c0b6816d6 100644 --- a/gui/src/components/MainLayout.scss +++ b/gui/src/components/MainLayout.scss @@ -5,11 +5,13 @@ 's c' calc(100% - var(--topbar-h)) / var(--navbar-w) calc(100% - var(--navbar-w)); - &:has(.widgets) { + &.full { grid-template: 't t t' var(--topbar-h) - 's c w' calc(100% - var(--topbar-h)) - / var(--navbar-w) calc(100% - var(--navbar-w) - var(--widget-w)) var(--widget-w); + 's r r' var(--toolbar-h) + 's c l' calc(70% - var(--topbar-h) - var(--toolbar-h)) + 's c p' calc(30%) + / var(--navbar-w) calc(75% - var(--navbar-w)) calc(25%); } @screen mobile { diff --git a/gui/src/components/MainLayout.tsx b/gui/src/components/MainLayout.tsx index 3cd488eaa3..9066249e33 100644 --- a/gui/src/components/MainLayout.tsx +++ b/gui/src/components/MainLayout.tsx @@ -9,19 +9,21 @@ import { import { Navbar } from './Navbar'; import { TopBar } from './TopBar'; import { useWebsocketAPI } from '@/hooks/websocket-api'; -import { WidgetsComponent } from './WidgetsComponent'; import './MainLayout.scss'; +import { Toolbar } from './Toolbar'; +import { SkeletonVisualizerWidget } from './widgets/SkeletonVisualizerWidget'; +import { SessionFlightList } from './SessionFlightList'; export function MainLayout({ children, background = true, - widgets = true, + full = false, isMobile = undefined, }: { children: ReactNode; background?: boolean; isMobile?: boolean; - widgets?: boolean; + full?: boolean; }) { const { sendRPCPacket } = useWebsocketAPI(); const [ProportionsLastPageOpen, setProportionsLastPageOpen] = useState(true); @@ -58,33 +60,49 @@ export function MainLayout({ }); return ( -
-
-
- -
-
- -
-
- {children} -
- {!isMobile && widgets && ( +
+
+ +
+
+ +
+ +
+ {children} +
+ {!isMobile && full && ( + <> +
+ +
- +
- )} -
+
+ {/* */} + +
+ + )}
); } diff --git a/gui/src/components/Navbar.tsx b/gui/src/components/Navbar.tsx index 50a5b0d43b..ce3d92e0b0 100644 --- a/gui/src/components/Navbar.tsx +++ b/gui/src/components/Navbar.tsx @@ -2,14 +2,15 @@ import { useLocalization } from '@fluent/react'; import classnames from 'classnames'; import { ReactNode } from 'react'; import { NavLink, useMatch } from 'react-router-dom'; -import { CubeIcon } from './commons/icon/CubeIcon'; import { GearIcon } from './commons/icon/GearIcon'; import { HumanIcon } from './commons/icon/HumanIcon'; import { RulerIcon } from './commons/icon/RulerIcon'; import { SparkleIcon } from './commons/icon/SparkleIcon'; -import { WrenchIcon } from './commons/icon/WrenchIcons'; import { useBreakpoint } from '@/hooks/breakpoint'; import { useConfig } from '@/hooks/config'; +import { Tooltip } from './commons/Tooltip'; +import { HomeIcon } from './commons/icon/HomeIcon'; +import { SkiIcon } from './commons/icon/SkiIcon'; export function NavButton({ to, @@ -24,43 +25,43 @@ export function NavButton({ state?: any; icon: ReactNode; }) { + const { isMobile } = useBreakpoint('mobile'); const doesMatch = useMatch({ path: match || to, }); return ( - -
-
- {icon} -
-
-
- {children} -
-
+
+
+ {icon} +
+
+ + ); } @@ -70,7 +71,7 @@ export function MainLinks() { return ( <> - }> + }> {l10n.getString('navbar-home')} } + icon={} > {l10n.getString('navbar-mounting')} diff --git a/gui/src/components/SessionFlightList.tsx b/gui/src/components/SessionFlightList.tsx new file mode 100644 index 0000000000..0c25ad96c5 --- /dev/null +++ b/gui/src/components/SessionFlightList.tsx @@ -0,0 +1,51 @@ +import { + flightlistIdtoLabel, + FlightListStep, + useSessionFlightlist, +} from '@/hooks/session-flightlist'; +import { CheckIcon } from './commons/icon/CheckIcon'; +import classNames from 'classnames'; +import { Localized } from '@fluent/react'; + +function Step({ + step: { status, id, optional }, + index, +}: { + step: FlightListStep; + index: number; +}) { + return ( +
+
+ {status === 'complete' && } + {status === 'invalid' && index} + {status === 'blocked' && index} + {status === 'skipped' && index} +
+
+ +
+
+ ); +} + +export function SessionFlightList() { + const { steps } = useSessionFlightlist(); + + return ( +
+ {steps.map((step, index) => ( + + ))} +
+ ); +} diff --git a/gui/src/components/Toolbar.tsx b/gui/src/components/Toolbar.tsx new file mode 100644 index 0000000000..329e1fb09b --- /dev/null +++ b/gui/src/components/Toolbar.tsx @@ -0,0 +1,38 @@ +import { + ResetType, + RpcMessage, + SettingsRequestT, + SettingsResponseT, +} from 'solarxr-protocol'; +import { ResetButton } from './home/ResetButton'; +import { ClearDriftCompensationButton } from './ClearDriftCompensationButton'; +import { useWebsocketAPI } from '@/hooks/websocket-api'; +import { useEffect, useState } from 'react'; +import { ClearMountingButton } from './ClearMountingButton'; + +export function Toolbar() { + const { useRPCPacket, sendRPCPacket } = useWebsocketAPI(); + const [driftCompensationEnabled, setDriftCompensationEnabled] = + useState(false); + + useEffect(() => { + sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT()); + }, []); + + useRPCPacket(RpcMessage.SettingsResponse, (settings: SettingsResponseT) => { + if (settings.driftCompensation != null) + setDriftCompensationEnabled(settings.driftCompensation.enabled); + }); + + return ( +
+ + + + + +
+ ); +} diff --git a/gui/src/components/commons/Tooltip.tsx b/gui/src/components/commons/Tooltip.tsx index 0359cff1ae..53e6451dca 100644 --- a/gui/src/components/commons/Tooltip.tsx +++ b/gui/src/components/commons/Tooltip.tsx @@ -13,11 +13,14 @@ import { createPortal } from 'react-dom'; import { Typography } from './Typography'; import { CloseIcon } from './icon/CloseIcon'; +type Direction = 'top' | 'left' | 'right' | 'bottom'; interface TooltipProps { content: ReactNode; children: ReactElement; - preferedDirection: 'top' | 'left' | 'right' | 'bottom'; + preferedDirection: Direction; + blockedDirections?: Direction[]; mode?: 'corner' | 'center'; + variant?: 'auto' | 'drawer' | 'floating'; disabled?: boolean; } @@ -77,6 +80,7 @@ const clamp = (v: number, min: number, max: number) => const getFloatingTooltipPosition = ( preferedDirection: TooltipProps['preferedDirection'], + blockedDirections: Direction[], mode: TooltipProps['mode'], childrenRect: DOMRect, tooltipRect: DOMRect @@ -135,9 +139,10 @@ const getFloatingTooltipPosition = ( const pos = getPosition(preferedDirection); if (isNotInside({ ...pos, height: tooltipRect.height }, windowRect)) { const [firstPos] = ['left', 'top', 'right', 'bottom'] + .filter((dir) => !blockedDirections.includes(dir as Direction)) .map((dir) => ({ dir, - area: getPosition(dir as TooltipProps['preferedDirection']), + area: getPosition(dir as Direction), })) .toSorted( (a, b) => @@ -226,12 +231,13 @@ const getFloatingTooltipPosition = ( export function FloatingTooltip({ childRef, preferedDirection, + blockedDirections = [], mode, children, }: { childRef: MutableRefObject; children: ReactNode; -} & Pick) { +} & Pick) { const tooltipRef = useRef(null); const [tooltipStyle, setTooltipStyle] = useState(); @@ -245,6 +251,7 @@ export function FloatingTooltip({ setTooltipStyle( getFloatingTooltipPosition( preferedDirection, + blockedDirections, mode, childrenRect, tooltipRect @@ -444,33 +451,51 @@ export function Tooltip({ content, children, preferedDirection, + blockedDirections = [], mode = 'center', + variant = 'auto', disabled = false, }: TooltipProps) { const childRef = useRef(null); const { isMobile } = useBreakpoint('mobile'); - const portal = createPortal( - isMobile ? ( + let portal = null; + if (variant === 'auto') { + portal = isMobile ? ( {content} ) : ( {content} - ), - document.body - ); + ); + } + + if (variant === 'drawer') + portal = {content}; + + if (variant === 'floating') + portal = ( + + {content} + + ); return ( <>
{children}
- {!disabled && portal} + {!disabled && createPortal(portal, document.body)} ); } diff --git a/gui/src/components/commons/icon/CrossIcon.tsx b/gui/src/components/commons/icon/CrossIcon.tsx index 4980d685c1..43dc51ea95 100644 --- a/gui/src/components/commons/icon/CrossIcon.tsx +++ b/gui/src/components/commons/icon/CrossIcon.tsx @@ -3,7 +3,7 @@ export function CrossIcon({ size = 20 }: { size: number }) { + + + ); +} diff --git a/gui/src/components/commons/icon/SkiIcon.tsx b/gui/src/components/commons/icon/SkiIcon.tsx new file mode 100644 index 0000000000..4befe76de0 --- /dev/null +++ b/gui/src/components/commons/icon/SkiIcon.tsx @@ -0,0 +1,12 @@ +export function SkiIcon() { + return ( + + + + ); +} diff --git a/gui/src/components/onboarding/OnboardingLayout.tsx b/gui/src/components/onboarding/OnboardingLayout.tsx index 671ada5e6d..3302ac65d0 100644 --- a/gui/src/components/onboarding/OnboardingLayout.tsx +++ b/gui/src/components/onboarding/OnboardingLayout.tsx @@ -34,8 +34,6 @@ export function OnboardingLayout({ children }: { children: ReactNode }) {
) : ( - - {children} - + {children} ); } diff --git a/gui/src/components/widgets/SkeletonVisualizerWidget.tsx b/gui/src/components/widgets/SkeletonVisualizerWidget.tsx index 8ddada69ce..a0fac0aa8b 100644 --- a/gui/src/components/widgets/SkeletonVisualizerWidget.tsx +++ b/gui/src/components/widgets/SkeletonVisualizerWidget.tsx @@ -33,7 +33,7 @@ declare module '@react-three/fiber' { } } -const GROUND_COLOR = '#4444aa'; +const GROUND_COLOR = '#2c2c6b'; const FRUSTUM_SIZE = 10; const FACTOR = 2; // Not currently used but nice to have diff --git a/gui/src/hooks/session-flightlist.ts b/gui/src/hooks/session-flightlist.ts new file mode 100644 index 0000000000..33f448ee5d --- /dev/null +++ b/gui/src/hooks/session-flightlist.ts @@ -0,0 +1,99 @@ +import { + FlightListRequestT, + FlightListResponseT, + FlightListStepChangeResponseT, + FlightListStepId, + FlightListStepT, + FlightListStepVisibility, + RpcMessage, +} from 'solarxr-protocol'; +import { useWebsocketAPI } from './websocket-api'; +import { useEffect, useState } from 'react'; + +export const flightlistIdtoLabel: Record = { + [FlightListStepId.UNKNOWN]: '', + [FlightListStepId.TRACKERS_CALIBRATION]: 'flight_list-TRACKERS_CALIBRATION', + [FlightListStepId.FULL_RESET]: 'flight_list-FULL_RESET', + [FlightListStepId.VRCHAT_SETTINGS]: 'flight_list-VRCHAT_SETTINGS', + [FlightListStepId.STEAMVR_DISCONNECTED]: 'flight_list-STEAMVR_DISCONNECTED', + [FlightListStepId.UNASSIGNED_HMD]: 'flight_list-UNASSIGNED_HMD', + [FlightListStepId.TRACKER_ERROR]: 'flight_list-TRACKER_ERROR', +}; + +export type FlightListStepStatus = 'complete' | 'skipped' | 'blocked' | 'invalid'; +export type FlightListStep = FlightListStepT & { status: FlightListStepStatus }; + +export function useSessionFlightlist() { + const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); + const [steps, setSteps] = useState([]); + const [ignoredSteps, setIgnoredSteps] = useState([]); + + const createStep = ( + steps: FlightListStepT[], + step: FlightListStepT, + index: number + ) => { + const previousSteps = steps.slice(0, index); + const blocked = previousSteps.some(({ valid }) => !valid); + + let status = 'complete'; + if (blocked) status = 'blocked'; + if (!blocked && !step.valid) status = 'invalid'; + if (!blocked && step.optional && !step.valid) status = 'skipped'; + + return { ...step, status } as FlightListStep; + }; + + useRPCPacket(RpcMessage.FlightListResponse, (data: FlightListResponseT) => { + setIgnoredSteps(data.ignoredSteps); + const visibleSteps = data.steps.filter( + (step) => !data.ignoredSteps.includes(step.id) + ); + const steps = visibleSteps.map((step: FlightListStepT, index) => + createStep(visibleSteps, step, index) + ); + // .filter( + // ({ visibility, status }) => + // visibility == FlightListStepVisibility.ALWAYS || + // (visibility === FlightListStepVisibility.WHEN_INVALID && + // ['invalid'].includes(status)) + // ); + setSteps(steps); + + console.log(steps); + }); + + useRPCPacket( + RpcMessage.FlightListStepChangeResponse, + (data: FlightListStepChangeResponseT) => { + const step = data.step; + if (!step) throw 'invalid state - step should be set'; + console.log('Update', step); + setSteps((steps) => { + const visibleSteps = steps.filter((step) => !ignoredSteps.includes(step.id)); + const newsteps = visibleSteps.map((step: FlightListStepT, index) => + createStep(visibleSteps, step, index) + ); + // .filter( + // ({ visibility, status }) => + // visibility == FlightListStepVisibility.ALWAYS || + // (visibility === FlightListStepVisibility.WHEN_INVALID && + // ['invalid', 'skipped'].includes(status)) + // ); + const stepIndex = newsteps.findIndex(({ id }) => step.id === id); + if (stepIndex == -1) return newsteps; // skip the step because it is not visible + newsteps[stepIndex] = createStep(newsteps, step, stepIndex); + return newsteps; + }); + } + ); + + useEffect(() => { + sendRPCPacket(RpcMessage.FlightListRequest, new FlightListRequestT()); + }, []); + + return { + steps, + ignoredSteps, + }; +} diff --git a/gui/src/index.scss b/gui/src/index.scss index 5286b0a49c..e77e80ad29 100644 --- a/gui/src/index.scss +++ b/gui/src/index.scss @@ -87,13 +87,15 @@ body { // overflow: hidden; -- NEVER EVER BRING THIS BACK <3 background: theme('colors.background.20'); - --navbar-w: 101px; - --widget-w: 274px; + --navbar-w: 66px; --topbar-h: 38px; + --toolbar-h: 65px; + --preview-w: 400px; + --flightlist-w: 272px; @screen mobile { --topbar-h: 44px; - --navbar-h: 90px; + --navbar-h: 60px; } } diff --git a/gui/src/utils/skeletonHelper.ts b/gui/src/utils/skeletonHelper.ts index 0ff8f8e23b..9e3d9edd0c 100644 --- a/gui/src/utils/skeletonHelper.ts +++ b/gui/src/utils/skeletonHelper.ts @@ -192,7 +192,7 @@ export class BoneKind extends Bone { case BodyPart.NECK: return new Color('silver'); case BodyPart.UPPER_CHEST: - return new Color('blue'); + return new Color('chartreuse'); case BodyPart.CHEST: return new Color('purple'); case BodyPart.WAIST: @@ -201,13 +201,13 @@ export class BoneKind extends Bone { return new Color('orange'); case BodyPart.LEFT_UPPER_LEG: case BodyPart.RIGHT_UPPER_LEG: - return new Color('blue'); + return new Color('chartreuse'); case BodyPart.LEFT_LOWER_LEG: case BodyPart.RIGHT_LOWER_LEG: return new Color('teal'); case BodyPart.LEFT_FOOT: case BodyPart.RIGHT_FOOT: - return new Color('#00ffcc'); + return new Color('gold'); case BodyPart.LEFT_LOWER_ARM: case BodyPart.RIGHT_LOWER_ARM: return new Color('red'); diff --git a/server/core/src/main/java/dev/slimevr/VRServer.kt b/server/core/src/main/java/dev/slimevr/VRServer.kt index 0dd33d4a8b..e1e697b2e2 100644 --- a/server/core/src/main/java/dev/slimevr/VRServer.kt +++ b/server/core/src/main/java/dev/slimevr/VRServer.kt @@ -10,6 +10,7 @@ import dev.slimevr.firmware.SerialFlashingHandler import dev.slimevr.games.vrchat.VRCConfigHandler import dev.slimevr.games.vrchat.VRCConfigHandlerStub import dev.slimevr.games.vrchat.VRChatConfigManager +import dev.slimevr.flightlist.FlightListManager import dev.slimevr.osc.OSCHandler import dev.slimevr.osc.OSCRouter import dev.slimevr.osc.VMCHandler @@ -116,6 +117,8 @@ class VRServer @JvmOverloads constructor( @JvmField val handshakeHandler = HandshakeHandler() + val flightListManager: FlightListManager + init { // UwU configManager = ConfigManager(configPath) @@ -131,6 +134,7 @@ class VRServer @JvmOverloads constructor( autoBoneHandler = AutoBoneHandler(this) firmwareUpdateHandler = FirmwareUpdateHandler(this) vrcConfigManager = VRChatConfigManager(this, vrcConfigHandlerProvider(this)) + flightListManager = FlightListManager(this) protocolAPI = ProtocolAPI(this) val computedTrackers = humanPoseManager.computedTrackers @@ -169,6 +173,9 @@ class VRServer @JvmOverloads constructor( for (tracker in computedTrackers) { registerTracker(tracker) } + + + instance = this } diff --git a/server/core/src/main/java/dev/slimevr/bridge/Bridge.kt b/server/core/src/main/java/dev/slimevr/bridge/Bridge.kt index ca869e3670..863d2a473d 100644 --- a/server/core/src/main/java/dev/slimevr/bridge/Bridge.kt +++ b/server/core/src/main/java/dev/slimevr/bridge/Bridge.kt @@ -53,4 +53,6 @@ interface ISteamVRBridge : Bridge { fun getAutomaticSharedTrackers(): Boolean fun setAutomaticSharedTrackers(value: Boolean) + + fun getBridgeConfigKey(): String; } diff --git a/server/core/src/main/java/dev/slimevr/config/FlightListConfig.kt b/server/core/src/main/java/dev/slimevr/config/FlightListConfig.kt new file mode 100644 index 0000000000..c77bfb7cc7 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/config/FlightListConfig.kt @@ -0,0 +1,5 @@ +package dev.slimevr.config + +class FlightListConfig { + val ignoredStepsIds: MutableList = mutableListOf(); +} diff --git a/server/core/src/main/java/dev/slimevr/config/VRConfig.kt b/server/core/src/main/java/dev/slimevr/config/VRConfig.kt index 65487f07de..138b82e7b9 100644 --- a/server/core/src/main/java/dev/slimevr/config/VRConfig.kt +++ b/server/core/src/main/java/dev/slimevr/config/VRConfig.kt @@ -52,6 +52,8 @@ class VRConfig { val overlay: OverlayConfig = OverlayConfig() + val flightList: FlightListConfig = FlightListConfig() + init { // Initialize default settings for OSC Router oscRouter.portIn = 9002 @@ -102,7 +104,7 @@ class VRConfig { tracker.readConfig(config) if (tracker.isImu()) tracker.resetsHandler.readDriftCompensationConfig(driftCompensation) tracker.resetsHandler.readResetConfig(resetsConfig) - if (tracker.needsReset) { + if (tracker.allowReset) { tracker.saveMountingResetOrientation(config) } if (tracker.allowFiltering) { diff --git a/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt b/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt new file mode 100644 index 0000000000..cd39e0986a --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt @@ -0,0 +1,246 @@ +package dev.slimevr.flightlist + +import dev.slimevr.VRServer +import dev.slimevr.bridge.ISteamVRBridge +import dev.slimevr.games.vrchat.VRCConfigListener +import dev.slimevr.games.vrchat.VRCConfigRecommendedValues +import dev.slimevr.games.vrchat.VRCConfigValidity +import dev.slimevr.games.vrchat.VRCConfigValues +import dev.slimevr.tracking.trackers.Tracker +import dev.slimevr.tracking.trackers.TrackerStatus +import dev.slimevr.tracking.trackers.udp.TrackerDataType +import solarxr_protocol.datatypes.DeviceIdT +import solarxr_protocol.datatypes.TrackerIdT +import solarxr_protocol.rpc.* +import java.util.* +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.concurrent.timerTask +import kotlin.system.measureTimeMillis + + +interface FlightListListener { + fun onStepUpdate(step: FlightListStepT) +} + +class FlightListManager(private val vrServer: VRServer) : VRCConfigListener { + + private val listeners: MutableList = CopyOnWriteArrayList() + val steps: MutableList = mutableListOf() + + private val updateFlightListTimer = Timer("FetchVRCConfigTimer") + + init { + vrServer.vrcConfigManager.addListener(this) + + createSteps() + updateFlightListTimer.scheduleAtFixedRate( + timerTask { + updateFligtlist() + }, + 0, + 1000, + ) + } + + fun addListener(channel: FlightListListener) { + listeners.add(channel) + } + + fun removeListener(channel: FlightListListener) { + listeners.removeIf { channel == it } + } + + fun buildTrackersIds(trackers: List): Array { + return trackers.map { tracker -> + TrackerIdT().apply { + if (tracker.device != null) { + deviceId = DeviceIdT().apply { id = tracker.device.id } + } + trackerNum = tracker.trackerNum + } + }.toTypedArray() + } + + private fun createSteps() { + steps.add(FlightListStepT().apply { + id = FlightListStepId.TRACKERS_CALIBRATION + optional = false + ignorable = false + visibility = FlightListStepVisibility.ALWAYS + }) + + steps.add(FlightListStepT().apply { + id = FlightListStepId.FULL_RESET + optional = false + ignorable = false + visibility = FlightListStepVisibility.ALWAYS + }) + + steps.add(FlightListStepT().apply { + id = FlightListStepId.STEAMVR_DISCONNECTED + optional = true + ignorable = true + visibility = FlightListStepVisibility.WHEN_INVALID + }) + + steps.add(FlightListStepT().apply { + id = FlightListStepId.UNASSIGNED_HMD + optional = false + ignorable = false + visibility = FlightListStepVisibility.WHEN_INVALID + }) + + steps.add(FlightListStepT().apply { + id = FlightListStepId.TRACKER_ERROR + valid = true; // Default to valid + optional = false + ignorable = false + visibility = FlightListStepVisibility.WHEN_INVALID + }) + + if (vrServer.vrcConfigManager.isSupported) { + steps.add(FlightListStepT().apply { + id = FlightListStepId.VRCHAT_SETTINGS + optional = true + ignorable = true + visibility = FlightListStepVisibility.WHEN_INVALID + }) + } + } + + fun updateFligtlist() { + println(measureTimeMillis { + val assignedTrackers = + vrServer.allTrackers.filter { it.trackerPosition != null && it.status != TrackerStatus.DISCONNECTED } + val imuTrackers = + assignedTrackers.filter { it.isImu() && it.trackerDataType != TrackerDataType.FLEX_ANGLE } + + val trackersWithError = + imuTrackers.filter { it.status === TrackerStatus.ERROR } + updateValidity( + FlightListStepId.TRACKER_ERROR, + trackersWithError.isEmpty() + ) { + if (trackersWithError.isNotEmpty()) { + it.extraData = FlightListExtraDataUnion().apply { + type = FlightListExtraData.FlightListTrackerError + value = FlightListTrackerErrorT().apply { + trackersId = buildTrackersIds(trackersWithError) + } + } + } else { + it.extraData = null; + } + } + + val trackerRequireReset = imuTrackers.filter { + it.status !== TrackerStatus.ERROR && !it.isInternal && it.allowReset && it.needReset + } + updateValidity(FlightListStepId.FULL_RESET, trackerRequireReset.isEmpty()) { + if (trackerRequireReset.isNotEmpty()) { + it.extraData = FlightListExtraDataUnion().apply { + type = FlightListExtraData.FlightListTrackerReset + value = FlightListTrackerResetT().apply { + trackersId = buildTrackersIds(trackerRequireReset) + } + } + } else { + it.extraData = null; + } + } + + val hmd = + assignedTrackers.firstOrNull { it.isHmd && !it.isInternal && it.status.sendData } + val assignedHmd = hmd == null || vrServer.humanPoseManager.skeleton.headTracker != null + updateValidity(FlightListStepId.UNASSIGNED_HMD, assignedHmd) { + if (!assignedHmd && hmd !== null) { + it.extraData = FlightListExtraDataUnion().apply { + type = FlightListExtraData.FlightListUnassignedHMD + value = FlightListUnassignedHMDT().apply { + trackerId = TrackerIdT().apply { + if (hmd.device != null) { + deviceId = DeviceIdT().apply { id = hmd.device.id } + } + trackerNum = hmd.trackerNum + } + } + } + } else { + it.extraData = null + } + } + + val trackersNeedCalibration = imuTrackers.filter { + it.hasCompletedRestCalibration == false + } + updateValidity( + FlightListStepId.TRACKERS_CALIBRATION, + trackersNeedCalibration.isEmpty() + ) { + if (trackersNeedCalibration.isNotEmpty()) { + it.extraData = FlightListExtraDataUnion().apply { + type = FlightListExtraData.FlightListNeedCalibration + value = FlightListNeedCalibrationT().apply { + trackersId = buildTrackersIds(trackersNeedCalibration) + } + } + } else { + it.extraData = null + } + } + + val steamVRBridge = vrServer.getVRBridge(ISteamVRBridge::class.java) + if (steamVRBridge != null) { + val steamvrConnected = steamVRBridge.isConnected() + updateValidity( + FlightListStepId.STEAMVR_DISCONNECTED, + steamvrConnected + ) { + if (!steamvrConnected) { + it.extraData = FlightListExtraDataUnion().apply { + type = FlightListExtraData.FlightListSteamVRDisconnected + value = FlightListSteamVRDisconnectedT().apply { + bridgeSettingsName = steamVRBridge.getBridgeConfigKey() + } + } + } else { + it.extraData = null; + } + } + } + }) + } + + private fun updateValidity(id: Int, valid: Boolean, beforeUpdate: ((step: FlightListStepT) -> Unit)? = null) { + require(id != FlightListStepId.UNKNOWN) { + "id is unknown" + } + val step = steps.find { it.id == id } ?: return; + step.valid = valid + if (beforeUpdate != null) { + beforeUpdate(step) + } + listeners.forEach { it.onStepUpdate(step) } + } + + override fun onChange( + validity: VRCConfigValidity, + values: VRCConfigValues, + recommended: VRCConfigRecommendedValues + ) { + updateValidity( + FlightListStepId.VRCHAT_SETTINGS, + validity.javaClass.declaredFields.asSequence().all { p -> p.get(validity) == true } + ) + } + + + fun toggleStep(step: FlightListStepT) { + val ignoredSteps = vrServer.configManager.vrConfig.flightList.ignoredStepsIds; + if (!ignoredSteps.contains(step.id)) + ignoredSteps.add(step.id) + else + ignoredSteps.remove(step.id) + vrServer.configManager.saveConfig() + } +} diff --git a/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt b/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt index 8e54d5de8a..645021acd1 100644 --- a/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt +++ b/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt @@ -4,6 +4,7 @@ import dev.slimevr.VRServer import dev.slimevr.tracking.processor.config.SkeletonConfigToggles import dev.slimevr.tracking.trackers.TrackerPosition import dev.slimevr.tracking.trackers.TrackerUtils +import solarxr_protocol.rpc.FlightListStepId import java.util.concurrent.CopyOnWriteArrayList import kotlin.math.* diff --git a/server/core/src/main/java/dev/slimevr/osc/VMCHandler.kt b/server/core/src/main/java/dev/slimevr/osc/VMCHandler.kt index 473a94a251..281c022b87 100644 --- a/server/core/src/main/java/dev/slimevr/osc/VMCHandler.kt +++ b/server/core/src/main/java/dev/slimevr/osc/VMCHandler.kt @@ -290,7 +290,7 @@ class VMCHandler( userEditable = true, isComputed = position != null, usesTimeout = true, - needsReset = position != null, + allowReset = position != null, ) trackerDevice!!.trackers[trackerDevice!!.trackers.size] = tracker byTrackerNameTracker[name] = tracker diff --git a/server/core/src/main/java/dev/slimevr/osc/VRCOSCHandler.kt b/server/core/src/main/java/dev/slimevr/osc/VRCOSCHandler.kt index 7199082c08..8723f96f76 100644 --- a/server/core/src/main/java/dev/slimevr/osc/VRCOSCHandler.kt +++ b/server/core/src/main/java/dev/slimevr/osc/VRCOSCHandler.kt @@ -275,7 +275,7 @@ class VRCOSCHandler( hasPosition = true, userEditable = true, isComputed = true, - needsReset = trackerPosition != TrackerPosition.HEAD, + allowReset = trackerPosition != TrackerPosition.HEAD, usesTimeout = true, ) vrsystemTrackersDevice!!.trackers[trackerPosition.ordinal] = tracker @@ -368,7 +368,7 @@ class VRCOSCHandler( hasPosition = true, userEditable = true, isComputed = true, - needsReset = true, + allowReset = true, usesTimeout = true, ) oscTrackersDevice!!.trackers[trackerId] = tracker diff --git a/server/core/src/main/java/dev/slimevr/protocol/datafeed/DataFeedBuilder.java b/server/core/src/main/java/dev/slimevr/protocol/datafeed/DataFeedBuilder.java index ea9c8b6b1e..712d01fbaa 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/datafeed/DataFeedBuilder.java +++ b/server/core/src/main/java/dev/slimevr/protocol/datafeed/DataFeedBuilder.java @@ -130,7 +130,7 @@ public static int createTrackerInfos( TrackerInfo.addAllowDriftCompensation(fbb, false); } - if (tracker.getNeedsMounting()) { + if (tracker.getAllowMounting()) { Quaternion quaternion = tracker.getResetsHandler().getMountingOrientation(); Quaternion mountResetFix = tracker.getResetsHandler().getMountRotFix(); TrackerInfo.addMountingOrientation(fbb, createQuat(fbb, quaternion)); @@ -208,7 +208,7 @@ public static int createTrackerData( if (trackerTemperatureOffset != 0) TrackerData.addTemp(fbb, trackerTemperatureOffset); } - if (tracker.getNeedsMounting() && tracker.getHasRotation()) { + if (tracker.getAllowMounting() && tracker.getHasRotation()) { if (mask.getRotationReferenceAdjusted()) { TrackerData .addRotationReferenceAdjusted(fbb, createQuat(fbb, tracker.getRotation())); @@ -220,7 +220,7 @@ public static int createTrackerData( createQuat(fbb, tracker.getIdentityAdjustedRotation()) ); } - } else if (tracker.getNeedsReset() && tracker.getHasRotation()) { + } else if (tracker.getAllowReset() && tracker.getHasRotation()) { if (mask.getRotationReferenceAdjusted()) { TrackerData .addRotationReferenceAdjusted(fbb, createQuat(fbb, tracker.getRotation())); diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt index 6cf4615f90..db0b2923ed 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt @@ -8,6 +8,7 @@ import dev.slimevr.protocol.ProtocolHandler import dev.slimevr.protocol.datafeed.DataFeedBuilder import dev.slimevr.protocol.rpc.autobone.RPCAutoBoneHandler import dev.slimevr.protocol.rpc.firmware.RPCFirmwareUpdateHandler +import dev.slimevr.protocol.rpc.flightlist.RPCFlightListHandler import dev.slimevr.protocol.rpc.games.vrchat.RPCVRChatHandler import dev.slimevr.protocol.rpc.reset.RPCResetHandler import dev.slimevr.protocol.rpc.serial.RPCProvisioningHandler @@ -46,6 +47,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler + apiServer.apiConnections.forEach { it.send(fbb.dataBuffer()) } + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/status/StatusSystem.kt b/server/core/src/main/java/dev/slimevr/status/StatusSystem.kt index d7723a4500..81eb0b52d7 100644 --- a/server/core/src/main/java/dev/slimevr/status/StatusSystem.kt +++ b/server/core/src/main/java/dev/slimevr/status/StatusSystem.kt @@ -9,7 +9,6 @@ class StatusSystem { private val listeners: MutableList = CopyOnWriteArrayList() private val statuses: MutableMap = HashMap() private val prioritizedStatuses: MutableSet = HashSet() - private val idCounter = AtomicInteger(1) fun addListener(listener: StatusListener) { listeners.add(listener) @@ -27,36 +26,6 @@ class StatusSystem { status }.toTypedArray() - /** - * @return the ID of the status, 0 is not a valid ID, can be used as replacement of null - */ - @JvmName("addStatusInt") - fun addStatus(statusData: StatusDataUnion, prioritized: Boolean = false): UInt { - val id = idCounter.getAndUpdate { - (it.toUInt() + 1u).toInt() // the simple way of making unsigned math - } - statuses[id] = statusData - if (prioritized) { - prioritizedStatuses.add(id) - } - - listeners.forEach { - it.onStatusChanged(id.toUInt(), statusData, prioritized) - } - - return id.toUInt() - } - - @JvmName("removeStatusInt") - fun removeStatus(id: UInt) { - statuses.remove(id.toInt()) - prioritizedStatuses.remove(id.toInt()) - - listeners.forEach { - it.onStatusRemoved(id) - } - } - fun hasStatusType(dataType: Byte): Boolean = statuses.any { it.value.type == dataType } diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt index 9c878e554c..e500054e8f 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt @@ -23,11 +23,6 @@ import io.github.axisangles.ktmath.Quaternion.Companion.IDENTITY import io.github.axisangles.ktmath.Vector3 import io.github.axisangles.ktmath.Vector3.Companion.POS_Y import org.apache.commons.math3.util.Precision -import solarxr_protocol.datatypes.DeviceIdT -import solarxr_protocol.datatypes.TrackerIdT -import solarxr_protocol.rpc.StatusData -import solarxr_protocol.rpc.StatusDataUnion -import solarxr_protocol.rpc.StatusUnassignedHMDT import java.util.function.Consumer import kotlin.math.* @@ -526,7 +521,7 @@ class HumanPoseManager(val server: VRServer?) { for (tracker in server!!.allTrackers) { if (( tracker.isImu() && - tracker.needsReset + tracker.allowReset ) && tracker.resetsHandler.lastResetQuaternion != null ) { if (trackersDriftText.isNotEmpty()) { @@ -665,51 +660,12 @@ class HumanPoseManager(val server: VRServer?) { return } server.allTrackers - .filter { !it.isInternal && it.trackerPosition != null } + .filter { it.trackerPosition != null } .forEach { - it.checkReportRequireReset() - } - } - - private var lastMissingHmdStatus = 0u - fun checkReportMissingHmd() { - // Check if this is main skeleton, there is no head tracker currently, - // and there is an available HMD one - if (server == null) return - val tracker = VRServer.instance.allTrackers.firstOrNull { it.isHmd && !it.isInternal && it.status.sendData } - if (skeleton.headTracker == null && - lastMissingHmdStatus == 0u && - tracker != null - ) { - reportMissingHmd(tracker) - } else if (lastMissingHmdStatus != 0u && - (skeleton.headTracker != null || tracker == null) - ) { - server.statusSystem.removeStatus(lastMissingHmdStatus) - lastMissingHmdStatus = 0u - } - } - - private fun reportMissingHmd(tracker: Tracker) { - require(lastMissingHmdStatus == 0u) { - "${::lastMissingHmdStatus.name} must be 0u, but was $lastMissingHmdStatus" - } - require(server != null) { - "${::server.name} must not be null" - } - - val status = StatusDataUnion().apply { - type = StatusData.StatusUnassignedHMD - value = StatusUnassignedHMDT().apply { - trackerId = TrackerIdT().apply { - if (tracker.device != null) { - deviceId = DeviceIdT().apply { id = tracker.device.id } - } - trackerNum = tracker.trackerNum + if (it.allowReset && !it.needReset) { + it.needReset = true } } - } - lastMissingHmdStatus = server.statusSystem.addStatus(status, true) } // #endregion diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt index 3068c65bd2..d6a0af0b1f 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt @@ -119,7 +119,6 @@ class HumanSkeleton( var headTracker: Tracker? by Delegates.observable(null) { _, old, new -> if (old == new) return@observable - humanPoseManager.checkReportMissingHmd() humanPoseManager.checkTrackersRequiringReset() } var neckTracker: Tracker? = null @@ -1512,7 +1511,7 @@ class HumanSkeleton( // Resets all axes of the trackers with the HMD as reference. for (tracker in trackersToReset) { // Only reset if tracker needsReset - if (tracker != null && (tracker.needsReset || tracker.isHmd)) { + if (tracker != null && (tracker.allowReset || tracker.isHmd)) { tracker.resetsHandler.resetFull(referenceRotation) } } @@ -1533,14 +1532,14 @@ class HumanSkeleton( var referenceRotation = IDENTITY headTracker?.let { // Only reset if head needsReset and isn't computed - if (it.needsReset && !it.isComputed) { + if (it.allowReset && !it.isComputed) { it.resetsHandler.resetYaw(referenceRotation) } referenceRotation = it.getRotation() } for (tracker in trackersToReset) { // Only reset if tracker needsReset - if (tracker != null && tracker.needsReset) { + if (tracker != null && tracker.allowReset) { tracker.resetsHandler.resetYaw(referenceRotation) } } @@ -1557,7 +1556,7 @@ class HumanSkeleton( // If there's a server present (required for status) and any tracker reports a // non-zero reset status (indicates reset required), then block mounting reset, // as it requires a full reset first - if (humanPoseManager.server != null && trackersToReset.any { it != null && it.lastResetStatus != 0u }) { + if (humanPoseManager.server != null && trackersToReset.any { it != null && it.needReset }) { LogManager.info("[HumanSkeleton] Reset: mounting ($resetSourceName) failed, reset required") return } @@ -1566,14 +1565,14 @@ class HumanSkeleton( var referenceRotation = IDENTITY headTracker?.let { // Only reset if head needsMounting or is computed but not HMD - if (it.needsMounting || (it.isComputed && !it.isHmd)) { + if (it.allowMounting || (it.isComputed && !it.isHmd)) { it.resetsHandler.resetMounting(referenceRotation) } referenceRotation = it.getRotation() } for (tracker in trackersToReset) { // Only reset if tracker needsMounting - if (tracker != null && tracker.needsMounting) { + if (tracker != null && tracker.allowMounting) { tracker.resetsHandler.resetMounting(referenceRotation) } } @@ -1585,11 +1584,11 @@ class HumanSkeleton( @VRServerThread fun clearTrackersMounting(resetSourceName: String?) { headTracker?.let { - if (it.needsMounting) it.resetsHandler.clearMounting() + if (it.allowMounting) it.resetsHandler.clearMounting() } for (tracker in trackersToReset) { if (tracker != null && - tracker.needsMounting + tracker.allowMounting ) { tracker.resetsHandler.clearMounting() } diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt index ddbd172a95..43d08185c7 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt @@ -9,12 +9,6 @@ import dev.slimevr.tracking.trackers.udp.TrackerDataType import io.eiren.util.BufferedTimer import io.github.axisangles.ktmath.Quaternion import io.github.axisangles.ktmath.Vector3 -import solarxr_protocol.datatypes.DeviceIdT -import solarxr_protocol.datatypes.TrackerIdT -import solarxr_protocol.rpc.StatusData -import solarxr_protocol.rpc.StatusDataUnion -import solarxr_protocol.rpc.StatusTrackerErrorT -import solarxr_protocol.rpc.StatusTrackerResetT import kotlin.properties.Delegates const val TIMEOUT_MS = 2_000L @@ -69,9 +63,23 @@ class Tracker @JvmOverloads constructor( * [trackRotDirection] is set to false. */ val allowFiltering: Boolean = false, - val needsReset: Boolean = false, - val needsMounting: Boolean = false, + + /** + * If true, the tracker can be reset + */ + val allowReset: Boolean = false, + /** + * If true, the tracker can do mounting calibration + */ + val allowMounting: Boolean = false, + val isHmd: Boolean = false, + + /** + * If true, the tracker need the user to perform a reset + */ + var needReset: Boolean = false, + /** * Whether to track the direction of the tracker's rotation * (positive vs negative rotation). This needs to be disabled for AutoBone and @@ -105,33 +113,26 @@ class Tracker @JvmOverloads constructor( var magStatus: MagnetometerStatus = magStatus private set + /** + * Watch the rest calibration status + */ + var hasCompletedRestCalibration: Boolean? = null /** * If the tracker has gotten disconnected after it was initialized first time */ - var statusResetRecently = false - private var alreadyInitialized = false var status: TrackerStatus by Delegates.observable(TrackerStatus.DISCONNECTED) { _, old, new -> if (old == new) return@observable - if (!new.reset) { - if (alreadyInitialized) { - statusResetRecently = true - } - alreadyInitialized = true + if (allowReset && !old.reset && new.reset && !needReset) { + needReset = true; } + if (!isInternal && VRServer.instanceInitialized) { // If the status of a non-internal tracker has changed, inform // the VRServer to recreate the skeleton, as it may need to // assign or un-assign the tracker to a body part VRServer.instance.updateSkeletonModel() VRServer.instance.refreshTrackersDriftCompensationEnabled() - - if (isHmd) { - VRServer.instance.humanPoseManager.checkReportMissingHmd() - } - checkReportErrorStatus() - checkReportRequireReset() - VRServer.instance.trackerStatusChanged(this, old, new) } } @@ -139,16 +140,18 @@ class Tracker @JvmOverloads constructor( var trackerPosition: TrackerPosition? by Delegates.observable(trackerPosition) { _, old, new -> if (old == new) return@observable + if (allowReset && !needReset) { + needReset = true; + } + if (!isInternal) { // Set default mounting orientation for that body part new?.let { resetsHandler.mountingOrientation = it.defaultMounting() } - - checkReportRequireReset() } } // Computed value to simplify availability checks - val hasAdjustedRotation = hasRotation && (allowFiltering || needsReset) + val hasAdjustedRotation = hasRotation && (allowFiltering || allowReset) /** * It's like the ID, but it should be local to the device if it has one @@ -157,11 +160,11 @@ class Tracker @JvmOverloads constructor( init { // IMPORTANT: Look here for the required states of inputs - require(!needsReset || (hasRotation && needsReset)) { - "If ${::needsReset.name} is true, then ${::hasRotation.name} must also be true" + require(!allowReset || (hasRotation && allowReset)) { + "If ${::allowReset.name} is true, then ${::hasRotation.name} must also be true" } - require(!needsMounting || (needsReset && needsMounting)) { - "If ${::needsMounting.name} is true, then ${::needsReset.name} must also be true" + require(!allowMounting || (allowReset && allowMounting)) { + "If ${::allowMounting.name} is true, then ${::allowReset.name} must also be true" } require(!isHmd || (hasPosition && isHmd)) { "If ${::isHmd.name} is true, then ${::hasPosition.name} must also be true" @@ -171,73 +174,6 @@ class Tracker @JvmOverloads constructor( // } } - fun checkReportRequireReset() { - if (needsReset && trackerPosition != null && lastResetStatus == 0u && - !status.reset && (isImu() || !statusResetRecently && trackerDataType != TrackerDataType.FLEX_ANGLE) - ) { - reportRequireReset() - } else if (lastResetStatus != 0u && (trackerPosition == null || status.reset)) { - VRServer.instance.statusSystem.removeStatus(lastResetStatus) - lastResetStatus = 0u - } - } - - /** - * If 0 then it's null - */ - var lastResetStatus = 0u - private fun reportRequireReset() { - require(lastResetStatus == 0u) { - "lastResetStatus must be 0u, but was $lastResetStatus" - } - - val tempTrackerNum = this.trackerNum - val statusMsg = StatusTrackerResetT().apply { - trackerId = TrackerIdT().apply { - if (device != null) { - deviceId = DeviceIdT().apply { id = device.id } - } - trackerNum = tempTrackerNum - } - } - val status = StatusDataUnion().apply { - type = StatusData.StatusTrackerReset - value = statusMsg - } - lastResetStatus = VRServer.instance.statusSystem.addStatus(status, true) - } - - private fun checkReportErrorStatus() { - if (status == TrackerStatus.ERROR && lastErrorStatus == 0u) { - reportErrorStatus() - } else if (lastErrorStatus != 0u && status != TrackerStatus.ERROR) { - VRServer.instance.statusSystem.removeStatus(lastErrorStatus) - lastErrorStatus = 0u - } - } - - var lastErrorStatus = 0u - private fun reportErrorStatus() { - require(lastErrorStatus == 0u) { - "lastResetStatus must be 0u, but was $lastErrorStatus" - } - - val tempTrackerNum = this.trackerNum - val statusMsg = StatusTrackerErrorT().apply { - trackerId = TrackerIdT().apply { - if (device != null) { - deviceId = DeviceIdT().apply { id = device.id } - } - trackerNum = tempTrackerNum - } - } - val status = StatusDataUnion().apply { - type = StatusData.StatusTrackerError - value = statusMsg - } - lastErrorStatus = VRServer.instance.statusSystem.addStatus(status, true) - } - /** * Reads/loads from the given config */ @@ -248,7 +184,7 @@ class Tracker @JvmOverloads constructor( config.designation?.let { designation -> getByDesignation(designation)?.let { trackerPosition = it } } ?: run { trackerPosition = null } - if (needsMounting) { + if (allowMounting) { // Load manual mounting config.mountingOrientation?.let { resetsHandler.mountingOrientation = it.toValue() } } @@ -262,11 +198,6 @@ class Tracker @JvmOverloads constructor( resetsHandler.allowDriftCompensation = it } } - if (!isInternal && - !(!isImu() && (trackerPosition == TrackerPosition.LEFT_HAND || trackerPosition == TrackerPosition.RIGHT_HAND)) - ) { - checkReportRequireReset() - } } /** @@ -275,7 +206,7 @@ class Tracker @JvmOverloads constructor( fun writeConfig(config: TrackerConfig) { trackerPosition?.let { config.designation = it.designation } ?: run { config.designation = null } customName?.let { config.customName = it } - if (needsMounting) { + if (allowMounting) { // Save manual mounting config.mountingOrientation = resetsHandler.mountingOrientation.toObject() } @@ -345,7 +276,7 @@ class Tracker @JvmOverloads constructor( */ private fun getAdjustedRotation(): Quaternion { // Reset if needed and is not computed and internal - return if (needsReset && !(isComputed && isInternal) && trackerDataType == TrackerDataType.ROTATION) { + return if (allowReset && !(isComputed && isInternal) && trackerDataType == TrackerDataType.ROTATION) { // Adjust to reset, mounting and drift compensation resetsHandler.getReferenceAdjustedDriftRotationFrom(_rotation) } else { @@ -360,7 +291,7 @@ class Tracker @JvmOverloads constructor( */ fun getIdentityAdjustedRotation(): Quaternion { // Reset if needed or is a computed tracker besides head - return if (needsReset && !(isComputed && trackerPosition != TrackerPosition.HEAD) && trackerDataType == TrackerDataType.ROTATION) { + return if (allowReset && !(isComputed && trackerPosition != TrackerPosition.HEAD) && trackerDataType == TrackerDataType.ROTATION) { // Adjust to reset and mounting resetsHandler.getIdentityAdjustedDriftRotationFrom(_rotation) } else { @@ -381,7 +312,7 @@ class Tracker @JvmOverloads constructor( /** * Gets the world-adjusted acceleration */ - fun getAcceleration(): Vector3 = if (needsReset) { + fun getAcceleration(): Vector3 = if (allowReset) { resetsHandler.getReferenceAdjustedAccel(_rotation, _acceleration) } else { _acceleration diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerResetsHandler.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerResetsHandler.kt index 8231945c15..416573285b 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerResetsHandler.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerResetsHandler.kt @@ -296,7 +296,7 @@ class TrackerResetsHandler(val tracker: Tracker) { val mountingAdjustedRotation = tracker.getRawRotation() * mountingOrientation // Gyrofix - if (tracker.needsMounting || (tracker.trackerPosition == TrackerPosition.HEAD && !tracker.isHmd)) { + if (tracker.allowMounting || (tracker.trackerPosition == TrackerPosition.HEAD && !tracker.isHmd)) { gyroFix = if (tracker.isComputed) { fixGyroscope(tracker.getRawRotation()) } else { @@ -328,7 +328,7 @@ class TrackerResetsHandler(val tracker: Tracker) { } // Rotate attachmentFix by 180 degrees as a workaround for t-pose (down) - if (tposeDownFix != Quaternion.IDENTITY && tracker.needsMounting) { + if (tposeDownFix != Quaternion.IDENTITY && tracker.allowMounting) { attachmentFix *= HalfHorizontal } @@ -346,9 +346,8 @@ class TrackerResetsHandler(val tracker: Tracker) { } private fun postProcessResetFull() { - if (this.tracker.lastResetStatus != 0u) { - VRServer.instance.statusSystem.removeStatus(this.tracker.lastResetStatus) - this.tracker.lastResetStatus = 0u + if (this.tracker.needReset) { + this.tracker.needReset = false } tracker.resetFilteringQuats() @@ -390,13 +389,7 @@ class TrackerResetsHandler(val tracker: Tracker) { yawFixSmoothIncremental = yawFixOld / yawFix } - // Remove the status if yaw reset was performed after the tracker - // was disconnected and connected. - if (this.tracker.lastResetStatus != 0u && this.tracker.statusResetRecently) { - VRServer.instance.statusSystem.removeStatus(this.tracker.lastResetStatus) - this.tracker.statusResetRecently = false - this.tracker.lastResetStatus = 0u - } + this.tracker.needReset = false tracker.resetFilteringQuats() } diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt index 484e11583f..257e884780 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt @@ -175,7 +175,16 @@ class TrackersUDPServer(private val port: Int, name: String, private val tracker // Set up new sensor for older firmware. // Firmware after 7 should send sensor status packet and sensor // will be created when it's received - setUpSensor(connection, 0, handshake.imuType, 1, MagnetometerStatus.NOT_SUPPORTED, null, TrackerDataType.ROTATION) + setUpSensor( + connection, + 0, + handshake.imuType, + 1, + MagnetometerStatus.NOT_SUPPORTED, + null, + TrackerDataType.ROTATION, + null, + ) } connection } @@ -187,7 +196,16 @@ class TrackersUDPServer(private val port: Int, name: String, private val tracker } private val mainScope = CoroutineScope(SupervisorJob()) - private fun setUpSensor(connection: UDPDevice, trackerId: Int, sensorType: IMUType, sensorStatus: Int, magStatus: MagnetometerStatus, trackerPosition: TrackerPosition?, trackerDataType: TrackerDataType) { + private fun setUpSensor( + connection: UDPDevice, + trackerId: Int, + sensorType: IMUType, + sensorStatus: Int, + magStatus: MagnetometerStatus, + trackerPosition: TrackerPosition?, + trackerDataType: TrackerDataType, + hasCompletedRestCalibration: Boolean? + ) { LogManager.info("[TrackerServer] Sensor $trackerId for ${connection.name} status: $sensorStatus") var imuTracker = connection.getTracker(trackerId) if (imuTracker == null) { @@ -208,8 +226,8 @@ class TrackersUDPServer(private val port: Int, name: String, private val tracker userEditable = true, imuType = if (trackerDataType == TrackerDataType.ROTATION) sensorType else null, allowFiltering = true, - needsReset = true, - needsMounting = true, + allowReset = true, + allowMounting = true, usesTimeout = true, magStatus = magStatus, trackerDataType = trackerDataType, @@ -221,6 +239,8 @@ class TrackersUDPServer(private val port: Int, name: String, private val tracker val status = UDPPacket15SensorInfo.getStatus(sensorStatus) if (status != null) imuTracker.status = status + imuTracker.hasCompletedRestCalibration = hasCompletedRestCalibration + if (magStatus == MagnetometerStatus.NOT_SUPPORTED) return if (magStatus == MagnetometerStatus.ENABLED && (!VRServer.instance.configManager.vrConfig.server.useMagnetometerOnAllTrackers || imuTracker.config.shouldHaveMagEnabled == false) @@ -481,6 +501,7 @@ class TrackersUDPServer(private val port: Int, name: String, private val tracker magStatus, packet.trackerPosition, packet.trackerDataType, + packet.hasCompletedRestCalibration, ) // Send ack bb.limit(bb.capacity()) diff --git a/server/core/src/test/java/dev/slimevr/unit/LegTweaksTests.kt b/server/core/src/test/java/dev/slimevr/unit/LegTweaksTests.kt index 4676adc5fd..830a60f9db 100644 --- a/server/core/src/test/java/dev/slimevr/unit/LegTweaksTests.kt +++ b/server/core/src/test/java/dev/slimevr/unit/LegTweaksTests.kt @@ -26,8 +26,8 @@ class LegTweaksTests { hasRotation = true, isComputed = true, imuType = null, - needsReset = false, - needsMounting = false, + allowReset = false, + allowMounting = false, isHmd = true, trackRotDirection = false, ) diff --git a/server/core/src/test/java/dev/slimevr/unit/MountingResetTests.kt b/server/core/src/test/java/dev/slimevr/unit/MountingResetTests.kt index c046ab9144..c7de28c0d3 100644 --- a/server/core/src/test/java/dev/slimevr/unit/MountingResetTests.kt +++ b/server/core/src/test/java/dev/slimevr/unit/MountingResetTests.kt @@ -45,8 +45,8 @@ class MountingResetTests { null, hasRotation = true, imuType = IMUType.UNKNOWN, - needsReset = true, - needsMounting = true, + allowReset = true, + allowMounting = true, trackRotDirection = false, ) @@ -130,8 +130,8 @@ class MountingResetTests { null, hasRotation = true, imuType = IMUType.UNKNOWN, - needsReset = true, - needsMounting = true, + allowReset = true, + allowMounting = true, trackRotDirection = false, ) diff --git a/server/core/src/test/java/dev/slimevr/unit/ReferenceAdjustmentsTests.kt b/server/core/src/test/java/dev/slimevr/unit/ReferenceAdjustmentsTests.kt index a28d8c85e8..5a90b1ddb9 100644 --- a/server/core/src/test/java/dev/slimevr/unit/ReferenceAdjustmentsTests.kt +++ b/server/core/src/test/java/dev/slimevr/unit/ReferenceAdjustmentsTests.kt @@ -93,7 +93,7 @@ class ReferenceAdjustmentsTests { null, hasRotation = true, imuType = IMUType.UNKNOWN, - needsReset = true, + allowReset = true, ) tracker.setRotation(trackerQuat) tracker.resetsHandler.resetFull(referenceQuat) @@ -125,7 +125,7 @@ class ReferenceAdjustmentsTests { null, hasRotation = true, imuType = IMUType.UNKNOWN, - needsReset = true, + allowReset = true, ) tracker.setRotation(trackerQuat) tracker.resetsHandler.resetYaw(referenceQuat) @@ -153,7 +153,7 @@ class ReferenceAdjustmentsTests { null, hasRotation = true, imuType = IMUType.UNKNOWN, - needsReset = true, + allowReset = true, ) tracker.setRotation(trackerQuat) tracker.resetsHandler.resetFull(referenceQuat) diff --git a/server/core/src/test/java/dev/slimevr/unit/TestTrackerSet.kt b/server/core/src/test/java/dev/slimevr/unit/TestTrackerSet.kt index 92ec22b602..cac3f38514 100644 --- a/server/core/src/test/java/dev/slimevr/unit/TestTrackerSet.kt +++ b/server/core/src/test/java/dev/slimevr/unit/TestTrackerSet.kt @@ -58,8 +58,8 @@ class TestTrackerSet( hasPosition = positional || isHmd, hasRotation = true, isComputed = computed || isHmd, - needsReset = resetHead || !isHmd, - needsMounting = resetHead || !isHmd, + allowReset = resetHead || !isHmd, + allowMounting = resetHead || !isHmd, isHmd = isHmd, trackRotDirection = false, ) diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/SteamVRBridge.kt b/server/desktop/src/main/java/dev/slimevr/desktop/platform/SteamVRBridge.kt index 52fc936115..ee651a807d 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/SteamVRBridge.kt +++ b/server/desktop/src/main/java/dev/slimevr/desktop/platform/SteamVRBridge.kt @@ -15,20 +15,18 @@ import dev.slimevr.tracking.trackers.TrackerRole.Companion.getById import dev.slimevr.tracking.trackers.TrackerUtils.getTrackerForSkeleton import dev.slimevr.util.ann.VRServerThread import io.eiren.util.collections.FastList -import solarxr_protocol.rpc.StatusData -import solarxr_protocol.rpc.StatusDataUnion -import solarxr_protocol.rpc.StatusSteamVRDisconnectedT abstract class SteamVRBridge( protected val server: VRServer, threadName: String, bridgeName: String, - protected val bridgeSettingsKey: String, + val bridgeSettingsKey: String, protected val shareableTrackers: List, ) : ProtobufBridge(bridgeName), Runnable { protected val runnerThread: Thread = Thread(this, threadName) protected val config: BridgeConfig = server.configManager.vrConfig.getBridge(bridgeSettingsKey) + var connected: Boolean = false; @VRServerThread override fun startBridge() { @@ -187,7 +185,7 @@ abstract class SteamVRBridge( hasRotation = true, userEditable = true, isComputed = true, - needsReset = true, + allowReset = true, isHmd = isHmd, ) @@ -427,33 +425,13 @@ abstract class SteamVRBridge( } } - /** - * When 0, then it means null - */ - protected var lastSteamVRStatus: Int = 0 - @BridgeThread protected fun reportDisconnected() { - if (lastSteamVRStatus != 0) { - return - } - val statusData = StatusSteamVRDisconnectedT() - statusData.bridgeSettingsName = bridgeSettingsKey - - val status = StatusDataUnion() - status.type = StatusData.StatusSteamVRDisconnected - status.value = statusData - lastSteamVRStatus = instance.statusSystem - .addStatus(status, false).toInt() + connected = false; } @BridgeThread protected fun reportConnected() { - if (lastSteamVRStatus == 0) { - return - } - instance.statusSystem - .removeStatus(lastSteamVRStatus.toUInt()) - lastSteamVRStatus = 0 + connected = true; } } diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketBridge.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketBridge.java index 3b3580c73e..50436a8895 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketBridge.java +++ b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketBridge.java @@ -8,6 +8,7 @@ import dev.slimevr.tracking.trackers.Tracker; import io.eiren.util.ann.ThreadSafe; import io.eiren.util.logging.LogManager; +import org.jetbrains.annotations.NotNull; import java.io.File; import java.io.IOException; @@ -25,6 +26,7 @@ public class UnixSocketBridge extends SteamVRBridge implements AutoCloseable { public final String socketPath; public final UnixDomainSocketAddress socketAddress; + private final String bridgeSettingsKey; private final ByteBuffer dst = ByteBuffer.allocate(2048).order(ByteOrder.LITTLE_ENDIAN); private final ByteBuffer src = ByteBuffer.allocate(2048).order(ByteOrder.LITTLE_ENDIAN); @@ -41,6 +43,7 @@ public UnixSocketBridge( List shareableTrackers ) { super(server, "Named socket thread", bridgeName, bridgeSettingsKey, shareableTrackers); + this.bridgeSettingsKey = bridgeSettingsKey; this.socketPath = socketPath; this.socketAddress = UnixDomainSocketAddress.of(socketPath); @@ -241,5 +244,11 @@ public void close() throws Exception { public boolean isConnected() { return channel != null && channel.isConnected(); } + + @NotNull + @Override + public String getBridgeConfigKey() { + return bridgeSettingsKey; + } } diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeBridge.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeBridge.java index ab577eb9af..5099fccd8d 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeBridge.java +++ b/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeBridge.java @@ -14,6 +14,7 @@ import dev.slimevr.tracking.trackers.Tracker; import io.eiren.util.ann.ThreadSafe; import io.eiren.util.logging.LogManager; +import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.util.List; @@ -37,6 +38,7 @@ public class WindowsNamedPipeBridge extends SteamVRBridge { private static final Advapi32 adv32 = Advapi32.INSTANCE; protected final String pipeName; + protected final String bridgeSettingsKey; private final byte[] buffArray = new byte[2048]; protected WindowsPipe pipe; protected WinNT.HANDLE openEvent = k32.CreateEvent(null, false, false, null); @@ -63,6 +65,7 @@ public WindowsNamedPipeBridge( ) { super(server, "Named pipe thread", bridgeName, bridgeSettingsKey, shareableTrackers); this.pipeName = pipeName; + this.bridgeSettingsKey = bridgeSettingsKey; overlappedWait.hEvent = rxEvent; } @@ -321,4 +324,10 @@ private boolean tryOpeningPipe(WindowsPipe pipe) { public boolean isConnected() { return pipe != null && pipe.state == PipeState.OPEN; } + + @NotNull + @Override + public String getBridgeConfigKey() { + return this.bridgeSettingsKey; + } } diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/tracking/trackers/hid/TrackersHID.kt b/server/desktop/src/main/java/dev/slimevr/desktop/tracking/trackers/hid/TrackersHID.kt index 5a61e7a810..5472715d76 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/tracking/trackers/hid/TrackersHID.kt +++ b/server/desktop/src/main/java/dev/slimevr/desktop/tracking/trackers/hid/TrackersHID.kt @@ -24,7 +24,6 @@ import org.hid4java.jna.HidApi import org.hid4java.jna.HidDeviceInfoStructure import java.nio.ByteBuffer import java.util.function.Consumer -import kotlin.experimental.and import kotlin.math.* private const val HID_TRACKER_RECEIVER_VID = 0x1209 @@ -130,8 +129,8 @@ class TrackersHID(name: String, private val trackersConsumer: Consumer) userEditable = true, imuType = sensorType, allowFiltering = true, - needsReset = true, - needsMounting = true, + allowReset = true, + allowMounting = true, usesTimeout = false, magStatus = magStatus, ) From 53e5d29e7af4f71200e99cc208bd1d3228d7f4e4 Mon Sep 17 00:00:00 2001 From: loucass003 Date: Tue, 13 May 2025 01:46:30 +0200 Subject: [PATCH 02/34] sync with main --- solarxr-protocol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solarxr-protocol b/solarxr-protocol index 98cb652b40..d17cc46bd8 160000 --- a/solarxr-protocol +++ b/solarxr-protocol @@ -1 +1 @@ -Subproject commit 98cb652b40be5ec7b88b4e413e33888a944ebd63 +Subproject commit d17cc46bd834ea624a7c7316a8391a208a2ef1e8 From d15695212f3f5d19c61120d6fd945a91521966b4 Mon Sep 17 00:00:00 2001 From: loucass003 Date: Tue, 13 May 2025 17:16:34 +0200 Subject: [PATCH 03/34] progress --- gui/src/hooks/session-flightlist.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/gui/src/hooks/session-flightlist.ts b/gui/src/hooks/session-flightlist.ts index 7067e353aa..f7f9f20c00 100644 --- a/gui/src/hooks/session-flightlist.ts +++ b/gui/src/hooks/session-flightlist.ts @@ -58,10 +58,7 @@ export function useSessionFlightlist() { (visibility === FlightListStepVisibility.WHEN_INVALID && ['invalid', 'skipped'].includes(status)) ); - setSteps(steps); - - console.log(steps); }); useRPCPacket( @@ -69,7 +66,6 @@ export function useSessionFlightlist() { (data: FlightListStepChangeResponseT) => { const step = data.step; if (!step) throw 'invalid state - step should be set'; - console.log('Update', step); setSteps((steps) => { const visibleSteps = steps.filter((step) => !ignoredSteps.includes(step.id)); const newsteps = visibleSteps From 0f29e90eb51f0fa6ff29f2846828020cab4c4fa8 Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Wed, 9 Jul 2025 17:33:52 +0200 Subject: [PATCH 04/34] FUCK --- gui/src/components/SessionFlightList.tsx | 94 ++++++++++++++----- gui/src/components/Toolbar.tsx | 26 +---- .../trackers-assign/TrackerAssignment.tsx | 2 +- gui/src/hooks/session-flightlist.ts | 86 ++++++++++------- .../slimevr/flightlist/FlightListManager.kt | 5 +- 5 files changed, 130 insertions(+), 83 deletions(-) diff --git a/gui/src/components/SessionFlightList.tsx b/gui/src/components/SessionFlightList.tsx index ea7fea1da4..ba363b5593 100644 --- a/gui/src/components/SessionFlightList.tsx +++ b/gui/src/components/SessionFlightList.tsx @@ -1,50 +1,102 @@ import { flightlistIdtoLabel, FlightListStep, + SessionFlightListContext, useSessionFlightlist, } from '@/hooks/session-flightlist'; import { CheckIcon } from './commons/icon/CheckIcon'; import classNames from 'classnames'; import { Localized } from '@fluent/react'; +import { + FlightListStepId, + FlightListTrackerResetT, + ResetType, +} from 'solarxr-protocol'; +import { ReactNode } from 'react'; +import { Typography } from './commons/Typography'; +import { Button } from './commons/Button'; +import { ResetButton } from './home/ResetButton'; function Step({ - step: { status, id, optional }, + step: { status, id, optional, firstInvalid: firstActive }, index, + children, }: { step: FlightListStep; index: number; + children: ReactNode; }) { return ( -
-
- {status === 'complete' && } - {status === 'invalid' && index} - {status === 'blocked' && index} - {status === 'skipped' && index} -
-
- +
+
+
+ {status === 'complete' && } + {status === 'invalid' && index} + {status === 'blocked' && index} + {status === 'skipped' && index} +
+
+ +
+ {firstActive &&
{children}
}
); } +const stepContentLookup: Record< + number, + (step: FlightListStep, context: SessionFlightListContext) => JSX.Element +> = { + [FlightListStepId.FULL_RESET]: (step) => { + const data = step.extraData as FlightListTrackerResetT; + return ( +
+ Some Trackers need a reset to be performed +
+ +
+
+ ); + }, + [FlightListStepId.STEAMVR_DISCONNECTED]: (step, { toggle }) => { + return ( + <> +
+ + SteamVR is not running. Are you using it for vr? + +
+ + +
+
+ + ); + }, +}; + export function SessionFlightList() { - const { steps } = useSessionFlightlist(); + const context = useSessionFlightlist(); + const { steps } = context; return (
{steps.map((step, index) => ( - + + {stepContentLookup[step.id]?.(step, context) || undefined} + ))}
); diff --git a/gui/src/components/Toolbar.tsx b/gui/src/components/Toolbar.tsx index 329e1fb09b..f1da523b6a 100644 --- a/gui/src/components/Toolbar.tsx +++ b/gui/src/components/Toolbar.tsx @@ -1,38 +1,14 @@ -import { - ResetType, - RpcMessage, - SettingsRequestT, - SettingsResponseT, -} from 'solarxr-protocol'; +import { ResetType } from 'solarxr-protocol'; import { ResetButton } from './home/ResetButton'; -import { ClearDriftCompensationButton } from './ClearDriftCompensationButton'; -import { useWebsocketAPI } from '@/hooks/websocket-api'; -import { useEffect, useState } from 'react'; import { ClearMountingButton } from './ClearMountingButton'; export function Toolbar() { - const { useRPCPacket, sendRPCPacket } = useWebsocketAPI(); - const [driftCompensationEnabled, setDriftCompensationEnabled] = - useState(false); - - useEffect(() => { - sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT()); - }, []); - - useRPCPacket(RpcMessage.SettingsResponse, (settings: SettingsResponseT) => { - if (settings.driftCompensation != null) - setDriftCompensationEnabled(settings.driftCompensation.enabled); - }); - return (
-
); } diff --git a/gui/src/components/onboarding/pages/trackers-assign/TrackerAssignment.tsx b/gui/src/components/onboarding/pages/trackers-assign/TrackerAssignment.tsx index 35a400e04e..e08c1c14cd 100644 --- a/gui/src/components/onboarding/pages/trackers-assign/TrackerAssignment.tsx +++ b/gui/src/components/onboarding/pages/trackers-assign/TrackerAssignment.tsx @@ -281,7 +281,7 @@ export function TrackersAssignPage() {
- + {l10n.getString('onboarding-assign_trackers-title')} diff --git a/gui/src/hooks/session-flightlist.ts b/gui/src/hooks/session-flightlist.ts index f7f9f20c00..030568d8ea 100644 --- a/gui/src/hooks/session-flightlist.ts +++ b/gui/src/hooks/session-flightlist.ts @@ -6,6 +6,7 @@ import { FlightListStepT, FlightListStepVisibility, RpcMessage, + ToggleFlightListStepRequestT, } from 'solarxr-protocol'; import { useWebsocketAPI } from './websocket-api'; import { useEffect, useState } from 'react'; @@ -18,46 +19,58 @@ export const flightlistIdtoLabel: Record = { [FlightListStepId.STEAMVR_DISCONNECTED]: 'flight_list-STEAMVR_DISCONNECTED', [FlightListStepId.UNASSIGNED_HMD]: 'flight_list-UNASSIGNED_HMD', [FlightListStepId.TRACKER_ERROR]: 'flight_list-TRACKER_ERROR', + [FlightListStepId.NETWORK_PROFILE_PUBLIC]: 'flight_list-NETWORK_PROFILE_PUBLIC', }; export type FlightListStepStatus = 'complete' | 'skipped' | 'blocked' | 'invalid'; -export type FlightListStep = FlightListStepT & { status: FlightListStepStatus }; +export type FlightListStep = FlightListStepT & { + status: FlightListStepStatus; + firstInvalid: boolean; +}; + +const createStep = ( + steps: FlightListStepT[], + step: FlightListStepT, + index: number +): FlightListStep => { + const previousSteps = steps.slice(0, index); + const blocked = previousSteps.some(({ valid, optional }) => !valid && !optional); + + let status: FlightListStepStatus = 'complete'; + if (blocked) status = 'blocked'; + if (!blocked && !step.valid) status = 'invalid'; + if (!blocked && step.optional && !step.valid && index !== previousSteps.length) + status = 'skipped'; + + return { + ...step, + status, + firstInvalid: steps.findIndex((s) => !s.valid) == index, + pack: () => 0, + }; +}; + +export type SessionFlightListContext = ReturnType; + +const stepVisibility = ({ visibility, status, firstInvalid }: FlightListStep) => + firstInvalid || + visibility == FlightListStepVisibility.ALWAYS || + (visibility === FlightListStepVisibility.WHEN_INVALID && + ['invalid', 'blocked', 'skipped'].includes(status)); export function useSessionFlightlist() { const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); const [steps, setSteps] = useState([]); const [ignoredSteps, setIgnoredSteps] = useState([]); - const createStep = ( - steps: FlightListStepT[], - step: FlightListStepT, - index: number - ) => { - const previousSteps = steps.slice(0, index); - const blocked = previousSteps.some(({ valid, optional }) => !valid && !optional); - - let status = 'complete'; - if (blocked) status = 'blocked'; - if (!blocked && !step.valid) status = 'invalid'; - if (!blocked && step.optional && !step.valid && index !== previousSteps.length) - status = 'skipped'; - - return { ...step, status } as FlightListStep; - }; - useRPCPacket(RpcMessage.FlightListResponse, (data: FlightListResponseT) => { setIgnoredSteps(data.ignoredSteps); const visibleSteps = data.steps.filter( (step) => !data.ignoredSteps.includes(step.id) ); - const steps = visibleSteps - .map((step: FlightListStepT, index) => createStep(visibleSteps, step, index)) - .filter( - ({ visibility, status }) => - visibility == FlightListStepVisibility.ALWAYS || - (visibility === FlightListStepVisibility.WHEN_INVALID && - ['invalid', 'skipped'].includes(status)) - ); + const steps = data.steps.map((step: FlightListStepT, index) => + createStep(visibleSteps, step, index) + ); setSteps(steps); }); @@ -65,17 +78,13 @@ export function useSessionFlightlist() { RpcMessage.FlightListStepChangeResponse, (data: FlightListStepChangeResponseT) => { const step = data.step; + console.log('STATE CHANGE', step); if (!step) throw 'invalid state - step should be set'; setSteps((steps) => { const visibleSteps = steps.filter((step) => !ignoredSteps.includes(step.id)); - const newsteps = visibleSteps - .map((step: FlightListStepT, index) => createStep(visibleSteps, step, index)) - .filter( - ({ visibility, status }) => - visibility == FlightListStepVisibility.ALWAYS || - (visibility === FlightListStepVisibility.WHEN_INVALID && - ['invalid', 'blocked', 'skipped'].includes(status)) - ); + const newsteps = steps.map((step: FlightListStepT, index) => + createStep(visibleSteps, step, index) + ); const stepIndex = newsteps.findIndex(({ id }) => step.id === id); if (stepIndex == -1) return newsteps; // skip the step because it is not visible newsteps[stepIndex] = createStep(newsteps, step, stepIndex); @@ -89,7 +98,14 @@ export function useSessionFlightlist() { }, []); return { - steps, + steps: steps + .filter((step) => !ignoredSteps.includes(step.id)) + .filter(stepVisibility), ignoredSteps, + toggle: (step: FlightListStepId) => { + const res = new ToggleFlightListStepRequestT(); + res.stepId = step; + sendRPCPacket(RpcMessage.ToggleFlightListStepRequest, res); + }, }; } diff --git a/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt b/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt index 555511b334..74007873b4 100644 --- a/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt +++ b/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt @@ -227,11 +227,14 @@ class FlightListManager(private val vrServer: VRServer) : VRCConfigListener { "id is unknown" } val step = steps.find { it.id == id } ?: return + val wasValid = step.valid; step.valid = valid if (beforeUpdate != null) { beforeUpdate(step) } - listeners.forEach { it.onStepUpdate(step) } + if (wasValid != valid) { //FIXME: this does not cover extraData changing + listeners.forEach { it.onStepUpdate(step) } + } } override fun onChange( From 401f72a0fee5938509da67dea3ee495bb2e7b24f Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Fri, 18 Jul 2025 00:05:19 +0200 Subject: [PATCH 05/34] Current progres --- Cargo.lock | 1272 +++++++++++------ gui/package.json | 3 +- gui/public/i18n/en/translation.ftl | 31 +- gui/src-tauri/Cargo.toml | 3 +- gui/src-tauri/capabilities/migrated.json | 25 +- gui/src-tauri/src/main.rs | 1 + gui/src/App.tsx | 6 +- gui/src/components/MainLayout.scss | 6 +- gui/src/components/MainLayout.tsx | 6 +- gui/src/components/SessionFlightList.tsx | 103 -- gui/src/components/WidgetsComponent.tsx | 38 +- gui/src/components/commons/A.tsx | 13 +- gui/src/components/commons/Button.tsx | 21 +- gui/src/components/commons/Typography.tsx | 22 +- .../flight-list/FlightListProvider.tsx | 15 + .../flight-list/SessionFlightList.tsx | 295 ++++ gui/src/components/home/Home.tsx | 75 +- gui/src/components/home/ResetButton.tsx | 12 +- .../onboarding/pages/ConnectTracker.tsx | 65 +- .../providers/StatusSystemContext.tsx | 10 - .../settings/pages/GeneralSettings.tsx | 36 +- gui/src/components/tracker/TrackersTable.tsx | 14 +- gui/src/components/vrc/VRCWarningsPage.tsx | 4 +- gui/src/hooks/reset-settings.ts | 62 + gui/src/hooks/session-flightlist.ts | 79 +- gui/src/hooks/status-system.ts | 207 --- gui/src/hooks/vrc-config.ts | 26 +- pnpm-lock.yaml | 28 +- .../java/dev/slimevr/NetworkProfileChecker.kt | 60 + .../src/main/java/dev/slimevr/VRServer.kt | 4 + .../java/dev/slimevr/config/ResetsConfig.kt | 20 + .../dev/slimevr/config/VRCWarningsConfig.kt | 6 + .../main/java/dev/slimevr/config/VRConfig.kt | 2 + .../slimevr/flightlist/FlightListManager.kt | 100 +- .../slimevr/games/vrchat/VRCConfigHandler.kt | 45 +- .../dev/slimevr/protocol/rpc/RPCHandler.kt | 3 + .../rpc/flightlist/RPCFlightListHandler.kt | 10 +- .../rpc/games/vrchat/RPCVRCBuilder.kt | 9 +- .../rpc/games/vrchat/RPCVRChatHandler.kt | 18 +- .../rpc/settings/RPCSettingsHandler.kt | 1 + .../tracking/processor/HumanPoseManager.kt | 7 + ...ker.kt => DesktopNetworkProfileChecker.kt} | 86 +- .../src/main/java/dev/slimevr/desktop/Main.kt | 3 +- 43 files changed, 1720 insertions(+), 1132 deletions(-) delete mode 100644 gui/src/components/SessionFlightList.tsx create mode 100644 gui/src/components/flight-list/FlightListProvider.tsx create mode 100644 gui/src/components/flight-list/SessionFlightList.tsx delete mode 100644 gui/src/components/providers/StatusSystemContext.tsx create mode 100644 gui/src/hooks/reset-settings.ts delete mode 100644 gui/src/hooks/status-system.ts create mode 100644 server/core/src/main/java/dev/slimevr/NetworkProfileChecker.kt create mode 100644 server/core/src/main/java/dev/slimevr/config/VRCWarningsConfig.kt rename server/desktop/src/main/java/dev/slimevr/desktop/{NetworkProfileChecker.kt => DesktopNetworkProfileChecker.kt} (78%) diff --git a/Cargo.lock b/Cargo.lock index a3823723de..9fac230bf4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,7 +147,7 @@ dependencies = [ "wayland-backend", "wayland-client", "wayland-protocols", - "zbus", + "zbus 4.0.1", ] [[package]] @@ -174,6 +174,31 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-executor" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + [[package]] name = "async-io" version = "2.3.4" @@ -231,7 +256,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -266,7 +291,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -346,12 +371,6 @@ dependencies = [ "serde", ] -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - [[package]] name = "block-buffer" version = "0.10.4" @@ -367,7 +386,16 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "objc2", + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" +dependencies = [ + "objc2 0.6.1", ] [[package]] @@ -385,9 +413,9 @@ dependencies = [ [[package]] name = "brotli" -version = "6.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -396,9 +424,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "4.0.1" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -448,7 +476,7 @@ dependencies = [ "glib", "libc", "once_cell", - "thiserror", + "thiserror 1.0.64", ] [[package]] @@ -482,33 +510,33 @@ dependencies = [ [[package]] name = "cargo_metadata" -version = "0.18.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" dependencies = [ "camino", "cargo-platform", "semver", "serde", "serde_json", - "thiserror", + "thiserror 2.0.12", ] [[package]] name = "cargo_toml" -version = "0.17.2" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a969e13a7589e9e3e4207e153bae624ade2b5622fb4684a4923b23ec3d57719" +checksum = "02260d489095346e5cafd04dea8e8cb54d1d74fcd759022a9b72986ebe9a1257" dependencies = [ "serde", - "toml 0.8.2", + "toml 0.8.23", ] [[package]] name = "cc" -version = "1.1.30" +version = "1.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" +checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" dependencies = [ "jobserver", "libc", @@ -608,7 +636,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -617,36 +645,6 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" -[[package]] -name = "cocoa" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" -dependencies = [ - "bitflags 2.6.0", - "block", - "cocoa-foundation", - "core-foundation", - "core-graphics", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" -dependencies = [ - "bitflags 2.6.0", - "block", - "core-foundation", - "core-graphics-types", - "libc", - "objc", -] - [[package]] name = "color-eyre" version = "0.6.3" @@ -731,6 +729,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.10.0" @@ -816,15 +824,15 @@ dependencies = [ [[package]] name = "cssparser" -version = "0.27.2" +version = "0.29.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" dependencies = [ "cssparser-macros", "dtoa-short", - "itoa 0.4.8", + "itoa", "matches", - "phf 0.8.0", + "phf 0.10.1", "proc-macro2", "quote", "smallvec", @@ -838,7 +846,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -848,7 +856,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -872,7 +880,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -883,7 +891,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -923,7 +931,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -938,9 +946,9 @@ dependencies = [ [[package]] name = "dirs" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] @@ -957,14 +965,14 @@ dependencies = [ [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", - "windows-sys 0.48.0", + "redox_users 0.5.0", + "windows-sys 0.59.0", ] [[package]] @@ -974,7 +982,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", - "redox_users", + "redox_users 0.4.6", "winapi", ] @@ -995,12 +1003,12 @@ dependencies = [ "serde", "serde_json", "serde_repr", - "thiserror", + "thiserror 1.0.64", "time", "tokio", "tracing", "url", - "winreg", + "winreg 0.52.0", ] [[package]] @@ -1009,6 +1017,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.6.0", + "objc2 0.6.1", +] + [[package]] name = "dlib" version = "0.5.2" @@ -1038,7 +1056,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1091,16 +1109,16 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "embed-resource" -version = "2.5.0" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e24052d7be71f0efb50c201557f6fe7d237cfd5a64fd5bcd7fd8fe32dbbffa" +checksum = "4c6d81016d6c977deefb2ef8d8290da019e27cc26167e102185da528e6c0ab38" dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.8.2", + "toml 0.9.2", "vswhom", - "winreg", + "winreg 0.55.0", ] [[package]] @@ -1142,7 +1160,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1247,16 +1265,7 @@ dependencies = [ "log", "nu-ansi-term", "regex", - "thiserror", -] - -[[package]] -name = "fluent-uri" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" -dependencies = [ - "bitflags 1.3.2", + "thiserror 1.0.64", ] [[package]] @@ -1283,7 +1292,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1364,7 +1373,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1546,6 +1555,18 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + [[package]] name = "gimli" version = "0.28.1" @@ -1568,7 +1589,7 @@ dependencies = [ "once_cell", "pin-project-lite", "smallvec", - "thiserror", + "thiserror 1.0.64", ] [[package]] @@ -1617,7 +1638,7 @@ dependencies = [ "memchr", "once_cell", "smallvec", - "thiserror", + "thiserror 1.0.64", ] [[package]] @@ -1627,11 +1648,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" dependencies = [ "heck 0.4.1", - "proc-macro-crate 2.0.2", + "proc-macro-crate 2.0.0", "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1710,7 +1731,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1766,16 +1787,14 @@ dependencies = [ [[package]] name = "html5ever" -version = "0.26.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" dependencies = [ "log", "mac", "markup5ever", - "proc-macro2", - "quote", - "syn 1.0.109", + "match_token", ] [[package]] @@ -1786,7 +1805,7 @@ checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", - "itoa 1.0.11", + "itoa", ] [[package]] @@ -1830,7 +1849,7 @@ dependencies = [ "http", "http-body", "httparse", - "itoa 1.0.11", + "itoa", "pin-project-lite", "smallvec", "tokio", @@ -1899,9 +1918,9 @@ dependencies = [ [[package]] name = "ico" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3804960be0bb5e4edb1e1ad67afd321a9ecfd875c3e65c099468fd2717d7cae" +checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" dependencies = [ "byteorder", "png", @@ -1965,22 +1984,13 @@ dependencies = [ [[package]] name = "infer" -version = "0.16.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc150e5ce2330295b8616ce0e3f53250e53af31759a9dbedad1621ba29151847" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" dependencies = [ "cfb", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "ipnet" version = "2.10.1" @@ -2027,12 +2037,6 @@ dependencies = [ "either", ] -[[package]] -name = "itoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - [[package]] name = "itoa" version = "1.0.11" @@ -2073,7 +2077,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.64", "walkdir", "windows-sys 0.45.0", ] @@ -2104,23 +2108,22 @@ dependencies = [ [[package]] name = "json-patch" -version = "2.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1fb8864823fad91877e6caea0baca82e49e8db50f8e5c9f9a453e27d3330fc" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" dependencies = [ "jsonptr", "serde", "serde_json", - "thiserror", + "thiserror 1.0.64", ] [[package]] name = "jsonptr" -version = "0.4.7" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c6e529149475ca0b2820835d3dce8fcc41c6b943ca608d32f35b449255e4627" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" dependencies = [ - "fluent-uri", "serde", "serde_json", ] @@ -2138,14 +2141,13 @@ dependencies = [ [[package]] name = "kuchikiki" -version = "0.8.2" +version = "0.8.8-speedreader" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ "cssparser", "html5ever", - "indexmap 1.9.3", - "matches", + "indexmap 2.6.0", "selectors", ] @@ -2277,29 +2279,31 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - [[package]] name = "markup5ever" -version = "0.11.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" dependencies = [ "log", - "phf 0.10.1", - "phf_codegen 0.10.0", + "phf 0.11.2", + "phf_codegen 0.11.3", "string_cache", "string_cache_codegen", "tendril", ] +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "matches" version = "0.1.10" @@ -2360,21 +2364,22 @@ dependencies = [ [[package]] name = "muda" -version = "0.15.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8123dfd4996055ac9b15a60ad263b44b01e539007523ad7a4a533a3d93b0591" +checksum = "58b89bf91c19bf036347f1ab85a81c560f08c0667c8601bece664d860a600988" dependencies = [ "crossbeam-channel", "dpi", "gtk", "keyboard-types", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.6.1", + "objc2-app-kit 0.3.1", + "objc2-core-foundation", + "objc2-foundation 0.3.1", "once_cell", "png", "serde", - "thiserror", + "thiserror 2.0.12", "windows-sys 0.59.0", ] @@ -2390,7 +2395,7 @@ dependencies = [ "ndk-sys", "num_enum", "raw-window-handle", - "thiserror", + "thiserror 1.0.64", ] [[package]] @@ -2426,6 +2431,19 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -2471,10 +2489,10 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 2.0.2", + "proc-macro-crate 2.0.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -2486,23 +2504,11 @@ dependencies = [ "libc", ] -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - [[package]] name = "objc-sys" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" -dependencies = [ - "cc", -] [[package]] name = "objc2" @@ -2514,6 +2520,16 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + [[package]] name = "objc2-app-kit" version = "0.2.2" @@ -2521,37 +2537,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ "bitflags 2.6.0", - "block2", + "block2 0.5.1", "libc", - "objc2", - "objc2-core-data", - "objc2-core-image", - "objc2-foundation", - "objc2-quartz-core", + "objc2 0.5.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", ] [[package]] -name = "objc2-cloud-kit" -version = "0.2.2" +name = "objc2-app-kit" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" dependencies = [ "bitflags 2.6.0", - "block2", - "objc2", - "objc2-core-location", - "objc2-foundation", + "block2 0.6.1", + "libc", + "objc2 0.6.1", + "objc2-cloud-kit", + "objc2-core-data 0.3.1", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image 0.3.1", + "objc2-foundation 0.3.1", + "objc2-quartz-core 0.3.1", ] [[package]] -name = "objc2-contacts" -version = "0.2.2" +name = "objc2-cloud-kit" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d" dependencies = [ - "block2", - "objc2", - "objc2-foundation", + "bitflags 2.6.0", + "objc2 0.6.1", + "objc2-foundation 0.3.1", ] [[package]] @@ -2561,9 +2583,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.6.0", - "block2", - "objc2", - "objc2-foundation", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d" +dependencies = [ + "bitflags 2.6.0", + "objc2 0.6.1", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +dependencies = [ + "bitflags 2.6.0", + "dispatch2", + "objc2 0.6.1", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" +dependencies = [ + "bitflags 2.6.0", + "dispatch2", + "objc2 0.6.1", + "objc2-core-foundation", + "objc2-io-surface", ] [[package]] @@ -2572,146 +2629,134 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "block2", - "objc2", - "objc2-foundation", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", "objc2-metal", ] [[package]] -name = "objc2-core-location" -version = "0.2.2" +name = "objc2-core-image" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e" dependencies = [ - "block2", - "objc2", - "objc2-contacts", - "objc2-foundation", + "objc2 0.6.1", + "objc2-foundation 0.3.1", ] [[package]] name = "objc2-encode" -version = "4.0.3" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] -name = "objc2-foundation" -version = "0.2.2" +name = "objc2-exception-helper" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" dependencies = [ - "bitflags 2.6.0", - "block2", - "dispatch", - "libc", - "objc2", + "cc", ] [[package]] -name = "objc2-link-presentation" +name = "objc2-foundation" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "block2", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "bitflags 2.6.0", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", ] [[package]] -name = "objc2-metal" -version = "0.2.2" +name = "objc2-foundation" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ "bitflags 2.6.0", - "block2", - "objc2", - "objc2-foundation", + "block2 0.6.1", + "libc", + "objc2 0.6.1", + "objc2-core-foundation", ] [[package]] -name = "objc2-quartz-core" -version = "0.2.2" +name = "objc2-io-surface" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" dependencies = [ "bitflags 2.6.0", - "block2", - "objc2", - "objc2-foundation", - "objc2-metal", + "objc2 0.6.1", + "objc2-core-foundation", ] [[package]] -name = "objc2-symbols" +name = "objc2-metal" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "objc2", - "objc2-foundation", + "bitflags 2.6.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] -name = "objc2-ui-kit" +name = "objc2-quartz-core" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.6.0", - "block2", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-image", - "objc2-core-location", - "objc2-foundation", - "objc2-link-presentation", - "objc2-quartz-core", - "objc2-symbols", - "objc2-uniform-type-identifiers", - "objc2-user-notifications", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", ] [[package]] -name = "objc2-uniform-type-identifiers" -version = "0.2.2" +name = "objc2-quartz-core" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5" dependencies = [ - "block2", - "objc2", - "objc2-foundation", + "bitflags 2.6.0", + "objc2 0.6.1", + "objc2-foundation 0.3.1", ] [[package]] -name = "objc2-user-notifications" -version = "0.2.2" +name = "objc2-ui-kit" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" dependencies = [ "bitflags 2.6.0", - "block2", - "objc2", - "objc2-core-location", - "objc2-foundation", + "objc2 0.6.1", + "objc2-core-foundation", + "objc2-foundation 0.3.1", ] [[package]] name = "objc2-web-kit" -version = "0.2.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68bc69301064cebefc6c4c90ce9cba69225239e4b8ff99d445a2b5563797da65" +checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad" dependencies = [ "bitflags 2.6.0", - "block2", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "block2 0.6.1", + "objc2 0.6.1", + "objc2-app-kit 0.3.1", + "objc2-core-foundation", + "objc2-foundation 0.3.1", ] [[package]] @@ -2855,9 +2900,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros 0.8.0", "phf_shared 0.8.0", - "proc-macro-hack", ] [[package]] @@ -2866,7 +2909,9 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" dependencies = [ + "phf_macros 0.10.0", "phf_shared 0.10.0", + "proc-macro-hack", ] [[package]] @@ -2891,12 +2936,12 @@ dependencies = [ [[package]] name = "phf_codegen" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", + "phf_generator 0.11.2", + "phf_shared 0.11.2", ] [[package]] @@ -2931,12 +2976,12 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", + "phf_generator 0.10.0", + "phf_shared 0.10.0", "proc-macro-hack", "proc-macro2", "quote", @@ -2953,7 +2998,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -3086,21 +3131,29 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" dependencies = [ - "toml_datetime", "toml_edit 0.20.2", ] [[package]] -name = "proc-macro-error" -version = "1.0.4" +name = "proc-macro-crate" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ - "proc-macro-error-attr", + "toml_edit 0.22.27", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", "proc-macro2", "quote", "syn 1.0.109", @@ -3164,7 +3217,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror", + "thiserror 1.0.64", "tokio", "tracing", ] @@ -3181,7 +3234,7 @@ dependencies = [ "rustc-hash", "rustls", "slab", - "thiserror", + "thiserror 1.0.64", "tinyvec", "tracing", ] @@ -3209,6 +3262,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.7.3" @@ -3313,7 +3372,18 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 1.0.64", +] + +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 2.0.12", ] [[package]] @@ -3396,15 +3466,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8af382a047821a08aa6bfc09ab0d80ff48d45d8726f7cd8e44891f7cb4a4278e" dependencies = [ "ashpd", - "block2", + "block2 0.5.1", "glib-sys", "gobject-sys", "gtk-sys", "js-sys", "log", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "raw-window-handle", "wasm-bindgen", "wasm-bindgen-futures", @@ -3540,7 +3610,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -3557,22 +3627,20 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "selectors" -version = "0.22.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" dependencies = [ "bitflags 1.3.2", "cssparser", "derive_more", "fxhash", "log", - "matches", "phf 0.8.0", "phf_codegen 0.8.0", "precomputed-hash", "servo_arc", "smallvec", - "thin-slice", ] [[package]] @@ -3612,7 +3680,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -3623,7 +3691,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -3632,7 +3700,7 @@ version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ - "itoa 1.0.11", + "itoa", "memchr", "ryu", "serde", @@ -3646,14 +3714,23 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" dependencies = [ "serde", ] @@ -3665,7 +3742,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.11", + "itoa", "ryu", "serde", ] @@ -3697,7 +3774,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -3724,9 +3801,9 @@ dependencies = [ [[package]] name = "servo_arc" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" dependencies = [ "nodrop", "stable_deref_trait", @@ -3850,6 +3927,7 @@ dependencies = [ "tauri-build", "tauri-plugin-dialog", "tauri-plugin-fs", + "tauri-plugin-opener", "tauri-plugin-os", "tauri-plugin-shell", "tauri-plugin-store", @@ -3858,7 +3936,7 @@ dependencies = [ "tokio", "which", "win32job", - "winreg", + "winreg 0.52.0", ] [[package]] @@ -3889,9 +3967,9 @@ dependencies = [ "foreign-types", "js-sys", "log", - "objc2", - "objc2-foundation", - "objc2-quartz-core", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", "raw-window-handle", "redox_syscall", "wasm-bindgen", @@ -4005,9 +4083,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.79" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -4041,18 +4119,17 @@ dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml 0.8.2", + "toml 0.8.23", "version-compare", ] [[package]] name = "tao" -version = "0.30.3" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0dbbebe82d02044dfa481adca1550d6dd7bd16e086bc34fa0fbecceb5a63751" +checksum = "49c380ca75a231b87b6c9dd86948f035012e7171d1a7c40a9c2890489a7ffd8a" dependencies = [ "bitflags 2.6.0", - "cocoa", "core-foundation", "core-graphics", "crossbeam-channel", @@ -4062,7 +4139,6 @@ dependencies = [ "gdkwayland-sys", "gdkx11-sys", "gtk", - "instant", "jni", "lazy_static", "libc", @@ -4070,7 +4146,9 @@ dependencies = [ "ndk", "ndk-context", "ndk-sys", - "objc", + "objc2 0.6.1", + "objc2-app-kit 0.3.1", + "objc2-foundation 0.3.1", "once_cell", "parking_lot", "raw-window-handle", @@ -4079,7 +4157,7 @@ dependencies = [ "unicode-segmentation", "url", "windows", - "windows-core 0.58.0", + "windows-core 0.61.2", "windows-version", "x11-dl", ] @@ -4092,7 +4170,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -4103,17 +4181,16 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.0.3" +version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd96d46534b10765ce0c6208f9451d98ea38636364a41b272d3610c70dd0e4c3" +checksum = "124e129c9c0faa6bec792c5948c89e86c90094133b0b9044df0ce5f0a8efaa0d" dependencies = [ "anyhow", "bytes", "dirs", "dunce", "embed_plist", - "futures-util", - "getrandom 0.2.15", + "getrandom 0.3.3", "glob", "gtk", "heck 0.5.0", @@ -4124,9 +4201,10 @@ dependencies = [ "log", "mime", "muda", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.6.1", + "objc2-app-kit 0.3.1", + "objc2-foundation 0.3.1", + "objc2-ui-kit", "percent-encoding", "plist", "raw-window-handle", @@ -4141,7 +4219,7 @@ dependencies = [ "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "thiserror", + "thiserror 2.0.12", "tokio", "tray-icon", "url", @@ -4154,9 +4232,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.0.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "935f9b3c49b22b3e2e485a57f46d61cd1ae07b1cbb2ba87387a387caf2d8c4e7" +checksum = "12f025c389d3adb83114bec704da973142e82fc6ec799c7c750c5e21cefaec83" dependencies = [ "anyhow", "cargo_toml", @@ -4170,15 +4248,15 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.8.2", + "toml 0.8.23", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.0.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95d7443dd4f0b597704b6a14b964ee2ed16e99928d8e6292ae9825f09fbcd30e" +checksum = "f5df493a1075a241065bc865ed5ef8d0fbc1e76c7afdc0bf0eccfaa7d4f0e406" dependencies = [ "base64 0.22.1", "brotli", @@ -4192,9 +4270,9 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn 2.0.79", + "syn 2.0.87", "tauri-utils", - "thiserror", + "thiserror 2.0.12", "time", "url", "uuid", @@ -4203,23 +4281,23 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.0.1" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d2c0963ccfc3f5194415f2cce7acc975942a8797fbabfb0aa1ed6f59326ae7f" +checksum = "f237fbea5866fa5f2a60a21bea807a2d6e0379db070d89c3a10ac0f2d4649bbc" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", "tauri-codegen", "tauri-utils", ] [[package]] name = "tauri-plugin" -version = "2.0.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e6660a409963e4d57b9bfab4addd141eeff41bd3a7fb14e13004a832cf7ef6" +checksum = "1d9a0bd00bf1930ad1a604d08b0eb6b2a9c1822686d65d7f4731a7723b8901d3" dependencies = [ "anyhow", "glob", @@ -4228,7 +4306,7 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.8.2", + "toml 0.8.23", "walkdir", ] @@ -4246,7 +4324,7 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-plugin-fs", - "thiserror", + "thiserror 1.0.64", "url", ] @@ -4266,11 +4344,33 @@ dependencies = [ "serde_repr", "tauri", "tauri-plugin", - "thiserror", + "thiserror 1.0.64", "url", "uuid", ] +[[package]] +name = "tauri-plugin-opener" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecee219f11cdac713ab32959db5d0cceec4810ba4f4458da992292ecf9660321" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit 0.3.1", + "objc2-foundation 0.3.1", + "open", + "schemars", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.12", + "url", + "windows", + "zbus 5.4.0", +] + [[package]] name = "tauri-plugin-os" version = "2.0.1" @@ -4286,14 +4386,14 @@ dependencies = [ "sys-locale", "tauri", "tauri-plugin", - "thiserror", + "thiserror 1.0.64", ] [[package]] name = "tauri-plugin-shell" -version = "2.0.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "371fb9aca2823990a2d0db7970573be5fdf07881fcaa2b835b29631feb84aec1" +checksum = "2b9ffadec5c3523f11e8273465cacb3d86ea7652a28e6e2a2e9b5c182f791d25" dependencies = [ "encoding_rs", "log", @@ -4306,7 +4406,7 @@ dependencies = [ "shared_child", "tauri", "tauri-plugin", - "thiserror", + "thiserror 2.0.12", "tokio", ] @@ -4322,42 +4422,46 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror", + "thiserror 1.0.64", "tokio", ] [[package]] name = "tauri-runtime" -version = "2.1.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8f437293d6f5e5dce829250f4dbdce4e0b52905e297a6689cc2963eb53ac728" +checksum = "9e7bb73d1bceac06c20b3f755b2c8a2cb13b20b50083084a8cf3700daf397ba4" dependencies = [ + "cookie", "dpi", "gtk", "http", "jni", + "objc2 0.6.1", + "objc2-ui-kit", "raw-window-handle", "serde", "serde_json", "tauri-utils", - "thiserror", + "thiserror 2.0.12", "url", "windows", ] [[package]] name = "tauri-runtime-wry" -version = "2.1.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaac63b65df8e85570993eaf93ae1dd73a6fb66d8bd99674ce65f41dc3c63e7d" +checksum = "902b5aa9035e16f342eb64f8bf06ccdc2808e411a2525ed1d07672fa4e780bad" dependencies = [ "gtk", "http", "jni", "log", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.6.1", + "objc2-app-kit 0.3.1", + "objc2-foundation 0.3.1", + "once_cell", "percent-encoding", "raw-window-handle", "softbuffer", @@ -4373,16 +4477,18 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.0.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c38b0230d6880cf6dd07b6d7dd7789a0869f98ac12146e0d18d1c1049215a045" +checksum = "41743bbbeb96c3a100d234e5a0b60a46d5aa068f266160862c7afdbf828ca02e" dependencies = [ + "anyhow", "brotli", "cargo_metadata", "ctor", "dunce", "glob", "html5ever", + "http", "infer", "json-patch", "kuchikiki", @@ -4399,8 +4505,8 @@ dependencies = [ "serde_json", "serde_with", "swift-rs", - "thiserror", - "toml 0.8.2", + "thiserror 2.0.12", + "toml 0.8.23", "url", "urlpattern", "uuid", @@ -4409,12 +4515,13 @@ dependencies = [ [[package]] name = "tauri-winres" -version = "0.1.1" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5993dc129e544393574288923d1ec447c857f3f644187f4fbf7d9a875fbfc4fb" +checksum = "e8d321dbc6f998d825ab3f0d62673e810c861aac2d0de2cc2c395328f1d113b4" dependencies = [ "embed-resource", - "toml 0.7.8", + "indexmap 2.6.0", + "toml 0.8.23", ] [[package]] @@ -4442,18 +4549,21 @@ dependencies = [ ] [[package]] -name = "thin-slice" -version = "0.1.1" +name = "thiserror" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl 1.0.64", +] [[package]] name = "thiserror" -version = "1.0.64" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.12", ] [[package]] @@ -4464,7 +4574,18 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] @@ -4484,7 +4605,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", - "itoa 1.0.11", + "itoa", "libc", "num-conv", "num_threads", @@ -4551,7 +4672,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -4580,33 +4701,45 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.8" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", - "toml_edit 0.19.15", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] name = "toml" -version = "0.8.2" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" dependencies = [ + "indexmap 2.6.0", "serde", - "serde_spanned", - "toml_datetime", - "toml_edit 0.20.2", + "serde_spanned 1.0.0", + "toml_datetime 0.7.0", + "toml_parser", + "toml_writer", + "winnow 0.7.12", ] [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" dependencies = [ "serde", ] @@ -4618,10 +4751,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap 2.6.0", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", + "toml_datetime 0.6.11", + "winnow 0.5.40", ] [[package]] @@ -4629,14 +4760,47 @@ name = "toml_edit" version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.6.0", + "toml_datetime 0.6.11", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap 2.6.0", "serde", - "serde_spanned", - "toml_datetime", - "winnow", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.12", ] +[[package]] +name = "toml_parser" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" +dependencies = [ + "winnow 0.7.12", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" + [[package]] name = "tower-service" version = "0.3.3" @@ -4662,7 +4826,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -4698,22 +4862,23 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.19.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c92af36a182b46206723bdf8a7942e20838cde1cf062e5b97854d57eb01763b" +checksum = "2da75ec677957aa21f6e0b361df0daab972f13a5bee3606de0638fd4ee1c666a" dependencies = [ - "core-graphics", "crossbeam-channel", "dirs", "libappindicator", "muda", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.6.1", + "objc2-app-kit 0.3.1", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.1", "once_cell", "png", "serde", - "thiserror", + "thiserror 2.0.12", "windows-sys 0.59.0", ] @@ -4976,6 +5141,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.95" @@ -4998,7 +5172,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", "wasm-bindgen-shared", ] @@ -5032,7 +5206,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5181,14 +5355,14 @@ dependencies = [ [[package]] name = "webview2-com" -version = "0.33.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f61ff3d9d0ee4efcb461b14eb3acfda2702d10dc329f339303fc3e57215ae2c" +checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" dependencies = [ "webview2-com-macros", "webview2-com-sys", "windows", - "windows-core 0.58.0", + "windows-core 0.61.2", "windows-implement", "windows-interface", ] @@ -5201,18 +5375,18 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] name = "webview2-com-sys" -version = "0.33.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3a3e2eeb58f82361c93f9777014668eb3d07e7d174ee4c819575a9208011886" +checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" dependencies = [ - "thiserror", + "thiserror 2.0.12", "windows", - "windows-core 0.58.0", + "windows-core 0.61.2", ] [[package]] @@ -5233,7 +5407,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1e915da468fb933fad27a318d59e163b1c8ae079be5efc080ff75990d1555dd" dependencies = [ - "thiserror", + "thiserror 1.0.64", "winapi", ] @@ -5270,13 +5444,14 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "window-vibrancy" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ea403deff7b51fff19e261330f71608ff2cdef5721d72b64180bb95be7c4150" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.6.1", + "objc2-app-kit 0.3.1", + "objc2-core-foundation", + "objc2-foundation 0.3.1", "raw-window-handle", "windows-sys 0.59.0", "windows-version", @@ -5284,12 +5459,24 @@ dependencies = [ [[package]] name = "windows" -version = "0.58.0" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", ] [[package]] @@ -5303,37 +5490,64 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.58.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-result", - "windows-strings", - "windows-targets 0.52.6", + "windows-link", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link", + "windows-threading", ] [[package]] name = "windows-implement" -version = "0.58.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] name = "windows-interface" -version = "0.58.0" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link", ] [[package]] @@ -5342,8 +5556,8 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" dependencies = [ - "windows-result", - "windows-strings", + "windows-result 0.2.0", + "windows-strings 0.1.0", "windows-targets 0.52.6", ] @@ -5356,16 +5570,34 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-strings" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-result", + "windows-result 0.2.0", "windows-targets 0.52.6", ] +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -5448,6 +5680,15 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-version" version = "0.1.1" @@ -5598,6 +5839,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.52.0" @@ -5608,20 +5858,40 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + [[package]] name = "winsafe" version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "wry" -version = "0.46.0" +version = "0.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469a3765ecc3e8aa9ccdf3c5a52c82697ec03037cd60494488763880d31a1b3a" +checksum = "12a714d9ba7075aae04a6e50229d6109e3d584774b99a6a8c60de1698ca111b9" dependencies = [ "base64 0.22.1", - "block2", + "block2 0.6.1", + "cookie", "crossbeam-channel", "dpi", "dunce", @@ -5634,9 +5904,10 @@ dependencies = [ "kuchikiki", "libc", "ndk", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.6.1", + "objc2-app-kit 0.3.1", + "objc2-core-foundation", + "objc2-foundation 0.3.1", "objc2-ui-kit", "objc2-web-kit", "once_cell", @@ -5645,12 +5916,13 @@ dependencies = [ "sha2", "soup3", "tao-macros", - "thiserror", + "thiserror 2.0.12", + "url", "webkit2gtk", "webkit2gtk-sys", "webview2-com", "windows", - "windows-core 0.58.0", + "windows-core 0.61.2", "windows-version", "x11-dl", ] @@ -5709,7 +5981,7 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix", + "nix 0.27.1", "ordered-stream", "rand 0.8.5", "serde", @@ -5721,9 +5993,45 @@ dependencies = [ "uds_windows", "windows-sys 0.52.0", "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", + "zbus_macros 4.0.1", + "zbus_names 3.0.0", + "zvariant 4.0.0", +] + +[[package]] +name = "zbus" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbddd8b6cb25d5d8ec1b23277b45299a98bfb220f1761ca11e186d5c702507f8" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-util", + "hex", + "nix 0.29.0", + "ordered-stream", + "serde", + "serde_repr", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.59.0", + "winnow 0.7.12", + "xdg-home", + "zbus_macros 5.4.0", + "zbus_names 4.2.0", + "zvariant 5.6.0", ] [[package]] @@ -5737,7 +6045,22 @@ dependencies = [ "quote", "regex", "syn 1.0.109", - "zvariant_utils", + "zvariant_utils 1.1.0", +] + +[[package]] +name = "zbus_macros" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac404d48b4e9cf193c8b49589f3280ceca5ff63519e7e64f55b4cf9c47ce146" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.87", + "zbus_names 4.2.0", + "zvariant 5.6.0", + "zvariant_utils 3.2.0", ] [[package]] @@ -5748,7 +6071,19 @@ checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", - "zvariant", + "zvariant 4.0.0", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.12", + "zvariant 5.6.0", ] [[package]] @@ -5769,7 +6104,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -5789,7 +6124,21 @@ dependencies = [ "serde", "static_assertions", "url", - "zvariant_derive", + "zvariant_derive 4.0.0", +] + +[[package]] +name = "zvariant" +version = "5.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91b3680bb339216abd84714172b5138a4edac677e641ef17e1d8cb1b3ca6e6f" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 0.7.12", + "zvariant_derive 5.6.0", + "zvariant_utils 3.2.0", ] [[package]] @@ -5802,7 +6151,20 @@ dependencies = [ "proc-macro2", "quote", "syn 1.0.109", - "zvariant_utils", + "zvariant_utils 1.1.0", +] + +[[package]] +name = "zvariant_derive" +version = "5.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8c68501be459a8dbfffbe5d792acdd23b4959940fc87785fb013b32edbc208" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.87", + "zvariant_utils 3.2.0", ] [[package]] @@ -5815,3 +6177,17 @@ dependencies = [ "quote", "syn 1.0.109", ] + +[[package]] +name = "zvariant_utils" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "static_assertions", + "syn 2.0.87", + "winnow 0.7.12", +] diff --git a/gui/package.json b/gui/package.json index 0f5d984708..a2bed286ea 100644 --- a/gui/package.json +++ b/gui/package.json @@ -19,8 +19,9 @@ "@tauri-apps/api": "^2.0.2", "@tauri-apps/plugin-dialog": "^2.0.0", "@tauri-apps/plugin-fs": "^2.0.0", + "@tauri-apps/plugin-opener": "^2.4.0", "@tauri-apps/plugin-os": "^2.0.0", - "@tauri-apps/plugin-shell": "^2.0.0", + "@tauri-apps/plugin-shell": "^2.3.0", "@tauri-apps/plugin-store": "^2.0.0", "@tweenjs/tween.js": "^25.0.0", "@twemoji/svg": "^15.0.0", diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index 27ccb3c267..b0217cabc9 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -1303,7 +1303,7 @@ status_system-StatusSteamVRDisconnected = { $type -> } status_system-StatusTrackerError = The { $trackerName } tracker has an error. status_system-StatusUnassignedHMD = The VR headset should be assigned as a head tracker. -status_system-StatusPublicNetwork = {$count -> +status_system-StatusPublicNetwork = {$count -> [one] Your network profile is currently set to Public ({$adapters}). This is not recommended for SlimeVR to function properly. See how to fix it here. *[many] Some of your network adapters are set to public: {$adapters}. This is not recommended for SlimeVR to function properly. See how to fix it here. } @@ -1543,3 +1543,32 @@ error_collection_modal-description_v2 = { settings-interface-behavior-error_trac You can change this setting later in the Behavior section of the settings page. error_collection_modal-confirm = I agree error_collection_modal-cancel = I don't want to + + +flight_list-MOUNTING_CALIBRATION = Perform a mounting calibration +flight_list-FULL_RESET = Perform a full Reset +flight_list-FULL_RESET-desc = Some Trackers need a reset to be performed +flight_list-STEAMVR_DISCONNECTED = SteamVR not running +flight_list-STEAMVR_DISCONNECTED-desc = SteamVR is not running. Are you using it for vr? +flight_list-STEAMVR_DISCONNECTED-open = Launch SteamVR +flight_list-TRACKERS_CALIBRATION = Calibrate your trackers +flight_list-TRACKERS_CALIBRATION-desc = You didnt perform the tracker calibration. Please let your slimes, highlited in yellow, rest on a static surface for a few secconds +flight_list-TRACKER_ERROR = Trackers with Errors +flight_list-TRACKER_ERROR-desc = Some of your trackers have an error. Please restart the tracker. +flight_list-VRCHAT_SETTINGS = Misconfigured VRChat settings +flight_list-VRCHAT_SETTINGS-desc = You have misconfigured VRchat Settings! This can impact your tracking experience. +flight_list-VRCHAT_SETTINGS-open = Go to VRChat Warnings +flight_list-UNASSIGNED_HMD = VR Headset not assigned to Head +flight_list-UNASSIGNED_HMD-desc = The VR headset should be assigned as a head tracker. +flight_list-NETWORK_PROFILE_PUBLIC = Wrong network profile used +flight_list-NETWORK_PROFILE_PUBLIC-desc = {$count -> + [one] Your network profile is currently set to Public ({$adapters}). + This is not recommended for SlimeVR to function properly. + See how to fix it here. + *[many] Some of your network adapters are set to public: + {$adapters} + This is not recommended for SlimeVR to function properly. + See how to fix it here. +} +flight_list-NETWORK_PROFILE_PUBLIC-open = Open Control Panel +flight_list-ignore = Ignore diff --git a/gui/src-tauri/Cargo.toml b/gui/src-tauri/Cargo.toml index dfda2bb448..263e9dc629 100644 --- a/gui/src-tauri/Cargo.toml +++ b/gui/src-tauri/Cargo.toml @@ -33,7 +33,7 @@ tauri-runtime = "2.0" tauri-plugin-dialog = "2.0" tauri-plugin-fs = "2.0" tauri-plugin-os = "2.0" -tauri-plugin-shell = "2.0" +tauri-plugin-shell = "2.3.0" tauri-plugin-store = "2.0" flexi_logger = "0.29" log-panics = { version = "2", features = ["with-backtrace"] } @@ -54,6 +54,7 @@ dirs-next = "2.0.0" discord-sdk = "0.3.6" tokio = { version = "1.37.0", features = ["time"] } itertools = "0.13.0" +tauri-plugin-opener = "2.4.0" [target.'cfg(windows)'.dependencies] win32job = "1" diff --git a/gui/src-tauri/capabilities/migrated.json b/gui/src-tauri/capabilities/migrated.json index 2a4ed4923c..162d498f00 100644 --- a/gui/src-tauri/capabilities/migrated.json +++ b/gui/src-tauri/capabilities/migrated.json @@ -2,9 +2,7 @@ "identifier": "migrated", "description": "permissions that were migrated from v1", "local": true, - "windows": [ - "main" - ], + "windows": ["main"], "permissions": [ "core:default", "core:window:allow-close", @@ -19,7 +17,6 @@ "core:window:allow-set-decorations", "store:default", "os:allow-os-type", - "dialog:allow-open", "dialog:allow-save", "shell:allow-open", "store:allow-get", @@ -30,7 +27,25 @@ "fs:allow-exists", { "identifier": "fs:scope", - "allow": [{ "path": "$APPDATA" }, { "path": "$APPDATA/**" }] + "allow": [ + { + "path": "$APPDATA" + }, + { + "path": "$APPDATA/**" + } + ] + }, + { + "identifier": "opener:allow-open-url", + "allow": [ + { + "url": "steam:*" + }, + { + "url": "ms-settings:network" + } + ] } ] } diff --git a/gui/src-tauri/src/main.rs b/gui/src-tauri/src/main.rs index 8125c9869f..2a6cc22695 100644 --- a/gui/src-tauri/src/main.rs +++ b/gui/src-tauri/src/main.rs @@ -249,6 +249,7 @@ fn setup_tauri( ) -> Result { let exit_flag_terminated = exit_flag.clone(); tauri::Builder::default() + .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_os::init()) diff --git a/gui/src/App.tsx b/gui/src/App.tsx index d4d2264d17..756e0177b0 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -39,7 +39,6 @@ import { OSCRouterSettings } from './components/settings/pages/OSCRouterSettings import * as os from '@tauri-apps/plugin-os'; import { VMCSettings } from './components/settings/pages/VMCSettings'; import { MountingChoose } from './components/onboarding/pages/mounting/MountingChoose'; -import { StatusProvider } from './components/providers/StatusSystemContext'; import { VersionUpdateModal } from './components/VersionUpdateModal'; import { CalibrationTutorialPage } from './components/onboarding/pages/CalibrationTutorial'; import { AssignmentTutorialPage } from './components/onboarding/pages/assignment-preparation/AssignmentTutorial'; @@ -61,6 +60,7 @@ 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'; +import { FlightListProvider } from './components/flight-list/FlightListProvider'; export const GH_REPO = 'SlimeVR/SlimeVR-Server'; export const VersionContext = createContext(''); @@ -292,7 +292,7 @@ export default function App() { - +
@@ -302,7 +302,7 @@ export default function App() { {websocketAPI.isConnected && }
-
+
diff --git a/gui/src/components/MainLayout.scss b/gui/src/components/MainLayout.scss index 9c0b6816d6..3668a56c9c 100644 --- a/gui/src/components/MainLayout.scss +++ b/gui/src/components/MainLayout.scss @@ -8,9 +8,9 @@ &.full { grid-template: 't t t' var(--topbar-h) - 's r r' var(--toolbar-h) - 's c l' calc(70% - var(--topbar-h) - var(--toolbar-h)) - 's c p' calc(30%) + 's r l' var(--toolbar-h) + 's c l' calc(60% - var(--topbar-h) - var(--toolbar-h)) + 's c p' calc(40%) / var(--navbar-w) calc(75% - var(--navbar-w)) calc(25%); } diff --git a/gui/src/components/MainLayout.tsx b/gui/src/components/MainLayout.tsx index 9066249e33..18aebed18f 100644 --- a/gui/src/components/MainLayout.tsx +++ b/gui/src/components/MainLayout.tsx @@ -12,7 +12,7 @@ import { useWebsocketAPI } from '@/hooks/websocket-api'; import './MainLayout.scss'; import { Toolbar } from './Toolbar'; import { SkeletonVisualizerWidget } from './widgets/SkeletonVisualizerWidget'; -import { SessionFlightList } from './SessionFlightList'; +import { SessionFlightList } from './flight-list/SessionFlightList'; export function MainLayout({ children, @@ -90,13 +90,13 @@ export function MainLayout({
{/* */} diff --git a/gui/src/components/SessionFlightList.tsx b/gui/src/components/SessionFlightList.tsx deleted file mode 100644 index ba363b5593..0000000000 --- a/gui/src/components/SessionFlightList.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { - flightlistIdtoLabel, - FlightListStep, - SessionFlightListContext, - useSessionFlightlist, -} from '@/hooks/session-flightlist'; -import { CheckIcon } from './commons/icon/CheckIcon'; -import classNames from 'classnames'; -import { Localized } from '@fluent/react'; -import { - FlightListStepId, - FlightListTrackerResetT, - ResetType, -} from 'solarxr-protocol'; -import { ReactNode } from 'react'; -import { Typography } from './commons/Typography'; -import { Button } from './commons/Button'; -import { ResetButton } from './home/ResetButton'; - -function Step({ - step: { status, id, optional, firstInvalid: firstActive }, - index, - children, -}: { - step: FlightListStep; - index: number; - children: ReactNode; -}) { - return ( -
-
-
- {status === 'complete' && } - {status === 'invalid' && index} - {status === 'blocked' && index} - {status === 'skipped' && index} -
-
- -
-
- {firstActive &&
{children}
} -
- ); -} - -const stepContentLookup: Record< - number, - (step: FlightListStep, context: SessionFlightListContext) => JSX.Element -> = { - [FlightListStepId.FULL_RESET]: (step) => { - const data = step.extraData as FlightListTrackerResetT; - return ( -
- Some Trackers need a reset to be performed -
- -
-
- ); - }, - [FlightListStepId.STEAMVR_DISCONNECTED]: (step, { toggle }) => { - return ( - <> -
- - SteamVR is not running. Are you using it for vr? - -
- - -
-
- - ); - }, -}; - -export function SessionFlightList() { - const context = useSessionFlightlist(); - const { steps } = context; - - return ( -
- {steps.map((step, index) => ( - - {stepContentLookup[step.id]?.(step, context) || undefined} - - ))} -
- ); -} diff --git a/gui/src/components/WidgetsComponent.tsx b/gui/src/components/WidgetsComponent.tsx index 12560bff58..7fdbe8d465 100644 --- a/gui/src/components/WidgetsComponent.tsx +++ b/gui/src/components/WidgetsComponent.tsx @@ -1,49 +1,15 @@ -import { Localized, useLocalization } from '@fluent/react'; import { BVHButton } from './BVHButton'; import { TrackingPauseButton } from './TrackingPauseButton'; import { ResetButton } from './home/ResetButton'; import { OverlayWidget } from './widgets/OverlayWidget'; -import { TipBox } from './commons/TipBox'; import { DeveloperModeWidget } from './widgets/DeveloperModeWidget'; import { useConfig } from '@/hooks/config'; -import { ResetType, StatusData } from 'solarxr-protocol'; -import { useMemo } from 'react'; -import { parseStatusToLocale, useStatusContext } from '@/hooks/status-system'; +import { ResetType } from 'solarxr-protocol'; import { ClearMountingButton } from './ClearMountingButton'; import { ToggleableSkeletonVisualizerWidget } from './widgets/SkeletonVisualizerWidget'; -import { useAtomValue } from 'jotai'; -import { flatTrackersAtom } from '@/store/app-store'; -import { A } from './commons/A'; function UnprioritizedStatuses() { - const { l10n } = useLocalization(); - const trackers = useAtomValue(flatTrackersAtom); - const { statuses } = useStatusContext(); - const unprioritizedStatuses = useMemo( - () => Object.values(statuses).filter((status) => !status.prioritized), - [statuses] - ); - - return ( -
- {unprioritizedStatuses.map((status) => ( - - ), - }} - > - - {`Warning, you should fix ${StatusData[status.dataType]}`} - - - ))} -
- ); + return
; } export function WidgetsComponent() { diff --git a/gui/src/components/commons/A.tsx b/gui/src/components/commons/A.tsx index 9085b8a976..834f0abca3 100644 --- a/gui/src/components/commons/A.tsx +++ b/gui/src/components/commons/A.tsx @@ -1,14 +1,23 @@ import { open } from '@tauri-apps/plugin-shell'; +import classNames from 'classnames'; import { ReactNode } from 'react'; -export function A({ href, children }: { href?: string; children?: ReactNode }) { +export function A({ + href, + children, + className, +}: { + href?: string; + children?: ReactNode; + className?: string; +}) { return ( href && open(href).catch(() => window.open(href, '_blank')) } - className="underline" + className={classNames(className, 'underline', 'cursor-pointer')} > {children} diff --git a/gui/src/components/commons/Button.tsx b/gui/src/components/commons/Button.tsx index c85b579f85..608f1d001a 100644 --- a/gui/src/components/commons/Button.tsx +++ b/gui/src/components/commons/Button.tsx @@ -2,6 +2,7 @@ import classNames from 'classnames'; import React, { ReactNode, useMemo } from 'react'; import { NavLink } from 'react-router-dom'; import { LoaderIcon, SlimeState } from './icon/LoaderIcon'; +import { Localized, LocalizedProps } from '@fluent/react'; function ButtonContent({ loading, @@ -44,7 +45,9 @@ export type ButtonProps = { loading?: boolean; rounded?: boolean; state?: any; -} & React.ButtonHTMLAttributes; + id?: string; +} & React.ButtonHTMLAttributes & + Omit; export function Button({ children, @@ -55,6 +58,10 @@ export function Button({ state = {}, icon, rounded = false, + attrs, + id, + vars, + elems, ...props }: ButtonProps) { const classes = useMemo(() => { @@ -95,7 +102,7 @@ export function Button({ ); }, [variant, disabled, rounded, props.className]); - return to ? ( + const content = to ? ( ); + + if (id) { + return ( + + {content} + + ); + } + + return content; } diff --git a/gui/src/components/commons/Typography.tsx b/gui/src/components/commons/Typography.tsx index fb1917ede7..eb6d78a686 100644 --- a/gui/src/components/commons/Typography.tsx +++ b/gui/src/components/commons/Typography.tsx @@ -1,4 +1,5 @@ import { useConfig } from '@/hooks/config'; +import { Localized, LocalizedProps } from '@fluent/react'; import classNames from 'classnames'; import { createElement, ReactNode, useMemo } from 'react'; @@ -12,6 +13,10 @@ export function Typography({ truncate = false, textAlign, sentryMask = false, + id, + attrs, + elems, + vars, }: { variant?: | 'main-title' @@ -39,7 +44,8 @@ export function Typography({ | 'text-end'; children?: ReactNode; sentryMask?: boolean; -}) { + id?: string; +} & Omit) { const tag = useMemo(() => { const tags = { 'main-title': 'h1', @@ -52,7 +58,7 @@ export function Typography({ }, [variant]); const { config } = useConfig(); - return createElement( + const element = createElement( tag, { className: classNames([ @@ -77,6 +83,16 @@ export function Typography({ sentryMask && 'sentry-mask', ]), }, - children || [] + children || id || [] ); + + if (id) { + return ( + + {element} + + ); + } + + return element; } diff --git a/gui/src/components/flight-list/FlightListProvider.tsx b/gui/src/components/flight-list/FlightListProvider.tsx new file mode 100644 index 0000000000..7263b4db50 --- /dev/null +++ b/gui/src/components/flight-list/FlightListProvider.tsx @@ -0,0 +1,15 @@ +import { + FlightListContextC, + provideSessionFlightlist, +} from '@/hooks/session-flightlist'; +import { ReactNode } from 'react'; + +export function FlightListProvider({ children }: { children: ReactNode }) { + const context = provideSessionFlightlist(); + + return ( + + {children} + + ); +} diff --git a/gui/src/components/flight-list/SessionFlightList.tsx b/gui/src/components/flight-list/SessionFlightList.tsx new file mode 100644 index 0000000000..e0fdaa7ec6 --- /dev/null +++ b/gui/src/components/flight-list/SessionFlightList.tsx @@ -0,0 +1,295 @@ +import { + flightlistIdtoLabel, + FlightListStep, + SessionFlightListContext, + useSessionFlightlist, +} from '@/hooks/session-flightlist'; +import classNames from 'classnames'; +import { + FlightListPublicNetworksT, + FlightListStepId, + ResetType, +} from 'solarxr-protocol'; +import { ReactNode } from 'react'; +import { openUrl } from '@tauri-apps/plugin-opener'; +import { CheckIcon } from '@/components/commons/icon/CheckIcon'; +import { Typography } from '@/components/commons/Typography'; +import { Button } from '@/components/commons/Button'; +import { ResetButton } from '@/components/home/ResetButton'; +import { A } from '@/components/commons/A'; +import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon'; +import { ProgressBar } from '@/components/commons/ProgressBar'; +import { CrossIcon } from '@/components/commons/icon/CrossIcon'; +import { Localized } from '@fluent/react'; + +function Step({ + step: { status, id, optional, firstInvalid }, + children, +}: { + step: FlightListStep; + index: number; + children: ReactNode; +}) { + return ( +
+
+
+
+ {status === 'complete' && } + {status !== 'complete' && ( +
+ )} +
+
+ +
+
+ + {(firstInvalid || (status === 'invalid' && optional)) && children && ( +
{children}
+ )} +
+ ); +} + +const stepContentLookup: Record< + number, + (step: FlightListStep, context: SessionFlightListContext) => JSX.Element +> = { + [FlightListStepId.TRACKERS_CALIBRATION]: (step, { toggle }) => { + return ( +
+ +
+ {step.ignorable && ( + + )} +
+
+ ); + }, + [FlightListStepId.FULL_RESET]: () => { + return ( +
+ +
+ + + + + + + + + +
+
+
+ + Reset position +
+
+ + Reset position side +
+
+ + Reset position wrong +
+
+
+ +
+
+ ); + }, + [FlightListStepId.STEAMVR_DISCONNECTED]: (step, { toggle }) => { + return ( + <> +
+ +
+ + {step.ignorable && ( + + )} +
+
+ + ); + }, + [FlightListStepId.TRACKER_ERROR]: () => { + return ; + }, + [FlightListStepId.UNASSIGNED_HMD]: () => { + return ; + }, + [FlightListStepId.NETWORK_PROFILE_PUBLIC]: (step, { toggle }) => { + const data = step.extraData as FlightListPublicNetworksT | null; + return ( + <> +
+ + ), + }} + whitespace="whitespace-pre-wrap" + > +
+ + {step.ignorable && ( + + )} +
+
+ + ); + }, + [FlightListStepId.VRCHAT_SETTINGS]: (step, { toggle }) => { + return ( + <> +
+ +
+ + {step.ignorable && ( + + )} +
+
+ + ); + }, + [FlightListStepId.MOUNTING_CALIBRATION]: (step, { toggle }) => { + return ( +
+ + +
+ mounting reset ski pose +
+
+ + {step.ignorable && ( + + )} +
+
+ ); + }, +}; + +export function SessionFlightList() { + const context = useSessionFlightlist(); + const { steps } = context; + + return ( +
+
+ {steps.map((step, index) => ( + + {stepContentLookup[step.id]?.(step, context) || undefined} + + ))} +
+ {/*
*/} +
+
+ +
+
+ + You are not prepared to use SlimeVR! + +
+
+
+ +
+ +
+
+
+
+
+ ); +} diff --git a/gui/src/components/home/Home.tsx b/gui/src/components/home/Home.tsx index 8685c27252..70bd18400c 100644 --- a/gui/src/components/home/Home.tsx +++ b/gui/src/components/home/Home.tsx @@ -1,31 +1,20 @@ -import { Localized, useLocalization } from '@fluent/react'; -import { Link, NavLink, useNavigate } from 'react-router-dom'; -import { StatusData, TrackerDataT } from 'solarxr-protocol'; +import { useLocalization } from '@fluent/react'; +import { NavLink, useNavigate } from 'react-router-dom'; +import { TrackerDataT } from 'solarxr-protocol'; import { useConfig } from '@/hooks/config'; import { Typography } from '@/components/commons/Typography'; import { TrackerCard } from '@/components/tracker/TrackerCard'; import { TrackersTable } from '@/components/tracker/TrackersTable'; -import { - parseStatusToLocale, - trackerStatusRelated, - useStatusContext, -} from '@/hooks/status-system'; -import { useMemo } from 'react'; -import { WarningBox } from '@/components/commons/TipBox'; import { HeadsetIcon } from '@/components/commons/icon/HeadsetIcon'; -import classNames from 'classnames'; import { useAtomValue } from 'jotai'; import { flatTrackersAtom } from '@/store/app-store'; -import { useVRCConfig } from '@/hooks/vrc-config'; - -const DONT_REPEAT_STATUSES = [StatusData.StatusTrackerReset]; +import { useSessionFlightlist } from '@/hooks/session-flightlist'; export function Home() { const { l10n } = useLocalization(); const { config } = useConfig(); const trackers = useAtomValue(flatTrackersAtom); - const { statuses } = useStatusContext(); - const { invalidConfig } = useVRCConfig(); + const { hightlightedTrackers } = useSessionFlightlist(); const navigate = useNavigate(); const sendToSettings = (tracker: TrackerDataT) => { @@ -34,15 +23,6 @@ export function Home() { ); }; - const filteredStatuses = useMemo(() => { - const dontRepeat = new Map(DONT_REPEAT_STATUSES.map((x) => [x, false])); - return Object.entries(statuses).filter(([, value]) => { - if (dontRepeat.get(value.dataType)) return false; - if (dontRepeat.has(value.dataType)) dontRepeat.set(value.dataType, true); - return true; - }); - }, [statuses]); - return (
-
- {filteredStatuses - .filter(([, status]) => status.prioritized) - .map(([, status]) => ( - - - {`Warning, you should fix ${StatusData[status.dataType]}`} - - - ))} - {invalidConfig && ( - -
-
- -
-
- -
- -
- -
-
-
- )} -
{trackers.length === 0 && (
@@ -107,9 +52,13 @@ export function Home() { smol showUpdates interactable - warning={Object.values(statuses).some((status) => - trackerStatusRelated(tracker, status) - )} + warning={ + !!hightlightedTrackers.find( + (t) => + t?.deviceId?.id === tracker.trackerId?.deviceId?.id && + t?.trackerNum === tracker.trackerId?.trackerNum + ) + } /> ))}
diff --git a/gui/src/components/home/ResetButton.tsx b/gui/src/components/home/ResetButton.tsx index 0bd4874f0a..54c0651eac 100644 --- a/gui/src/components/home/ResetButton.tsx +++ b/gui/src/components/home/ResetButton.tsx @@ -1,10 +1,10 @@ import { useLocalization } from '@fluent/react'; import { useEffect, useMemo, useRef, useState } from 'react'; import { + FlightListStepId, ResetRequestT, ResetType, RpcMessage, - StatusData, } from 'solarxr-protocol'; import { useConfig } from '@/hooks/config'; import { useCountdown } from '@/hooks/countdown'; @@ -20,8 +20,8 @@ import { YawResetIcon, FullResetIcon, } from '@/components/commons/icon/ResetIcon'; -import { useStatusContext } from '@/hooks/status-system'; import classNames from 'classnames'; +import { useSessionFlightlist } from '@/hooks/session-flightlist'; export function ResetButton({ type, @@ -36,7 +36,7 @@ export function ResetButton({ }) { const { l10n } = useLocalization(); const { sendRPCPacket } = useWebsocketAPI(); - const { statuses } = useStatusContext(); + const { steps } = useSessionFlightlist(); const { config } = useConfig(); const finishedTimeoutRef = useRef(-1); const [isFinished, setFinished] = useState(false); @@ -44,10 +44,10 @@ export function ResetButton({ const needsFullReset = useMemo( () => type === ResetType.Mounting && - Object.values(statuses).some( - (status) => status.dataType === StatusData.StatusTrackerReset + steps.some( + (step) => step.id === FlightListStepId.FULL_RESET && !step.valid ), - [statuses] + [steps, type] ); const reset = () => { diff --git a/gui/src/components/onboarding/pages/ConnectTracker.tsx b/gui/src/components/onboarding/pages/ConnectTracker.tsx index a7ceea6049..9686ab08ca 100644 --- a/gui/src/components/onboarding/pages/ConnectTracker.tsx +++ b/gui/src/components/onboarding/pages/ConnectTracker.tsx @@ -3,9 +3,10 @@ import classNames from 'classnames'; import { useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { + FlightListPublicNetworksT, + FlightListStepId, RpcMessage, StartWifiProvisioningRequestT, - StatusData, StopWifiProvisioningRequestT, WifiProvisioningStatus, WifiProvisioningStatusResponseT, @@ -24,9 +25,9 @@ import './ConnectTracker.scss'; import { useAtomValue } from 'jotai'; import { connectedIMUTrackersAtom } from '@/store/app-store'; import { BaseModal } from '@/components/commons/BaseModal'; -import { useStatusContext } from '@/hooks/status-system'; import { A } from '@/components/commons/A'; import { CONNECT_TRACKER } from '@/utils/tauri'; +import { useSessionFlightlist } from '@/hooks/session-flightlist'; const statusLabelMap = { [WifiProvisioningStatus.NONE]: @@ -74,9 +75,34 @@ const statusProgressMap = { [WifiProvisioningStatus.COULD_NOT_FIND_SERVER]: 0.8, }; +export function InvalidNetworkProfileWarning({ + extraData, +}: { + extraData: FlightListPublicNetworksT; +}) { + return ( +
+ + ), + }} + vars={{ + count: extraData.adapters.length, + adapters: extraData.adapters.join(', '), + }} + > + WARNING + +
+ ); +} + export function ConnectTrackersPage() { const { l10n } = useLocalization(); - const { statuses } = useStatusContext(); + const { steps } = useSessionFlightlist(); const connectedIMUTrackers = useAtomValue(connectedIMUTrackersAtom); const { applyProgress, state } = useOnboarding(); @@ -165,11 +191,12 @@ export function ConnectTrackersPage() { [connectedIMUTrackers.length] ); - const filteredStatuses = useMemo(() => { - return Object.entries(statuses).filter( - ([, value]) => value.dataType == StatusData.StatusPublicNetwork + const invalidNetworkProfile = useMemo(() => { + return steps.find( + (step) => + step.id === FlightListStepId.NETWORK_PROFILE_PUBLIC && !step.valid ); - }, [statuses]); + }, [steps]); return ( <> @@ -243,23 +270,13 @@ export function ConnectTrackersPage() { > Conditional tip - {filteredStatuses.map(([, status]) => ( -
- - ), - }} - > - - {`Warning, you should fix ${StatusData[status.dataType]}`} - - -
- ))} + {invalidNetworkProfile && ( + + )}
{children} - ); -} diff --git a/gui/src/components/settings/pages/GeneralSettings.tsx b/gui/src/components/settings/pages/GeneralSettings.tsx index e128074a40..e53ab15c0e 100644 --- a/gui/src/components/settings/pages/GeneralSettings.tsx +++ b/gui/src/components/settings/pages/GeneralSettings.tsx @@ -9,6 +9,7 @@ import { ModelRatiosT, ModelSettingsT, ModelTogglesT, + MountingMethod, ResetsSettingsT, RpcMessage, SettingsRequestT, @@ -38,6 +39,11 @@ import { serializeStayAlignedSettings, deserializeStayAlignedSettings, } from './components/StayAlignedSettings'; +import { + defaultResetSettings, + loadResetSettings, + ResetSettingsForm, +} from '@/hooks/reset-settings'; export type SettingsForm = { trackers: { @@ -95,13 +101,7 @@ export type SettingsForm = { legTweaks: { correctionStrength: number; }; - resetsSettings: { - resetMountingFeet: boolean; - armsMountingResetMode: number; - yawResetSmoothTime: number; - saveMountingReset: boolean; - resetHmdPitch: boolean; - }; + resetsSettings: ResetSettingsForm; stayAligned: StayAlignedSettingsForm; }; @@ -156,22 +156,14 @@ const defaultValues: SettingsForm = { numberTrackersOverThreshold: 1, }, legTweaks: { correctionStrength: 0.3 }, - resetsSettings: { - resetMountingFeet: false, - armsMountingResetMode: 0, - yawResetSmoothTime: 0.0, - saveMountingReset: false, - resetHmdPitch: false, - }, + resetsSettings: defaultResetSettings, stayAligned: defaultStayAlignedSettings, }; export function GeneralSettings() { const { l10n } = useLocalization(); const { config } = useConfig(); - // const { state } = useLocation(); const { currentLocales } = useLocaleConfig(); - // const pageRef = useRef(null); const percentageFormat = new Intl.NumberFormat(currentLocales, { style: 'percent', @@ -288,17 +280,7 @@ export function GeneralSettings() { settings.stayAligned = serializeStayAlignedSettings(values.stayAligned); if (values.resetsSettings) { - const resetsSettings = new ResetsSettingsT(); - resetsSettings.resetMountingFeet = - values.resetsSettings.resetMountingFeet; - resetsSettings.armsMountingResetMode = - values.resetsSettings.armsMountingResetMode; - resetsSettings.yawResetSmoothTime = - values.resetsSettings.yawResetSmoothTime; - resetsSettings.saveMountingReset = - values.resetsSettings.saveMountingReset; - resetsSettings.resetHmdPitch = values.resetsSettings.resetHmdPitch; - settings.resetsSettings = resetsSettings; + settings.resetsSettings = loadResetSettings(values.resetsSettings); } sendRPCPacket(RpcMessage.ChangeSettingsRequest, settings); diff --git a/gui/src/components/tracker/TrackersTable.tsx b/gui/src/components/tracker/TrackersTable.tsx index 78064204b8..6a62876658 100644 --- a/gui/src/components/tracker/TrackersTable.tsx +++ b/gui/src/components/tracker/TrackersTable.tsx @@ -15,9 +15,9 @@ import { formatVector3 } from '@/utils/formatting'; import { TrackerBattery } from './TrackerBattery'; import { TrackerStatus } from './TrackerStatus'; import { TrackerWifi } from './TrackerWifi'; -import { trackerStatusRelated, useStatusContext } from '@/hooks/status-system'; import { FlatDeviceTracker } from '@/store/app-store'; import { StayAlignedInfo } from '@/components/stay-aligned/StayAlignedInfo'; +import { useSessionFlightlist } from '@/hooks/session-flightlist'; enum DisplayColumn { NAME, @@ -169,7 +169,7 @@ export function TrackersTable({ const { l10n } = useLocalization(); const [hoverTracker, setHoverTracker] = useState(null); const { config } = useConfig(); - const { statuses } = useStatusContext(); + const { hightlightedTrackers } = useSessionFlightlist(); const trackerEqual = (id: TrackerIdT | null) => id?.trackerNum == hoverTracker?.trackerNum && @@ -242,9 +242,13 @@ export function TrackersTable({ hover={trackerEqual(data.tracker.trackerId)} onMouseOver={() => setHoverTracker(data.tracker.trackerId)} onMouseOut={() => setHoverTracker(null)} - warning={Object.values(statuses).some((status) => - trackerStatusRelated(data.tracker, status) - )} + warning={ + !!hightlightedTrackers.find( + (t) => + t?.deviceId?.id === data.tracker.trackerId?.deviceId?.id && + t?.trackerNum === data.tracker.trackerId?.trackerNum + ) + } > {row(data) || <>} diff --git a/gui/src/components/vrc/VRCWarningsPage.tsx b/gui/src/components/vrc/VRCWarningsPage.tsx index 132fddbc1d..171de7581d 100644 --- a/gui/src/components/vrc/VRCWarningsPage.tsx +++ b/gui/src/components/vrc/VRCWarningsPage.tsx @@ -104,7 +104,7 @@ const onOffKey = (value: boolean) => export function VRCWarningsPage() { const { l10n } = useLocalization(); - const { state, toggleMutedSettings, mutedSettings } = useVRCConfig(); + const { state, toggleMutedSettings } = useVRCConfig(); const { currentLocales } = useLocaleConfig(); const meterFormat = Intl.NumberFormat(currentLocales, { @@ -119,7 +119,7 @@ export function VRCWarningsPage() { const settingRowProps = (key: keyof VRCConfigStateSupported['validity']) => ({ mute: () => toggleMutedSettings(key), - muted: mutedSettings.includes(key), + muted: state.muted.includes(key), valid: state.validity[key] == true, }); diff --git a/gui/src/hooks/reset-settings.ts b/gui/src/hooks/reset-settings.ts new file mode 100644 index 0000000000..7ea5be5353 --- /dev/null +++ b/gui/src/hooks/reset-settings.ts @@ -0,0 +1,62 @@ +import { + ChangeSettingsRequestT, + MountingMethod, + ResetsSettingsT, + RpcMessage, + SettingsResetRequestT, + SettingsResponseT, +} from 'solarxr-protocol'; +import { useWebsocketAPI } from './websocket-api'; +import { useEffect, useState } from 'react'; + +export interface ResetSettingsForm { + resetMountingFeet: boolean; + armsMountingResetMode: number; + yawResetSmoothTime: number; + saveMountingReset: boolean; + resetHmdPitch: boolean; + preferedMountingMethod: number; +} + +export const defaultResetSettings = { + resetMountingFeet: false, + armsMountingResetMode: 0, + yawResetSmoothTime: 0.0, + saveMountingReset: false, + resetHmdPitch: false, + preferedMountingMethod: MountingMethod.AUTOMATIC, +}; + +export function loadResetSettings(resetSettingsForm: ResetSettingsForm) { + const resetsSettings = new ResetsSettingsT(); + resetsSettings.resetMountingFeet = resetSettingsForm.resetMountingFeet; + resetsSettings.armsMountingResetMode = resetSettingsForm.armsMountingResetMode; + resetsSettings.yawResetSmoothTime = resetSettingsForm.yawResetSmoothTime; + resetsSettings.saveMountingReset = resetSettingsForm.saveMountingReset; + resetsSettings.resetHmdPitch = resetSettingsForm.resetHmdPitch; + resetsSettings.preferedMountingMethod = resetSettingsForm.preferedMountingMethod; + + return resetsSettings; +} + +export function useResetSettings() { + const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); + const [settings, setSettings] = useState(defaultResetSettings); + + useEffect(() => + sendRPCPacket(RpcMessage.SettingsRequest, new SettingsResetRequestT()) + ); + + useRPCPacket(RpcMessage.SettingsResponse, (settings: SettingsResponseT) => { + if (settings.resetsSettings) setSettings(settings.resetsSettings); + }); + + return { + update: (resetSettingsForm: Partial) => { + const req = new ChangeSettingsRequestT(); + const res = loadResetSettings({ ...settings, ...resetSettingsForm }); + req.resetsSettings = res; + sendRPCPacket(RpcMessage.ChangeSettingsRequest, req); + }, + }; +} diff --git a/gui/src/hooks/session-flightlist.ts b/gui/src/hooks/session-flightlist.ts index 030568d8ea..3a7407b6f9 100644 --- a/gui/src/hooks/session-flightlist.ts +++ b/gui/src/hooks/session-flightlist.ts @@ -1,7 +1,6 @@ import { FlightListRequestT, FlightListResponseT, - FlightListStepChangeResponseT, FlightListStepId, FlightListStepT, FlightListStepVisibility, @@ -9,7 +8,7 @@ import { ToggleFlightListStepRequestT, } from 'solarxr-protocol'; import { useWebsocketAPI } from './websocket-api'; -import { useEffect, useState } from 'react'; +import { createContext, useContext, useEffect, useMemo, useState } from 'react'; export const flightlistIdtoLabel: Record = { [FlightListStepId.UNKNOWN]: '', @@ -20,6 +19,7 @@ export const flightlistIdtoLabel: Record = { [FlightListStepId.UNASSIGNED_HMD]: 'flight_list-UNASSIGNED_HMD', [FlightListStepId.TRACKER_ERROR]: 'flight_list-TRACKER_ERROR', [FlightListStepId.NETWORK_PROFILE_PUBLIC]: 'flight_list-NETWORK_PROFILE_PUBLIC', + [FlightListStepId.MOUNTING_CALIBRATION]: 'flight_list-MOUNTING_CALIBRATION', }; export type FlightListStepStatus = 'complete' | 'skipped' | 'blocked' | 'invalid'; @@ -37,7 +37,7 @@ const createStep = ( const blocked = previousSteps.some(({ valid, optional }) => !valid && !optional); let status: FlightListStepStatus = 'complete'; - if (blocked) status = 'blocked'; + if (blocked && !step.valid) status = 'blocked'; if (!blocked && !step.valid) status = 'invalid'; if (!blocked && step.optional && !step.valid && index !== previousSteps.length) status = 'skipped'; @@ -50,58 +50,59 @@ const createStep = ( }; }; -export type SessionFlightListContext = ReturnType; +export type SessionFlightListContext = ReturnType; const stepVisibility = ({ visibility, status, firstInvalid }: FlightListStep) => firstInvalid || - visibility == FlightListStepVisibility.ALWAYS || + visibility === FlightListStepVisibility.ALWAYS || (visibility === FlightListStepVisibility.WHEN_INVALID && - ['invalid', 'blocked', 'skipped'].includes(status)); + ['invalid', 'blocked'].includes(status)); -export function useSessionFlightlist() { +export function provideSessionFlightlist() { const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); const [steps, setSteps] = useState([]); const [ignoredSteps, setIgnoredSteps] = useState([]); useRPCPacket(RpcMessage.FlightListResponse, (data: FlightListResponseT) => { setIgnoredSteps(data.ignoredSteps); - const visibleSteps = data.steps.filter( - (step) => !data.ignoredSteps.includes(step.id) + const activeSteps = data.steps.filter( + (step) => !data.ignoredSteps.includes(step.id) && step.enabled ); - const steps = data.steps.map((step: FlightListStepT, index) => - createStep(visibleSteps, step, index) + const steps = activeSteps.map((step: FlightListStepT, index) => + createStep(activeSteps, step, index) ); setSteps(steps); }); - useRPCPacket( - RpcMessage.FlightListStepChangeResponse, - (data: FlightListStepChangeResponseT) => { - const step = data.step; - console.log('STATE CHANGE', step); - if (!step) throw 'invalid state - step should be set'; - setSteps((steps) => { - const visibleSteps = steps.filter((step) => !ignoredSteps.includes(step.id)); - const newsteps = steps.map((step: FlightListStepT, index) => - createStep(visibleSteps, step, index) - ); - const stepIndex = newsteps.findIndex(({ id }) => step.id === id); - if (stepIndex == -1) return newsteps; // skip the step because it is not visible - newsteps[stepIndex] = createStep(newsteps, step, stepIndex); - return newsteps; - }); - } - ); - useEffect(() => { sendRPCPacket(RpcMessage.FlightListRequest, new FlightListRequestT()); }, []); + const visibleSteps = useMemo(() => steps.filter(stepVisibility), [steps]); + const firstInvalid = useMemo( + () => + visibleSteps.find( + (step) => !step.valid && step.status != 'blocked' && !step.optional + ), + [visibleSteps] + ); + + const hightlightedTrackers = useMemo(() => { + if (!firstInvalid || !firstInvalid.extraData) return []; + if ('trackersId' in firstInvalid.extraData) { + return firstInvalid.extraData.trackersId; + } + if ('trackerId' in firstInvalid.extraData) { + return [firstInvalid.extraData.trackerId]; + } + return []; + }, [firstInvalid]); + return { - steps: steps - .filter((step) => !ignoredSteps.includes(step.id)) - .filter(stepVisibility), + steps: steps.filter(stepVisibility), + firstInvalid, ignoredSteps, + hightlightedTrackers, toggle: (step: FlightListStepId) => { const res = new ToggleFlightListStepRequestT(); res.stepId = step; @@ -109,3 +110,15 @@ export function useSessionFlightlist() { }, }; } + +export const FlightListContextC = createContext( + undefined as never +); + +export function useSessionFlightlist() { + const context = useContext(FlightListContextC); + if (!context) { + throw new Error('useSessionFlightlist must be within a FlightList Provider'); + } + return context; +} diff --git a/gui/src/hooks/status-system.ts b/gui/src/hooks/status-system.ts deleted file mode 100644 index 38226301e6..0000000000 --- a/gui/src/hooks/status-system.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { createContext, useEffect, useReducer, useContext } from 'react'; -import { - BodyPart, - RpcMessage, - StatusData, - StatusMessageT, - StatusPublicNetworkT, - StatusSteamVRDisconnectedT, - StatusSystemFixedT, - StatusSystemRequestT, - StatusSystemResponseT, - StatusSystemUpdateT, - StatusTrackerErrorT, - StatusTrackerResetT, - StatusUnassignedHMDT, - TrackerDataT, -} from 'solarxr-protocol'; -import { useWebsocketAPI } from './websocket-api'; -import { FluentVariable } from '@fluent/bundle'; -import { ReactLocalization } from '@fluent/react'; -import { FlatDeviceTracker } from '@/store/app-store'; - -type StatusSystemStateAction = - | StatusSystemStateFixedAction - | StatusSystemStateNewAction - | StatusSystemStateUpdateAction; - -interface StatusSystemStateFixedAction { - type: RpcMessage.StatusSystemFixed; - data: number; -} - -interface StatusSystemStateUpdateAction { - type: RpcMessage.StatusSystemUpdate; - data: StatusMessageT; -} - -interface StatusSystemStateNewAction { - type: RpcMessage.StatusSystemResponse; - data: StatusMessageT[]; -} - -interface StatusSystemState { - statuses: { - [id: number]: StatusMessageT; - }; -} - -export interface StatusSystemContext { - statuses: { - [id: number]: StatusMessageT; - }; -} - -function reducer( - state: StatusSystemState, - action: StatusSystemStateAction -): StatusSystemState { - switch (action.type) { - case RpcMessage.StatusSystemFixed: { - const newState = { - statuses: { ...state.statuses }, - }; - delete newState.statuses[action.data]; - return newState; - } - case RpcMessage.StatusSystemUpdate: { - return { - statuses: { ...state.statuses, [action.data.id]: action.data }, - }; - } - case RpcMessage.StatusSystemResponse: { - return { - // Convert the array into an object, we dont want to have an array on our map! - statuses: action.data.reduce((prev, cur) => ({ ...prev, [cur.id]: cur }), {}), - }; - } - } -} - -export function useProvideStatusContext(): StatusSystemContext { - const { useRPCPacket, sendRPCPacket, isConnected } = useWebsocketAPI(); - const [state, dispatch] = useReducer(reducer, { statuses: {} }); - - useRPCPacket( - RpcMessage.StatusSystemResponse, - ({ currentStatuses }: StatusSystemResponseT) => - dispatch({ type: RpcMessage.StatusSystemResponse, data: currentStatuses }) - ); - - useRPCPacket(RpcMessage.StatusSystemFixed, ({ fixedStatusId }: StatusSystemFixedT) => - dispatch({ type: RpcMessage.StatusSystemFixed, data: fixedStatusId }) - ); - - useRPCPacket( - RpcMessage.StatusSystemUpdate, - ({ newStatus }: StatusSystemUpdateT) => - newStatus && dispatch({ type: RpcMessage.StatusSystemUpdate, data: newStatus }) - ); - - useEffect(() => { - if (!isConnected) return; - sendRPCPacket(RpcMessage.StatusSystemRequest, new StatusSystemRequestT()); - }, [isConnected]); - - return state; -} - -export const StatusSystemC = createContext(undefined as never); - -export function useStatusContext() { - const context = useContext(StatusSystemC); - if (!context) { - throw new Error('useStatusContext must be within a StatusSystemContext Provider'); - } - return context; -} - -export function parseStatusToLocale( - status: StatusMessageT, - trackers: FlatDeviceTracker[] | null, - l10n: ReactLocalization -): Record { - switch (status.dataType) { - case StatusData.NONE: - case StatusData.StatusTrackerReset: - case StatusData.StatusUnassignedHMD: - return {}; - case StatusData.StatusPublicNetwork: { - const data = status.data as StatusPublicNetworkT; - return { - adapters: data.adapters.join(', '), - count: data.adapters.length, - }; - } - case StatusData.StatusSteamVRDisconnected: { - const data = status.data as StatusSteamVRDisconnectedT; - if (typeof data.bridgeSettingsName === 'string') { - return { type: data.bridgeSettingsName }; - } - return {}; - } - case StatusData.StatusTrackerError: { - const data = status.data as StatusTrackerErrorT; - if (data.trackerId?.trackerNum === undefined || !trackers) { - return {}; - } - - const tracker = trackers.find( - ({ tracker }) => - tracker?.trackerId?.trackerNum == data.trackerId?.trackerNum && - tracker?.trackerId?.deviceId?.id == data.trackerId?.deviceId?.id - ); - if (!tracker) - return { - trackerName: 'unknown', - }; - const name = tracker.tracker.info?.customName - ? tracker.tracker.info?.customName - : tracker.tracker.info?.bodyPart - ? l10n.getString('body_part-' + BodyPart[tracker.tracker.info?.bodyPart]) - : tracker.tracker.info?.displayName || 'unknown'; - if (typeof name !== 'string') { - return { - trackerName: new TextDecoder().decode(name), - }; - } - return { trackerName: name }; - } - } -} - -export const doesntContainTrackerInfo: readonly StatusData[] = [StatusData.NONE]; -export function trackerStatusRelated( - tracker: TrackerDataT, - status: StatusMessageT -): boolean { - if (doesntContainTrackerInfo.includes(status.dataType)) { - return false; - } - - switch (status.dataType) { - case StatusData.StatusTrackerReset: { - const data = status.data as StatusTrackerResetT; - return ( - data.trackerId?.trackerNum == tracker.trackerId?.trackerNum && - data.trackerId?.deviceId?.id === tracker.trackerId?.deviceId?.id - ); - } - case StatusData.StatusTrackerError: { - const data = status.data as StatusTrackerErrorT; - return ( - data.trackerId?.trackerNum == tracker.trackerId?.trackerNum && - data.trackerId?.deviceId?.id === tracker.trackerId?.deviceId?.id - ); - } - case StatusData.StatusUnassignedHMD: { - const data = status.data as StatusUnassignedHMDT; - return ( - data.trackerId?.trackerNum == tracker.trackerId?.trackerNum && - data.trackerId?.deviceId?.id === tracker.trackerId?.deviceId?.id - ); - } - default: - return false; - } -} diff --git a/gui/src/hooks/vrc-config.ts b/gui/src/hooks/vrc-config.ts index 6251807b5e..e38305df4d 100644 --- a/gui/src/hooks/vrc-config.ts +++ b/gui/src/hooks/vrc-config.ts @@ -3,19 +3,19 @@ import { useWebsocketAPI } from './websocket-api'; import { RpcMessage, VRCAvatarMeasurementType, + VRCConfigSettingToggleMuteT, VRCConfigStateChangeResponseT, VRCConfigStateRequestT, VRCSpineMode, VRCTrackerModel, } from 'solarxr-protocol'; -import { useConfig } from './config'; type NonNull = { [P in keyof T]: NonNullable; }; export type VRCConfigStateSupported = { isSupported: true } & NonNull< - Pick + Pick >; export type VRCConfigState = { isSupported: false } | VRCConfigStateSupported; @@ -45,7 +45,6 @@ export const avatarMeasurementTypeTranslationMap: Record< export function useVRCConfig() { const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); - const { config, setConfig } = useConfig(); const [state, setState] = useState(null); useLayoutEffect(() => { @@ -59,31 +58,20 @@ export function useVRCConfig() { } ); - const mutedSettings = useMemo(() => { - if (!state?.isSupported) return []; - return Object.keys(state.validity).filter((k) => - config?.vrcMutedWarnings.includes(k) - ); - }, [state, config]); - const invalidConfig = useMemo(() => { if (!state?.isSupported) return false; return Object.entries(state.validity) - .filter(([k]) => !config?.vrcMutedWarnings.includes(k)) + .filter(([k]) => !state.muted.includes(k)) .some(([, v]) => !v); - }, [state, config]); + }, [state]); return { state, invalidConfig, - mutedSettings, toggleMutedSettings: async (key: keyof VRCConfigStateSupported['validity']) => { - if (!config) return; - const index = config.vrcMutedWarnings.findIndex((v) => v === key); - if (index === -1) config.vrcMutedWarnings.push(key); - else config?.vrcMutedWarnings.splice(index, 1); - await setConfig(config); - console.log(config.vrcMutedWarnings); + const req = new VRCConfigSettingToggleMuteT(); + req.key = key; + sendRPCPacket(RpcMessage.VRCConfigSettingToggleMute, req); }, }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24a27df12a..7c3b23a8a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,12 +62,15 @@ importers: '@tauri-apps/plugin-fs': specifier: ^2.0.0 version: 2.0.0 + '@tauri-apps/plugin-opener': + specifier: ^2.4.0 + version: 2.4.0 '@tauri-apps/plugin-os': specifier: ^2.0.0 version: 2.0.0 '@tauri-apps/plugin-shell': - specifier: ^2.0.0 - version: 2.0.0 + specifier: ^2.3.0 + version: 2.3.0 '@tauri-apps/plugin-store': specifier: ^2.0.0 version: 2.0.0 @@ -485,6 +488,7 @@ packages: '@dword-design/functions@5.0.27': resolution: {integrity: sha512-16A97RKtn21xLWI7Y7VhFz4WCWui+TLit/J5/l77BDpWmPi2LzgjQzDbafDDw7OUeY6GEAi/M4jdbuQhsIbSEg==} engines: {node: '>=14'} + deprecated: Use lodash and endent '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} @@ -1179,6 +1183,9 @@ packages: '@tauri-apps/api@2.0.2': resolution: {integrity: sha512-3wSwmG+1kr6WrgAFKK5ijkNFPp8TT3FLj3YHUb5EwMO+3FxX4uWlfSWkeeBy+Kc1RsKzugtYLuuya+98Flj+3w==} + '@tauri-apps/api@2.6.0': + resolution: {integrity: sha512-hRNcdercfgpzgFrMXWwNDBN0B7vNzOzRepy6ZAmhxi5mDLVPNrTpo9MGg2tN/F7JRugj4d2aF7E1rtPXAHaetg==} + '@tauri-apps/cli-darwin-arm64@2.0.3': resolution: {integrity: sha512-jIsbxGWS+As1ZN7umo90nkql/ZAbrDK0GBT6UsgHSz5zSwwArICsZFFwE1pLZip5yoiV5mn3TGG2c1+v+0puzQ==} engines: {node: '>= 10'} @@ -1250,11 +1257,14 @@ packages: '@tauri-apps/plugin-fs@2.0.0': resolution: {integrity: sha512-BNEeQQ5aH8J5SwYuWgRszVyItsmquRuzK2QRkVj8Z0sCsLnSvJFYI3JHRzzr3ltZGq1nMPtblrlZzuKqVzRawA==} + '@tauri-apps/plugin-opener@2.4.0': + resolution: {integrity: sha512-43VyN8JJtvKWJY72WI/KNZszTpDpzHULFxQs0CJBIYUdCRowQ6Q1feWTDb979N7nldqSuDOaBupZ6wz2nvuWwQ==} + '@tauri-apps/plugin-os@2.0.0': resolution: {integrity: sha512-M7hG/nNyQYTJxVG/UhTKhp9mpXriwWzrs9mqDreB8mIgqA3ek5nHLdwRZJWhkKjZrnDT4v9CpA9BhYeplTlAiA==} - '@tauri-apps/plugin-shell@2.0.0': - resolution: {integrity: sha512-OpW2+ycgJLrEoZityWeWYk+6ZWP9VyiAfbO+N/O8VfLkqyOym8kXh7odKDfINx9RAotkSGBtQM4abyKfJDkcUg==} + '@tauri-apps/plugin-shell@2.3.0': + resolution: {integrity: sha512-6GIRxO2z64uxPX4CCTuhQzefvCC0ew7HjdBhMALiGw74vFBDY95VWueAHOHgNOMV4UOUAFupyidN9YulTe5xlA==} '@tauri-apps/plugin-store@2.0.0': resolution: {integrity: sha512-l4xsbxAXrKGdBdYNNswrLfcRv3v1kOatdycOcVPYW+jKwkznCr1HEOrPXkPhXsZLSLyYmNXpgfOmdSZNmcykDg==} @@ -5405,6 +5415,8 @@ snapshots: '@tauri-apps/api@2.0.2': {} + '@tauri-apps/api@2.6.0': {} + '@tauri-apps/cli-darwin-arm64@2.0.3': optional: true @@ -5456,13 +5468,17 @@ snapshots: dependencies: '@tauri-apps/api': 2.0.2 + '@tauri-apps/plugin-opener@2.4.0': + dependencies: + '@tauri-apps/api': 2.6.0 + '@tauri-apps/plugin-os@2.0.0': dependencies: '@tauri-apps/api': 2.0.2 - '@tauri-apps/plugin-shell@2.0.0': + '@tauri-apps/plugin-shell@2.3.0': dependencies: - '@tauri-apps/api': 2.0.2 + '@tauri-apps/api': 2.6.0 '@tauri-apps/plugin-store@2.0.0': dependencies: diff --git a/server/core/src/main/java/dev/slimevr/NetworkProfileChecker.kt b/server/core/src/main/java/dev/slimevr/NetworkProfileChecker.kt new file mode 100644 index 0000000000..66e12e7716 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/NetworkProfileChecker.kt @@ -0,0 +1,60 @@ +package dev.slimevr + +data class NetworkInfo( + val name: String?, + val description: String?, + val category: NetworkCategory?, + val connectivity: Set?, + val connected: Boolean?, +) + +/** + * @see NLM_NETWORK_CATEGORY enumeration (netlistmgr.h) + */ +enum class NetworkCategory(val value: Int) { + PUBLIC(0), + PRIVATE(1), + DOMAIN_AUTHENTICATED(2), + ; + + companion object { + fun fromInt(value: Int) = values().find { it.value == value } + } +} + +/** + * @see NLM_CONNECTIVITY enumeration (netlistmgr.h) + */ +enum class ConnectivityFlags(val value: Int) { + DISCONNECTED(0), + IPV4_NOTRAFFIC(0x1), + IPV6_NOTRAFFIC(0x2), + IPV4_SUBNET(0x10), + IPV4_LOCALNETWORK(0x20), + IPV4_INTERNET(0x40), + IPV6_SUBNET(0x100), + IPV6_LOCALNETWORK(0x200), + IPV6_INTERNET(0x400), + ; + + companion object { + fun fromInt(value: Int): Set = + if (value == 0) { + setOf(DISCONNECTED) + } else { + values().filter { it != DISCONNECTED && (value and it.value) != 0 }.toSet() + } + } +} + +abstract class NetworkProfileChecker { + abstract val isSupported: Boolean + abstract val publicNetworks: List +} + +class NetworkProfileCheckerStub: NetworkProfileChecker() { + override val isSupported: Boolean + get() = false; + override val publicNetworks: List + get() = listOf() +} diff --git a/server/core/src/main/java/dev/slimevr/VRServer.kt b/server/core/src/main/java/dev/slimevr/VRServer.kt index a10971b12f..b1813bfd92 100644 --- a/server/core/src/main/java/dev/slimevr/VRServer.kt +++ b/server/core/src/main/java/dev/slimevr/VRServer.kt @@ -55,6 +55,7 @@ class VRServer @JvmOverloads constructor( serialHandlerProvider: (VRServer) -> SerialHandler = { _ -> SerialHandlerStub() }, flashingHandlerProvider: (VRServer) -> SerialFlashingHandler? = { _ -> null }, vrcConfigHandlerProvider: (VRServer) -> VRCConfigHandler = { _ -> VRCConfigHandlerStub() }, + networkProfileProvider: (VRServer) -> NetworkProfileChecker = { _ -> NetworkProfileCheckerStub() }, acquireMulticastLock: () -> Any? = { null }, // configPath is used by VRWorkout, do not remove! configPath: String, @@ -119,6 +120,8 @@ class VRServer @JvmOverloads constructor( val flightListManager: FlightListManager + val networkProfileChecker: NetworkProfileChecker; + init { // UwU configManager = ConfigManager(configPath) @@ -134,6 +137,7 @@ class VRServer @JvmOverloads constructor( autoBoneHandler = AutoBoneHandler(this) firmwareUpdateHandler = FirmwareUpdateHandler(this) vrcConfigManager = VRChatConfigManager(this, vrcConfigHandlerProvider(this)) + networkProfileChecker = networkProfileProvider(this) flightListManager = FlightListManager(this) protocolAPI = ProtocolAPI(this) val computedTrackers = humanPoseManager.computedTrackers diff --git a/server/core/src/main/java/dev/slimevr/config/ResetsConfig.kt b/server/core/src/main/java/dev/slimevr/config/ResetsConfig.kt index 0945ea7da5..907ddd519d 100644 --- a/server/core/src/main/java/dev/slimevr/config/ResetsConfig.kt +++ b/server/core/src/main/java/dev/slimevr/config/ResetsConfig.kt @@ -29,6 +29,23 @@ enum class ArmsResetModes(val id: Int) { } } +enum class MountingMethods(val id: Int) { + MANUAL(0), + AUTOMATIC(1); + + companion object { + val values = MountingMethods.entries.toTypedArray() + + @JvmStatic + fun fromId(id: Int): MountingMethods? { + for (filter in values) { + if (filter.id == id) return filter + } + return null + } + } +} + class ResetsConfig { // Enable mounting reset for feet? @@ -46,6 +63,9 @@ class ResetsConfig { // Reset the HMD's pitch upon full reset var resetHmdPitch = false + var preferedMountingMethod = MountingMethods.AUTOMATIC + + fun updateTrackersResetsSettings() { for (t in VRServer.instance.allTrackers) { t.resetsHandler.readResetConfig(this) diff --git a/server/core/src/main/java/dev/slimevr/config/VRCWarningsConfig.kt b/server/core/src/main/java/dev/slimevr/config/VRCWarningsConfig.kt new file mode 100644 index 0000000000..fd40b13dda --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/config/VRCWarningsConfig.kt @@ -0,0 +1,6 @@ +package dev.slimevr.config + +class VRCConfig { + // List of fields ignored in vrc warnings - @see VRCConfigValidity + val mutedWarnings: MutableList = mutableListOf() +} diff --git a/server/core/src/main/java/dev/slimevr/config/VRConfig.kt b/server/core/src/main/java/dev/slimevr/config/VRConfig.kt index d0f25395f7..1d18b93fcb 100644 --- a/server/core/src/main/java/dev/slimevr/config/VRConfig.kt +++ b/server/core/src/main/java/dev/slimevr/config/VRConfig.kt @@ -56,6 +56,8 @@ class VRConfig { val flightList: FlightListConfig = FlightListConfig() + val vrcConfig: VRCConfig = VRCConfig() + init { // Initialize default settings for OSC Router oscRouter.portIn = 9002 diff --git a/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt b/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt index 74007873b4..7209d1b5eb 100644 --- a/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt +++ b/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt @@ -2,13 +2,16 @@ package dev.slimevr.flightlist import dev.slimevr.VRServer import dev.slimevr.bridge.ISteamVRBridge +import dev.slimevr.config.MountingMethods import dev.slimevr.games.vrchat.VRCConfigListener import dev.slimevr.games.vrchat.VRCConfigRecommendedValues import dev.slimevr.games.vrchat.VRCConfigValidity import dev.slimevr.games.vrchat.VRCConfigValues +import dev.slimevr.reset.ResetListener import dev.slimevr.tracking.trackers.Tracker import dev.slimevr.tracking.trackers.TrackerStatus import dev.slimevr.tracking.trackers.udp.TrackerDataType +import io.eiren.util.OperatingSystem import solarxr_protocol.datatypes.DeviceIdT import solarxr_protocol.datatypes.TrackerIdT import solarxr_protocol.rpc.* @@ -18,7 +21,7 @@ import kotlin.concurrent.timerTask import kotlin.system.measureTimeMillis interface FlightListListener { - fun onStepUpdate(step: FlightListStepT) + fun onStepsUpdate() } class FlightListManager(private val vrServer: VRServer) : VRCConfigListener { @@ -28,13 +31,17 @@ class FlightListManager(private val vrServer: VRServer) : VRCConfigListener { private val updateFlightListTimer = Timer("FetchVRCConfigTimer") + // Simple flag set to true if reset mounting was performed at least once. + // This value is only runtime and never saved + var resetMountingCompleted = false + init { vrServer.vrcConfigManager.addListener(this) createSteps() updateFlightListTimer.scheduleAtFixedRate( timerTask { - updateFligtlist() + updateFlightlist() }, 0, 1000, @@ -62,8 +69,9 @@ class FlightListManager(private val vrServer: VRServer) : VRCConfigListener { steps.add( FlightListStepT().apply { id = FlightListStepId.TRACKERS_CALIBRATION + enabled = true optional = false - ignorable = false + ignorable = true visibility = FlightListStepVisibility.ALWAYS }, ) @@ -71,15 +79,28 @@ class FlightListManager(private val vrServer: VRServer) : VRCConfigListener { steps.add( FlightListStepT().apply { id = FlightListStepId.FULL_RESET + enabled = true optional = false ignorable = false visibility = FlightListStepVisibility.ALWAYS }, ) + steps.add( + FlightListStepT().apply { + id = FlightListStepId.MOUNTING_CALIBRATION + valid = false + enabled = vrServer.configManager.vrConfig.resetsConfig.preferedMountingMethod == MountingMethods.AUTOMATIC + optional = false + ignorable = true + visibility = FlightListStepVisibility.ALWAYS + }, + ) + steps.add( FlightListStepT().apply { id = FlightListStepId.STEAMVR_DISCONNECTED + enabled = true optional = true ignorable = true visibility = FlightListStepVisibility.WHEN_INVALID @@ -89,6 +110,7 @@ class FlightListManager(private val vrServer: VRServer) : VRCConfigListener { steps.add( FlightListStepT().apply { id = FlightListStepId.UNASSIGNED_HMD + enabled = true optional = false ignorable = false visibility = FlightListStepVisibility.WHEN_INVALID @@ -98,26 +120,36 @@ class FlightListManager(private val vrServer: VRServer) : VRCConfigListener { steps.add( FlightListStepT().apply { id = FlightListStepId.TRACKER_ERROR - valid = true; // Default to valid + valid = true // Default to valid + enabled = true optional = false ignorable = false visibility = FlightListStepVisibility.WHEN_INVALID }, ) - if (vrServer.vrcConfigManager.isSupported) { - steps.add( - FlightListStepT().apply { - id = FlightListStepId.VRCHAT_SETTINGS - optional = true - ignorable = true - visibility = FlightListStepVisibility.WHEN_INVALID - }, - ) - } + steps.add( + FlightListStepT().apply { + id = FlightListStepId.VRCHAT_SETTINGS + enabled = vrServer.vrcConfigManager.isSupported + optional = true + ignorable = true + visibility = FlightListStepVisibility.WHEN_INVALID + }, + ) + + steps.add( + FlightListStepT().apply { + id = FlightListStepId.NETWORK_PROFILE_PUBLIC + enabled = vrServer.networkProfileChecker.isSupported + optional = true + ignorable = true + visibility = FlightListStepVisibility.WHEN_INVALID + }, + ) } - fun updateFligtlist() { + fun updateFlightlist() { println( measureTimeMillis { val assignedTrackers = @@ -187,6 +219,10 @@ class FlightListManager(private val vrServer: VRServer) : VRCConfigListener { FlightListStepId.TRACKERS_CALIBRATION, trackersNeedCalibration.isEmpty(), ) { + // Don't show the step if none of the trackers connected support IMU calibration + it.enabled = imuTrackers.any { t -> + t.hasCompletedRestCalibration != null + } if (trackersNeedCalibration.isNotEmpty()) { it.extraData = FlightListExtraDataUnion().apply { type = FlightListExtraData.FlightListNeedCalibration @@ -218,6 +254,27 @@ class FlightListManager(private val vrServer: VRServer) : VRCConfigListener { } } } + + if (vrServer.networkProfileChecker.isSupported) { + updateValidity(FlightListStepId.NETWORK_PROFILE_PUBLIC, vrServer.networkProfileChecker.publicNetworks.isEmpty()) { + if (vrServer.networkProfileChecker.publicNetworks.isNotEmpty()) { + it.extraData = FlightListExtraDataUnion().apply { + type = FlightListExtraData.FlightListPublicNetworks + value = FlightListPublicNetworksT().apply { + adapters = vrServer.networkProfileChecker.publicNetworks.map { it.name }.toTypedArray() + } + } + } else { + it.extraData = null; + } + } + } + + updateValidity(FlightListStepId.MOUNTING_CALIBRATION, resetMountingCompleted) { + it.enabled = vrServer.configManager.vrConfig.resetsConfig.preferedMountingMethod == MountingMethods.AUTOMATIC + } + + listeners.forEach { it.onStepsUpdate() } }, ) } @@ -227,25 +284,27 @@ class FlightListManager(private val vrServer: VRServer) : VRCConfigListener { "id is unknown" } val step = steps.find { it.id == id } ?: return - val wasValid = step.valid; step.valid = valid if (beforeUpdate != null) { beforeUpdate(step) } - if (wasValid != valid) { //FIXME: this does not cover extraData changing - listeners.forEach { it.onStepUpdate(step) } - } } + override fun onChange( validity: VRCConfigValidity, values: VRCConfigValues, recommended: VRCConfigRecommendedValues, + muted: List ) { updateValidity( FlightListStepId.VRCHAT_SETTINGS, - validity.javaClass.declaredFields.asSequence().all { p -> p.get(validity) == true }, + VRCConfigValidity::class.java.declaredFields.asSequence().all { p -> + p.isAccessible = true + return@all p.get(validity) == true || muted.contains(p.name) + }, ) + listeners.forEach { it.onStepsUpdate() } } fun toggleStep(step: FlightListStepT) { @@ -257,4 +316,5 @@ class FlightListManager(private val vrServer: VRServer) : VRCConfigListener { } vrServer.configManager.saveConfig() } + } diff --git a/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt b/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt index 8e54d5de8a..fcf4a62f73 100644 --- a/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt +++ b/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt @@ -78,11 +78,11 @@ data class VRCConfigValidity( val shoulderTrackingOk: Boolean, val shoulderWidthCompensationOk: Boolean, val userHeightOk: Boolean, - val calibrationOk: Boolean, + val calibrationRangeOk: Boolean, val calibrationVisualsOk: Boolean, - val tackerModelOk: Boolean, + val trackerModelOk: Boolean, val spineModeOk: Boolean, - val avatarMeasurementOk: Boolean, + val avatarMeasurementTypeOk: Boolean, ) abstract class VRCConfigHandler { @@ -98,13 +98,14 @@ class VRCConfigHandlerStub : VRCConfigHandler() { } interface VRCConfigListener { - fun onChange(validity: VRCConfigValidity, values: VRCConfigValues, recommended: VRCConfigRecommendedValues) + fun onChange(validity: VRCConfigValidity, values: VRCConfigValues, recommended: VRCConfigRecommendedValues, muted: List) } class VRChatConfigManager(val server: VRServer, private val handler: VRCConfigHandler) { private val listeners: MutableList = CopyOnWriteArrayList() var currentValues: VRCConfigValues? = null + var currentValidity: VRCConfigValidity? = null val isSupported: Boolean get() = handler.isSupported @@ -113,6 +114,31 @@ class VRChatConfigManager(val server: VRServer, private val handler: VRCConfigHa handler.initHandler(::onChange) } + fun toggleMuteWarning(key: String) { + val keys = VRCConfigValidity::class.java.declaredFields.asSequence().map { p -> p.name } + if (!keys.contains(key)) return; + + if (!server.configManager.vrConfig.vrcConfig.mutedWarnings.contains(key)) + server.configManager.vrConfig.vrcConfig.mutedWarnings.add(key) + else + server.configManager.vrConfig.vrcConfig.mutedWarnings.remove(key) + + server.configManager.saveConfig() + + val recommended = recommendedValues() + val validity = currentValidity ?: return + val values = currentValues ?: return + listeners.forEach { + println("CALLED") + it.onChange( + validity, + values, + recommended, + server.configManager.vrConfig.vrcConfig.mutedWarnings + ) + } + } + /** * shoulderTrackingDisabled should be true if: * The user isn't tracking their whole arms from their controllers: @@ -160,20 +186,21 @@ class VRChatConfigManager(val server: VRServer, private val handler: VRCConfigHa legacyModeOk = values.legacyMode == recommended.legacyMode, shoulderTrackingOk = values.shoulderTrackingDisabled == recommended.shoulderTrackingDisabled, spineModeOk = recommended.spineMode.contains(values.spineMode), - tackerModelOk = values.trackerModel == recommended.trackerModel, - calibrationOk = abs(values.calibrationRange - recommended.calibrationRange) < 0.1, + trackerModelOk = values.trackerModel == recommended.trackerModel, + calibrationRangeOk = abs(values.calibrationRange - recommended.calibrationRange) < 0.1, userHeightOk = abs(server.humanPoseManager.realUserHeight - values.userHeight) < 0.1, calibrationVisualsOk = values.calibrationVisuals == recommended.calibrationVisuals, - avatarMeasurementOk = values.avatarMeasurementType == recommended.avatarMeasurementType, + avatarMeasurementTypeOk = values.avatarMeasurementType == recommended.avatarMeasurementType, shoulderWidthCompensationOk = values.shoulderWidthCompensation == recommended.shoulderWidthCompensation, ) fun onChange(values: VRCConfigValues) { val recommended = recommendedValues() - val validity = checkValidity(values, recommended) + val validity = checkValidity(values, recommended); + currentValidity = validity currentValues = values listeners.forEach { - it.onChange(validity, values, recommended) + it.onChange(validity, values, recommended, server.configManager.vrConfig.vrcConfig.mutedWarnings) } } } diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt index abef3362f9..4b689ba51c 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt @@ -1,6 +1,7 @@ package dev.slimevr.protocol.rpc import com.google.flatbuffers.FlatBufferBuilder +import dev.slimevr.config.MountingMethods import dev.slimevr.config.config import dev.slimevr.protocol.GenericConnection import dev.slimevr.protocol.ProtocolAPI @@ -400,6 +401,8 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler, ): Int { if (!isSupported) { VRCConfigStateChangeResponse.startVRCConfigStateChangeResponse(fbb) @@ -71,11 +72,13 @@ fun buildVRCConfigStateResponse( val validityOffset = buildVRCConfigValidity(fbb, validity) val valuesOffset = buildVRCConfigValues(fbb, values) val recommendedOffset = buildVRCConfigRecommendedValues(fbb, recommended) + val mutedOffset = VRCConfigStateChangeResponse.createMutedVector(fbb, muted.map { fbb.createString(it) }.toIntArray()) VRCConfigStateChangeResponse.startVRCConfigStateChangeResponse(fbb) VRCConfigStateChangeResponse.addIsSupported(fbb, true) VRCConfigStateChangeResponse.addValidity(fbb, validityOffset) VRCConfigStateChangeResponse.addState(fbb, valuesOffset) VRCConfigStateChangeResponse.addRecommended(fbb, recommendedOffset) + VRCConfigStateChangeResponse.addMuted(fbb, mutedOffset) return VRCConfigStateChangeResponse.endVRCConfigStateChangeResponse(fbb) } diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/games/vrchat/RPCVRChatHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/games/vrchat/RPCVRChatHandler.kt index 2d4bd92776..5bcca22011 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/games/vrchat/RPCVRChatHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/games/vrchat/RPCVRChatHandler.kt @@ -18,12 +18,8 @@ class RPCVRChatHandler( init { api.server.vrcConfigManager.addListener(this) - rpcHandler.registerPacketListener(RpcMessage.VRCConfigStateRequest) { conn: GenericConnection, messageHeader: RpcMessageHeader -> - this.onConfigStateRequest( - conn, - messageHeader, - ) - } + rpcHandler.registerPacketListener(RpcMessage.VRCConfigStateRequest, ::onConfigStateRequest) + rpcHandler.registerPacketListener(RpcMessage.VRCConfigSettingToggleMute, ::onToggleMuteRequest) } private fun onConfigStateRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { @@ -41,6 +37,7 @@ class RPCVRChatHandler( validity = validity, values = values, recommended = api.server.vrcConfigManager.recommendedValues(), + muted = api.server.configManager.vrConfig.vrcConfig.mutedWarnings ) val outbound = rpcHandler.createRPCMessage( @@ -52,7 +49,13 @@ class RPCVRChatHandler( conn.send(fbb.dataBuffer()) } - override fun onChange(validity: VRCConfigValidity, values: VRCConfigValues, recommended: VRCConfigRecommendedValues) { + private fun onToggleMuteRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { + val req = messageHeader.message(VRCConfigSettingToggleMute()) as VRCConfigSettingToggleMute? + ?: return + api.server.vrcConfigManager.toggleMuteWarning(req.key()); + } + + override fun onChange(validity: VRCConfigValidity, values: VRCConfigValues, recommended: VRCConfigRecommendedValues, muted: List) { val fbb = FlatBufferBuilder(32) val response = buildVRCConfigStateResponse( @@ -61,6 +64,7 @@ class RPCVRChatHandler( validity = validity, values = values, recommended = recommended, + muted ) val outbound = rpcHandler.createRPCMessage( diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt index 149aa8553e..9f3651c137 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt @@ -3,6 +3,7 @@ package dev.slimevr.protocol.rpc.settings import com.google.flatbuffers.FlatBufferBuilder import dev.slimevr.bridge.ISteamVRBridge import dev.slimevr.config.ArmsResetModes +import dev.slimevr.config.MountingMethods import dev.slimevr.filtering.TrackerFilters import dev.slimevr.protocol.GenericConnection import dev.slimevr.protocol.ProtocolAPI diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt index 0abf8f0e82..4df063529c 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt @@ -5,6 +5,7 @@ import dev.slimevr.VRServer import dev.slimevr.VRServer.Companion.getNextLocalTrackerId import dev.slimevr.autobone.errors.BodyProportionError import dev.slimevr.config.ConfigManager +import dev.slimevr.config.MountingMethods import dev.slimevr.tracking.processor.config.SkeletonConfigManager import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets import dev.slimevr.tracking.processor.config.SkeletonConfigToggles @@ -568,6 +569,12 @@ class HumanPoseManager(val server: VRServer?) { fun resetTrackersMounting(resetSourceName: String?) { skeleton.resetTrackersMounting(resetSourceName) + if (server != null) { + server.configManager.vrConfig.resetsConfig.preferedMountingMethod = + MountingMethods.AUTOMATIC + server.flightListManager.resetMountingCompleted = true; + server.configManager.saveConfig() + } } fun clearTrackersMounting(resetSourceName: String?) { diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/NetworkProfileChecker.kt b/server/desktop/src/main/java/dev/slimevr/desktop/DesktopNetworkProfileChecker.kt similarity index 78% rename from server/desktop/src/main/java/dev/slimevr/desktop/NetworkProfileChecker.kt rename to server/desktop/src/main/java/dev/slimevr/desktop/DesktopNetworkProfileChecker.kt index bee7205086..74344cdf6b 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/NetworkProfileChecker.kt +++ b/server/desktop/src/main/java/dev/slimevr/desktop/DesktopNetworkProfileChecker.kt @@ -13,58 +13,15 @@ import com.sun.jna.platform.win32.WTypes import com.sun.jna.platform.win32.WinNT.HRESULT import com.sun.jna.ptr.IntByReference import com.sun.jna.ptr.PointerByReference +import dev.slimevr.ConnectivityFlags +import dev.slimevr.NetworkCategory +import dev.slimevr.NetworkInfo +import dev.slimevr.NetworkProfileChecker import dev.slimevr.VRServer import io.eiren.util.OperatingSystem import java.util.* import kotlin.concurrent.scheduleAtFixedRate -data class NetworkInfo( - val name: String?, - val description: String?, - val category: NetworkCategory?, - val connectivity: Set?, - val connected: Boolean?, -) - -/** - * @see NLM_NETWORK_CATEGORY enumeration (netlistmgr.h) - */ -enum class NetworkCategory(val value: Int) { - PUBLIC(0), - PRIVATE(1), - DOMAIN_AUTHENTICATED(2), - ; - - companion object { - fun fromInt(value: Int) = values().find { it.value == value } - } -} - -/** - * @see NLM_CONNECTIVITY enumeration (netlistmgr.h) - */ -enum class ConnectivityFlags(val value: Int) { - DISCONNECTED(0), - IPV4_NOTRAFFIC(0x1), - IPV6_NOTRAFFIC(0x2), - IPV4_SUBNET(0x10), - IPV4_LOCALNETWORK(0x20), - IPV4_INTERNET(0x40), - IPV6_SUBNET(0x100), - IPV6_LOCALNETWORK(0x200), - IPV6_INTERNET(0x400), - ; - - companion object { - fun fromInt(value: Int): Set = - if (value == 0) { - setOf(DISCONNECTED) - } else { - values().filter { it != DISCONNECTED && (value and it.value) != 0 }.toSet() - } - } -} - /** * @see INetworkConnection interface (netlistmgr.h) */ @@ -306,38 +263,23 @@ fun enumerateNetworks(): List? { return null } -class NetworkProfileChecker(private val server: VRServer) { +class DesktopNetworkProfileChecker(private val server: VRServer): + NetworkProfileChecker() { private val updateTickTimer = Timer("NetworkProfileCheck") - private var lastPublicNetworkStatus: UInt = 0u - private var numPublicNetworks = 0 + private var publicNetworksLocal: List = listOf() + + override val isSupported: Boolean + get() = OperatingSystem.currentPlatform == OperatingSystem.WINDOWS + + override val publicNetworks: List + get() = publicNetworksLocal; init { if (OperatingSystem.currentPlatform == OperatingSystem.WINDOWS) { this.updateTickTimer.scheduleAtFixedRate(0, 3000) { - val currentNumPublicNetworks = enumerateNetworks()?.filter { net -> + publicNetworksLocal = enumerateNetworks()?.filter { net -> net.connected == true && net.category == NetworkCategory.PUBLIC } ?: listOf() - val currentNumPublicNetworksCount = currentNumPublicNetworks.count() - - if (numPublicNetworks != currentNumPublicNetworksCount) { - numPublicNetworks = currentNumPublicNetworksCount -// if (lastPublicNetworkStatus != 0u) { -// server.statusSystem.removeStatus(lastPublicNetworkStatus) -// lastPublicNetworkStatus = 0u -// } -// -// if (lastPublicNetworkStatus == 0u && numPublicNetworks > 0) { -// lastPublicNetworkStatus = server.statusSystem.addStatus( -// StatusDataUnion().apply { -// type = StatusData.StatusPublicNetwork -// value = StatusPublicNetworkT().apply { -// adapters = currentNumPublicNetworks.map { it.name }.toTypedArray() -// } -// }, -// false, -// ) -// } - } } } } diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt b/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt index e586bef969..0a304e44a7 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt +++ b/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt @@ -3,6 +3,7 @@ package dev.slimevr.desktop import dev.slimevr.Keybinding +import dev.slimevr.NetworkProfileChecker import dev.slimevr.SLIMEVR_IDENTIFIER import dev.slimevr.VRServer import dev.slimevr.bridge.Bridge @@ -125,11 +126,11 @@ fun main(args: Array) { { _ -> DesktopSerialHandler() }, { _ -> DesktopSerialFlashingHandler() }, { _ -> DesktopVRCConfigHandler() }, + { server -> DesktopNetworkProfileChecker(server) }, configPath = configDir, ) vrServer.start() - NetworkProfileChecker(vrServer) // Start service for USB HID trackers TrackersHID( From 471b18a95de382306db85232edac7e2e6a49500e Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Fri, 18 Jul 2025 00:06:49 +0200 Subject: [PATCH 06/34] Set solarxr --- solarxr-protocol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solarxr-protocol b/solarxr-protocol index 1dae117bc8..2db7d29723 160000 --- a/solarxr-protocol +++ b/solarxr-protocol @@ -1 +1 @@ -Subproject commit 1dae117bc82456c3977df0241381c212d800b1f3 +Subproject commit 2db7d29723ddc6c8cf6b09cf1bd58f4d5a14988c From dee429932f77e74f3ddb8de22bfc708d2a85b928 Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Tue, 22 Jul 2025 00:37:59 +0200 Subject: [PATCH 07/34] Progress --- gui/public/i18n/en/translation.ftl | 10 +- gui/src/components/commons/Checkbox.tsx | 4 +- gui/src/components/commons/ProgressBar.tsx | 3 +- .../flight-list/FlightListSettingsModal.tsx | 59 +++++ .../flight-list/SessionFlightList.tsx | 226 ++++++++++++++---- gui/src/components/home/ResetButton.tsx | 2 +- .../onboarding/pages/ConnectTracker.tsx | 2 +- .../settings/pages/GeneralSettings.tsx | 2 - gui/src/hooks/reset-settings.ts | 4 - gui/src/hooks/session-flightlist.ts | 80 ++++--- .../slimevr/flightlist/FlightListManager.kt | 56 +++-- solarxr-protocol | 2 +- 12 files changed, 328 insertions(+), 122 deletions(-) create mode 100644 gui/src/components/flight-list/FlightListSettingsModal.tsx diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index b0217cabc9..6aa884c7af 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -1551,11 +1551,11 @@ flight_list-FULL_RESET-desc = Some Trackers need a reset to be performed flight_list-STEAMVR_DISCONNECTED = SteamVR not running flight_list-STEAMVR_DISCONNECTED-desc = SteamVR is not running. Are you using it for vr? flight_list-STEAMVR_DISCONNECTED-open = Launch SteamVR -flight_list-TRACKERS_CALIBRATION = Calibrate your trackers -flight_list-TRACKERS_CALIBRATION-desc = You didnt perform the tracker calibration. Please let your slimes, highlited in yellow, rest on a static surface for a few secconds +flight_list-TRACKERS_REST_CALIBRATION = Calibrate your trackers +flight_list-TRACKERS_REST_CALIBRATION-desc = You didnt perform the tracker calibration. Please let your slimes, highlited in yellow, rest on a static surface for a few secconds flight_list-TRACKER_ERROR = Trackers with Errors flight_list-TRACKER_ERROR-desc = Some of your trackers have an error. Please restart the tracker. -flight_list-VRCHAT_SETTINGS = Misconfigured VRChat settings +flight_list-VRCHAT_SETTINGS = Configure VRChat settings flight_list-VRCHAT_SETTINGS-desc = You have misconfigured VRchat Settings! This can impact your tracking experience. flight_list-VRCHAT_SETTINGS-open = Go to VRChat Warnings flight_list-UNASSIGNED_HMD = VR Headset not assigned to Head @@ -1571,4 +1571,8 @@ flight_list-NETWORK_PROFILE_PUBLIC-desc = {$count -> See how to fix it here. } flight_list-NETWORK_PROFILE_PUBLIC-open = Open Control Panel +flight_list-STAY_ALIGNED_CONFIGURED = Configure Stay Aligned +flight_list-STAY_ALIGNED_CONFIGURED-desc = Record the stay aligned poses for an improved imu drift +flight_list-STAY_ALIGNED_CONFIGURED-open = Open Stay Aligned Wizard + flight_list-ignore = Ignore diff --git a/gui/src/components/commons/Checkbox.tsx b/gui/src/components/commons/Checkbox.tsx index ac2d824118..79fe01ae7a 100644 --- a/gui/src/components/commons/Checkbox.tsx +++ b/gui/src/components/commons/Checkbox.tsx @@ -3,7 +3,7 @@ import { useMemo } from 'react'; import { Control, Controller } from 'react-hook-form'; export const CHECKBOX_CLASSES = classNames( - 'bg-background-50 border-background-50 rounded-md w-5 h-5 text-accent-background-30 focus:border-accent-background-40 focus:ring-transparent focus:ring-offset-transparent focus:outline-transparent' + 'bg-background-50 border-background-50 cursor-pointer rounded-md w-5 h-5 text-accent-background-30 focus:border-accent-background-40 focus:ring-transparent focus:ring-offset-transparent focus:outline-transparent' ); export function CheckBox({ @@ -66,7 +66,7 @@ export function CheckBox({ 'w-full py-3 flex gap-2 items-center text-standard-bold', { 'px-3': outlined, - 'cursor-pointer': !disabled || !loading, + 'cursor-pointer': !disabled && !loading, 'cursor-default': disabled || loading, } )} diff --git a/gui/src/components/commons/ProgressBar.tsx b/gui/src/components/commons/ProgressBar.tsx index 61afbae020..7095fa4d01 100644 --- a/gui/src/components/commons/ProgressBar.tsx +++ b/gui/src/components/commons/ProgressBar.tsx @@ -53,8 +53,9 @@ export function Bar({ }) { const value = useMemo( () => Math.min(Math.max((progress * parts) / 1 - index, 0), 1), - [index, progress] + [index, progress, parts] ); + return (
>]; +}) { + const { ignoredSteps, steps, toggle } = useSessionFlightlist(); + + const { control } = useForm<{ steps: boolean[] }>({ + defaultValues: { steps: steps.map(({ id }) => ignoredSteps.includes(id)) }, + }); + + return ( + { + open[1](false); + }} + > +
+ Flight List Settings + Ignored Steps +
+ {steps.map((step) => ( +
toggle(step.id)} + > + +
+ +
+
+ ))} +
+
+ +
+
+
+ ); +} diff --git a/gui/src/components/flight-list/SessionFlightList.tsx b/gui/src/components/flight-list/SessionFlightList.tsx index e0fdaa7ec6..304890af85 100644 --- a/gui/src/components/flight-list/SessionFlightList.tsx +++ b/gui/src/components/flight-list/SessionFlightList.tsx @@ -10,7 +10,7 @@ import { FlightListStepId, ResetType, } from 'solarxr-protocol'; -import { ReactNode } from 'react'; +import { ReactNode, useEffect, useMemo, useState } from 'react'; import { openUrl } from '@tauri-apps/plugin-opener'; import { CheckIcon } from '@/components/commons/icon/CheckIcon'; import { Typography } from '@/components/commons/Typography'; @@ -20,52 +20,79 @@ import { A } from '@/components/commons/A'; import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon'; import { ProgressBar } from '@/components/commons/ProgressBar'; import { CrossIcon } from '@/components/commons/icon/CrossIcon'; +import { ArrowDownIcon } from '@/components/commons/icon/ArrowIcons'; import { Localized } from '@fluent/react'; +import { GearIcon } from '@/components/commons/icon/GearIcon'; +import { WrenchIcon } from '../commons/icon/WrenchIcons'; +import { FlightListSettingsModal } from './FlightListSettingsModal'; function Step({ - step: { status, id, optional, firstInvalid }, + step: { status, id, optional, firstRequired }, children, }: { step: FlightListStep; index: number; children: ReactNode; }) { + const [open, setOpen] = useState(firstRequired); + + const canBeOpened = + (status === 'skipped' || status === 'invalid') && !firstRequired; + + useEffect(() => { + if (!canBeOpened) setOpen(false); + }, [open]); + return ( -
-
-
+
+
{ + if (canBeOpened) setOpen((open) => !open); + }} + >
+ {status === 'skipped' && } {status === 'complete' && } - {status !== 'complete' && ( + {(status === 'invalid' || status === 'blocked') && (
)}
-
- +
+ + {canBeOpened && ( +
+ +
+ )}
- - {(firstInvalid || (status === 'invalid' && optional)) && children && ( -
{children}
+ {(firstRequired || open) && children && ( +
{children}
)}
); @@ -75,10 +102,10 @@ const stepContentLookup: Record< number, (step: FlightListStep, context: SessionFlightListContext) => JSX.Element > = { - [FlightListStepId.TRACKERS_CALIBRATION]: (step, { toggle }) => { + [FlightListStepId.TRACKERS_REST_CALIBRATION]: (step, { toggle }) => { return (
- +
{step.ignorable && ( + {step.ignorable && ( + + )} +
+
+ + ); + }, }; export function SessionFlightList() { const context = useSessionFlightlist(); - const { steps } = context; + const { visibleSteps } = context; + + const progress = useMemo(() => { + const completeSteps = visibleSteps.filter( + (step) => step.status === 'complete' || step.status === 'skipped' + ); + return Math.min(1, completeSteps.length / visibleSteps.length); + }, [visibleSteps]); + + const completion = useMemo(() => { + if ( + progress === 1 && + visibleSteps.find((step) => step.status === 'skipped') + ) + return 'partial'; + return progress === 1 ? 'complete' : 'incomplete'; + }, [progress, visibleSteps]); + + const slimeState = useMemo(() => { + if (completion === 'complete') return SlimeState.HAPPY; + if (completion === 'incomplete') return SlimeState.CURIOUS; + return SlimeState.HAPPY; + }, [completion]); + + const settingsOpenState = useState(false); + const [, setSettingsOpen] = settingsOpenState; return ( -
-
- {steps.map((step, index) => ( - - {stepContentLookup[step.id]?.(step, context) || undefined} - - ))} + <> +
+
+
+ SlimeVR Flight list +
+
setSettingsOpen(true)} + > + +
+
+
+ {visibleSteps.map((step, index) => ( + + {stepContentLookup[step.id]?.(step, context) || undefined} + + ))} +
- {/*
*/}
-
- +
+
- - You are not prepared to use SlimeVR! - + {completion === 'incomplete' && ( + + You are not prepared to use SlimeVR! + + )} + {(completion == 'complete' || completion === 'partial') && ( + + You are prepared to use SlimeVR! + + )}
-
- -
- +
+
+ +
+
+
-
+ + ); } diff --git a/gui/src/components/home/ResetButton.tsx b/gui/src/components/home/ResetButton.tsx index 54c0651eac..c1aee64f9a 100644 --- a/gui/src/components/home/ResetButton.tsx +++ b/gui/src/components/home/ResetButton.tsx @@ -36,7 +36,7 @@ export function ResetButton({ }) { const { l10n } = useLocalization(); const { sendRPCPacket } = useWebsocketAPI(); - const { steps } = useSessionFlightlist(); + const { visibleSteps: steps } = useSessionFlightlist(); const { config } = useConfig(); const finishedTimeoutRef = useRef(-1); const [isFinished, setFinished] = useState(false); diff --git a/gui/src/components/onboarding/pages/ConnectTracker.tsx b/gui/src/components/onboarding/pages/ConnectTracker.tsx index 9686ab08ca..9f62a384b5 100644 --- a/gui/src/components/onboarding/pages/ConnectTracker.tsx +++ b/gui/src/components/onboarding/pages/ConnectTracker.tsx @@ -102,7 +102,7 @@ export function InvalidNetworkProfileWarning({ export function ConnectTrackersPage() { const { l10n } = useLocalization(); - const { steps } = useSessionFlightlist(); + const { visibleSteps: steps } = useSessionFlightlist(); const connectedIMUTrackers = useAtomValue(connectedIMUTrackersAtom); const { applyProgress, state } = useOnboarding(); diff --git a/gui/src/components/settings/pages/GeneralSettings.tsx b/gui/src/components/settings/pages/GeneralSettings.tsx index e53ab15c0e..ac54002d61 100644 --- a/gui/src/components/settings/pages/GeneralSettings.tsx +++ b/gui/src/components/settings/pages/GeneralSettings.tsx @@ -9,8 +9,6 @@ import { ModelRatiosT, ModelSettingsT, ModelTogglesT, - MountingMethod, - ResetsSettingsT, RpcMessage, SettingsRequestT, SettingsResponseT, diff --git a/gui/src/hooks/reset-settings.ts b/gui/src/hooks/reset-settings.ts index 7ea5be5353..8b94e20fad 100644 --- a/gui/src/hooks/reset-settings.ts +++ b/gui/src/hooks/reset-settings.ts @@ -1,6 +1,5 @@ import { ChangeSettingsRequestT, - MountingMethod, ResetsSettingsT, RpcMessage, SettingsResetRequestT, @@ -15,7 +14,6 @@ export interface ResetSettingsForm { yawResetSmoothTime: number; saveMountingReset: boolean; resetHmdPitch: boolean; - preferedMountingMethod: number; } export const defaultResetSettings = { @@ -24,7 +22,6 @@ export const defaultResetSettings = { yawResetSmoothTime: 0.0, saveMountingReset: false, resetHmdPitch: false, - preferedMountingMethod: MountingMethod.AUTOMATIC, }; export function loadResetSettings(resetSettingsForm: ResetSettingsForm) { @@ -34,7 +31,6 @@ export function loadResetSettings(resetSettingsForm: ResetSettingsForm) { resetsSettings.yawResetSmoothTime = resetSettingsForm.yawResetSmoothTime; resetsSettings.saveMountingReset = resetSettingsForm.saveMountingReset; resetsSettings.resetHmdPitch = resetSettingsForm.resetHmdPitch; - resetsSettings.preferedMountingMethod = resetSettingsForm.preferedMountingMethod; return resetsSettings; } diff --git a/gui/src/hooks/session-flightlist.ts b/gui/src/hooks/session-flightlist.ts index 3a7407b6f9..32f2ab6f1e 100644 --- a/gui/src/hooks/session-flightlist.ts +++ b/gui/src/hooks/session-flightlist.ts @@ -12,7 +12,7 @@ import { createContext, useContext, useEffect, useMemo, useState } from 'react'; export const flightlistIdtoLabel: Record = { [FlightListStepId.UNKNOWN]: '', - [FlightListStepId.TRACKERS_CALIBRATION]: 'flight_list-TRACKERS_CALIBRATION', + [FlightListStepId.TRACKERS_REST_CALIBRATION]: 'flight_list-TRACKERS_REST_CALIBRATION', [FlightListStepId.FULL_RESET]: 'flight_list-FULL_RESET', [FlightListStepId.VRCHAT_SETTINGS]: 'flight_list-VRCHAT_SETTINGS', [FlightListStepId.STEAMVR_DISCONNECTED]: 'flight_list-STEAMVR_DISCONNECTED', @@ -20,66 +20,81 @@ export const flightlistIdtoLabel: Record = { [FlightListStepId.TRACKER_ERROR]: 'flight_list-TRACKER_ERROR', [FlightListStepId.NETWORK_PROFILE_PUBLIC]: 'flight_list-NETWORK_PROFILE_PUBLIC', [FlightListStepId.MOUNTING_CALIBRATION]: 'flight_list-MOUNTING_CALIBRATION', + [FlightListStepId.STAY_ALIGNED_CONFIGURED]: 'flight_list-STAY_ALIGNED_CONFIGURED', }; export type FlightListStepStatus = 'complete' | 'skipped' | 'blocked' | 'invalid'; export type FlightListStep = FlightListStepT & { status: FlightListStepStatus; - firstInvalid: boolean; + firstRequired: boolean; }; +const stepVisibility = ({ visibility, status, firstRequired }: FlightListStep) => + firstRequired || + visibility === FlightListStepVisibility.ALWAYS || + (visibility === FlightListStepVisibility.WHEN_INVALID && status != 'complete'); + const createStep = ( steps: FlightListStepT[], step: FlightListStepT, index: number ): FlightListStep => { const previousSteps = steps.slice(0, index); - const blocked = previousSteps.some(({ valid, optional }) => !valid && !optional); + const previousBlocked = previousSteps.some( + ({ valid, optional }) => !valid && !optional + ); let status: FlightListStepStatus = 'complete'; - if (blocked && !step.valid) status = 'blocked'; - if (!blocked && !step.valid) status = 'invalid'; - if (!blocked && step.optional && !step.valid && index !== previousSteps.length) - status = 'skipped'; + if (previousBlocked && !step.valid) status = 'blocked'; + if (!previousBlocked && !step.valid) status = 'invalid'; + + const firstRequiredIndex = steps.findIndex( + (s, index) => !s.valid || (index === steps.length - 1 && !s.valid) + ); + + const skipped = + steps.find( + (s, sIndex) => + (sIndex > index && s.valid && !s.optional) || sIndex === steps.length - 1 + ) || index === steps.length - 1; + if (!step.valid && step.optional && skipped) status = 'skipped'; return { ...step, status, - firstInvalid: steps.findIndex((s) => !s.valid) == index, + firstRequired: + firstRequiredIndex === index || + (firstRequiredIndex === -1 && index === steps.length - 1 && !step.valid), pack: () => 0, }; }; export type SessionFlightListContext = ReturnType; -const stepVisibility = ({ visibility, status, firstInvalid }: FlightListStep) => - firstInvalid || - visibility === FlightListStepVisibility.ALWAYS || - (visibility === FlightListStepVisibility.WHEN_INVALID && - ['invalid', 'blocked'].includes(status)); - export function provideSessionFlightlist() { const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); - const [steps, setSteps] = useState([]); + const [steps, setSteps] = useState([]); const [ignoredSteps, setIgnoredSteps] = useState([]); useRPCPacket(RpcMessage.FlightListResponse, (data: FlightListResponseT) => { setIgnoredSteps(data.ignoredSteps); - const activeSteps = data.steps.filter( - (step) => !data.ignoredSteps.includes(step.id) && step.enabled - ); - const steps = activeSteps.map((step: FlightListStepT, index) => - createStep(activeSteps, step, index) - ); - setSteps(steps); + setSteps(data.steps); }); useEffect(() => { sendRPCPacket(RpcMessage.FlightListRequest, new FlightListRequestT()); }, []); - const visibleSteps = useMemo(() => steps.filter(stepVisibility), [steps]); - const firstInvalid = useMemo( + const visibleSteps = useMemo(() => { + const activeSteps = steps.filter( + (step) => !ignoredSteps.includes(step.id) && step.enabled + ); + return steps + .map((step: FlightListStepT, index) => createStep(activeSteps, step, index)) + .filter(stepVisibility); + }, [steps, ignoredSteps]); + + const firstRequired = useMemo( () => visibleSteps.find( (step) => !step.valid && step.status != 'blocked' && !step.optional @@ -88,19 +103,20 @@ export function provideSessionFlightlist() { ); const hightlightedTrackers = useMemo(() => { - if (!firstInvalid || !firstInvalid.extraData) return []; - if ('trackersId' in firstInvalid.extraData) { - return firstInvalid.extraData.trackersId; + if (!firstRequired || !firstRequired.extraData) return []; + if ('trackersId' in firstRequired.extraData) { + return firstRequired.extraData.trackersId; } - if ('trackerId' in firstInvalid.extraData) { - return [firstInvalid.extraData.trackerId]; + if ('trackerId' in firstRequired.extraData) { + return [firstRequired.extraData.trackerId]; } return []; - }, [firstInvalid]); + }, [firstRequired]); return { - steps: steps.filter(stepVisibility), - firstInvalid, + steps, + visibleSteps, + firstRequired, ignoredSteps, hightlightedTrackers, toggle: (step: FlightListStepId) => { diff --git a/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt b/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt index 7209d1b5eb..cfb05444cd 100644 --- a/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt +++ b/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt @@ -68,29 +68,39 @@ class FlightListManager(private val vrServer: VRServer) : VRCConfigListener { private fun createSteps() { steps.add( FlightListStepT().apply { - id = FlightListStepId.TRACKERS_CALIBRATION + id = FlightListStepId.NETWORK_PROFILE_PUBLIC + enabled = vrServer.networkProfileChecker.isSupported + optional = false + ignorable = true + visibility = FlightListStepVisibility.WHEN_INVALID + }, + ) + + steps.add( + FlightListStepT().apply { + id = FlightListStepId.STEAMVR_DISCONNECTED enabled = true optional = false ignorable = true - visibility = FlightListStepVisibility.ALWAYS + visibility = FlightListStepVisibility.WHEN_INVALID }, ) steps.add( FlightListStepT().apply { - id = FlightListStepId.FULL_RESET + id = FlightListStepId.TRACKER_ERROR + valid = true // Default to valid enabled = true optional = false ignorable = false - visibility = FlightListStepVisibility.ALWAYS + visibility = FlightListStepVisibility.WHEN_INVALID }, ) steps.add( FlightListStepT().apply { - id = FlightListStepId.MOUNTING_CALIBRATION - valid = false - enabled = vrServer.configManager.vrConfig.resetsConfig.preferedMountingMethod == MountingMethods.AUTOMATIC + id = FlightListStepId.TRACKERS_REST_CALIBRATION + enabled = true optional = false ignorable = true visibility = FlightListStepVisibility.ALWAYS @@ -99,28 +109,28 @@ class FlightListManager(private val vrServer: VRServer) : VRCConfigListener { steps.add( FlightListStepT().apply { - id = FlightListStepId.STEAMVR_DISCONNECTED + id = FlightListStepId.FULL_RESET enabled = true - optional = true - ignorable = true - visibility = FlightListStepVisibility.WHEN_INVALID + optional = false + ignorable = false + visibility = FlightListStepVisibility.ALWAYS }, ) steps.add( FlightListStepT().apply { - id = FlightListStepId.UNASSIGNED_HMD - enabled = true + id = FlightListStepId.MOUNTING_CALIBRATION + valid = false + enabled = vrServer.configManager.vrConfig.resetsConfig.preferedMountingMethod == MountingMethods.AUTOMATIC optional = false - ignorable = false - visibility = FlightListStepVisibility.WHEN_INVALID + ignorable = true + visibility = FlightListStepVisibility.ALWAYS }, ) steps.add( FlightListStepT().apply { - id = FlightListStepId.TRACKER_ERROR - valid = true // Default to valid + id = FlightListStepId.UNASSIGNED_HMD enabled = true optional = false ignorable = false @@ -130,8 +140,8 @@ class FlightListManager(private val vrServer: VRServer) : VRCConfigListener { steps.add( FlightListStepT().apply { - id = FlightListStepId.VRCHAT_SETTINGS - enabled = vrServer.vrcConfigManager.isSupported + id = FlightListStepId.STAY_ALIGNED_CONFIGURED + enabled = true optional = true ignorable = true visibility = FlightListStepVisibility.WHEN_INVALID @@ -140,8 +150,8 @@ class FlightListManager(private val vrServer: VRServer) : VRCConfigListener { steps.add( FlightListStepT().apply { - id = FlightListStepId.NETWORK_PROFILE_PUBLIC - enabled = vrServer.networkProfileChecker.isSupported + id = FlightListStepId.VRCHAT_SETTINGS + enabled = vrServer.vrcConfigManager.isSupported optional = true ignorable = true visibility = FlightListStepVisibility.WHEN_INVALID @@ -216,7 +226,7 @@ class FlightListManager(private val vrServer: VRServer) : VRCConfigListener { it.hasCompletedRestCalibration == false } updateValidity( - FlightListStepId.TRACKERS_CALIBRATION, + FlightListStepId.TRACKERS_REST_CALIBRATION, trackersNeedCalibration.isEmpty(), ) { // Don't show the step if none of the trackers connected support IMU calibration @@ -274,6 +284,8 @@ class FlightListManager(private val vrServer: VRServer) : VRCConfigListener { it.enabled = vrServer.configManager.vrConfig.resetsConfig.preferedMountingMethod == MountingMethods.AUTOMATIC } + updateValidity(FlightListStepId.STAY_ALIGNED_CONFIGURED, vrServer.configManager.vrConfig.stayAlignedConfig.enabled) + listeners.forEach { it.onStepsUpdate() } }, ) diff --git a/solarxr-protocol b/solarxr-protocol index 2db7d29723..fbac1fabdd 160000 --- a/solarxr-protocol +++ b/solarxr-protocol @@ -1 +1 @@ -Subproject commit 2db7d29723ddc6c8cf6b09cf1bd58f4d5a14988c +Subproject commit fbac1fabddcfbdb1ade03784b617e9029df93835 From ba9b6482a4e3b355dc2d586165f9a42b02d42334 Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Tue, 22 Jul 2025 01:32:37 +0200 Subject: [PATCH 08/34] Fix --- .../flight-list/SessionFlightList.tsx | 4 ++-- gui/src/hooks/session-flightlist.ts | 23 ++++++++----------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/gui/src/components/flight-list/SessionFlightList.tsx b/gui/src/components/flight-list/SessionFlightList.tsx index 304890af85..97d0caa927 100644 --- a/gui/src/components/flight-list/SessionFlightList.tsx +++ b/gui/src/components/flight-list/SessionFlightList.tsx @@ -407,9 +407,9 @@ export function SessionFlightList() {
- + > */} ); } diff --git a/gui/src/hooks/session-flightlist.ts b/gui/src/hooks/session-flightlist.ts index 32f2ab6f1e..8fdf5ad3bc 100644 --- a/gui/src/hooks/session-flightlist.ts +++ b/gui/src/hooks/session-flightlist.ts @@ -73,27 +73,25 @@ export type SessionFlightListContext = ReturnType([]); + const [steps, setSteps] = useState([]); const [ignoredSteps, setIgnoredSteps] = useState([]); useRPCPacket(RpcMessage.FlightListResponse, (data: FlightListResponseT) => { setIgnoredSteps(data.ignoredSteps); - setSteps(data.steps); + const activeSteps = data.steps.filter( + (step) => !data.ignoredSteps.includes(step.id) && step.enabled + ); + const steps = activeSteps.map((step: FlightListStepT, index) => + createStep(activeSteps, step, index) + ); + setSteps(steps); }); useEffect(() => { sendRPCPacket(RpcMessage.FlightListRequest, new FlightListRequestT()); }, []); - const visibleSteps = useMemo(() => { - const activeSteps = steps.filter( - (step) => !ignoredSteps.includes(step.id) && step.enabled - ); - return steps - .map((step: FlightListStepT, index) => createStep(activeSteps, step, index)) - .filter(stepVisibility); - }, [steps, ignoredSteps]); - + const visibleSteps = useMemo(() => steps.filter(stepVisibility), [steps]); const firstRequired = useMemo( () => visibleSteps.find( @@ -114,8 +112,7 @@ export function provideSessionFlightlist() { }, [firstRequired]); return { - steps, - visibleSteps, + visibleSteps: steps.filter(stepVisibility), firstRequired, ignoredSteps, hightlightedTrackers, From 4eb54c9f083bdf9e4d32285113dce3d284cc4419 Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Tue, 22 Jul 2025 02:13:27 +0200 Subject: [PATCH 09/34] Progress --- gui/src/components/Navbar.tsx | 8 ++++---- gui/src/components/flight-list/SessionFlightList.tsx | 4 ++-- gui/src/index.scss | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gui/src/components/Navbar.tsx b/gui/src/components/Navbar.tsx index ce3d92e0b0..736cbabccc 100644 --- a/gui/src/components/Navbar.tsx +++ b/gui/src/components/Navbar.tsx @@ -34,7 +34,7 @@ export function NavButton({ {children}
} variant="floating" >
{icon} diff --git a/gui/src/components/flight-list/SessionFlightList.tsx b/gui/src/components/flight-list/SessionFlightList.tsx index 97d0caa927..7e5b71a06a 100644 --- a/gui/src/components/flight-list/SessionFlightList.tsx +++ b/gui/src/components/flight-list/SessionFlightList.tsx @@ -344,7 +344,7 @@ export function SessionFlightList() {
- SlimeVR Flight list + Flight list
diff --git a/gui/src/index.scss b/gui/src/index.scss index c94a80be39..104f34a75a 100644 --- a/gui/src/index.scss +++ b/gui/src/index.scss @@ -62,7 +62,7 @@ body { // overflow: hidden; -- NEVER EVER BRING THIS BACK <3 background: theme('colors.background.20'); - --navbar-w: 66px; + --navbar-w: 91px; --topbar-h: 38px; --toolbar-h: 65px; --preview-w: 400px; From 7f386d0b9d525c0733e817f9349e3edf84f4ad95 Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Tue, 22 Jul 2025 02:17:10 +0200 Subject: [PATCH 10/34] Progress --- gui/src/components/Navbar.tsx | 2 +- gui/src/index.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/src/components/Navbar.tsx b/gui/src/components/Navbar.tsx index 736cbabccc..f567850f0c 100644 --- a/gui/src/components/Navbar.tsx +++ b/gui/src/components/Navbar.tsx @@ -42,7 +42,7 @@ export function NavButton({ state={state} className={classnames( 'flex flex-col justify-center xs:gap-4 mobile:gap-2', - 'xs:w-[75px] xs:h-[75px] mobile:w-[75px] mobile:h-[75px]', + 'xs:w-[65px] xs:h-[65px] mobile:w-[65px] mobile:h-[65px]', 'xs:py-3 mobile:py-4 rounded-md mobile:rounded-b-none group select-text', { 'bg-accent-background-50 fill-accent-background-20': doesMatch, diff --git a/gui/src/index.scss b/gui/src/index.scss index 104f34a75a..15cdde7b96 100644 --- a/gui/src/index.scss +++ b/gui/src/index.scss @@ -62,7 +62,7 @@ body { // overflow: hidden; -- NEVER EVER BRING THIS BACK <3 background: theme('colors.background.20'); - --navbar-w: 91px; + --navbar-w: 81px; --topbar-h: 38px; --toolbar-h: 65px; --preview-w: 400px; From f339307428c92f02b8e0c7ba794cf4fb4ec28b41 Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Wed, 23 Jul 2025 05:37:15 +0200 Subject: [PATCH 11/34] Progress --- gui/public/i18n/en/translation.ftl | 12 +- gui/src/App.tsx | 4 +- gui/src/components/MainLayout.scss | 42 +++- gui/src/components/MainLayout.tsx | 28 +-- gui/src/components/Navbar.tsx | 2 +- gui/src/components/Toolbar.tsx | 187 +++++++++++++++++- gui/src/components/WidgetsComponent.tsx | 25 --- gui/src/components/commons/Button.tsx | 26 +-- gui/src/components/commons/Tooltip.tsx | 47 ++--- gui/src/components/commons/icon/ClearIcon.tsx | 12 ++ gui/src/components/commons/icon/GearIcon.tsx | 6 +- gui/src/components/commons/icon/SkiIcon.tsx | 6 +- .../flight-list/SessionFlightList.tsx | 12 +- gui/src/components/home/Home.tsx | 78 ++++---- gui/src/components/home/HomeSettingsModal.tsx | 103 ++++++++++ gui/src/components/home/ResetButton.tsx | 147 +++----------- .../body-proportions/ManualProportions.tsx | 1 - .../autobone-steps/Preparation.tsx | 6 +- .../mounting/mounting-steps/MountingReset.tsx | 1 - .../mounting/mounting-steps/Preparation.tsx | 6 +- .../stay-aligned-steps/PreparationStep.tsx | 6 +- .../settings/pages/InterfaceSettings.tsx | 24 ++- gui/src/components/tracker/TrackerCard.tsx | 2 +- gui/src/components/vr-mode/VRModePage.tsx | 6 +- .../widgets/DeveloperModeWidget.tsx | 9 +- gui/src/hooks/config.ts | 2 + gui/src/hooks/reset.ts | 126 ++++++++++++ gui/src/index.scss | 5 +- gui/src/store/app-store.ts | 10 +- gui/tailwind.config.ts | 1 + 30 files changed, 627 insertions(+), 315 deletions(-) create mode 100644 gui/src/components/commons/icon/ClearIcon.tsx create mode 100644 gui/src/components/home/HomeSettingsModal.tsx create mode 100644 gui/src/hooks/reset.ts diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index 6aa884c7af..3d5fdd8622 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -236,7 +236,7 @@ reset-reset_all_warning_default-v2 = Are you sure you want to do this? reset-full = Full Reset -reset-mounting = Reset Mounting +reset-mounting = Mounting Calibration reset-yaw = Yaw Reset ## Serial detection stuff @@ -273,8 +273,8 @@ widget-overlay-is_mirrored_label = Display Overlay as Mirror ## Widget: Drift compensation widget-drift_compensation-clear = Clear drift compensation -## Widget: Clear Reset Mounting -widget-clear_mounting = Clear reset mounting +## Widget: Clear Mounting calibration +widget-clear_mounting = Clear mounting calibration ## Widget: Developer settings widget-developer_mode = Developer Mode @@ -645,7 +645,7 @@ settings-general-gesture_control-yawResetTaps = Taps for yaw reset settings-general-gesture_control-fullResetEnabled = Enable tap to full reset settings-general-gesture_control-fullResetDelay = Full reset delay settings-general-gesture_control-fullResetTaps = Taps for full reset -settings-general-gesture_control-mountingResetEnabled = Enable tap to reset mounting +settings-general-gesture_control-mountingResetEnabled = Enable tap to mounting calibration settings-general-gesture_control-mountingResetDelay = Mounting reset delay settings-general-gesture_control-mountingResetTaps = Taps for mounting reset # The number of trackers that can have higher acceleration before a tap is rejected @@ -1114,7 +1114,7 @@ onboarding-automatic_mounting-done-description = Your mounting calibration is co onboarding-automatic_mounting-done-restart = Try again onboarding-automatic_mounting-mounting_reset-title = Mounting Reset onboarding-automatic_mounting-mounting_reset-step-0 = 1. Squat in a "skiing" pose with your legs bent, your upper body tilted forwards, and your arms bent. -onboarding-automatic_mounting-mounting_reset-step-1 = 2. Press the "Reset Mounting" button and wait for 3 seconds before the trackers' mounting orientations will reset. +onboarding-automatic_mounting-mounting_reset-step-1 = 2. Press the "Mounting calibration" button and wait for 3 seconds before the trackers' mounting orientations will reset. onboarding-automatic_mounting-preparation-title = Preparation onboarding-automatic_mounting-preparation-v2-step-0 = 1. Press the "Full Reset" button. onboarding-automatic_mounting-preparation-v2-step-1 = 2. Stand upright with your arms to your sides. Make sure to look forward. @@ -1560,7 +1560,7 @@ flight_list-VRCHAT_SETTINGS-desc = You have misconfigured VRchat Settings! This flight_list-VRCHAT_SETTINGS-open = Go to VRChat Warnings flight_list-UNASSIGNED_HMD = VR Headset not assigned to Head flight_list-UNASSIGNED_HMD-desc = The VR headset should be assigned as a head tracker. -flight_list-NETWORK_PROFILE_PUBLIC = Wrong network profile used +flight_list-NETWORK_PROFILE_PUBLIC = Change your network profile flight_list-NETWORK_PROFILE_PUBLIC-desc = {$count -> [one] Your network profile is currently set to Public ({$adapters}). This is not recommended for SlimeVR to function properly. diff --git a/gui/src/App.tsx b/gui/src/App.tsx index 756e0177b0..d5ef02e183 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -83,7 +83,7 @@ function Layout() { + } @@ -99,7 +99,7 @@ function Layout() { + } diff --git a/gui/src/components/MainLayout.scss b/gui/src/components/MainLayout.scss index 3668a56c9c..2dac343554 100644 --- a/gui/src/components/MainLayout.scss +++ b/gui/src/components/MainLayout.scss @@ -1,3 +1,7 @@ +:root { + --toolbar-h: 140px; +} + .main-layout { display: grid; grid-template: @@ -8,13 +12,43 @@ &.full { grid-template: 't t t' var(--topbar-h) - 's r l' var(--toolbar-h) - 's c l' calc(60% - var(--topbar-h) - var(--toolbar-h)) - 's c p' calc(40%) - / var(--navbar-w) calc(75% - var(--navbar-w)) calc(25%); + 's b l' var(--toolbar-h) + 's c l' calc(100% - var(--topbar-h) - var(--toolbar-h)) + / var(--navbar-w) calc(100% - var(--navbar-w) - var(--right-section-w)) var(--right-section-w); + } + + @screen nsm { + --right-section-w: 40%; + } + + @screen sm { + --right-section-w: 35%; + } + + @screen md { + --right-section-w: 30%; + } + + @screen lg { + --right-section-w: 25%; + } + + @screen xl { + --right-section-w: 20%; } @screen mobile { + --toolbar-h: 125px; + + &.full { + grid-template: + 't' var(--topbar-h) + 'b' var(--toolbar-h) + 'c' calc(100% - var(--topbar-h) - var(--toolbar-h) - var(--navbar-h)) + 's' calc(var(--navbar-h)) + / 100%; + } + grid-template: 't' var(--topbar-h) 'c' calc(100% - var(--topbar-h) - var(--navbar-h)) diff --git a/gui/src/components/MainLayout.tsx b/gui/src/components/MainLayout.tsx index 18aebed18f..cbd3422edf 100644 --- a/gui/src/components/MainLayout.tsx +++ b/gui/src/components/MainLayout.tsx @@ -11,7 +11,6 @@ import { TopBar } from './TopBar'; import { useWebsocketAPI } from '@/hooks/websocket-api'; import './MainLayout.scss'; import { Toolbar } from './Toolbar'; -import { SkeletonVisualizerWidget } from './widgets/SkeletonVisualizerWidget'; import { SessionFlightList } from './flight-list/SessionFlightList'; export function MainLayout({ @@ -19,10 +18,12 @@ export function MainLayout({ background = true, full = false, isMobile = undefined, + showToolbarSettings = false, }: { children: ReactNode; background?: boolean; isMobile?: boolean; + showToolbarSettings?: boolean; full?: boolean; }) { const { sendRPCPacket } = useWebsocketAPI(); @@ -60,12 +61,7 @@ export function MainLayout({ }); return ( -
+
@@ -78,29 +74,25 @@ export function MainLayout({ className={classNames( 'overflow-y-auto mr-2 my-2 mobile:m-0', 'flex flex-col rounded-md', - background && 'bg-background-70' + background && 'bg-background-70', + { 'rounded-t-none': !isMobile && full } )} > {children}
+ {full && ( +
+ +
+ )} {!isMobile && full && ( <> -
- -
-
- {/* */} - -
)}
diff --git a/gui/src/components/Navbar.tsx b/gui/src/components/Navbar.tsx index f567850f0c..649840b105 100644 --- a/gui/src/components/Navbar.tsx +++ b/gui/src/components/Navbar.tsx @@ -52,7 +52,7 @@ export function NavButton({ >
+ classNames( + 'p-2 flex justify-center px-4 gap-4 h-full items-center hover:bg-background-50 bg-background-60 relative overflow-clip aspect-square md:aspect-auto', + { + 'cursor-pointer': !disabled, + 'cursor-not-allowed': disabled, + } + ); + +function ButtonProgress({ + progress, + status, +}: { + progress: number; + status: ResetBtnStatus; +}) { + return ( +
+ ); +} + +function BasicResetButton({ type }: { type: ResetType }) { + const { isNmd } = useBreakpoint('nmd'); + const { triggerReset, status, name, timer, disabled, duration } = + useReset(type); + + const icon = useMemo(() => { + switch (type) { + case ResetType.Yaw: + return ; + } + return ; + }, [type]); + + const progress = status === 'counting' ? 1 - (timer - 1) / duration : 0; + + return ( + } + preferedDirection="top" + > +
!disabled && triggerReset()} + > + {icon} + +
+ +
+ +
+
+ ); +} + +function MountingCalibrationButton() { + const { isNmd } = useBreakpoint('nmd'); + const { triggerReset, status, name, timer, disabled, duration } = useReset( + ResetType.Mounting + ); + + const progress = status === 'counting' ? 1 - (timer - 1) / duration : 0; -export function Toolbar() { return ( -
- - - - +
+ } + preferedDirection="top" + > +
!disabled && triggerReset()} + > +
+ +
+
+ +
+
+
+ + Clear Mounting Calibration + + } + preferedDirection="top" + > +
+
+ +
+
+
+
); } + +export function Toolbar({ showSettings }: { showSettings: boolean }) { + const trackers = useAtomValue(connectedTrackersAtom); + const settingsOpenState = useState(false); + const [, setSettingsOpen] = settingsOpenState; + + return ( + <> + +
+
+ + + +
+
+ + {trackers.length} trackers connected + +
+ {showSettings && ( +
setSettingsOpen(true)} + > + +
+ )} +
+
+ + ); +} diff --git a/gui/src/components/WidgetsComponent.tsx b/gui/src/components/WidgetsComponent.tsx index 7fdbe8d465..08cc388e63 100644 --- a/gui/src/components/WidgetsComponent.tsx +++ b/gui/src/components/WidgetsComponent.tsx @@ -1,27 +1,11 @@ import { BVHButton } from './BVHButton'; import { TrackingPauseButton } from './TrackingPauseButton'; -import { ResetButton } from './home/ResetButton'; import { OverlayWidget } from './widgets/OverlayWidget'; -import { DeveloperModeWidget } from './widgets/DeveloperModeWidget'; -import { useConfig } from '@/hooks/config'; -import { ResetType } from 'solarxr-protocol'; -import { ClearMountingButton } from './ClearMountingButton'; -import { ToggleableSkeletonVisualizerWidget } from './widgets/SkeletonVisualizerWidget'; - -function UnprioritizedStatuses() { - return
; -} export function WidgetsComponent() { - const { config } = useConfig(); - return ( <>
- - - - {(typeof __ANDROID__ === 'undefined' || !__ANDROID__?.isThere()) && ( )} @@ -30,15 +14,6 @@ export function WidgetsComponent() {
-
- -
- - {config?.debug && ( -
- -
- )} ); } diff --git a/gui/src/components/commons/Button.tsx b/gui/src/components/commons/Button.tsx index 608f1d001a..7e7a4542a3 100644 --- a/gui/src/components/commons/Button.tsx +++ b/gui/src/components/commons/Button.tsx @@ -18,11 +18,11 @@ function ButtonContent({
{icon && ( -
+
{icon}
)} @@ -110,24 +110,26 @@ export function Button({ onClick={(ev) => disabled && ev.preventDefault()} > - {children} + {id && ( + + {children} + + )} + {!id && children} ) : ( ); - if (id) { - return ( - - {content} - - ); - } - return content; } diff --git a/gui/src/components/commons/Tooltip.tsx b/gui/src/components/commons/Tooltip.tsx index 53e6451dca..37d249be0c 100644 --- a/gui/src/components/commons/Tooltip.tsx +++ b/gui/src/components/commons/Tooltip.tsx @@ -380,29 +380,30 @@ export function DrawerTooltip({ setDrawerStyle(undefined); }; - useLayoutEffect(() => { - if (childRef.current && childRef.current.children[0]) { - const elem = childRef.current.children[0] as HTMLElement; - - elem.addEventListener('mousedown', touchStart); // for debug on desktop - elem.addEventListener('mouseup', touchEnd); // for debug on desktop - elem.addEventListener('scroll', scroll); - - elem.addEventListener('click', touchEnd); - elem.addEventListener('touchstart', touchStart); - elem.addEventListener('touchend', touchEnd); - - return () => { - elem.removeEventListener('mousedown', touchStart); // for debug on desktop - elem.removeEventListener('mouseup', touchEnd); // for debug on desktop - elem.removeEventListener('scroll', scroll); - - elem.removeEventListener('touchstart', touchStart); - elem.removeEventListener('touchend', touchEnd); - clearTimeout(touchTimeout.current); - }; - } - }, []); + // useLayoutEffect(() => { + // if (childRef.current && childRef.current.children[0]) { + // const elem = childRef.current.children[0] as HTMLElement; + + // elem.addEventListener('mousedown', touchStart); // for debug on desktop + // elem.addEventListener('mouseup', touchEnd); // for debug on desktop + // elem.addEventListener('scroll', scroll); + + // elem.addEventListener('click', touchEnd); + // elem.addEventListener('touchstart', touchStart); + // elem.addEventListener('touchend', touchEnd); + + // return () => { + // elem.removeEventListener('mousedown', touchStart); // for debug on desktop + // elem.removeEventListener('mouseup', touchEnd); // for debug on desktop + // elem.removeEventListener('scroll', scroll); + + // elem.removeEventListener('touchstart', touchStart); + // elem.removeEventListener('touchend', touchEnd); + // clearTimeout(touchTimeout.current); + // }; + // } + // }, []); + // FIXME: Completely broken not sure why. Will be solved when tooltips on mobile actually work return ( <> diff --git a/gui/src/components/commons/icon/ClearIcon.tsx b/gui/src/components/commons/icon/ClearIcon.tsx new file mode 100644 index 0000000000..85bbde08d3 --- /dev/null +++ b/gui/src/components/commons/icon/ClearIcon.tsx @@ -0,0 +1,12 @@ +export function ClearIcon({ size = 24 }: { size?: number }) { + return ( + + + + ); +} diff --git a/gui/src/components/commons/icon/GearIcon.tsx b/gui/src/components/commons/icon/GearIcon.tsx index 668ea636bd..87a46a1b32 100644 --- a/gui/src/components/commons/icon/GearIcon.tsx +++ b/gui/src/components/commons/icon/GearIcon.tsx @@ -1,8 +1,8 @@ -export function GearIcon() { +export function GearIcon({ size = 20 }: { size?: number }) { return ( diff --git a/gui/src/components/commons/icon/SkiIcon.tsx b/gui/src/components/commons/icon/SkiIcon.tsx index 4befe76de0..e6dee66267 100644 --- a/gui/src/components/commons/icon/SkiIcon.tsx +++ b/gui/src/components/commons/icon/SkiIcon.tsx @@ -1,10 +1,10 @@ -export function SkiIcon() { +export function SkiIcon({ size = 24 }: { size?: number }) { return ( diff --git a/gui/src/components/flight-list/SessionFlightList.tsx b/gui/src/components/flight-list/SessionFlightList.tsx index 7e5b71a06a..9a980cb277 100644 --- a/gui/src/components/flight-list/SessionFlightList.tsx +++ b/gui/src/components/flight-list/SessionFlightList.tsx @@ -22,9 +22,7 @@ import { ProgressBar } from '@/components/commons/ProgressBar'; import { CrossIcon } from '@/components/commons/icon/CrossIcon'; import { ArrowDownIcon } from '@/components/commons/icon/ArrowIcons'; import { Localized } from '@fluent/react'; -import { GearIcon } from '@/components/commons/icon/GearIcon'; -import { WrenchIcon } from '../commons/icon/WrenchIcons'; -import { FlightListSettingsModal } from './FlightListSettingsModal'; +import { WrenchIcon } from '@/components/commons/icon/WrenchIcons'; function Step({ step: { status, id, optional, firstRequired }, @@ -163,7 +161,7 @@ const stepContentLookup: Record<
- +
); @@ -272,7 +270,7 @@ const stepContentLookup: Record< />
- + {step.ignorable && ( +
+
+ + ); +} diff --git a/gui/src/components/home/ResetButton.tsx b/gui/src/components/home/ResetButton.tsx index c1aee64f9a..42ca152c05 100644 --- a/gui/src/components/home/ResetButton.tsx +++ b/gui/src/components/home/ResetButton.tsx @@ -1,152 +1,63 @@ -import { useLocalization } from '@fluent/react'; -import { useEffect, useMemo, useRef, useState } from 'react'; -import { - FlightListStepId, - ResetRequestT, - ResetType, - RpcMessage, -} from 'solarxr-protocol'; -import { useConfig } from '@/hooks/config'; -import { useCountdown } from '@/hooks/countdown'; -import { useWebsocketAPI } from '@/hooks/websocket-api'; -import { - playSoundOnResetEnded, - playSoundOnResetStarted, -} from '@/sounds/sounds'; -import { BigButton } from '@/components/commons/BigButton'; +import { Localized } from '@fluent/react'; +import { ResetType } from 'solarxr-protocol'; import { Button } from '@/components/commons/Button'; +import classNames from 'classnames'; +import { useReset } from '@/hooks/reset'; import { - MountingResetIcon, - YawResetIcon, FullResetIcon, + YawResetIcon, } from '@/components/commons/icon/ResetIcon'; -import classNames from 'classnames'; -import { useSessionFlightlist } from '@/hooks/session-flightlist'; +import { SkiIcon } from '@/components/commons/icon/SkiIcon'; +import { useMemo } from 'react'; export function ResetButton({ type, - size = 'big', className, onReseted, }: { className?: string; type: ResetType; - size: 'big' | 'small'; onReseted?: () => void; }) { - const { l10n } = useLocalization(); - const { sendRPCPacket } = useWebsocketAPI(); - const { visibleSteps: steps } = useSessionFlightlist(); - const { config } = useConfig(); - const finishedTimeoutRef = useRef(-1); - const [isFinished, setFinished] = useState(false); - - const needsFullReset = useMemo( - () => - type === ResetType.Mounting && - steps.some( - (step) => step.id === FlightListStepId.FULL_RESET && !step.valid - ), - [steps, type] + const { triggerReset, status, timer, disabled, name } = useReset( + type, + onReseted ); - const reset = () => { - const req = new ResetRequestT(); - req.resetType = type; - sendRPCPacket(RpcMessage.ResetRequest, req); - }; - - const { isCounting, startCountdown, timer } = useCountdown({ - duration: type === ResetType.Yaw ? 0 : undefined, - onCountdownEnd: () => { - maybePlaySoundOnResetEnd(type); - reset(); - setFinished(true); - if (finishedTimeoutRef.current !== -1) - clearTimeout(finishedTimeoutRef.current); - finishedTimeoutRef.current = setTimeout(() => { - setFinished(false); - finishedTimeoutRef.current = -1; - }, 2000); - if (onReseted) onReseted(); - }, - }); - - const text = useMemo(() => { + const icon = useMemo(() => { switch (type) { case ResetType.Yaw: - return l10n.getString('reset-yaw'); + return ; case ResetType.Mounting: - return l10n.getString('reset-mounting'); - case ResetType.Full: - return l10n.getString('reset-full'); + return ; } + return ; }, [type]); - const getIcon = () => { - switch (type) { - case ResetType.Yaw: - return ; - case ResetType.Mounting: - return ; - } - return ; - }; - - const maybePlaySoundOnResetEnd = (type: ResetType) => { - if (!config?.feedbackSound) return; - playSoundOnResetEnded(type, config?.feedbackSoundVolume); - }; - - const maybePlaySoundOnResetStart = () => { - if (!config?.feedbackSound) return; - if (type !== ResetType.Yaw) - playSoundOnResetStarted(config?.feedbackSoundVolume); - }; - - const triggerReset = () => { - setFinished(false); - startCountdown(); - maybePlaySoundOnResetStart(); - }; - - useEffect(() => { - return () => { - if (finishedTimeoutRef.current !== -1) - clearTimeout(finishedTimeoutRef.current); - }; - }, []); - - return size === 'small' ? ( + return ( - ) : ( - - {!isCounting || type === ResetType.Yaw ? text : String(timer)} - ); } diff --git a/gui/src/components/onboarding/pages/body-proportions/ManualProportions.tsx b/gui/src/components/onboarding/pages/body-proportions/ManualProportions.tsx index 0f3bfe2efc..95378ab55e 100644 --- a/gui/src/components/onboarding/pages/body-proportions/ManualProportions.tsx +++ b/gui/src/components/onboarding/pages/body-proportions/ManualProportions.tsx @@ -477,7 +477,6 @@ export function ManualProportionsPage() {
diff --git a/gui/src/components/onboarding/pages/body-proportions/autobone-steps/Preparation.tsx b/gui/src/components/onboarding/pages/body-proportions/autobone-steps/Preparation.tsx index 9f4a1f45f7..d3ed706977 100644 --- a/gui/src/components/onboarding/pages/body-proportions/autobone-steps/Preparation.tsx +++ b/gui/src/components/onboarding/pages/body-proportions/autobone-steps/Preparation.tsx @@ -69,11 +69,7 @@ export function PreparationStep({ > {l10n.getString('onboarding-automatic_mounting-prev_step')} - +
diff --git a/gui/src/components/onboarding/pages/mounting/mounting-steps/MountingReset.tsx b/gui/src/components/onboarding/pages/mounting/mounting-steps/MountingReset.tsx index bdd0881883..b934a1a9fa 100644 --- a/gui/src/components/onboarding/pages/mounting/mounting-steps/MountingReset.tsx +++ b/gui/src/components/onboarding/pages/mounting/mounting-steps/MountingReset.tsx @@ -58,7 +58,6 @@ export function MountingResetStep({ {l10n.getString('onboarding-automatic_mounting-prev_step')} diff --git a/gui/src/components/onboarding/pages/mounting/mounting-steps/Preparation.tsx b/gui/src/components/onboarding/pages/mounting/mounting-steps/Preparation.tsx index 76fe445464..eb356d96c2 100644 --- a/gui/src/components/onboarding/pages/mounting/mounting-steps/Preparation.tsx +++ b/gui/src/components/onboarding/pages/mounting/mounting-steps/Preparation.tsx @@ -69,11 +69,7 @@ export function PreparationStep({ > {l10n.getString('onboarding-automatic_mounting-prev_step')} - +
diff --git a/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/PreparationStep.tsx b/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/PreparationStep.tsx index a448abc743..9bd1013d5e 100644 --- a/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/PreparationStep.tsx +++ b/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/PreparationStep.tsx @@ -53,11 +53,7 @@ export function PreparationStep({ - +
); diff --git a/gui/src/components/settings/pages/InterfaceSettings.tsx b/gui/src/components/settings/pages/InterfaceSettings.tsx index 7973a8f2cb..10a4d0cb17 100644 --- a/gui/src/components/settings/pages/InterfaceSettings.tsx +++ b/gui/src/components/settings/pages/InterfaceSettings.tsx @@ -20,6 +20,7 @@ import { ArrowRightLeftIcon } from '@/components/commons/icon/ArrowIcons'; import { isTrayAvailable } from '@/utils/tauri'; import { isTauri } from '@tauri-apps/api/core'; import { TauriFileInput } from '@/components/commons/TauriFileInput'; +import { DeveloperModeWidget } from '@/components/widgets/DeveloperModeWidget'; interface InterfaceSettingsForm { appearance: { @@ -280,16 +281,19 @@ export function InterfaceSettings() { )}
-
- +
+
+ +
+ {config?.debug && }
diff --git a/gui/src/components/tracker/TrackerCard.tsx b/gui/src/components/tracker/TrackerCard.tsx index 84e738204f..bb23606986 100644 --- a/gui/src/components/tracker/TrackerCard.tsx +++ b/gui/src/components/tracker/TrackerCard.tsx @@ -28,7 +28,7 @@ function UpdateIcon({
diff --git a/gui/src/components/vr-mode/VRModePage.tsx b/gui/src/components/vr-mode/VRModePage.tsx index 093384cd94..38818158db 100644 --- a/gui/src/components/vr-mode/VRModePage.tsx +++ b/gui/src/components/vr-mode/VRModePage.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { useBreakpoint } from '@/hooks/breakpoint'; -import { WidgetsComponent } from '@/components/WidgetsComponent'; import { useNavigate } from 'react-router-dom'; +import { SkeletonVisualizerWidget } from '@/components/widgets/SkeletonVisualizerWidget'; export function VRModePage() { const nav = useNavigate(); @@ -12,8 +12,8 @@ export function VRModePage() { }, [isMobile]); return ( -
- +
+
); } diff --git a/gui/src/components/widgets/DeveloperModeWidget.tsx b/gui/src/components/widgets/DeveloperModeWidget.tsx index c06ca99de0..67f3d14fd7 100644 --- a/gui/src/components/widgets/DeveloperModeWidget.tsx +++ b/gui/src/components/widgets/DeveloperModeWidget.tsx @@ -4,7 +4,6 @@ import { useConfig } from '@/hooks/config'; import { useWebsocketAPI } from '@/hooks/websocket-api'; import { CheckBox } from '@/components/commons/Checkbox'; import { useLocalization } from '@fluent/react'; -import { Typography } from '@/components/commons/Typography'; export interface DeveloperModeWidgetForm { highContrast: boolean; @@ -57,6 +56,7 @@ export function DeveloperModeWidget() { key={index} control={control} variant="toggle" + outlined name={name} label={l10n.getString(`widget-developer_mode-${label}`)} > @@ -73,12 +73,7 @@ export function DeveloperModeWidget() { }; return ( -
-
- - {l10n.getString('widget-developer_mode')} - -
+ {Object.entries(toggles).map(makeToggle)}
); diff --git a/gui/src/hooks/config.ts b/gui/src/hooks/config.ts index e5693b9d0e..4eb89c07fe 100644 --- a/gui/src/hooks/config.ts +++ b/gui/src/hooks/config.ts @@ -46,6 +46,7 @@ export interface Config { showNavbarOnboarding: boolean; vrcMutedWarnings: string[]; bvhDirectory: string | null; + homeLayout: 'default' | 'table'; } export interface ConfigContext { @@ -75,6 +76,7 @@ export const defaultConfig: Config = { vrcMutedWarnings: [], devSettings: defaultDevSettings, bvhDirectory: null, + homeLayout: 'default', }; interface CrossStorage { diff --git a/gui/src/hooks/reset.ts b/gui/src/hooks/reset.ts new file mode 100644 index 0000000000..9573756f0f --- /dev/null +++ b/gui/src/hooks/reset.ts @@ -0,0 +1,126 @@ +import { + playSoundOnResetEnded, + playSoundOnResetStarted, +} from '@/sounds/sounds'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { + FlightListStepId, + ResetRequestT, + ResetResponseT, + ResetStatus, + ResetType, + RpcMessage, +} from 'solarxr-protocol'; +import { useConfig } from './config'; +import { useSessionFlightlist } from './session-flightlist'; +import { useWebsocketAPI } from './websocket-api'; +import { useCountdown } from './countdown'; + +export type ResetBtnStatus = 'idle' | 'counting' | 'finished'; +export function useReset(type: ResetType, onReseted?: () => void) { + const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); + + const { config } = useConfig(); + const finishedTimeoutRef = useRef(-1); + const [status, setStatus] = useState('idle'); + + const { visibleSteps } = useSessionFlightlist(); + const needsFullReset = useMemo( + () => + type == ResetType.Mounting && + visibleSteps.some( + (step) => step.id === FlightListStepId.FULL_RESET && !step.valid + ), + [visibleSteps, type] + ); + + const reset = () => { + const req = new ResetRequestT(); + req.resetType = type; + sendRPCPacket(RpcMessage.ResetRequest, req); + }; + + const duration = 3; + const { startCountdown, timer, abortCountdown } = useCountdown({ + duration: type === ResetType.Yaw ? 0 : duration, + onCountdownEnd: () => { + maybePlaySoundOnResetEnd(type); + reset(); + onResetFinished(); + }, + }); + + const onResetFinished = () => { + setStatus('finished'); + + // If a timer was already running / clear it + abortCountdown(); + if (finishedTimeoutRef.current !== -1) + clearTimeout(finishedTimeoutRef.current); + + // After 2s go back to idle state + finishedTimeoutRef.current = setTimeout(() => { + setStatus('idle'); + finishedTimeoutRef.current = -1; + }, 2000); + + if (onReseted) onReseted(); + }; + + const maybePlaySoundOnResetEnd = (type: ResetType) => { + if (!config?.feedbackSound) return; + playSoundOnResetEnded(type, config?.feedbackSoundVolume); + }; + + const maybePlaySoundOnResetStart = () => { + if (!config?.feedbackSound) return; + if (type !== ResetType.Yaw) + playSoundOnResetStarted(config?.feedbackSoundVolume); + }; + + const triggerReset = () => { + setStatus('counting'); + startCountdown(); + maybePlaySoundOnResetStart(); + }; + + useEffect(() => { + return () => { + if (finishedTimeoutRef.current !== -1) + clearTimeout(finishedTimeoutRef.current); + }; + }, []); + + useRPCPacket( + RpcMessage.ResetResponse, + ({ status, resetType }: ResetResponseT) => { + if (resetType !== type) return; + switch (status) { + case ResetStatus.FINISHED: { + onResetFinished(); + break; + } + } + } + ); + + const name = useMemo(() => { + switch (type) { + case ResetType.Yaw: + return 'reset-yaw'; + case ResetType.Mounting: + return 'reset-mounting'; + case ResetType.Full: + return 'reset-full'; + } + }, [type]); + + return { + triggerReset, + timer, + status, + disabled: status === 'counting' || needsFullReset, + name, + duration, + }; +} diff --git a/gui/src/index.scss b/gui/src/index.scss index 15cdde7b96..e34cec4a1e 100644 --- a/gui/src/index.scss +++ b/gui/src/index.scss @@ -64,13 +64,10 @@ body { --navbar-w: 81px; --topbar-h: 38px; - --toolbar-h: 65px; - --preview-w: 400px; - --flightlist-w: 272px; @screen mobile { --topbar-h: 44px; - --navbar-h: 60px; + --navbar-h: 73px; } } diff --git a/gui/src/store/app-store.ts b/gui/src/store/app-store.ts index 3b48ab44a2..4c06b20459 100644 --- a/gui/src/store/app-store.ts +++ b/gui/src/store/app-store.ts @@ -45,14 +45,18 @@ export const unassignedTrackersAtom = atom((get) => { return trackers.filter(({ tracker }) => tracker.info?.bodyPart === BodyPart.NONE); }); -export const connectedIMUTrackersAtom = atom((get) => { +export const connectedTrackersAtom = atom((get) => { const trackers = get(flatTrackersAtom); return trackers.filter( - ({ tracker }) => - tracker.status !== TrackerStatus.DISCONNECTED && tracker.info?.isImu + ({ tracker }) => tracker.status !== TrackerStatus.DISCONNECTED ); }); +export const connectedIMUTrackersAtom = atom((get) => { + const trackers = get(connectedTrackersAtom); + return trackers.filter(({ tracker }) => tracker.info?.isImu); +}); + export const computedTrackersAtom = selectAtom( datafeedAtom, (datafeed) => datafeed.syntheticTrackers.map((tracker) => ({ tracker })), diff --git a/gui/tailwind.config.ts b/gui/tailwind.config.ts index 34ab01a0bb..996a71d57a 100644 --- a/gui/tailwind.config.ts +++ b/gui/tailwind.config.ts @@ -172,6 +172,7 @@ const config = { nsm: { raw: 'not (min-width: 900px)' }, sm: '900px', md: '1100px', + nmd: { raw: 'not (min-width: 1100px)' }, 'md-max': { raw: 'not (min-width: 1100px)' }, lg: '1300px', xl: '1600px', From 691ec008048e14c95e0bfa39a76bb4f3c948befb Mon Sep 17 00:00:00 2001 From: sctanf <36978460+sctanf@users.noreply.github.com> Date: Wed, 23 Jul 2025 02:55:35 -0500 Subject: [PATCH 12/34] CalibrationTutorial use correct Id --- .../components/onboarding/pages/CalibrationTutorial.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gui/src/components/onboarding/pages/CalibrationTutorial.tsx b/gui/src/components/onboarding/pages/CalibrationTutorial.tsx index 6f102562c0..3f9f3ceca3 100644 --- a/gui/src/components/onboarding/pages/CalibrationTutorial.tsx +++ b/gui/src/components/onboarding/pages/CalibrationTutorial.tsx @@ -45,16 +45,17 @@ export function CalibrationTutorialPage() { useEffect(() => { const accelLength = restCalibrationTrackers.every((x) => { if ( + x.device?.id?.id === undefined || x.tracker.trackerId?.trackerNum === undefined || x.tracker.trackerId.deviceId?.id === undefined || !x.tracker.linearAcceleration ) return false; - const trackerId = - x.tracker.trackerId.trackerNum + (x.tracker.trackerId.trackerNum << 8); - const lastValues = lastValueMap.current.get(trackerId) ?? []; - lastValueMap.current.set(trackerId, lastValues); + const trackerId = x.device.id.id; +// x.tracker.trackerId.trackerNum + (x.tracker.trackerId.deviceId.id << 8); + const lastValue = lastValueMap.current.get(trackerId) ?? new Vector3(); + lastValueMap.current.set(trackerId, lastValue); const vec3 = Vector3FromVec3fT(x.tracker.linearAcceleration); if (lastValues.length > 5) { From 0504b868b75c3245e5c173192b9c76e37cf738c5 Mon Sep 17 00:00:00 2001 From: sctanf <36978460+sctanf@users.noreply.github.com> Date: Wed, 23 Jul 2025 02:56:19 -0500 Subject: [PATCH 13/34] CalibrationTutorial store and compare accel --- .../onboarding/pages/CalibrationTutorial.tsx | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/gui/src/components/onboarding/pages/CalibrationTutorial.tsx b/gui/src/components/onboarding/pages/CalibrationTutorial.tsx index 3f9f3ceca3..ddb136f61a 100644 --- a/gui/src/components/onboarding/pages/CalibrationTutorial.tsx +++ b/gui/src/components/onboarding/pages/CalibrationTutorial.tsx @@ -23,7 +23,9 @@ export enum CalibrationStatus { } export const IMU_CALIBRATION_TIME = 4; -const ACCEL_TOLERANCE = 0.2; // m/s^2 +export const IMU_SETTLE_TIME = 1; +const ACCEL_TOLERANCE = 0.5; // m/s^2 +const ACCEL_HYSTERESIS = 0.1; // m/s^2 export function CalibrationTutorialPage() { const { l10n } = useLocalization(); @@ -32,16 +34,17 @@ export function CalibrationTutorialPage() { CalibrationStatus.WAITING ); const [skipButton, setSkipButton] = useState(false); + const [settled, setSettled] = useState(false); const { timer, isCounting, startCountdown, abortCountdown } = useCountdown({ - duration: IMU_CALIBRATION_TIME, - onCountdownEnd: () => setCalibrationStatus(CalibrationStatus.SUCCESS), + duration: settled ? IMU_CALIBRATION_TIME : IMU_SETTLE_TIME, + onCountdownEnd: () => settled ? setCalibrationStatus(CalibrationStatus.SUCCESS) : setSettled(true), }); useTimeout(() => setSkipButton(true), 10000); const connectedIMUTrackers = useAtomValue(connectedIMUTrackersAtom); const restCalibrationTrackers = useRestCalibrationTrackers(connectedIMUTrackers); const [rested, setRested] = useState(false); - const lastValueMap = useRef(new Map()); + const lastValueMap = useRef(new Map()); useEffect(() => { const accelLength = restCalibrationTrackers.every((x) => { if ( @@ -58,17 +61,32 @@ export function CalibrationTutorialPage() { lastValueMap.current.set(trackerId, lastValue); const vec3 = Vector3FromVec3fT(x.tracker.linearAcceleration); - if (lastValues.length > 5) { - lastValues.shift(); - const avg = averageVector(lastValues).lengthSq(); - lastValues.push(vec3); - return vec3.lengthSq() <= avg + ACCEL_TOLERANCE ** 2; + + if (vec3.lengthSq() > ACCEL_TOLERANCE ** 2) { + return false; + } + + const delta = new Vector3(); + delta.subVectors(lastValue, vec3); + + if (delta.lengthSq() > ACCEL_HYSTERESIS ** 2) { + lastValue.copy(vec3); + return false; } - lastValues.push(vec3); - return false; + + return true; }); - setRested(accelLength || restCalibrationTrackers.length === 0); + if (accelLength && !settled && !isCounting) { + abortCountdown(); + startCountdown(); + } else if (!accelLength && !settled && isCounting) { + abortCountdown(); + } else if (!accelLength && settled) { + setSettled(false); + } + + setRested(settled || restCalibrationTrackers.length === 0); }, [restCalibrationTrackers]); useEffect(() => { @@ -148,7 +166,7 @@ export function CalibrationTutorialPage() {
Date: Wed, 23 Jul 2025 17:12:59 +0200 Subject: [PATCH 14/34] Progress --- gui/src/components/MainLayout.scss | 10 +- gui/src/components/MainLayout.tsx | 15 +-- gui/src/components/Sidebar.tsx | 65 +++++++++++ gui/src/components/Toolbar.tsx | 15 ++- .../flight-list/SessionFlightList.tsx | 105 +++++++++++------- .../onboarding/pages/ConnectTracker.tsx | 6 +- gui/src/hooks/session-flightlist.ts | 15 +++ gui/tailwind.config.ts | 11 ++ 8 files changed, 180 insertions(+), 62 deletions(-) create mode 100644 gui/src/components/Sidebar.tsx diff --git a/gui/src/components/MainLayout.scss b/gui/src/components/MainLayout.scss index 2dac343554..a9c4e5fcaa 100644 --- a/gui/src/components/MainLayout.scss +++ b/gui/src/components/MainLayout.scss @@ -6,14 +6,14 @@ display: grid; grid-template: 't t' var(--topbar-h) - 's c' calc(100% - var(--topbar-h)) + 'n c' calc(100% - var(--topbar-h)) / var(--navbar-w) calc(100% - var(--navbar-w)); &.full { grid-template: 't t t' var(--topbar-h) - 's b l' var(--toolbar-h) - 's c l' calc(100% - var(--topbar-h) - var(--toolbar-h)) + 'n b s' var(--toolbar-h) + 'n c s' calc(100% - var(--topbar-h) - var(--toolbar-h)) / var(--navbar-w) calc(100% - var(--navbar-w) - var(--right-section-w)) var(--right-section-w); } @@ -45,14 +45,14 @@ 't' var(--topbar-h) 'b' var(--toolbar-h) 'c' calc(100% - var(--topbar-h) - var(--toolbar-h) - var(--navbar-h)) - 's' calc(var(--navbar-h)) + 'n' calc(var(--navbar-h)) / 100%; } grid-template: 't' var(--topbar-h) 'c' calc(100% - var(--topbar-h) - var(--navbar-h)) - 's' calc(var(--navbar-h)) + 'n' calc(var(--navbar-h)) / 100%; } } diff --git a/gui/src/components/MainLayout.tsx b/gui/src/components/MainLayout.tsx index cbd3422edf..7cbb672b71 100644 --- a/gui/src/components/MainLayout.tsx +++ b/gui/src/components/MainLayout.tsx @@ -11,7 +11,7 @@ import { TopBar } from './TopBar'; import { useWebsocketAPI } from '@/hooks/websocket-api'; import './MainLayout.scss'; import { Toolbar } from './Toolbar'; -import { SessionFlightList } from './flight-list/SessionFlightList'; +import { Sidebar } from './Sidebar'; export function MainLayout({ children, @@ -65,7 +65,7 @@ export function MainLayout({
-
+
@@ -86,14 +86,9 @@ export function MainLayout({
)} {!isMobile && full && ( - <> -
- -
- +
+ +
)}
); diff --git a/gui/src/components/Sidebar.tsx b/gui/src/components/Sidebar.tsx new file mode 100644 index 0000000000..070e17550b --- /dev/null +++ b/gui/src/components/Sidebar.tsx @@ -0,0 +1,65 @@ +import { useSessionFlightlist } from '@/hooks/session-flightlist'; +import { SessionFlightList } from './flight-list/SessionFlightList'; +import { SkeletonVisualizerWidget } from './widgets/SkeletonVisualizerWidget'; +import { useEffect, useLayoutEffect, useState } from 'react'; +import classNames from 'classnames'; + +export function Sidebar() { + const { completion } = useSessionFlightlist(); + const [closed, setClosed] = useState(true); + const [closing, setClosing] = useState(false); + + const closedHight = '90px'; + const flightlistSize = closed ? closedHight : 'calc(100% - 16px)'; + const previewSize = closed ? `calc(100% - ${closedHight} - 24px)` : '0%'; + + const toggleClosed = () => setClosed((closed) => !closed); + + useLayoutEffect(() => { + setClosing(true); + const ref = setTimeout(() => setClosing(false), 1000); + return () => { + clearTimeout(ref); + setClosing(false); + }; + }, [closed]); + + useEffect(() => { + if (completion === 'complete') { + setClosed(true); + } else { + setClosed(false); + } + }, [completion]); + + return ( + <> +
+ +
+
+
+ +
+
+ + ); +} diff --git a/gui/src/components/Toolbar.tsx b/gui/src/components/Toolbar.tsx index 73a7d55a51..57372adcbd 100644 --- a/gui/src/components/Toolbar.tsx +++ b/gui/src/components/Toolbar.tsx @@ -32,7 +32,7 @@ function ButtonProgress({ return (
!disabled && triggerReset()} > - {icon} +
+ {icon} +
void; +}) { const context = useSessionFlightlist(); - const { visibleSteps } = context; - - const progress = useMemo(() => { - const completeSteps = visibleSteps.filter( - (step) => step.status === 'complete' || step.status === 'skipped' - ); - return Math.min(1, completeSteps.length / visibleSteps.length); - }, [visibleSteps]); - - const completion = useMemo(() => { - if ( - progress === 1 && - visibleSteps.find((step) => step.status === 'skipped') - ) - return 'partial'; - return progress === 1 ? 'complete' : 'incomplete'; - }, [progress, visibleSteps]); + const { visibleSteps, progress, completion } = context; const slimeState = useMemo(() => { if (completion === 'complete') return SlimeState.HAPPY; @@ -339,19 +332,42 @@ export function SessionFlightList() { return ( <> -
-
+
+
Tracking checklist
-
setSettingsOpen(true)} - > - +
+
toggleClosed()} + > + +
+
setSettingsOpen(true)} + > + +
-
+
{visibleSteps.map((step, index) => ( {stepContentLookup[step.id]?.(step, context) || undefined} @@ -359,10 +375,10 @@ export function SessionFlightList() { ))}
@@ -375,7 +391,7 @@ export function SessionFlightList() { )} >
-
+
{completion === 'incomplete' && ( You are not prepared to use SlimeVR! @@ -389,16 +405,23 @@ export function SessionFlightList() {
-
- -
+
+ {!closed && ( + + )} + +
diff --git a/gui/src/components/onboarding/pages/ConnectTracker.tsx b/gui/src/components/onboarding/pages/ConnectTracker.tsx index 9f62a384b5..52076fa140 100644 --- a/gui/src/components/onboarding/pages/ConnectTracker.tsx +++ b/gui/src/components/onboarding/pages/ConnectTracker.tsx @@ -102,7 +102,7 @@ export function InvalidNetworkProfileWarning({ export function ConnectTrackersPage() { const { l10n } = useLocalization(); - const { visibleSteps: steps } = useSessionFlightlist(); + const { visibleSteps } = useSessionFlightlist(); const connectedIMUTrackers = useAtomValue(connectedIMUTrackersAtom); const { applyProgress, state } = useOnboarding(); @@ -192,11 +192,11 @@ export function ConnectTrackersPage() { ); const invalidNetworkProfile = useMemo(() => { - return steps.find( + return visibleSteps.find( (step) => step.id === FlightListStepId.NETWORK_PROFILE_PUBLIC && !step.valid ); - }, [steps]); + }, [visibleSteps]); return ( <> diff --git a/gui/src/hooks/session-flightlist.ts b/gui/src/hooks/session-flightlist.ts index 8fdf5ad3bc..c055441e2e 100644 --- a/gui/src/hooks/session-flightlist.ts +++ b/gui/src/hooks/session-flightlist.ts @@ -111,11 +111,26 @@ export function provideSessionFlightlist() { return []; }, [firstRequired]); + const progress = useMemo(() => { + const completeSteps = visibleSteps.filter( + (step) => step.status === 'complete' || step.status === 'skipped' + ); + return Math.min(1, completeSteps.length / visibleSteps.length); + }, [visibleSteps]); + + const completion: 'complete' | 'partial' | 'incomplete' = useMemo(() => { + if (progress === 1 && visibleSteps.find((step) => step.status === 'skipped')) + return 'partial'; + return progress === 1 || visibleSteps.length === 0 ? 'complete' : 'incomplete'; + }, [progress, visibleSteps]); + return { visibleSteps: steps.filter(stepVisibility), firstRequired, ignoredSteps, hightlightedTrackers, + progress, + completion, toggle: (step: FlightListStepId) => { const res = new ToggleFlightListStepRequestT(); res.stepId = step; diff --git a/gui/tailwind.config.ts b/gui/tailwind.config.ts index 996a71d57a..594f1f5b2e 100644 --- a/gui/tailwind.config.ts +++ b/gui/tailwind.config.ts @@ -229,6 +229,14 @@ const config = { 'animation-timing-function': 'cubic-bezier(0.8, 0, 1, 1)', }, }, + 'spin-ccw': { + '0%': { + transform: 'rotate(0deg)', + }, + '100%': { + transform: 'rotate(-360deg)', + }, + }, }, backgroundImage: { slime: `linear-gradient(135deg, ${colors.purple[100]} 50%, ${colors['blue-gray'][700]} 50% 100%)`, @@ -242,6 +250,9 @@ const config = { 'asexual-flag': `linear-gradient(135deg, ${colors['asexual'][100]} 30%, ${colors['asexual'][200]} 30% 50%, ${colors['asexual'][300]} 50% 70%, ${colors['asexual'][400]} 70% 100%)`, }, }, + animation: { + 'spin-ccw': 'spin-ccw 1s linear infinite', + }, data: { checked: 'checked=true', }, From 902212432aefadc9e1eb97386bf27d74b056f29d Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Fri, 25 Jul 2025 09:34:42 +0200 Subject: [PATCH 15/34] Current progress --- gui/src/components/Sidebar.tsx | 136 +++++++++++++++++- gui/src/components/Toolbar.tsx | 67 ++++++--- gui/src/components/commons/Checkbox.tsx | 10 +- .../components/commons/icon/LayoutIcon.tsx | 12 ++ .../flight-list/FlightListSettingsModal.tsx | 112 +++++++++++---- .../flight-list/SessionFlightList.tsx | 75 ++++++---- gui/src/hooks/session-flightlist.ts | 70 +++++---- gui/src/utils/skeletonHelper.ts | 2 +- gui/tailwind.config.ts | 40 +++++- .../slimevr/flightlist/FlightListManager.kt | 7 +- .../slimevr/games/vrchat/VRCConfigHandler.kt | 1 - .../rpc/flightlist/RPCFlightListHandler.kt | 6 +- .../tracking/processor/HumanPoseManager.kt | 4 + 13 files changed, 424 insertions(+), 118 deletions(-) create mode 100644 gui/src/components/commons/icon/LayoutIcon.tsx diff --git a/gui/src/components/Sidebar.tsx b/gui/src/components/Sidebar.tsx index 070e17550b..693c968b93 100644 --- a/gui/src/components/Sidebar.tsx +++ b/gui/src/components/Sidebar.tsx @@ -1,13 +1,31 @@ import { useSessionFlightlist } from '@/hooks/session-flightlist'; import { SessionFlightList } from './flight-list/SessionFlightList'; import { SkeletonVisualizerWidget } from './widgets/SkeletonVisualizerWidget'; -import { useEffect, useLayoutEffect, useState } from 'react'; +import { useEffect, useLayoutEffect, useMemo, useState } from 'react'; import classNames from 'classnames'; +import { Typography } from './commons/Typography'; +import { useLocaleConfig } from '@/i18n/config'; +import { useWebsocketAPI } from '@/hooks/websocket-api'; +import { + RpcMessage, + SkeletonConfigRequestT, + SkeletonConfigResponseT, +} from 'solarxr-protocol'; +import { Tooltip } from './commons/Tooltip'; +import { Vector3 } from 'three'; +import { RecordIcon } from './commons/icon/RecordIcon'; +import { PauseIcon } from './commons/icon/PauseIcon'; +import { HumanIcon } from './commons/icon/HumanIcon'; +import { EyeIcon } from './commons/icon/EyeIcon'; export function Sidebar() { const { completion } = useSessionFlightlist(); const [closed, setClosed] = useState(true); const [closing, setClosing] = useState(false); + const [userHeight, setUserHeight] = useState(0); + + const { currentLocales } = useLocaleConfig(); + const { useRPCPacket, sendRPCPacket } = useWebsocketAPI(); const closedHight = '90px'; const flightlistSize = closed ? closedHight : 'calc(100% - 16px)'; @@ -27,11 +45,34 @@ export function Sidebar() { useEffect(() => { if (completion === 'complete') { setClosed(true); - } else { + } else if (completion === 'incomplete') { setClosed(false); } }, [completion]); + const { cmFormat } = useMemo(() => { + const cmFormat = Intl.NumberFormat(currentLocales, { + style: 'unit', + unit: 'centimeter', + maximumFractionDigits: 1, + }); + return { cmFormat }; + }, [currentLocales]); + + useRPCPacket( + RpcMessage.SkeletonConfigResponse, + (data: SkeletonConfigResponseT) => { + if (data.userHeight) setUserHeight(data.userHeight); + } + ); + + useEffect(() => { + sendRPCPacket( + RpcMessage.SkeletonConfigRequest, + new SkeletonConfigRequestT() + ); + }, []); + return ( <>
- + + } + > +
+ + {cmFormat.format((userHeight * 100) / 0.936)} + +
+
+ Disable rendering} + > +
+ +
+
+ {/* Enable Rendering} + > +
+ +
+
*/} +
+
+ Record BVH + } + preferedDirection="top" + > +
+ +
+
+ + Pause tracking + + } + preferedDirection="top" + > +
+ +
+
+ Mocap Mode + } + preferedDirection="top" + > +
+ +
+
+
+
+ { + context.addView({ + left: 0, + bottom: 0, + width: 1, + height: 1, + position: new Vector3(3, 2.5, -3), + onHeightChange(v, newHeight) { + v.controls.target.set(0, newHeight / 2.2, 0.1); + const scale = Math.max(1, newHeight) / 1.3; + v.camera.zoom = 1 / scale; + }, + }); + }} + >
diff --git a/gui/src/components/Toolbar.tsx b/gui/src/components/Toolbar.tsx index 57372adcbd..b385deb7da 100644 --- a/gui/src/components/Toolbar.tsx +++ b/gui/src/components/Toolbar.tsx @@ -1,24 +1,32 @@ import { Typography } from './commons/Typography'; import classNames from 'classnames'; -import { ResetType } from 'solarxr-protocol'; +import { + ClearMountingResetRequestT, + ResetType, + RpcMessage, +} from 'solarxr-protocol'; import { ResetBtnStatus, useReset } from '@/hooks/reset'; import { Tooltip } from './commons/Tooltip'; import { GearIcon } from './commons/icon/GearIcon'; import { useAtomValue } from 'jotai'; -import { connectedTrackersAtom } from '@/store/app-store'; +import { assignedTrackersAtom, connectedTrackersAtom } from '@/store/app-store'; import { ClearIcon } from './commons/icon/ClearIcon'; import { useBreakpoint } from '@/hooks/breakpoint'; import { useMemo, useState } from 'react'; import { HomeSettingsModal } from './home/HomeSettingsModal'; import { SkiIcon } from './commons/icon/SkiIcon'; import { FullResetIcon, YawResetIcon } from './commons/icon/ResetIcon'; +import { useWebsocketAPI } from '@/hooks/websocket-api'; +import { QuaternionFromQuatT, similarQuaternions } from '@/maths/quaternion'; +import { Quaternion } from 'three'; +import { LayoutIcon } from './commons/icon/LayoutIcon'; const MAINBUTTON_CLASSES = ({ disabled }: { disabled: boolean }) => classNames( - 'p-2 flex justify-center px-4 gap-4 h-full items-center hover:bg-background-50 bg-background-60 relative overflow-clip aspect-square md:aspect-auto', + 'p-2 flex justify-center px-4 gap-4 h-full items-center bg-background-60 relative overflow-clip aspect-square md:aspect-auto', { - 'cursor-pointer': !disabled, - 'cursor-not-allowed': disabled, + 'cursor-pointer hover:bg-background-50 bg-background-60': !disabled, + 'cursor-not-allowed bg-background-70 brightness-75': disabled, } ); @@ -62,13 +70,7 @@ function BasicResetButton({ type }: { type: ResetType }) { preferedDirection="top" >
+ assignedTrackers.some( + (d) => + !similarQuaternions( + QuaternionFromQuatT(d?.tracker.info?.mountingResetOrientation), + _q + ) + ), + [assignedTrackers] + ); + + const clearMounting = () => { + const record = new ClearMountingResetRequestT(); + sendRPCPacket(RpcMessage.ClearMountingResetRequest, record); + }; const progress = status === 'counting' ? 1 - (timer - 1) / duration : 0; return (
!disabled && triggerReset()} > -
+
@@ -146,9 +176,10 @@ function MountingCalibrationButton() { >
trackerWithMounting && !disabled && clearMounting} >
@@ -184,7 +215,7 @@ export function Toolbar({ showSettings }: { showSettings: boolean }) { className="fill-background-30 hover:fill-background-20 cursor-pointer" onClick={() => setSettingsOpen(true)} > - +
)}
diff --git a/gui/src/components/commons/Checkbox.tsx b/gui/src/components/commons/Checkbox.tsx index 79fe01ae7a..93e33b0894 100644 --- a/gui/src/components/commons/Checkbox.tsx +++ b/gui/src/components/commons/Checkbox.tsx @@ -29,7 +29,9 @@ export function CheckBox({ const classes = useMemo(() => { const vriantsMap = { checkbox: { - checkbox: CHECKBOX_CLASSES, + checkbox: classNames(CHECKBOX_CLASSES, { + 'brightness-50 hover:cursor-not-allowed': disabled, + }), toggle: '', pin: '', }, @@ -42,7 +44,7 @@ export function CheckBox({ }, }; return vriantsMap[variant]; - }, [variant]); + }, [variant, disabled]); return ( diff --git a/gui/src/components/commons/icon/LayoutIcon.tsx b/gui/src/components/commons/icon/LayoutIcon.tsx new file mode 100644 index 0000000000..a82a78480d --- /dev/null +++ b/gui/src/components/commons/icon/LayoutIcon.tsx @@ -0,0 +1,12 @@ +export function LayoutIcon({ size = 16 }: { size?: number }) { + return ( + + + + ); +} diff --git a/gui/src/components/flight-list/FlightListSettingsModal.tsx b/gui/src/components/flight-list/FlightListSettingsModal.tsx index 13c9a8bad3..e35a919a60 100644 --- a/gui/src/components/flight-list/FlightListSettingsModal.tsx +++ b/gui/src/components/flight-list/FlightListSettingsModal.tsx @@ -1,25 +1,62 @@ -import { Dispatch, SetStateAction } from 'react'; +import { Dispatch, SetStateAction, useEffect } from 'react'; import { BaseModal } from '@/components/commons/BaseModal'; import { flightlistIdtoLabel, useSessionFlightlist, } from '@/hooks/session-flightlist'; import { Typography } from '@/components/commons/Typography'; -import { CrossIcon } from '@/components/commons/icon/CrossIcon'; -import { Button } from '../commons/Button'; -import { CheckBox } from '../commons/Checkbox'; -import { Form, useForm } from 'react-hook-form'; +import { Button } from '@/components/commons/Button'; +import { CheckBox } from '@/components/commons/Checkbox'; +import { useForm } from 'react-hook-form'; +import { useLocalization } from '@fluent/react'; +import { FlightListStepId } from 'solarxr-protocol'; +type StepsForm = { steps: Record }; export function FlightListSettingsModal({ open, }: { open: [boolean, Dispatch>]; }) { - const { ignoredSteps, steps, toggle } = useSessionFlightlist(); + const { l10n } = useLocalization(); + const { ignoredSteps, steps, ignoreStep } = useSessionFlightlist(); - const { control } = useForm<{ steps: boolean[] }>({ - defaultValues: { steps: steps.map(({ id }) => ignoredSteps.includes(id)) }, - }); + const { control, reset, handleSubmit, watch, getValues } = useForm( + { + defaultValues: { + steps: steps.reduce( + (curr, { id }) => ({ [id]: !ignoredSteps.includes(id), ...curr }), + {} + ), + }, + mode: 'onChange', + } + ); + + useEffect(() => { + reset({ + steps: steps.reduce( + (curr, { id }) => ({ [id]: !ignoredSteps.includes(id), ...curr }), + {} + ), + }); + }, [ignoredSteps]); + + const onSubmit = (values: StepsForm) => { + for (const [id, value] of Object.entries(values.steps)) { + const stepId = +id; + if (!stepId) continue; + + // doing it this way prevents calling ignore step for every step. + // that prevent sending a packet for steps that didnt change + if (!value && !ignoredSteps.includes(stepId)) { + ignoreStep(stepId, true); + } + + if (value && ignoredSteps.includes(stepId)) { + ignoreStep(stepId, false); + } + } + }; return ( -
- Flight List Settings - Ignored Steps -
- {steps.map((step) => ( -
toggle(step.id)} - > - -
- -
-
- ))} -
+
+ + Tracking Checklist Settings + +
+
+ Active Steps + + List all the steps that will show in the tracking checklist. You + can either disable or enable ignorable steps + +
+
+ {steps + .filter((step) => step.enabled) + .map((step) => ( +
+ +
+ ))} +
+
+
- +
diff --git a/gui/src/components/flight-list/SessionFlightList.tsx b/gui/src/components/flight-list/SessionFlightList.tsx index e2b222a891..891c434c83 100644 --- a/gui/src/components/flight-list/SessionFlightList.tsx +++ b/gui/src/components/flight-list/SessionFlightList.tsx @@ -20,10 +20,14 @@ import { A } from '@/components/commons/A'; import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon'; import { ProgressBar } from '@/components/commons/ProgressBar'; import { CrossIcon } from '@/components/commons/icon/CrossIcon'; -import { ArrowDownIcon } from '@/components/commons/icon/ArrowIcons'; +import { + ArrowDownIcon, + ArrowUpIcon, +} from '@/components/commons/icon/ArrowIcons'; import { Localized } from '@fluent/react'; import { WrenchIcon } from '@/components/commons/icon/WrenchIcons'; import { c } from 'vite/dist/node/types.d-aGj9QkWt'; +import { FlightListSettingsModal } from './FlightListSettingsModal'; function Step({ step: { status, id, optional, firstRequired }, @@ -319,11 +323,12 @@ export function SessionFlightList({ toggleClosed: () => void; }) { const context = useSessionFlightlist(); - const { visibleSteps, progress, completion } = context; + const { visibleSteps, progress, completion, warnings } = context; const slimeState = useMemo(() => { if (completion === 'complete') return SlimeState.HAPPY; if (completion === 'incomplete') return SlimeState.CURIOUS; + if (completion === 'partial') return SlimeState.SAD; return SlimeState.HAPPY; }, [completion]); @@ -342,24 +347,25 @@ export function SessionFlightList({ >
Tracking checklist
-
+
toggleClosed()} + onClick={() => setSettingsOpen(true)} > - +
setSettingsOpen(true)} + onClick={() => toggleClosed()} > - + {closed && } + {!closed && }
@@ -375,20 +381,30 @@ export function SessionFlightList({ ))}
-
-
+
toggleClosed()} + > +
@@ -397,7 +413,12 @@ export function SessionFlightList({ You are not prepared to use SlimeVR! )} - {(completion == 'complete' || completion === 'partial') && ( + {completion === 'partial' && ( + + You have {warnings.length} warnings! + + )} + {completion == 'complete' && ( You are prepared to use SlimeVR! @@ -414,9 +435,11 @@ export function SessionFlightList({ )} @@ -428,9 +451,9 @@ export function SessionFlightList({
- {/* */} + > ); } diff --git a/gui/src/hooks/session-flightlist.ts b/gui/src/hooks/session-flightlist.ts index c055441e2e..f704e2c48a 100644 --- a/gui/src/hooks/session-flightlist.ts +++ b/gui/src/hooks/session-flightlist.ts @@ -4,8 +4,8 @@ import { FlightListStepId, FlightListStepT, FlightListStepVisibility, + IgnoreFlightListStepRequestT, RpcMessage, - ToggleFlightListStepRequestT, } from 'solarxr-protocol'; import { useWebsocketAPI } from './websocket-api'; import { createContext, useContext, useEffect, useMemo, useState } from 'react'; @@ -70,34 +70,42 @@ const createStep = ( }; export type SessionFlightListContext = ReturnType; - +export type Steps = { + steps: FlightListStepT[]; + visibleSteps: FlightListStep[]; + ignoredSteps: FlightListStepId[]; +}; export function provideSessionFlightlist() { const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); - const [steps, setSteps] = useState([]); - const [ignoredSteps, setIgnoredSteps] = useState([]); + const [steps, setSteps] = useState({ + steps: [], + visibleSteps: [], + ignoredSteps: [], + }); useRPCPacket(RpcMessage.FlightListResponse, (data: FlightListResponseT) => { - setIgnoredSteps(data.ignoredSteps); const activeSteps = data.steps.filter( (step) => !data.ignoredSteps.includes(step.id) && step.enabled ); - const steps = activeSteps.map((step: FlightListStepT, index) => - createStep(activeSteps, step, index) - ); - setSteps(steps); + setSteps({ + steps: data.steps, + visibleSteps: activeSteps + .map((step: FlightListStepT, index) => createStep(activeSteps, step, index)) + .filter(stepVisibility), + ignoredSteps: data.ignoredSteps, + }); }); useEffect(() => { sendRPCPacket(RpcMessage.FlightListRequest, new FlightListRequestT()); }, []); - const visibleSteps = useMemo(() => steps.filter(stepVisibility), [steps]); const firstRequired = useMemo( () => - visibleSteps.find( + steps.visibleSteps.find( (step) => !step.valid && step.status != 'blocked' && !step.optional ), - [visibleSteps] + [steps] ); const hightlightedTrackers = useMemo(() => { @@ -112,30 +120,42 @@ export function provideSessionFlightlist() { }, [firstRequired]); const progress = useMemo(() => { - const completeSteps = visibleSteps.filter( + const completeSteps = steps.visibleSteps.filter( (step) => step.status === 'complete' || step.status === 'skipped' ); - return Math.min(1, completeSteps.length / visibleSteps.length); - }, [visibleSteps]); + return Math.min(1, completeSteps.length / steps.visibleSteps.length); + }, [steps]); const completion: 'complete' | 'partial' | 'incomplete' = useMemo(() => { - if (progress === 1 && visibleSteps.find((step) => step.status === 'skipped')) + if (progress === 1 && steps.visibleSteps.find((step) => step.status === 'skipped')) return 'partial'; - return progress === 1 || visibleSteps.length === 0 ? 'complete' : 'incomplete'; - }, [progress, visibleSteps]); + return progress === 1 || steps.visibleSteps.length === 0 + ? 'complete' + : 'incomplete'; + }, [progress, steps]); + + const warnings = useMemo( + () => steps.visibleSteps.filter((step) => !step.valid), + [steps] + ); + + const ignoreStep = (step: FlightListStepId, ignore: boolean) => { + const res = new IgnoreFlightListStepRequestT(); + res.stepId = step; + res.ignore = ignore; + sendRPCPacket(RpcMessage.IgnoreFlightListStepRequest, res); + }; return { - visibleSteps: steps.filter(stepVisibility), + ...steps, firstRequired, - ignoredSteps, hightlightedTrackers, progress, completion, - toggle: (step: FlightListStepId) => { - const res = new ToggleFlightListStepRequestT(); - res.stepId = step; - sendRPCPacket(RpcMessage.ToggleFlightListStepRequest, res); - }, + warnings, + ignoreStep, + toggle: (step: FlightListStepId) => + ignoreStep(step, !steps.ignoredSteps.includes(step)), }; } diff --git a/gui/src/utils/skeletonHelper.ts b/gui/src/utils/skeletonHelper.ts index 9e3d9edd0c..eb77d95a48 100644 --- a/gui/src/utils/skeletonHelper.ts +++ b/gui/src/utils/skeletonHelper.ts @@ -188,7 +188,7 @@ export class BoneKind extends Bone { case BodyPart.NONE: throw 'Unexpected body part'; case BodyPart.HEAD: - return new Color('black'); + return new Color('gold'); case BodyPart.NECK: return new Color('silver'); case BodyPart.UPPER_CHEST: diff --git a/gui/tailwind.config.ts b/gui/tailwind.config.ts index 594f1f5b2e..e7c6a3325b 100644 --- a/gui/tailwind.config.ts +++ b/gui/tailwind.config.ts @@ -237,6 +237,38 @@ const config = { transform: 'rotate(-360deg)', }, }, + skiing: { + '0%, 100%': { + transform: 'rotate(0deg) translateX(0%) translateY(0%)', + }, + '10%': { + transform: 'rotate(12deg) translateX(-2%) translateY(2%)', + }, + '20%': { + transform: 'rotate(10deg) translateX(0%) translateY(0%)', + }, + '30%': { + transform: 'rotate(12deg) translateX(2%) translateY(-2%)', + }, + '40%': { + transform: 'rotate(10deg) translateX(0%) translateY(0%)', + }, + '50%': { + transform: 'rotate(12deg) translateX(-2%) translateY(2%)', + }, + '60%': { + transform: 'rotate(10deg) translateX(0%) translateY(0%)', + }, + '70%': { + transform: 'rotate(12deg) translateX(2%) translateY(-2%)', + }, + '80%': { + transform: 'rotate(10deg) translateX(0%) translateY(0%)', + }, + '90%': { + transform: 'rotate(10deg) translateX(-2%) translateY(2%)', + }, + }, }, backgroundImage: { slime: `linear-gradient(135deg, ${colors.purple[100]} 50%, ${colors['blue-gray'][700]} 50% 100%)`, @@ -249,9 +281,11 @@ const config = { 'trans-flag': `linear-gradient(135deg, ${colors['trans-blue'][800]} 40%, ${colors['trans-blue'][700]} 40% 70%, ${colors['trans-blue'][600]} 70% 100%)`, 'asexual-flag': `linear-gradient(135deg, ${colors['asexual'][100]} 30%, ${colors['asexual'][200]} 30% 50%, ${colors['asexual'][300]} 50% 70%, ${colors['asexual'][400]} 70% 100%)`, }, - }, - animation: { - 'spin-ccw': 'spin-ccw 1s linear infinite', + animation: { + 'spin-ccw': 'spin-ccw 1s linear infinite', + skiing: 'skiing 1s linear infinite', + 'skiing-reset': 'skiing-reset 0.2s linear infinite', + }, }, data: { checked: 'checked=true', diff --git a/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt b/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt index cfb05444cd..22869f1cb4 100644 --- a/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt +++ b/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt @@ -319,11 +319,12 @@ class FlightListManager(private val vrServer: VRServer) : VRCConfigListener { listeners.forEach { it.onStepsUpdate() } } - fun toggleStep(step: FlightListStepT) { + fun ignoreStep(step: FlightListStepT, ignore: Boolean) { + if (!step.ignorable) return; val ignoredSteps = vrServer.configManager.vrConfig.flightList.ignoredStepsIds - if (!ignoredSteps.contains(step.id)) { + if (ignore && !ignoredSteps.contains(step.id)) { ignoredSteps.add(step.id) - } else { + } else if (!ignore) { ignoredSteps.remove(step.id) } vrServer.configManager.saveConfig() diff --git a/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt b/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt index fcf4a62f73..1852cf496d 100644 --- a/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt +++ b/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt @@ -129,7 +129,6 @@ class VRChatConfigManager(val server: VRServer, private val handler: VRCConfigHa val validity = currentValidity ?: return val values = currentValues ?: return listeners.forEach { - println("CALLED") it.onChange( validity, values, diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/flightlist/RPCFlightListHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/flightlist/RPCFlightListHandler.kt index 3812493436..7853b1811a 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/flightlist/RPCFlightListHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/flightlist/RPCFlightListHandler.kt @@ -16,7 +16,7 @@ class RPCFlightListHandler( api.server.flightListManager.addListener(this) rpcHandler.registerPacketListener(RpcMessage.FlightListRequest, ::onFlightListRequest) - rpcHandler.registerPacketListener(RpcMessage.ToggleFlightListStepRequest, ::onToggleFlightListRequest) + rpcHandler.registerPacketListener(RpcMessage.IgnoreFlightListStepRequest, ::onToggleFlightListRequest) } fun buildFlightListResponse(fbb: FlatBufferBuilder): Int = FlightListResponse.pack( @@ -40,11 +40,11 @@ class RPCFlightListHandler( } private fun onToggleFlightListRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - val req = messageHeader.message(ToggleFlightListStepRequest()) as ToggleFlightListStepRequest? + val req = messageHeader.message(IgnoreFlightListStepRequest()) as IgnoreFlightListStepRequest? ?: return val step = api.server.flightListManager.steps.find { it.id == req.stepId() } ?: error("invalid step id requested") - api.server.flightListManager.toggleStep(step) + api.server.flightListManager.ignoreStep(step, req.ignore()) val fbb = FlatBufferBuilder(32) val response = buildFlightListResponse(fbb) diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt index 4df063529c..77b3b37f5c 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt @@ -579,6 +579,10 @@ class HumanPoseManager(val server: VRServer?) { fun clearTrackersMounting(resetSourceName: String?) { skeleton.clearTrackersMounting(resetSourceName) + if (server != null) { + server.flightListManager.resetMountingCompleted = false; + server.configManager.saveConfig() + } } @get:ThreadSafe From 5e026ecb9244a6f7cdb2d7776fa4f8b572020334 Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Sat, 26 Jul 2025 04:45:59 +0200 Subject: [PATCH 16/34] Progress --- gui/src/components/Sidebar.tsx | 15 +++++++-------- gui/src/components/Toolbar.tsx | 1 - gui/src/index.scss | 12 ++++++------ 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/gui/src/components/Sidebar.tsx b/gui/src/components/Sidebar.tsx index 693c968b93..b6dd5b2c57 100644 --- a/gui/src/components/Sidebar.tsx +++ b/gui/src/components/Sidebar.tsx @@ -22,7 +22,7 @@ export function Sidebar() { const { completion } = useSessionFlightlist(); const [closed, setClosed] = useState(true); const [closing, setClosing] = useState(false); - const [userHeight, setUserHeight] = useState(0); + const [userHeight, setUserHeight] = useState(''); const { currentLocales } = useLocaleConfig(); const { useRPCPacket, sendRPCPacket } = useWebsocketAPI(); @@ -62,7 +62,8 @@ export function Sidebar() { useRPCPacket( RpcMessage.SkeletonConfigResponse, (data: SkeletonConfigResponseT) => { - if (data.userHeight) setUserHeight(data.userHeight); + if (data.userHeight) + setUserHeight(cmFormat.format((data.userHeight * 100) / 0.936)); } ); @@ -106,24 +107,22 @@ export function Sidebar() { >
- - {cmFormat.format((userHeight * 100) / 0.936)} - + {userHeight}
Disable rendering} > -
- +
+
{/* Date: Sat, 26 Jul 2025 04:46:34 +0200 Subject: [PATCH 17/34] Solarxr --- solarxr-protocol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solarxr-protocol b/solarxr-protocol index fbac1fabdd..2ba683cf16 160000 --- a/solarxr-protocol +++ b/solarxr-protocol @@ -1 +1 @@ -Subproject commit fbac1fabddcfbdb1ade03784b617e9029df93835 +Subproject commit 2ba683cf1620391cfcc65a0ec308dd7df5fcaf50 From 1e4e8c47344332599d6d14f0369d71734ac78dd3 Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Sat, 26 Jul 2025 06:51:57 +0200 Subject: [PATCH 18/34] Sync with main --- gui/src/hooks/reset-settings.ts | 3 --- solarxr-protocol | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/gui/src/hooks/reset-settings.ts b/gui/src/hooks/reset-settings.ts index 8b94e20fad..5593ca4999 100644 --- a/gui/src/hooks/reset-settings.ts +++ b/gui/src/hooks/reset-settings.ts @@ -9,7 +9,6 @@ import { useWebsocketAPI } from './websocket-api'; import { useEffect, useState } from 'react'; export interface ResetSettingsForm { - resetMountingFeet: boolean; armsMountingResetMode: number; yawResetSmoothTime: number; saveMountingReset: boolean; @@ -17,7 +16,6 @@ export interface ResetSettingsForm { } export const defaultResetSettings = { - resetMountingFeet: false, armsMountingResetMode: 0, yawResetSmoothTime: 0.0, saveMountingReset: false, @@ -26,7 +24,6 @@ export const defaultResetSettings = { export function loadResetSettings(resetSettingsForm: ResetSettingsForm) { const resetsSettings = new ResetsSettingsT(); - resetsSettings.resetMountingFeet = resetSettingsForm.resetMountingFeet; resetsSettings.armsMountingResetMode = resetSettingsForm.armsMountingResetMode; resetsSettings.yawResetSmoothTime = resetSettingsForm.yawResetSmoothTime; resetsSettings.saveMountingReset = resetSettingsForm.saveMountingReset; diff --git a/solarxr-protocol b/solarxr-protocol index 2ba683cf16..08beeb48a3 160000 --- a/solarxr-protocol +++ b/solarxr-protocol @@ -1 +1 @@ -Subproject commit 2ba683cf1620391cfcc65a0ec308dd7df5fcaf50 +Subproject commit 08beeb48a37158541ba14965ca5ee93db1b750b2 From faf645eb2ac9c30bb33f845b7f06cb8111bb042c Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Tue, 29 Jul 2025 19:33:22 +0200 Subject: [PATCH 19/34] Progress + Rename to tracking checklist --- gui/public/i18n/en/translation.ftl | 51 +-- gui/src/App.tsx | 8 +- gui/src/components/ClearMountingButton.tsx | 45 --- gui/src/components/Sidebar.tsx | 288 ++++++++------- .../flight-list/FlightListProvider.tsx | 15 - .../flight-list/FlightListSettingsModal.tsx | 111 ------ gui/src/components/home/Home.tsx | 4 +- gui/src/components/home/HomeSettingsModal.tsx | 77 +--- .../onboarding/pages/ConnectTracker.tsx | 17 +- .../settings/SettingsPageLayout.tsx | 2 +- .../components/settings/SettingsSidebar.tsx | 149 ++++---- .../settings/pages/HomeScreenSettings.tsx | 196 +++++++++++ gui/src/components/tracker/TrackersTable.tsx | 4 +- .../TrackingChecklist.tsx} | 97 +++-- .../TrackingChecklistModal.tsx | 34 ++ .../TrackingChecklistProvider.tsx | 19 + .../widgets/SkeletonVisualizerWidget.tsx | 136 ++++--- gui/src/hooks/bvh.ts | 53 +++ gui/src/hooks/config.ts | 2 + gui/src/hooks/pause-tracking.ts | 37 ++ gui/src/hooks/reset.ts | 39 +- gui/src/hooks/session-flightlist.ts | 172 --------- gui/src/hooks/tracking-checklist.ts | 186 ++++++++++ .../src/main/java/dev/slimevr/VRServer.kt | 6 +- .../dev/slimevr/config/FlightListConfig.kt | 2 +- .../main/java/dev/slimevr/config/VRConfig.kt | 2 +- .../slimevr/flightlist/FlightListManager.kt | 333 ------------------ .../dev/slimevr/protocol/rpc/RPCHandler.kt | 4 +- .../rpc/flightlist/RPCFlightListHandler.kt | 73 ---- .../RPCTrackingChecklistHandler.kt | 73 ++++ .../tracking/processor/HumanPoseManager.kt | 4 +- .../TrackingChecklistManager.kt | 326 +++++++++++++++++ solarxr-protocol | 2 +- 33 files changed, 1368 insertions(+), 1199 deletions(-) delete mode 100644 gui/src/components/ClearMountingButton.tsx delete mode 100644 gui/src/components/flight-list/FlightListProvider.tsx delete mode 100644 gui/src/components/flight-list/FlightListSettingsModal.tsx create mode 100644 gui/src/components/settings/pages/HomeScreenSettings.tsx rename gui/src/components/{flight-list/SessionFlightList.tsx => tracking-checklist/TrackingChecklist.tsx} (84%) create mode 100644 gui/src/components/tracking-checklist/TrackingChecklistModal.tsx create mode 100644 gui/src/components/tracking-checklist/TrackingChecklistProvider.tsx create mode 100644 gui/src/hooks/bvh.ts create mode 100644 gui/src/hooks/pause-tracking.ts delete mode 100644 gui/src/hooks/session-flightlist.ts create mode 100644 gui/src/hooks/tracking-checklist.ts delete mode 100644 server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt delete mode 100644 server/core/src/main/java/dev/slimevr/protocol/rpc/flightlist/RPCFlightListHandler.kt create mode 100644 server/core/src/main/java/dev/slimevr/protocol/rpc/trackingchecklist/RPCTrackingChecklistHandler.kt create mode 100644 server/core/src/main/java/dev/slimevr/trackingchecklist/TrackingChecklistManager.kt diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index 8a935e8be4..221e3dc67d 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -260,6 +260,7 @@ navbar-settings = Settings ## Biovision hierarchy recording bvh-start_recording = Record BVH +bvh-stop_recording = Save BVH recording bvh-recording = Recording... bvh-save_title = Save BVH recording @@ -452,6 +453,7 @@ mounting_selection_menu-close = Close ## Sidebar settings settings-sidebar-title = Settings settings-sidebar-general = General +settings-sidebar-steamvr = SteamVR settings-sidebar-tracker_mechanics = Tracker mechanics settings-sidebar-stay_aligned = Stay Aligned settings-sidebar-fk_settings = Tracking settings @@ -459,9 +461,12 @@ settings-sidebar-gesture_control = Gesture control settings-sidebar-interface = Interface settings-sidebar-osc_router = OSC router settings-sidebar-osc_trackers = VRChat OSC Trackers +settings-sidebar-osc_vmc = VMC settings-sidebar-utils = Utilities settings-sidebar-serial = Serial console settings-sidebar-appearance = Appearance +settings-sidebar-home = Home Screen +settings-sidebar-checklist = Tracking checklist settings-sidebar-notifications = Notifications settings-sidebar-behavior = Behavior settings-sidebar-firmware-tool = DIY Firmware Tool @@ -1548,23 +1553,23 @@ error_collection_modal-confirm = I agree error_collection_modal-cancel = I don't want to -flight_list-MOUNTING_CALIBRATION = Perform a mounting calibration -flight_list-FULL_RESET = Perform a full Reset -flight_list-FULL_RESET-desc = Some Trackers need a reset to be performed -flight_list-STEAMVR_DISCONNECTED = SteamVR not running -flight_list-STEAMVR_DISCONNECTED-desc = SteamVR is not running. Are you using it for vr? -flight_list-STEAMVR_DISCONNECTED-open = Launch SteamVR -flight_list-TRACKERS_REST_CALIBRATION = Calibrate your trackers -flight_list-TRACKERS_REST_CALIBRATION-desc = You didnt perform the tracker calibration. Please let your slimes, highlited in yellow, rest on a static surface for a few secconds -flight_list-TRACKER_ERROR = Trackers with Errors -flight_list-TRACKER_ERROR-desc = Some of your trackers have an error. Please restart the tracker. -flight_list-VRCHAT_SETTINGS = Configure VRChat settings -flight_list-VRCHAT_SETTINGS-desc = You have misconfigured VRchat Settings! This can impact your tracking experience. -flight_list-VRCHAT_SETTINGS-open = Go to VRChat Warnings -flight_list-UNASSIGNED_HMD = VR Headset not assigned to Head -flight_list-UNASSIGNED_HMD-desc = The VR headset should be assigned as a head tracker. -flight_list-NETWORK_PROFILE_PUBLIC = Change your network profile -flight_list-NETWORK_PROFILE_PUBLIC-desc = {$count -> +tracking_checklist-MOUNTING_CALIBRATION = Perform a mounting calibration +tracking_checklist-FULL_RESET = Perform a full Reset +tracking_checklist-FULL_RESET-desc = Some Trackers need a reset to be performed +tracking_checklist-STEAMVR_DISCONNECTED = SteamVR not running +tracking_checklist-STEAMVR_DISCONNECTED-desc = SteamVR is not running. Are you using it for vr? +tracking_checklist-STEAMVR_DISCONNECTED-open = Launch SteamVR +tracking_checklist-TRACKERS_REST_CALIBRATION = Calibrate your trackers +tracking_checklist-TRACKERS_REST_CALIBRATION-desc = You didnt perform the tracker calibration. Please let your slimes, highlited in yellow, rest on a static surface for a few secconds +tracking_checklist-TRACKER_ERROR = Trackers with Errors +tracking_checklist-TRACKER_ERROR-desc = Some of your trackers have an error. Please restart the tracker. +tracking_checklist-VRCHAT_SETTINGS = Configure VRChat settings +tracking_checklist-VRCHAT_SETTINGS-desc = You have misconfigured VRchat Settings! This can impact your tracking experience. +tracking_checklist-VRCHAT_SETTINGS-open = Go to VRChat Warnings +tracking_checklist-UNASSIGNED_HMD = VR Headset not assigned to Head +tracking_checklist-UNASSIGNED_HMD-desc = The VR headset should be assigned as a head tracker. +tracking_checklist-NETWORK_PROFILE_PUBLIC = Change your network profile +tracking_checklist-NETWORK_PROFILE_PUBLIC-desc = {$count -> [one] Your network profile is currently set to Public ({$adapters}). This is not recommended for SlimeVR to function properly. See how to fix it here. @@ -1573,9 +1578,11 @@ flight_list-NETWORK_PROFILE_PUBLIC-desc = {$count -> This is not recommended for SlimeVR to function properly. See how to fix it here. } -flight_list-NETWORK_PROFILE_PUBLIC-open = Open Control Panel -flight_list-STAY_ALIGNED_CONFIGURED = Configure Stay Aligned -flight_list-STAY_ALIGNED_CONFIGURED-desc = Record the stay aligned poses for an improved imu drift -flight_list-STAY_ALIGNED_CONFIGURED-open = Open Stay Aligned Wizard +tracking_checklist-NETWORK_PROFILE_PUBLIC-open = Open Control Panel +tracking_checklist-STAY_ALIGNED_CONFIGURED = Configure Stay Aligned +tracking_checklist-STAY_ALIGNED_CONFIGURED-desc = Record the stay aligned poses for an improved imu drift +tracking_checklist-STAY_ALIGNED_CONFIGURED-open = Open Stay Aligned Wizard -flight_list-ignore = Ignore +tracking_checklist-ignore = Ignore + +sidebar-preview-mocap_mode_soon = Mocap Mode (Soonâ„¢) diff --git a/gui/src/App.tsx b/gui/src/App.tsx index d5ef02e183..aeb03a17b5 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -60,7 +60,8 @@ 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'; -import { FlightListProvider } from './components/flight-list/FlightListProvider'; +import { TrackingChecklistProvider } from './components/tracking-checklist/TrackingChecklistProvider'; +import { HomeScreenSettings } from './components/settings/pages/HomeScreenSettings'; export const GH_REPO = 'SlimeVR/SlimeVR-Server'; export const VersionContext = createContext(''); @@ -135,6 +136,7 @@ function Layout() { } /> } /> } /> + } /> } /> - +
@@ -302,7 +304,7 @@ export default function App() { {websocketAPI.isConnected && }
-
+
diff --git a/gui/src/components/ClearMountingButton.tsx b/gui/src/components/ClearMountingButton.tsx deleted file mode 100644 index 74c8279850..0000000000 --- a/gui/src/components/ClearMountingButton.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Localized } from '@fluent/react'; -import { ClearMountingResetRequestT, RpcMessage } from 'solarxr-protocol'; -import { useWebsocketAPI } from '@/hooks/websocket-api'; -import { TrashIcon } from './commons/icon/TrashIcon'; -import { Quaternion } from 'three'; -import { QuaternionFromQuatT, similarQuaternions } from '@/maths/quaternion'; -import { useMemo } from 'react'; -import { Button } from './commons/Button'; -import { useAtomValue } from 'jotai'; -import { assignedTrackersAtom } from '@/store/app-store'; - -const _q = new Quaternion(); - -export function ClearMountingButton() { - const { sendRPCPacket } = useWebsocketAPI(); - const assignedTrackers = useAtomValue(assignedTrackersAtom); - - const trackerWithMounting = useMemo( - () => - assignedTrackers.some( - (d) => - !similarQuaternions( - QuaternionFromQuatT(d?.tracker.info?.mountingResetOrientation), - _q - ) - ), - [assignedTrackers] - ); - - const clearMounting = () => { - const record = new ClearMountingResetRequestT(); - sendRPCPacket(RpcMessage.ClearMountingResetRequest, record); - }; - - return ( - - - - ); -} diff --git a/gui/src/components/Sidebar.tsx b/gui/src/components/Sidebar.tsx index b6dd5b2c57..e0047351aa 100644 --- a/gui/src/components/Sidebar.tsx +++ b/gui/src/components/Sidebar.tsx @@ -1,5 +1,5 @@ -import { useSessionFlightlist } from '@/hooks/session-flightlist'; -import { SessionFlightList } from './flight-list/SessionFlightList'; +import { useTrackingChecklist } from '@/hooks/tracking-checklist'; +import { TrackingChecklist } from './tracking-checklist/TrackingChecklist'; import { SkeletonVisualizerWidget } from './widgets/SkeletonVisualizerWidget'; import { useEffect, useLayoutEffect, useMemo, useState } from 'react'; import classNames from 'classnames'; @@ -17,38 +17,23 @@ import { RecordIcon } from './commons/icon/RecordIcon'; import { PauseIcon } from './commons/icon/PauseIcon'; import { HumanIcon } from './commons/icon/HumanIcon'; import { EyeIcon } from './commons/icon/EyeIcon'; +import { useConfig } from '@/hooks/config'; +import { useBHV } from '@/hooks/bvh'; +import { usePauseTracking } from '@/hooks/pause-tracking'; +import { PlayIcon } from './commons/icon/PlayIcon'; -export function Sidebar() { - const { completion } = useSessionFlightlist(); - const [closed, setClosed] = useState(true); - const [closing, setClosing] = useState(false); +function PreviewSection({ closed }: { closed: boolean }) { + const { config, setConfig } = useConfig(); + const [disabledRender, setDisabledRender] = useState(config?.skeletonPreview); const [userHeight, setUserHeight] = useState(''); - const { currentLocales } = useLocaleConfig(); const { useRPCPacket, sendRPCPacket } = useWebsocketAPI(); - - const closedHight = '90px'; - const flightlistSize = closed ? closedHight : 'calc(100% - 16px)'; - const previewSize = closed ? `calc(100% - ${closedHight} - 24px)` : '0%'; - - const toggleClosed = () => setClosed((closed) => !closed); - - useLayoutEffect(() => { - setClosing(true); - const ref = setTimeout(() => setClosing(false), 1000); - return () => { - clearTimeout(ref); - setClosing(false); - }; - }, [closed]); - - useEffect(() => { - if (completion === 'complete') { - setClosed(true); - } else if (completion === 'incomplete') { - setClosed(false); - } - }, [completion]); + const { + state: bvhState, + toggle: toggleBVH, + available: bvhAvailable, + } = useBHV(); + const { paused, toggle: toggleTracking } = usePauseTracking(); const { cmFormat } = useMemo(() => { const cmFormat = Intl.NumberFormat(currentLocales, { @@ -74,119 +59,184 @@ export function Sidebar() { ); }, []); + const toggleRender = () => { + setConfig({ skeletonPreview: disabledRender }); + }; + + useLayoutEffect(() => { + // need useLayoutEffect to make sure that the state is corect before the first render of the skeleton + setDisabledRender(!config?.skeletonPreview); + }, [config]); + return ( - <> -
- -
-
+ toggleRender()} + onInit={(context) => { + context.addView({ + left: 0, + bottom: 0, + width: 1, + height: 1, + position: new Vector3(3, 2.5, -3), + onHeightChange(v, newHeight) { + v.controls.target.set(0, newHeight / 2.2, 0.1); + const scale = Math.max(1, newHeight) / 1.3; + v.camera.zoom = 1 / scale; + }, + }); + }} + > + + } >
+ {userHeight} +
+
+ Disable rendering} + > +
toggleRender()} + > + +
+
+
+
+ {bvhAvailable && ( + + } + preferedDirection="top" + > +
toggleBVH()} + > + {bvhState === 'idle' && } + {bvhState !== 'idle' && ( +
+ )} +
+ + )} + } + preferedDirection="top" >
toggleTracking()} > - {userHeight} + {!paused && } + {paused && }
Disable rendering} + content={ + + } + preferedDirection="top" > -
- +
+
- {/* Enable Rendering} - > -
- -
-
*/} -
-
- Record BVH - } - preferedDirection="top" - > -
- -
-
- - Pause tracking - - } - preferedDirection="top" - > -
- -
-
- Mocap Mode - } - preferedDirection="top" - > -
- -
-
-
-
- { - context.addView({ - left: 0, - bottom: 0, - width: 1, - height: 1, - position: new Vector3(3, 2.5, -3), - onHeightChange(v, newHeight) { - v.controls.target.set(0, newHeight / 2.2, 0.1); - const scale = Math.max(1, newHeight) / 1.3; - v.camera.zoom = 1 / scale; - }, - }); - }} - >
+
+ ); +} + +export function Sidebar() { + const { completion } = useTrackingChecklist(); + const [closed, setClosed] = useState(true); + const [closing, setClosing] = useState(false); + + const closedHight = '90px'; + const checklistSize = closed ? closedHight : 'calc(100% - 16px)'; + const previewSize = closed ? `calc(100% - ${closedHight} - 24px)` : '0%'; + + const toggleClosed = () => setClosed((closed) => !closed); + + useLayoutEffect(() => { + setClosing(true); + const ref = setTimeout(() => setClosing(false), 1000); + return () => { + clearTimeout(ref); + setClosing(false); + }; + }, [closed]); + + useEffect(() => { + if (completion === 'complete') { + setClosed(true); + } else if (completion === 'incomplete') { + setClosed(false); + } + }, [completion]); + + return ( + <> +
+ +
+
+ +
); } diff --git a/gui/src/components/flight-list/FlightListProvider.tsx b/gui/src/components/flight-list/FlightListProvider.tsx deleted file mode 100644 index 7263b4db50..0000000000 --- a/gui/src/components/flight-list/FlightListProvider.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { - FlightListContextC, - provideSessionFlightlist, -} from '@/hooks/session-flightlist'; -import { ReactNode } from 'react'; - -export function FlightListProvider({ children }: { children: ReactNode }) { - const context = provideSessionFlightlist(); - - return ( - - {children} - - ); -} diff --git a/gui/src/components/flight-list/FlightListSettingsModal.tsx b/gui/src/components/flight-list/FlightListSettingsModal.tsx deleted file mode 100644 index e35a919a60..0000000000 --- a/gui/src/components/flight-list/FlightListSettingsModal.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { Dispatch, SetStateAction, useEffect } from 'react'; -import { BaseModal } from '@/components/commons/BaseModal'; -import { - flightlistIdtoLabel, - useSessionFlightlist, -} from '@/hooks/session-flightlist'; -import { Typography } from '@/components/commons/Typography'; -import { Button } from '@/components/commons/Button'; -import { CheckBox } from '@/components/commons/Checkbox'; -import { useForm } from 'react-hook-form'; -import { useLocalization } from '@fluent/react'; -import { FlightListStepId } from 'solarxr-protocol'; - -type StepsForm = { steps: Record }; -export function FlightListSettingsModal({ - open, -}: { - open: [boolean, Dispatch>]; -}) { - const { l10n } = useLocalization(); - const { ignoredSteps, steps, ignoreStep } = useSessionFlightlist(); - - const { control, reset, handleSubmit, watch, getValues } = useForm( - { - defaultValues: { - steps: steps.reduce( - (curr, { id }) => ({ [id]: !ignoredSteps.includes(id), ...curr }), - {} - ), - }, - mode: 'onChange', - } - ); - - useEffect(() => { - reset({ - steps: steps.reduce( - (curr, { id }) => ({ [id]: !ignoredSteps.includes(id), ...curr }), - {} - ), - }); - }, [ignoredSteps]); - - const onSubmit = (values: StepsForm) => { - for (const [id, value] of Object.entries(values.steps)) { - const stepId = +id; - if (!stepId) continue; - - // doing it this way prevents calling ignore step for every step. - // that prevent sending a packet for steps that didnt change - if (!value && !ignoredSteps.includes(stepId)) { - ignoreStep(stepId, true); - } - - if (value && ignoredSteps.includes(stepId)) { - ignoreStep(stepId, false); - } - } - }; - - return ( - { - open[1](false); - }} - > -
- - Tracking Checklist Settings - -
-
- Active Steps - - List all the steps that will show in the tracking checklist. You - can either disable or enable ignorable steps - -
-
- {steps - .filter((step) => step.enabled) - .map((step) => ( -
- -
- ))} -
-
- -
- -
-
-
- ); -} diff --git a/gui/src/components/home/Home.tsx b/gui/src/components/home/Home.tsx index dd397ef2a5..988d56743b 100644 --- a/gui/src/components/home/Home.tsx +++ b/gui/src/components/home/Home.tsx @@ -8,13 +8,13 @@ import { TrackersTable } from '@/components/tracker/TrackersTable'; import { HeadsetIcon } from '@/components/commons/icon/HeadsetIcon'; import { useAtomValue } from 'jotai'; import { flatTrackersAtom } from '@/store/app-store'; -import { useSessionFlightlist } from '@/hooks/session-flightlist'; +import { useTrackingChecklist } from '@/hooks/tracking-checklist'; export function Home() { const { l10n } = useLocalization(); const { config } = useConfig(); const trackers = useAtomValue(flatTrackersAtom); - const { hightlightedTrackers } = useSessionFlightlist(); + const { hightlightedTrackers } = useTrackingChecklist(); const navigate = useNavigate(); const sendToSettings = (tracker: TrackerDataT) => { diff --git a/gui/src/components/home/HomeSettingsModal.tsx b/gui/src/components/home/HomeSettingsModal.tsx index 9c551b1aa4..b6a1ff80de 100644 --- a/gui/src/components/home/HomeSettingsModal.tsx +++ b/gui/src/components/home/HomeSettingsModal.tsx @@ -1,56 +1,14 @@ -import { Dispatch, ReactNode, SetStateAction } from 'react'; +import { Dispatch, SetStateAction } from 'react'; import { BaseModal } from '@/components/commons/BaseModal'; import { Typography } from '@/components/commons/Typography'; import { Button } from '@/components/commons/Button'; -import classNames from 'classnames'; -import { Config, useConfig } from '@/hooks/config'; - -export function LayoutSelector({ - children, - name, - active = false, - onClick, -}: { - children: ReactNode; - name: string; - active: boolean; - onClick: () => void; -}) { - return ( -
-
- -
-
- {children} -
- ); -} +import { HomeLayoutSettings } from '@/components/settings/pages/HomeScreenSettings'; export function HomeSettingsModal({ open, }: { open: [boolean, Dispatch>]; }) { - const { config, setConfig } = useConfig(); - - const setLayout = (layout: Config['homeLayout']) => - setConfig({ homeLayout: layout }); - return (
Home Page Settings -
- Trackers list layout -
- setLayout('default')} - > -
-
-
-
-
-
-
- setLayout('table')} - > -
-
-
-
-
-
-
-
-
- +
@@ -121,10 +120,10 @@ const stepContentLookup: Record<
); }, - [FlightListStepId.FULL_RESET]: () => { + [TrackingChecklistStepId.FULL_RESET]: () => { return (
- +
); }, - [FlightListStepId.STEAMVR_DISCONNECTED]: (step, { toggle }) => { + [TrackingChecklistStepId.STEAMVR_DISCONNECTED]: (step, { toggle }) => { return ( <>
- +
{step.ignorable && ( @@ -194,19 +193,21 @@ const stepContentLookup: Record< ); }, - [FlightListStepId.TRACKER_ERROR]: () => { - return ; + [TrackingChecklistStepId.TRACKER_ERROR]: () => { + return ; }, - [FlightListStepId.UNASSIGNED_HMD]: () => { - return ; + [TrackingChecklistStepId.UNASSIGNED_HMD]: () => { + return ( + + ); }, - [FlightListStepId.NETWORK_PROFILE_PUBLIC]: (step, { toggle }) => { - const data = step.extraData as FlightListPublicNetworksT | null; + [TrackingChecklistStepId.NETWORK_PROFILE_PUBLIC]: (step, { toggle }) => { + const data = step.extraData as TrackingChecklistPublicNetworksT | null; return ( <>
{step.ignorable && ( @@ -239,20 +240,20 @@ const stepContentLookup: Record< ); }, - [FlightListStepId.VRCHAT_SETTINGS]: (step, { toggle }) => { + [TrackingChecklistStepId.VRCHAT_SETTINGS]: (step, { toggle }) => { return ( <>
- +
{step.ignorable && ( @@ -262,7 +263,7 @@ const stepContentLookup: Record< ); }, - [FlightListStepId.MOUNTING_CALIBRATION]: (step, { toggle }) => { + [TrackingChecklistStepId.MOUNTING_CALIBRATION]: (step, { toggle }) => { return (
@@ -278,7 +279,7 @@ const stepContentLookup: Record< {step.ignorable && ( @@ -287,21 +288,21 @@ const stepContentLookup: Record<
); }, - [FlightListStepId.STAY_ALIGNED_CONFIGURED]: (step, { toggle }) => { + [TrackingChecklistStepId.STAY_ALIGNED_CONFIGURED]: (step, { toggle }) => { return ( <>
- +
{step.ignorable && ( @@ -313,7 +314,7 @@ const stepContentLookup: Record< }, }; -export function SessionFlightList({ +export function TrackingChecklist({ closed, closing, toggleClosed, @@ -322,7 +323,7 @@ export function SessionFlightList({ closing: boolean; toggleClosed: () => void; }) { - const context = useSessionFlightlist(); + const context = useTrackingChecklist(); const { visibleSteps, progress, completion, warnings } = context; const slimeState = useMemo(() => { @@ -451,9 +452,7 @@ export function SessionFlightList({
- + ); } diff --git a/gui/src/components/tracking-checklist/TrackingChecklistModal.tsx b/gui/src/components/tracking-checklist/TrackingChecklistModal.tsx new file mode 100644 index 0000000000..6f69d57413 --- /dev/null +++ b/gui/src/components/tracking-checklist/TrackingChecklistModal.tsx @@ -0,0 +1,34 @@ +import { Dispatch, SetStateAction } from 'react'; +import { BaseModal } from '@/components/commons/BaseModal'; +import { Typography } from '@/components/commons/Typography'; +import { Button } from '@/components/commons/Button'; +import { TrackingChecklistSettings } from '@/components/settings/pages/HomeScreenSettings'; + +export function TrackingChecklistModal({ + open, +}: { + open: [boolean, Dispatch>]; +}) { + return ( + { + open[1](false); + }} + > +
+ + Tracking Checklist Settings + + +
+ +
+
+
+ ); +} diff --git a/gui/src/components/tracking-checklist/TrackingChecklistProvider.tsx b/gui/src/components/tracking-checklist/TrackingChecklistProvider.tsx new file mode 100644 index 0000000000..0f65226dc4 --- /dev/null +++ b/gui/src/components/tracking-checklist/TrackingChecklistProvider.tsx @@ -0,0 +1,19 @@ +import { + TrackingChecklistContectC, + provideTrackingChecklist, +} from '@/hooks/tracking-checklist'; +import { ReactNode } from 'react'; + +export function TrackingChecklistProvider({ + children, +}: { + children: ReactNode; +}) { + const context = provideTrackingChecklist(); + + return ( + + {children} + + ); +} diff --git a/gui/src/components/widgets/SkeletonVisualizerWidget.tsx b/gui/src/components/widgets/SkeletonVisualizerWidget.tsx index 1778f3b651..9d775da47c 100644 --- a/gui/src/components/widgets/SkeletonVisualizerWidget.tsx +++ b/gui/src/components/widgets/SkeletonVisualizerWidget.tsx @@ -10,7 +10,6 @@ import * as THREE from 'three'; import { BodyPart, BoneT } from 'solarxr-protocol'; import { QuaternionFromQuatT, isIdentity } from '@/maths/quaternion'; import classNames from 'classnames'; -import { Button } from '@/components/commons/Button'; import { useLocalization } from '@fluent/react'; import { ErrorBoundary } from 'react-error-boundary'; import { Typography } from '@/components/commons/Typography'; @@ -18,6 +17,7 @@ import { useAtomValue } from 'jotai'; import { bonesAtom } from '@/store/app-store'; import { useConfig } from '@/hooks/config'; import { Tween } from '@tweenjs/tween.js'; +import { EyeIcon } from '@/components/commons/icon/EyeIcon'; const GROUND_COLOR = '#2c2c6b'; @@ -32,61 +32,6 @@ const Y_PARTS = [ BodyPart.LEFT_LOWER_LEG, ]; -interface SkeletonVisualizerWidgetProps { - height?: number | string; - maxHeight?: number | string; -} - -export function ToggleableSkeletonVisualizerWidget({ - height, - maxHeight, -}: SkeletonVisualizerWidgetProps) { - const { l10n } = useLocalization(); - const [enabled, setEnabled] = useState(false); - - useEffect(() => { - const state = localStorage.getItem('skeletonModelPreview'); - if (state) setEnabled(state === 'true'); - }, []); - - return ( - <> - {!enabled && ( - - )} - {enabled && ( -
- -
- -
-
- )} - - ); -} - export type SkeletonPreviewView = { left: number; bottom: number; @@ -110,7 +55,7 @@ function initializePreview( const resolution = new THREE.Vector2(canvas.clientWidth, canvas.clientHeight); const scene = new THREE.Scene(); - const renderer = new THREE.WebGLRenderer({ + let renderer: THREE.WebGLRenderer | null = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true, @@ -195,7 +140,7 @@ function initializePreview( const render = (delta: number) => { views.forEach((v) => { - if (v.hidden) return; + if (v.hidden || !renderer) return; v.controls.update(delta); const left = Math.floor(resolution.x * v.left); @@ -251,6 +196,7 @@ function initializePreview( resize: (width: number, height: number) => { resolution.set(width, height); skeletonHelper.resolution.copy(resolution); + if (!renderer) return; renderer.setSize(width, height); }, setFrameInterval: (interval: number) => { @@ -276,9 +222,11 @@ function initializePreview( } }, destroy: () => { + cancelAnimationFrame(animationFrameId); skeletonHelper.dispose(); + if (!renderer) return; renderer.dispose(); - cancelAnimationFrame(animationFrameId); + renderer = null; // Very important for js to free the WebGL context. dispose does not to it alone }, addView: ({ left, @@ -297,6 +245,8 @@ function initializePreview( hidden?: boolean; onHeightChange: (view: SkeletonPreviewView, newHeight: number) => void; }) => { + if (!renderer) return; + const camera = new THREE.PerspectiveCamera( 20, resolution.width / resolution.height, @@ -344,8 +294,10 @@ type PreviewContext = ReturnType; function SkeletonVisualizer({ onInit, + disabled = false, }: { onInit: (context: PreviewContext) => void; + disabled?: boolean; }) { const { config } = useConfig(); @@ -362,15 +314,15 @@ function SkeletonVisualizer({ useEffect(() => { if (bones.size === 0) return; const context = previewContext.current; - if (!context) return; + if (!context || disabled) return; context.rebuildSkeleton(createChildren(bones, BoneKind.root), bones); - }, [bones.size]); + }, [bones.size, disabled]); useEffect(() => { const context = previewContext.current; - if (!context) return; + if (!context || disabled) return; context.updatesBones(bones); - }, [bones]); + }, [bones, disabled]); const onResize = (e: ResizeObserverEntry) => { const context = previewContext.current; @@ -393,6 +345,7 @@ function SkeletonVisualizer({ }; useLayoutEffect(() => { + if (disabled) return; if (!canvasRef.current || !containerRef.current) throw 'invalid state - no canvas or container'; resizeObserver.current.observe(containerRef.current); @@ -416,11 +369,12 @@ function SkeletonVisualizer({ if (!previewContext.current || !containerRef.current) return; resizeObserver.current.unobserve(containerRef.current); previewContext.current.destroy(); + previewContext.current = null; containerRef.current.removeEventListener('mouseenter', onEnter); containerRef.current.removeEventListener('mouseleave', onLeave); }; - }, []); + }, [disabled]); return (
@@ -444,20 +398,56 @@ export function SkeletonVisualizerWidget({ }, }); }, + disabled = false, + toggleDisabled, }: { onInit?: (context: PreviewContext) => void; + disabled?: boolean; + toggleDisabled?: () => void; }) { const { l10n } = useLocalization(); + const [error, setError] = useState(false); return ( - - {l10n.getString('tips-failed_webgl')} - - } - > - - +
+
+ setError(true)} fallback={<>}> + + +
+
+
toggleDisabled?.()} + > + + Rendering disabled +
+
+
+
+ + {l10n.getString('tips-failed_webgl')} + +
+
+
); } diff --git a/gui/src/hooks/bvh.ts b/gui/src/hooks/bvh.ts new file mode 100644 index 0000000000..bf49642303 --- /dev/null +++ b/gui/src/hooks/bvh.ts @@ -0,0 +1,53 @@ +import { useLocalization } from '@fluent/react'; +import { isTauri } from '@tauri-apps/api/core'; +import { useEffect, useState } from 'react'; +import { RecordBVHRequestT, RecordBVHStatusT, RpcMessage } from 'solarxr-protocol'; +import { useWebsocketAPI } from './websocket-api'; +import { useConfig } from './config'; +import { save } from '@tauri-apps/plugin-dialog'; + +export function useBHV() { + const { config } = useConfig(); + const { useRPCPacket, sendRPCPacket } = useWebsocketAPI(); + const [state, setState] = useState<'idle' | 'recording' | 'saving'>('idle'); + const { l10n } = useLocalization(); + + useEffect(() => { + sendRPCPacket(RpcMessage.RecordBVHStatusRequest, new RecordBVHRequestT()); + }, []); + + const toggle = async () => { + const record = new RecordBVHRequestT(state === 'recording'); + + if (isTauri() && state === 'idle') { + if (config?.bvhDirectory) { + record.path = config.bvhDirectory; + } else { + setState('saving'); + record.path = await save({ + title: l10n.getString('bvh-save_title'), + filters: [ + { + name: 'BVH', + extensions: ['bvh'], + }, + ], + defaultPath: 'bvh-recording.bvh', + }); + setState('idle'); + } + } + + sendRPCPacket(RpcMessage.RecordBVHRequest, record); + }; + + useRPCPacket(RpcMessage.RecordBVHStatus, (data: RecordBVHStatusT) => { + setState(data.recording ? 'recording' : 'idle'); + }); + + return { + available: typeof __ANDROID__ === 'undefined' || !__ANDROID__?.isThere(), + state, + toggle, + }; +} diff --git a/gui/src/hooks/config.ts b/gui/src/hooks/config.ts index 4eb89c07fe..4b2ad485a3 100644 --- a/gui/src/hooks/config.ts +++ b/gui/src/hooks/config.ts @@ -47,6 +47,7 @@ export interface Config { vrcMutedWarnings: string[]; bvhDirectory: string | null; homeLayout: 'default' | 'table'; + skeletonPreview: boolean; } export interface ConfigContext { @@ -77,6 +78,7 @@ export const defaultConfig: Config = { devSettings: defaultDevSettings, bvhDirectory: null, homeLayout: 'default', + skeletonPreview: true, }; interface CrossStorage { diff --git a/gui/src/hooks/pause-tracking.ts b/gui/src/hooks/pause-tracking.ts new file mode 100644 index 0000000000..981929552a --- /dev/null +++ b/gui/src/hooks/pause-tracking.ts @@ -0,0 +1,37 @@ +import { useEffect, useState } from 'react'; +import { useWebsocketAPI } from './websocket-api'; +import { + RpcMessage, + SetPauseTrackingRequestT, + TrackingPauseStateRequestT, + TrackingPauseStateResponseT, +} from 'solarxr-protocol'; + +export function usePauseTracking() { + const { useRPCPacket, sendRPCPacket } = useWebsocketAPI(); + const [paused, setPaused] = useState(false); + + const toggle = () => { + const pause = new SetPauseTrackingRequestT(!paused); + sendRPCPacket(RpcMessage.SetPauseTrackingRequest, pause); + }; + + useRPCPacket( + RpcMessage.TrackingPauseStateResponse, + (data: TrackingPauseStateResponseT) => { + setPaused(data.trackingPaused); + } + ); + + useEffect(() => { + sendRPCPacket( + RpcMessage.TrackingPauseStateRequest, + new TrackingPauseStateRequestT() + ); + }, []); + + return { + toggle, + paused, + }; +} diff --git a/gui/src/hooks/reset.ts b/gui/src/hooks/reset.ts index 9573756f0f..a98bc5ee4a 100644 --- a/gui/src/hooks/reset.ts +++ b/gui/src/hooks/reset.ts @@ -1,18 +1,15 @@ -import { - playSoundOnResetEnded, - playSoundOnResetStarted, -} from '@/sounds/sounds'; +import { playSoundOnResetEnded, playSoundOnResetStarted } from '@/sounds/sounds'; import { useEffect, useMemo, useRef, useState } from 'react'; import { - FlightListStepId, ResetRequestT, ResetResponseT, ResetStatus, ResetType, RpcMessage, + TrackingChecklistStepId, } from 'solarxr-protocol'; import { useConfig } from './config'; -import { useSessionFlightlist } from './session-flightlist'; +import { useTrackingChecklist } from './tracking-checklist'; import { useWebsocketAPI } from './websocket-api'; import { useCountdown } from './countdown'; @@ -24,12 +21,12 @@ export function useReset(type: ResetType, onReseted?: () => void) { const finishedTimeoutRef = useRef(-1); const [status, setStatus] = useState('idle'); - const { visibleSteps } = useSessionFlightlist(); + const { visibleSteps } = useTrackingChecklist(); const needsFullReset = useMemo( () => type == ResetType.Mounting && visibleSteps.some( - (step) => step.id === FlightListStepId.FULL_RESET && !step.valid + (step) => step.id === TrackingChecklistStepId.FULL_RESET && !step.valid ), [visibleSteps, type] ); @@ -55,8 +52,7 @@ export function useReset(type: ResetType, onReseted?: () => void) { // If a timer was already running / clear it abortCountdown(); - if (finishedTimeoutRef.current !== -1) - clearTimeout(finishedTimeoutRef.current); + if (finishedTimeoutRef.current !== -1) clearTimeout(finishedTimeoutRef.current); // After 2s go back to idle state finishedTimeoutRef.current = setTimeout(() => { @@ -74,8 +70,7 @@ export function useReset(type: ResetType, onReseted?: () => void) { const maybePlaySoundOnResetStart = () => { if (!config?.feedbackSound) return; - if (type !== ResetType.Yaw) - playSoundOnResetStarted(config?.feedbackSoundVolume); + if (type !== ResetType.Yaw) playSoundOnResetStarted(config?.feedbackSoundVolume); }; const triggerReset = () => { @@ -86,23 +81,19 @@ export function useReset(type: ResetType, onReseted?: () => void) { useEffect(() => { return () => { - if (finishedTimeoutRef.current !== -1) - clearTimeout(finishedTimeoutRef.current); + if (finishedTimeoutRef.current !== -1) clearTimeout(finishedTimeoutRef.current); }; }, []); - useRPCPacket( - RpcMessage.ResetResponse, - ({ status, resetType }: ResetResponseT) => { - if (resetType !== type) return; - switch (status) { - case ResetStatus.FINISHED: { - onResetFinished(); - break; - } + useRPCPacket(RpcMessage.ResetResponse, ({ status, resetType }: ResetResponseT) => { + if (resetType !== type) return; + switch (status) { + case ResetStatus.FINISHED: { + onResetFinished(); + break; } } - ); + }); const name = useMemo(() => { switch (type) { diff --git a/gui/src/hooks/session-flightlist.ts b/gui/src/hooks/session-flightlist.ts deleted file mode 100644 index f704e2c48a..0000000000 --- a/gui/src/hooks/session-flightlist.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { - FlightListRequestT, - FlightListResponseT, - FlightListStepId, - FlightListStepT, - FlightListStepVisibility, - IgnoreFlightListStepRequestT, - RpcMessage, -} from 'solarxr-protocol'; -import { useWebsocketAPI } from './websocket-api'; -import { createContext, useContext, useEffect, useMemo, useState } from 'react'; - -export const flightlistIdtoLabel: Record = { - [FlightListStepId.UNKNOWN]: '', - [FlightListStepId.TRACKERS_REST_CALIBRATION]: 'flight_list-TRACKERS_REST_CALIBRATION', - [FlightListStepId.FULL_RESET]: 'flight_list-FULL_RESET', - [FlightListStepId.VRCHAT_SETTINGS]: 'flight_list-VRCHAT_SETTINGS', - [FlightListStepId.STEAMVR_DISCONNECTED]: 'flight_list-STEAMVR_DISCONNECTED', - [FlightListStepId.UNASSIGNED_HMD]: 'flight_list-UNASSIGNED_HMD', - [FlightListStepId.TRACKER_ERROR]: 'flight_list-TRACKER_ERROR', - [FlightListStepId.NETWORK_PROFILE_PUBLIC]: 'flight_list-NETWORK_PROFILE_PUBLIC', - [FlightListStepId.MOUNTING_CALIBRATION]: 'flight_list-MOUNTING_CALIBRATION', - [FlightListStepId.STAY_ALIGNED_CONFIGURED]: 'flight_list-STAY_ALIGNED_CONFIGURED', -}; - -export type FlightListStepStatus = 'complete' | 'skipped' | 'blocked' | 'invalid'; -export type FlightListStep = FlightListStepT & { - status: FlightListStepStatus; - firstRequired: boolean; -}; - -const stepVisibility = ({ visibility, status, firstRequired }: FlightListStep) => - firstRequired || - visibility === FlightListStepVisibility.ALWAYS || - (visibility === FlightListStepVisibility.WHEN_INVALID && status != 'complete'); - -const createStep = ( - steps: FlightListStepT[], - step: FlightListStepT, - index: number -): FlightListStep => { - const previousSteps = steps.slice(0, index); - const previousBlocked = previousSteps.some( - ({ valid, optional }) => !valid && !optional - ); - - let status: FlightListStepStatus = 'complete'; - if (previousBlocked && !step.valid) status = 'blocked'; - if (!previousBlocked && !step.valid) status = 'invalid'; - - const firstRequiredIndex = steps.findIndex( - (s, index) => !s.valid || (index === steps.length - 1 && !s.valid) - ); - - const skipped = - steps.find( - (s, sIndex) => - (sIndex > index && s.valid && !s.optional) || sIndex === steps.length - 1 - ) || index === steps.length - 1; - if (!step.valid && step.optional && skipped) status = 'skipped'; - - return { - ...step, - status, - firstRequired: - firstRequiredIndex === index || - (firstRequiredIndex === -1 && index === steps.length - 1 && !step.valid), - pack: () => 0, - }; -}; - -export type SessionFlightListContext = ReturnType; -export type Steps = { - steps: FlightListStepT[]; - visibleSteps: FlightListStep[]; - ignoredSteps: FlightListStepId[]; -}; -export function provideSessionFlightlist() { - const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); - const [steps, setSteps] = useState({ - steps: [], - visibleSteps: [], - ignoredSteps: [], - }); - - useRPCPacket(RpcMessage.FlightListResponse, (data: FlightListResponseT) => { - const activeSteps = data.steps.filter( - (step) => !data.ignoredSteps.includes(step.id) && step.enabled - ); - setSteps({ - steps: data.steps, - visibleSteps: activeSteps - .map((step: FlightListStepT, index) => createStep(activeSteps, step, index)) - .filter(stepVisibility), - ignoredSteps: data.ignoredSteps, - }); - }); - - useEffect(() => { - sendRPCPacket(RpcMessage.FlightListRequest, new FlightListRequestT()); - }, []); - - const firstRequired = useMemo( - () => - steps.visibleSteps.find( - (step) => !step.valid && step.status != 'blocked' && !step.optional - ), - [steps] - ); - - const hightlightedTrackers = useMemo(() => { - if (!firstRequired || !firstRequired.extraData) return []; - if ('trackersId' in firstRequired.extraData) { - return firstRequired.extraData.trackersId; - } - if ('trackerId' in firstRequired.extraData) { - return [firstRequired.extraData.trackerId]; - } - return []; - }, [firstRequired]); - - const progress = useMemo(() => { - const completeSteps = steps.visibleSteps.filter( - (step) => step.status === 'complete' || step.status === 'skipped' - ); - return Math.min(1, completeSteps.length / steps.visibleSteps.length); - }, [steps]); - - const completion: 'complete' | 'partial' | 'incomplete' = useMemo(() => { - if (progress === 1 && steps.visibleSteps.find((step) => step.status === 'skipped')) - return 'partial'; - return progress === 1 || steps.visibleSteps.length === 0 - ? 'complete' - : 'incomplete'; - }, [progress, steps]); - - const warnings = useMemo( - () => steps.visibleSteps.filter((step) => !step.valid), - [steps] - ); - - const ignoreStep = (step: FlightListStepId, ignore: boolean) => { - const res = new IgnoreFlightListStepRequestT(); - res.stepId = step; - res.ignore = ignore; - sendRPCPacket(RpcMessage.IgnoreFlightListStepRequest, res); - }; - - return { - ...steps, - firstRequired, - hightlightedTrackers, - progress, - completion, - warnings, - ignoreStep, - toggle: (step: FlightListStepId) => - ignoreStep(step, !steps.ignoredSteps.includes(step)), - }; -} - -export const FlightListContextC = createContext( - undefined as never -); - -export function useSessionFlightlist() { - const context = useContext(FlightListContextC); - if (!context) { - throw new Error('useSessionFlightlist must be within a FlightList Provider'); - } - return context; -} diff --git a/gui/src/hooks/tracking-checklist.ts b/gui/src/hooks/tracking-checklist.ts new file mode 100644 index 0000000000..a0aab2f7c6 --- /dev/null +++ b/gui/src/hooks/tracking-checklist.ts @@ -0,0 +1,186 @@ +import { + TrackingChecklistRequestT, + TrackingChecklistResponseT, + TrackingChecklistStepId, + TrackingChecklistStepT, + TrackingChecklistStepVisibility, + IgnoreTrackingChecklistStepRequestT, + RpcMessage, +} from 'solarxr-protocol'; +import { useWebsocketAPI } from './websocket-api'; +import { createContext, useContext, useEffect, useMemo, useState } from 'react'; + +export const trackingchecklistIdtoLabel: Record = { + [TrackingChecklistStepId.UNKNOWN]: '', + [TrackingChecklistStepId.TRACKERS_REST_CALIBRATION]: + 'tracking_checklist-TRACKERS_REST_CALIBRATION', + [TrackingChecklistStepId.FULL_RESET]: 'tracking_checklist-FULL_RESET', + [TrackingChecklistStepId.VRCHAT_SETTINGS]: 'tracking_checklist-VRCHAT_SETTINGS', + [TrackingChecklistStepId.STEAMVR_DISCONNECTED]: + 'tracking_checklist-STEAMVR_DISCONNECTED', + [TrackingChecklistStepId.UNASSIGNED_HMD]: 'tracking_checklist-UNASSIGNED_HMD', + [TrackingChecklistStepId.TRACKER_ERROR]: 'tracking_checklist-TRACKER_ERROR', + [TrackingChecklistStepId.NETWORK_PROFILE_PUBLIC]: + 'tracking_checklist-NETWORK_PROFILE_PUBLIC', + [TrackingChecklistStepId.MOUNTING_CALIBRATION]: + 'tracking_checklist-MOUNTING_CALIBRATION', + [TrackingChecklistStepId.STAY_ALIGNED_CONFIGURED]: + 'tracking_checklist-STAY_ALIGNED_CONFIGURED', +}; + +export type TrackingChecklistStepStatus = + | 'complete' + | 'skipped' + | 'blocked' + | 'invalid'; +export type TrackingChecklistStep = TrackingChecklistStepT & { + status: TrackingChecklistStepStatus; + firstRequired: boolean; +}; + +const stepVisibility = ({ visibility, status, firstRequired }: TrackingChecklistStep) => + firstRequired || + visibility === TrackingChecklistStepVisibility.ALWAYS || + (visibility === TrackingChecklistStepVisibility.WHEN_INVALID && status != 'complete'); + +const createStep = ( + steps: TrackingChecklistStepT[], + step: TrackingChecklistStepT, + index: number +): TrackingChecklistStep => { + const previousSteps = steps.slice(0, index); + const previousBlocked = previousSteps.some( + ({ valid, optional }) => !valid && !optional + ); + + let status: TrackingChecklistStepStatus = 'complete'; + if (previousBlocked && !step.valid) status = 'blocked'; + if (!previousBlocked && !step.valid) status = 'invalid'; + + const firstRequiredIndex = steps.findIndex( + (s, index) => !s.valid || (index === steps.length - 1 && !s.valid) + ); + + const skipped = + steps.find( + (s, sIndex) => + (sIndex > index && s.valid && !s.optional) || sIndex === steps.length - 1 + ) || index === steps.length - 1; + if (!step.valid && step.optional && skipped) status = 'skipped'; + + return { + ...step, + status, + firstRequired: + firstRequiredIndex === index || + (firstRequiredIndex === -1 && index === steps.length - 1 && !step.valid), + pack: () => 0, + }; +}; + +export type TrackingChecklistContext = ReturnType; +export type Steps = { + steps: TrackingChecklistStepT[]; + visibleSteps: TrackingChecklistStep[]; + ignoredSteps: TrackingChecklistStepId[]; +}; +export function provideTrackingChecklist() { + const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); + const [steps, setSteps] = useState({ + steps: [], + visibleSteps: [], + ignoredSteps: [], + }); + + useRPCPacket( + RpcMessage.TrackingChecklistResponse, + (data: TrackingChecklistResponseT) => { + const activeSteps = data.steps.filter( + (step) => !data.ignoredSteps.includes(step.id) && step.enabled + ); + setSteps({ + steps: data.steps, + visibleSteps: activeSteps + .map((step: TrackingChecklistStepT, index) => + createStep(activeSteps, step, index) + ) + .filter(stepVisibility), + ignoredSteps: data.ignoredSteps, + }); + } + ); + + useEffect(() => { + sendRPCPacket(RpcMessage.TrackingChecklistRequest, new TrackingChecklistRequestT()); + }, []); + + const firstRequired = useMemo( + () => + steps.visibleSteps.find( + (step) => !step.valid && step.status != 'blocked' && !step.optional + ), + [steps] + ); + + const hightlightedTrackers = useMemo(() => { + if (!firstRequired || !firstRequired.extraData) return []; + if ('trackersId' in firstRequired.extraData) { + return firstRequired.extraData.trackersId; + } + if ('trackerId' in firstRequired.extraData) { + return [firstRequired.extraData.trackerId]; + } + return []; + }, [firstRequired]); + + const progress = useMemo(() => { + const completeSteps = steps.visibleSteps.filter( + (step) => step.status === 'complete' || step.status === 'skipped' + ); + return Math.min(1, completeSteps.length / steps.visibleSteps.length); + }, [steps]); + + const completion: 'complete' | 'partial' | 'incomplete' = useMemo(() => { + if (progress === 1 && steps.visibleSteps.find((step) => step.status === 'skipped')) + return 'partial'; + return progress === 1 || steps.visibleSteps.length === 0 + ? 'complete' + : 'incomplete'; + }, [progress, steps]); + + const warnings = useMemo( + () => steps.visibleSteps.filter((step) => !step.valid), + [steps] + ); + + const ignoreStep = (step: TrackingChecklistStepId, ignore: boolean) => { + const res = new IgnoreTrackingChecklistStepRequestT(); + res.stepId = step; + res.ignore = ignore; + sendRPCPacket(RpcMessage.IgnoreTrackingChecklistStepRequest, res); + }; + + return { + ...steps, + firstRequired, + hightlightedTrackers, + progress, + completion, + warnings, + ignoreStep, + toggle: (step: TrackingChecklistStepId) => + ignoreStep(step, !steps.ignoredSteps.includes(step)), + }; +} + +export const TrackingChecklistContectC = createContext( + undefined as never +); + +export function useTrackingChecklist() { + const context = useContext(TrackingChecklistContectC); + if (!context) { + throw new Error('useTrackingChecklist must be within a TrackingChecklistProvider'); + } + return context; +} diff --git a/server/core/src/main/java/dev/slimevr/VRServer.kt b/server/core/src/main/java/dev/slimevr/VRServer.kt index 066c0d7267..14279686b5 100644 --- a/server/core/src/main/java/dev/slimevr/VRServer.kt +++ b/server/core/src/main/java/dev/slimevr/VRServer.kt @@ -7,7 +7,7 @@ import dev.slimevr.bridge.ISteamVRBridge import dev.slimevr.config.ConfigManager import dev.slimevr.firmware.FirmwareUpdateHandler import dev.slimevr.firmware.SerialFlashingHandler -import dev.slimevr.flightlist.FlightListManager +import dev.slimevr.trackingchecklist.TrackingChecklistManager import dev.slimevr.games.vrchat.VRCConfigHandler import dev.slimevr.games.vrchat.VRCConfigHandlerStub import dev.slimevr.games.vrchat.VRChatConfigManager @@ -119,7 +119,7 @@ class VRServer @JvmOverloads constructor( @JvmField val handshakeHandler = HandshakeHandler() - val flightListManager: FlightListManager + val trackingChecklistManager: TrackingChecklistManager val networkProfileChecker: NetworkProfileChecker; @@ -139,7 +139,7 @@ class VRServer @JvmOverloads constructor( firmwareUpdateHandler = FirmwareUpdateHandler(this) vrcConfigManager = VRChatConfigManager(this, vrcConfigHandlerProvider(this)) networkProfileChecker = networkProfileProvider(this) - flightListManager = FlightListManager(this) + trackingChecklistManager = TrackingChecklistManager(this) protocolAPI = ProtocolAPI(this) val computedTrackers = humanPoseManager.computedTrackers diff --git a/server/core/src/main/java/dev/slimevr/config/FlightListConfig.kt b/server/core/src/main/java/dev/slimevr/config/FlightListConfig.kt index e9c619208a..182a97bfff 100644 --- a/server/core/src/main/java/dev/slimevr/config/FlightListConfig.kt +++ b/server/core/src/main/java/dev/slimevr/config/FlightListConfig.kt @@ -1,5 +1,5 @@ package dev.slimevr.config -class FlightListConfig { +class TrackingChecklistConfig { val ignoredStepsIds: MutableList = mutableListOf() } diff --git a/server/core/src/main/java/dev/slimevr/config/VRConfig.kt b/server/core/src/main/java/dev/slimevr/config/VRConfig.kt index 1d18b93fcb..0a01a4d42b 100644 --- a/server/core/src/main/java/dev/slimevr/config/VRConfig.kt +++ b/server/core/src/main/java/dev/slimevr/config/VRConfig.kt @@ -54,7 +54,7 @@ class VRConfig { val overlay: OverlayConfig = OverlayConfig() - val flightList: FlightListConfig = FlightListConfig() + val trackingChecklist: TrackingChecklistConfig = TrackingChecklistConfig() val vrcConfig: VRCConfig = VRCConfig() diff --git a/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt b/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt deleted file mode 100644 index 22869f1cb4..0000000000 --- a/server/core/src/main/java/dev/slimevr/flightlist/FlightListManager.kt +++ /dev/null @@ -1,333 +0,0 @@ -package dev.slimevr.flightlist - -import dev.slimevr.VRServer -import dev.slimevr.bridge.ISteamVRBridge -import dev.slimevr.config.MountingMethods -import dev.slimevr.games.vrchat.VRCConfigListener -import dev.slimevr.games.vrchat.VRCConfigRecommendedValues -import dev.slimevr.games.vrchat.VRCConfigValidity -import dev.slimevr.games.vrchat.VRCConfigValues -import dev.slimevr.reset.ResetListener -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.TrackerStatus -import dev.slimevr.tracking.trackers.udp.TrackerDataType -import io.eiren.util.OperatingSystem -import solarxr_protocol.datatypes.DeviceIdT -import solarxr_protocol.datatypes.TrackerIdT -import solarxr_protocol.rpc.* -import java.util.* -import java.util.concurrent.CopyOnWriteArrayList -import kotlin.concurrent.timerTask -import kotlin.system.measureTimeMillis - -interface FlightListListener { - fun onStepsUpdate() -} - -class FlightListManager(private val vrServer: VRServer) : VRCConfigListener { - - private val listeners: MutableList = CopyOnWriteArrayList() - val steps: MutableList = mutableListOf() - - private val updateFlightListTimer = Timer("FetchVRCConfigTimer") - - // Simple flag set to true if reset mounting was performed at least once. - // This value is only runtime and never saved - var resetMountingCompleted = false - - init { - vrServer.vrcConfigManager.addListener(this) - - createSteps() - updateFlightListTimer.scheduleAtFixedRate( - timerTask { - updateFlightlist() - }, - 0, - 1000, - ) - } - - fun addListener(channel: FlightListListener) { - listeners.add(channel) - } - - fun removeListener(channel: FlightListListener) { - listeners.removeIf { channel == it } - } - - fun buildTrackersIds(trackers: List): Array = trackers.map { tracker -> - TrackerIdT().apply { - if (tracker.device != null) { - deviceId = DeviceIdT().apply { id = tracker.device.id } - } - trackerNum = tracker.trackerNum - } - }.toTypedArray() - - private fun createSteps() { - steps.add( - FlightListStepT().apply { - id = FlightListStepId.NETWORK_PROFILE_PUBLIC - enabled = vrServer.networkProfileChecker.isSupported - optional = false - ignorable = true - visibility = FlightListStepVisibility.WHEN_INVALID - }, - ) - - steps.add( - FlightListStepT().apply { - id = FlightListStepId.STEAMVR_DISCONNECTED - enabled = true - optional = false - ignorable = true - visibility = FlightListStepVisibility.WHEN_INVALID - }, - ) - - steps.add( - FlightListStepT().apply { - id = FlightListStepId.TRACKER_ERROR - valid = true // Default to valid - enabled = true - optional = false - ignorable = false - visibility = FlightListStepVisibility.WHEN_INVALID - }, - ) - - steps.add( - FlightListStepT().apply { - id = FlightListStepId.TRACKERS_REST_CALIBRATION - enabled = true - optional = false - ignorable = true - visibility = FlightListStepVisibility.ALWAYS - }, - ) - - steps.add( - FlightListStepT().apply { - id = FlightListStepId.FULL_RESET - enabled = true - optional = false - ignorable = false - visibility = FlightListStepVisibility.ALWAYS - }, - ) - - steps.add( - FlightListStepT().apply { - id = FlightListStepId.MOUNTING_CALIBRATION - valid = false - enabled = vrServer.configManager.vrConfig.resetsConfig.preferedMountingMethod == MountingMethods.AUTOMATIC - optional = false - ignorable = true - visibility = FlightListStepVisibility.ALWAYS - }, - ) - - steps.add( - FlightListStepT().apply { - id = FlightListStepId.UNASSIGNED_HMD - enabled = true - optional = false - ignorable = false - visibility = FlightListStepVisibility.WHEN_INVALID - }, - ) - - steps.add( - FlightListStepT().apply { - id = FlightListStepId.STAY_ALIGNED_CONFIGURED - enabled = true - optional = true - ignorable = true - visibility = FlightListStepVisibility.WHEN_INVALID - }, - ) - - steps.add( - FlightListStepT().apply { - id = FlightListStepId.VRCHAT_SETTINGS - enabled = vrServer.vrcConfigManager.isSupported - optional = true - ignorable = true - visibility = FlightListStepVisibility.WHEN_INVALID - }, - ) - } - - fun updateFlightlist() { - println( - measureTimeMillis { - val assignedTrackers = - vrServer.allTrackers.filter { it.trackerPosition != null && it.status != TrackerStatus.DISCONNECTED } - val imuTrackers = - assignedTrackers.filter { it.isImu() && it.trackerDataType != TrackerDataType.FLEX_ANGLE } - - val trackersWithError = - imuTrackers.filter { it.status === TrackerStatus.ERROR } - updateValidity( - FlightListStepId.TRACKER_ERROR, - trackersWithError.isEmpty(), - ) { - if (trackersWithError.isNotEmpty()) { - it.extraData = FlightListExtraDataUnion().apply { - type = FlightListExtraData.FlightListTrackerError - value = FlightListTrackerErrorT().apply { - trackersId = buildTrackersIds(trackersWithError) - } - } - } else { - it.extraData = null - } - } - - val trackerRequireReset = imuTrackers.filter { - it.status !== TrackerStatus.ERROR && !it.isInternal && it.allowReset && it.needReset - } - updateValidity(FlightListStepId.FULL_RESET, trackerRequireReset.isEmpty()) { - if (trackerRequireReset.isNotEmpty()) { - it.extraData = FlightListExtraDataUnion().apply { - type = FlightListExtraData.FlightListTrackerReset - value = FlightListTrackerResetT().apply { - trackersId = buildTrackersIds(trackerRequireReset) - } - } - } else { - it.extraData = null - } - } - - val hmd = - assignedTrackers.firstOrNull { it.isHmd && !it.isInternal && it.status.sendData } - val assignedHmd = hmd == null || vrServer.humanPoseManager.skeleton.headTracker != null - updateValidity(FlightListStepId.UNASSIGNED_HMD, assignedHmd) { - if (!assignedHmd) { - it.extraData = FlightListExtraDataUnion().apply { - type = FlightListExtraData.FlightListUnassignedHMD - value = FlightListUnassignedHMDT().apply { - trackerId = TrackerIdT().apply { - if (hmd.device != null) { - deviceId = DeviceIdT().apply { id = hmd.device.id } - } - trackerNum = hmd.trackerNum - } - } - } - } else { - it.extraData = null - } - } - - val trackersNeedCalibration = imuTrackers.filter { - it.hasCompletedRestCalibration == false - } - updateValidity( - FlightListStepId.TRACKERS_REST_CALIBRATION, - trackersNeedCalibration.isEmpty(), - ) { - // Don't show the step if none of the trackers connected support IMU calibration - it.enabled = imuTrackers.any { t -> - t.hasCompletedRestCalibration != null - } - if (trackersNeedCalibration.isNotEmpty()) { - it.extraData = FlightListExtraDataUnion().apply { - type = FlightListExtraData.FlightListNeedCalibration - value = FlightListNeedCalibrationT().apply { - trackersId = buildTrackersIds(trackersNeedCalibration) - } - } - } else { - it.extraData = null - } - } - - val steamVRBridge = vrServer.getVRBridge(ISteamVRBridge::class.java) - if (steamVRBridge != null) { - val steamvrConnected = steamVRBridge.isConnected() - updateValidity( - FlightListStepId.STEAMVR_DISCONNECTED, - steamvrConnected, - ) { - if (!steamvrConnected) { - it.extraData = FlightListExtraDataUnion().apply { - type = FlightListExtraData.FlightListSteamVRDisconnected - value = FlightListSteamVRDisconnectedT().apply { - bridgeSettingsName = steamVRBridge.getBridgeConfigKey() - } - } - } else { - it.extraData = null - } - } - } - - if (vrServer.networkProfileChecker.isSupported) { - updateValidity(FlightListStepId.NETWORK_PROFILE_PUBLIC, vrServer.networkProfileChecker.publicNetworks.isEmpty()) { - if (vrServer.networkProfileChecker.publicNetworks.isNotEmpty()) { - it.extraData = FlightListExtraDataUnion().apply { - type = FlightListExtraData.FlightListPublicNetworks - value = FlightListPublicNetworksT().apply { - adapters = vrServer.networkProfileChecker.publicNetworks.map { it.name }.toTypedArray() - } - } - } else { - it.extraData = null; - } - } - } - - updateValidity(FlightListStepId.MOUNTING_CALIBRATION, resetMountingCompleted) { - it.enabled = vrServer.configManager.vrConfig.resetsConfig.preferedMountingMethod == MountingMethods.AUTOMATIC - } - - updateValidity(FlightListStepId.STAY_ALIGNED_CONFIGURED, vrServer.configManager.vrConfig.stayAlignedConfig.enabled) - - listeners.forEach { it.onStepsUpdate() } - }, - ) - } - - private fun updateValidity(id: Int, valid: Boolean, beforeUpdate: ((step: FlightListStepT) -> Unit)? = null) { - require(id != FlightListStepId.UNKNOWN) { - "id is unknown" - } - val step = steps.find { it.id == id } ?: return - step.valid = valid - if (beforeUpdate != null) { - beforeUpdate(step) - } - } - - - override fun onChange( - validity: VRCConfigValidity, - values: VRCConfigValues, - recommended: VRCConfigRecommendedValues, - muted: List - ) { - updateValidity( - FlightListStepId.VRCHAT_SETTINGS, - VRCConfigValidity::class.java.declaredFields.asSequence().all { p -> - p.isAccessible = true - return@all p.get(validity) == true || muted.contains(p.name) - }, - ) - listeners.forEach { it.onStepsUpdate() } - } - - fun ignoreStep(step: FlightListStepT, ignore: Boolean) { - if (!step.ignorable) return; - val ignoredSteps = vrServer.configManager.vrConfig.flightList.ignoredStepsIds - if (ignore && !ignoredSteps.contains(step.id)) { - ignoredSteps.add(step.id) - } else if (!ignore) { - ignoredSteps.remove(step.id) - } - vrServer.configManager.saveConfig() - } - -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt index e2620194e3..746af142f4 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt @@ -9,7 +9,7 @@ import dev.slimevr.protocol.ProtocolHandler import dev.slimevr.protocol.datafeed.DataFeedBuilder import dev.slimevr.protocol.rpc.autobone.RPCAutoBoneHandler import dev.slimevr.protocol.rpc.firmware.RPCFirmwareUpdateHandler -import dev.slimevr.protocol.rpc.flightlist.RPCFlightListHandler +import dev.slimevr.protocol.rpc.trackingchecklist.RPCTrackingChecklistHandler import dev.slimevr.protocol.rpc.games.vrchat.RPCVRChatHandler import dev.slimevr.protocol.rpc.reset.RPCResetHandler import dev.slimevr.protocol.rpc.serial.RPCProvisioningHandler @@ -51,7 +51,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler - apiServer.apiConnections.forEach { it.send(fbb.dataBuffer()) } - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingchecklist/RPCTrackingChecklistHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingchecklist/RPCTrackingChecklistHandler.kt new file mode 100644 index 0000000000..f99f49f99d --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingchecklist/RPCTrackingChecklistHandler.kt @@ -0,0 +1,73 @@ +package dev.slimevr.protocol.rpc.trackingchecklist + +import com.google.flatbuffers.FlatBufferBuilder +import dev.slimevr.trackingchecklist.TrackingChecklistListener +import dev.slimevr.protocol.GenericConnection +import dev.slimevr.protocol.ProtocolAPI +import dev.slimevr.protocol.rpc.RPCHandler +import solarxr_protocol.rpc.* + +class RPCTrackingChecklistHandler( + private val rpcHandler: RPCHandler, + var api: ProtocolAPI, +) : TrackingChecklistListener { + + init { + api.server.trackingChecklistManager.addListener(this) + + rpcHandler.registerPacketListener(RpcMessage.TrackingChecklistRequest, ::onTrackingChecklistRequest) + rpcHandler.registerPacketListener(RpcMessage.IgnoreTrackingChecklistStepRequest, ::onToggleTrackingChecklistRequest) + } + + fun buildTrackingChecklistResponse(fbb: FlatBufferBuilder): Int = TrackingChecklistResponse.pack( + fbb, + TrackingChecklistResponseT().apply { + steps = api.server.trackingChecklistManager.steps.toTypedArray() + ignoredSteps = api.server.configManager.vrConfig.trackingChecklist.ignoredStepsIds.toIntArray() + }, + ) + + private fun onTrackingChecklistRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { + val fbb = FlatBufferBuilder(32) + val response = buildTrackingChecklistResponse(fbb) + val outbound = rpcHandler.createRPCMessage( + fbb, + RpcMessage.TrackingChecklistResponse, + response, + ) + fbb.finish(outbound) + conn.send(fbb.dataBuffer()) + } + + private fun onToggleTrackingChecklistRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { + val req = messageHeader.message(IgnoreTrackingChecklistStepRequest()) as IgnoreTrackingChecklistStepRequest? + ?: return + val step = api.server.trackingChecklistManager.steps.find { it.id == req.stepId() } ?: error("invalid step id requested") + + api.server.trackingChecklistManager.ignoreStep(step, req.ignore()) + + val fbb = FlatBufferBuilder(32) + val response = buildTrackingChecklistResponse(fbb) + val outbound = rpcHandler.createRPCMessage( + fbb, + RpcMessage.TrackingChecklistResponse, + response, + ) + fbb.finish(outbound) + conn.send(fbb.dataBuffer()) + } + + override fun onStepsUpdate() { + val fbb = FlatBufferBuilder(32) + val response = buildTrackingChecklistResponse(fbb) + val outbound = rpcHandler.createRPCMessage( + fbb, + RpcMessage.TrackingChecklistResponse, + response, + ) + fbb.finish(outbound) + this.api.apiServers.forEach { apiServer -> + apiServer.apiConnections.forEach { it.send(fbb.dataBuffer()) } + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt index 76564a4d2b..6dcc2a87be 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt @@ -573,7 +573,7 @@ class HumanPoseManager(val server: VRServer?) { if (server != null) { server.configManager.vrConfig.resetsConfig.preferedMountingMethod = MountingMethods.AUTOMATIC - server.flightListManager.resetMountingCompleted = true; + server.trackingChecklistManager.resetMountingCompleted = true; server.configManager.saveConfig() } } @@ -581,7 +581,7 @@ class HumanPoseManager(val server: VRServer?) { fun clearTrackersMounting(resetSourceName: String?) { skeleton.clearTrackersMounting(resetSourceName) if (server != null) { - server.flightListManager.resetMountingCompleted = false; + server.trackingChecklistManager.resetMountingCompleted = false; server.configManager.saveConfig() } } diff --git a/server/core/src/main/java/dev/slimevr/trackingchecklist/TrackingChecklistManager.kt b/server/core/src/main/java/dev/slimevr/trackingchecklist/TrackingChecklistManager.kt new file mode 100644 index 0000000000..650fdc5749 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/trackingchecklist/TrackingChecklistManager.kt @@ -0,0 +1,326 @@ +package dev.slimevr.trackingchecklist + +import dev.slimevr.VRServer +import dev.slimevr.bridge.ISteamVRBridge +import dev.slimevr.config.MountingMethods +import dev.slimevr.games.vrchat.VRCConfigListener +import dev.slimevr.games.vrchat.VRCConfigRecommendedValues +import dev.slimevr.games.vrchat.VRCConfigValidity +import dev.slimevr.games.vrchat.VRCConfigValues +import dev.slimevr.tracking.trackers.Tracker +import dev.slimevr.tracking.trackers.TrackerStatus +import dev.slimevr.tracking.trackers.udp.TrackerDataType +import solarxr_protocol.datatypes.DeviceIdT +import solarxr_protocol.datatypes.TrackerIdT +import solarxr_protocol.rpc.* +import java.util.* +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.concurrent.timerTask + +interface TrackingChecklistListener { + fun onStepsUpdate() +} + +class TrackingChecklistManager(private val vrServer: VRServer) : VRCConfigListener { + + private val listeners: MutableList = CopyOnWriteArrayList() + val steps: MutableList = mutableListOf() + + private val updateTrackingChecklistTimer = Timer("TrackingChecklistTimer") + + // Simple flag set to true if reset mounting was performed at least once. + // This value is only runtime and never saved + var resetMountingCompleted = false + + init { + vrServer.vrcConfigManager.addListener(this) + + createSteps() + updateTrackingChecklistTimer.scheduleAtFixedRate( + timerTask { + updateChecklist() + }, + 0, + 1000, + ) + } + + fun addListener(channel: TrackingChecklistListener) { + listeners.add(channel) + } + + fun removeListener(channel: TrackingChecklistListener) { + listeners.removeIf { channel == it } + } + + fun buildTrackersIds(trackers: List): Array = trackers.map { tracker -> + TrackerIdT().apply { + if (tracker.device != null) { + deviceId = DeviceIdT().apply { id = tracker.device.id } + } + trackerNum = tracker.trackerNum + } + }.toTypedArray() + + private fun createSteps() { + steps.add( + TrackingChecklistStepT().apply { + id = TrackingChecklistStepId.NETWORK_PROFILE_PUBLIC + enabled = vrServer.networkProfileChecker.isSupported + optional = false + ignorable = true + visibility = TrackingChecklistStepVisibility.WHEN_INVALID + }, + ) + + steps.add( + TrackingChecklistStepT().apply { + id = TrackingChecklistStepId.STEAMVR_DISCONNECTED + enabled = true + optional = false + ignorable = true + visibility = TrackingChecklistStepVisibility.WHEN_INVALID + }, + ) + + steps.add( + TrackingChecklistStepT().apply { + id = TrackingChecklistStepId.TRACKER_ERROR + valid = true // Default to valid + enabled = true + optional = false + ignorable = false + visibility = TrackingChecklistStepVisibility.WHEN_INVALID + }, + ) + + steps.add( + TrackingChecklistStepT().apply { + id = TrackingChecklistStepId.TRACKERS_REST_CALIBRATION + enabled = true + optional = false + ignorable = true + visibility = TrackingChecklistStepVisibility.ALWAYS + }, + ) + + steps.add( + TrackingChecklistStepT().apply { + id = TrackingChecklistStepId.FULL_RESET + enabled = true + optional = false + ignorable = false + visibility = TrackingChecklistStepVisibility.ALWAYS + }, + ) + + steps.add( + TrackingChecklistStepT().apply { + id = TrackingChecklistStepId.MOUNTING_CALIBRATION + valid = false + enabled = vrServer.configManager.vrConfig.resetsConfig.preferedMountingMethod == MountingMethods.AUTOMATIC + optional = false + ignorable = true + visibility = TrackingChecklistStepVisibility.ALWAYS + }, + ) + + steps.add( + TrackingChecklistStepT().apply { + id = TrackingChecklistStepId.UNASSIGNED_HMD + enabled = true + optional = false + ignorable = false + visibility = TrackingChecklistStepVisibility.WHEN_INVALID + }, + ) + + steps.add( + TrackingChecklistStepT().apply { + id = TrackingChecklistStepId.STAY_ALIGNED_CONFIGURED + enabled = true + optional = true + ignorable = true + visibility = TrackingChecklistStepVisibility.WHEN_INVALID + }, + ) + + steps.add( + TrackingChecklistStepT().apply { + id = TrackingChecklistStepId.VRCHAT_SETTINGS + enabled = vrServer.vrcConfigManager.isSupported + optional = true + ignorable = true + visibility = TrackingChecklistStepVisibility.WHEN_INVALID + }, + ) + } + + fun updateChecklist() { + val assignedTrackers = + vrServer.allTrackers.filter { it.trackerPosition != null && it.status != TrackerStatus.DISCONNECTED } + val imuTrackers = + assignedTrackers.filter { it.isImu() && it.trackerDataType != TrackerDataType.FLEX_ANGLE } + + val trackersWithError = + imuTrackers.filter { it.status === TrackerStatus.ERROR } + updateValidity( + TrackingChecklistStepId.TRACKER_ERROR, + trackersWithError.isEmpty(), + ) { + if (trackersWithError.isNotEmpty()) { + it.extraData = TrackingChecklistExtraDataUnion().apply { + type = TrackingChecklistExtraData.TrackingChecklistTrackerError + value = TrackingChecklistTrackerErrorT().apply { + trackersId = buildTrackersIds(trackersWithError) + } + } + } else { + it.extraData = null + } + } + + val trackerRequireReset = imuTrackers.filter { + it.status !== TrackerStatus.ERROR && !it.isInternal && it.allowReset && it.needReset + } + updateValidity(TrackingChecklistStepId.FULL_RESET, trackerRequireReset.isEmpty()) { + if (trackerRequireReset.isNotEmpty()) { + it.extraData = TrackingChecklistExtraDataUnion().apply { + type = TrackingChecklistExtraData.TrackingChecklistTrackerReset + value = TrackingChecklistTrackerResetT().apply { + trackersId = buildTrackersIds(trackerRequireReset) + } + } + } else { + it.extraData = null + } + } + + val hmd = + assignedTrackers.firstOrNull { it.isHmd && !it.isInternal && it.status.sendData } + val assignedHmd = hmd == null || vrServer.humanPoseManager.skeleton.headTracker != null + updateValidity(TrackingChecklistStepId.UNASSIGNED_HMD, assignedHmd) { + if (!assignedHmd) { + it.extraData = TrackingChecklistExtraDataUnion().apply { + type = TrackingChecklistExtraData.TrackingChecklistUnassignedHMD + value = TrackingChecklistUnassignedHMDT().apply { + trackerId = TrackerIdT().apply { + if (hmd.device != null) { + deviceId = DeviceIdT().apply { id = hmd.device.id } + } + trackerNum = hmd.trackerNum + } + } + } + } else { + it.extraData = null + } + } + + val trackersNeedCalibration = imuTrackers.filter { + it.hasCompletedRestCalibration == false + } + updateValidity( + TrackingChecklistStepId.TRACKERS_REST_CALIBRATION, + trackersNeedCalibration.isEmpty(), + ) { + // Don't show the step if none of the trackers connected support IMU calibration + it.enabled = imuTrackers.any { t -> + t.hasCompletedRestCalibration != null + } + if (trackersNeedCalibration.isNotEmpty()) { + it.extraData = TrackingChecklistExtraDataUnion().apply { + type = TrackingChecklistExtraData.TrackingChecklistNeedCalibration + value = TrackingChecklistNeedCalibrationT().apply { + trackersId = buildTrackersIds(trackersNeedCalibration) + } + } + } else { + it.extraData = null + } + } + + val steamVRBridge = vrServer.getVRBridge(ISteamVRBridge::class.java) + if (steamVRBridge != null) { + val steamvrConnected = steamVRBridge.isConnected() + updateValidity( + TrackingChecklistStepId.STEAMVR_DISCONNECTED, + steamvrConnected, + ) { + if (!steamvrConnected) { + it.extraData = TrackingChecklistExtraDataUnion().apply { + type = TrackingChecklistExtraData.TrackingChecklistSteamVRDisconnected + value = TrackingChecklistSteamVRDisconnectedT().apply { + bridgeSettingsName = steamVRBridge.getBridgeConfigKey() + } + } + } else { + it.extraData = null + } + } + } + + if (vrServer.networkProfileChecker.isSupported) { + updateValidity(TrackingChecklistStepId.NETWORK_PROFILE_PUBLIC, vrServer.networkProfileChecker.publicNetworks.isEmpty()) { + if (vrServer.networkProfileChecker.publicNetworks.isNotEmpty()) { + it.extraData = TrackingChecklistExtraDataUnion().apply { + type = TrackingChecklistExtraData.TrackingChecklistPublicNetworks + value = TrackingChecklistPublicNetworksT().apply { + adapters = vrServer.networkProfileChecker.publicNetworks.map { it.name }.toTypedArray() + } + } + } else { + it.extraData = null; + } + } + } + + updateValidity(TrackingChecklistStepId.MOUNTING_CALIBRATION, resetMountingCompleted) { + it.enabled = vrServer.configManager.vrConfig.resetsConfig.preferedMountingMethod == MountingMethods.AUTOMATIC + } + + updateValidity(TrackingChecklistStepId.STAY_ALIGNED_CONFIGURED, vrServer.configManager.vrConfig.stayAlignedConfig.enabled) + + listeners.forEach { it.onStepsUpdate() } + } + + private fun updateValidity(id: Int, valid: Boolean, beforeUpdate: ((step: TrackingChecklistStepT) -> Unit)? = null) { + require(id != TrackingChecklistStepId.UNKNOWN) { + "id is unknown" + } + val step = steps.find { it.id == id } ?: return + step.valid = valid + if (beforeUpdate != null) { + beforeUpdate(step) + } + } + + + override fun onChange( + validity: VRCConfigValidity, + values: VRCConfigValues, + recommended: VRCConfigRecommendedValues, + muted: List + ) { + updateValidity( + TrackingChecklistStepId.VRCHAT_SETTINGS, + VRCConfigValidity::class.java.declaredFields.asSequence().all { p -> + p.isAccessible = true + return@all p.get(validity) == true || muted.contains(p.name) + }, + ) + listeners.forEach { it.onStepsUpdate() } + } + + fun ignoreStep(step: TrackingChecklistStepT, ignore: Boolean) { + if (!step.ignorable) return; + val ignoredSteps = vrServer.configManager.vrConfig.trackingChecklist.ignoredStepsIds + if (ignore && !ignoredSteps.contains(step.id)) { + ignoredSteps.add(step.id) + } else if (!ignore) { + ignoredSteps.remove(step.id) + } + vrServer.configManager.saveConfig() + } + +} diff --git a/solarxr-protocol b/solarxr-protocol index 08beeb48a3..2ef68bf105 160000 --- a/solarxr-protocol +++ b/solarxr-protocol @@ -1 +1 @@ -Subproject commit 08beeb48a37158541ba14965ca5ee93db1b750b2 +Subproject commit 2ef68bf105ac5f4d92326e2a7a972f77a358a07b From 8da6801274182e1ae3962294d38e180441acd5da Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Fri, 19 Sep 2025 22:37:53 +0200 Subject: [PATCH 20/34] Current progress --- gui/src/components/MainLayout.scss | 4 +- gui/src/components/Navbar.tsx | 61 +++-- gui/src/components/Toolbar.tsx | 290 +++++++++++++++++++----- gui/src/components/WidgetsComponent.tsx | 33 --- gui/src/hooks/body-parts.ts | 35 +++ gui/src/hooks/reset.ts | 21 +- gui/src/index.scss | 2 +- gui/src/store/app-store.ts | 13 ++ 8 files changed, 320 insertions(+), 139 deletions(-) delete mode 100644 gui/src/components/WidgetsComponent.tsx create mode 100644 gui/src/hooks/body-parts.ts diff --git a/gui/src/components/MainLayout.scss b/gui/src/components/MainLayout.scss index a9c4e5fcaa..9f244f8b4f 100644 --- a/gui/src/components/MainLayout.scss +++ b/gui/src/components/MainLayout.scss @@ -1,5 +1,5 @@ :root { - --toolbar-h: 140px; + --toolbar-h: 152px; } .main-layout { @@ -34,7 +34,7 @@ } @screen xl { - --right-section-w: 20%; + --right-section-w: 22%; } @screen mobile { diff --git a/gui/src/components/Navbar.tsx b/gui/src/components/Navbar.tsx index 649840b105..71dffc54e9 100644 --- a/gui/src/components/Navbar.tsx +++ b/gui/src/components/Navbar.tsx @@ -8,7 +8,6 @@ import { RulerIcon } from './commons/icon/RulerIcon'; import { SparkleIcon } from './commons/icon/SparkleIcon'; import { useBreakpoint } from '@/hooks/breakpoint'; import { useConfig } from '@/hooks/config'; -import { Tooltip } from './commons/Tooltip'; import { HomeIcon } from './commons/icon/HomeIcon'; import { SkiIcon } from './commons/icon/SkiIcon'; @@ -25,43 +24,43 @@ export function NavButton({ state?: any; icon: ReactNode; }) { - const { isMobile } = useBreakpoint('mobile'); const doesMatch = useMatch({ path: match || to, }); return ( - {children}
} - variant="floating" + - -
-
- {icon} -
+
+
+ {icon}
- - +
+
+ {children} +
+ ); } diff --git a/gui/src/components/Toolbar.tsx b/gui/src/components/Toolbar.tsx index 5348a3fd2f..41258dcc99 100644 --- a/gui/src/components/Toolbar.tsx +++ b/gui/src/components/Toolbar.tsx @@ -19,6 +19,8 @@ import { useWebsocketAPI } from '@/hooks/websocket-api'; import { QuaternionFromQuatT, similarQuaternions } from '@/maths/quaternion'; import { Quaternion } from 'three'; import { LayoutIcon } from './commons/icon/LayoutIcon'; +import { FootIcon } from './commons/icon/FootIcon'; +import { FingersIcon } from './commons/icon/FingersIcon'; const MAINBUTTON_CLASSES = ({ disabled }: { disabled: boolean }) => classNames( @@ -48,7 +50,7 @@ function ButtonProgress({ } function BasicResetButton({ type }: { type: ResetType }) { - const { isNmd } = useBreakpoint('nmd'); + const { isMd } = useBreakpoint('md'); const { triggerReset, status, name, timer, disabled, duration } = useReset(type); @@ -64,7 +66,7 @@ function BasicResetButton({ type }: { type: ResetType }) { return ( } preferedDirection="top" > @@ -127,66 +129,209 @@ function MountingCalibrationButton() { const progress = status === 'counting' ? 1 - (timer - 1) / duration : 0; + const [open, setOpen] = useState(false); + return ( -
- } - preferedDirection="top" + <> +
-
!disabled && triggerReset()} - > -
+ + } + preferedDirection="top" > - -
-
- -
+
!disabled && setOpen(true)} + > +
+ +
+
+ +
+
+ + + Clear Mounting Calibration + + } + preferedDirection="top" + > +
trackerWithMounting && !disabled && clearMounting} + > +
+ +
+
+
+
- - - Clear Mounting Calibration - - } - preferedDirection="top" - > +
trackerWithMounting && !disabled && clearMounting} > -
- +
+ + } + preferedDirection="top" + > +
!disabled && setOpen(false) && triggerReset()} + > +
+ +
+
+ + Body mounting calibration + +
+
+
+
+
+ + } + preferedDirection="top" + > +
!disabled && setOpen(true)} + > +
+ +
+
+ + Feet mounting calibration + +
+
+
+
+
+ + } + preferedDirection="top" + > +
!disabled && setOpen(true)} + > +
+ +
+
+ + Fingers Mounting calibration + +
+
+
- - -
+
+
setOpen(false)} + >
+ ); } @@ -198,11 +343,42 @@ export function Toolbar({ showSettings }: { showSettings: boolean }) { return ( <> -
-
- - - +
+
+
+ Drift Resets +
+ + +
+
+
+
+ + Mounting Calibration + +
+
+
+ +
+ Body +
+
+
+ +
+ Foot +
+
+
+ +
+ Fingers +
+
+
+
diff --git a/gui/src/components/WidgetsComponent.tsx b/gui/src/components/WidgetsComponent.tsx deleted file mode 100644 index 906e9fd75d..0000000000 --- a/gui/src/components/WidgetsComponent.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { BVHButton } from './BVHButton'; -import { TrackingPauseButton } from './TrackingPauseButton'; -import { OverlayWidget } from './widgets/OverlayWidget'; - -export function WidgetsComponent() { - return ( - <> -
- {/* - - - */} - {/* - */} - {(typeof __ANDROID__ === 'undefined' || !__ANDROID__?.isThere()) && ( - - )} - -
-
- -
- - ); -} diff --git a/gui/src/hooks/body-parts.ts b/gui/src/hooks/body-parts.ts new file mode 100644 index 0000000000..9ff6a92cee --- /dev/null +++ b/gui/src/hooks/body-parts.ts @@ -0,0 +1,35 @@ +import { BodyPart } from 'solarxr-protocol'; + +export const FEET_BODY_PARTS = [BodyPart.LEFT_FOOT, BodyPart.RIGHT_FOOT]; +export const FINGER_BODY_PARTS = new Set([ + BodyPart.LEFT_THUMB_METACARPAL, + BodyPart.LEFT_THUMB_PROXIMAL, + BodyPart.LEFT_THUMB_DISTAL, + BodyPart.LEFT_INDEX_PROXIMAL, + BodyPart.LEFT_INDEX_INTERMEDIATE, + BodyPart.LEFT_INDEX_DISTAL, + BodyPart.LEFT_MIDDLE_PROXIMAL, + BodyPart.LEFT_MIDDLE_INTERMEDIATE, + BodyPart.LEFT_MIDDLE_DISTAL, + BodyPart.LEFT_RING_PROXIMAL, + BodyPart.LEFT_RING_INTERMEDIATE, + BodyPart.LEFT_RING_DISTAL, + BodyPart.LEFT_LITTLE_PROXIMAL, + BodyPart.LEFT_LITTLE_INTERMEDIATE, + BodyPart.LEFT_LITTLE_DISTAL, + BodyPart.RIGHT_THUMB_METACARPAL, + BodyPart.RIGHT_THUMB_PROXIMAL, + BodyPart.RIGHT_THUMB_DISTAL, + BodyPart.RIGHT_INDEX_PROXIMAL, + BodyPart.RIGHT_INDEX_INTERMEDIATE, + BodyPart.RIGHT_INDEX_DISTAL, + BodyPart.RIGHT_MIDDLE_PROXIMAL, + BodyPart.RIGHT_MIDDLE_INTERMEDIATE, + BodyPart.RIGHT_MIDDLE_DISTAL, + BodyPart.RIGHT_RING_PROXIMAL, + BodyPart.RIGHT_RING_INTERMEDIATE, + BodyPart.RIGHT_RING_DISTAL, + BodyPart.RIGHT_LITTLE_PROXIMAL, + BodyPart.RIGHT_LITTLE_INTERMEDIATE, + BodyPart.RIGHT_LITTLE_DISTAL, +]); diff --git a/gui/src/hooks/reset.ts b/gui/src/hooks/reset.ts index a98bc5ee4a..e4aea33103 100644 --- a/gui/src/hooks/reset.ts +++ b/gui/src/hooks/reset.ts @@ -6,10 +6,8 @@ import { ResetStatus, ResetType, RpcMessage, - TrackingChecklistStepId, } from 'solarxr-protocol'; import { useConfig } from './config'; -import { useTrackingChecklist } from './tracking-checklist'; import { useWebsocketAPI } from './websocket-api'; import { useCountdown } from './countdown'; @@ -21,19 +19,10 @@ export function useReset(type: ResetType, onReseted?: () => void) { const finishedTimeoutRef = useRef(-1); const [status, setStatus] = useState('idle'); - const { visibleSteps } = useTrackingChecklist(); - const needsFullReset = useMemo( - () => - type == ResetType.Mounting && - visibleSteps.some( - (step) => step.id === TrackingChecklistStepId.FULL_RESET && !step.valid - ), - [visibleSteps, type] - ); - const reset = () => { const req = new ResetRequestT(); req.resetType = type; + req.bodyParts = []; sendRPCPacket(RpcMessage.ResetRequest, req); }; @@ -99,10 +88,10 @@ export function useReset(type: ResetType, onReseted?: () => void) { switch (type) { case ResetType.Yaw: return 'reset-yaw'; - case ResetType.Mounting: - return 'reset-mounting'; case ResetType.Full: return 'reset-full'; + default: + return 'unhandled'; } }, [type]); @@ -110,8 +99,10 @@ export function useReset(type: ResetType, onReseted?: () => void) { triggerReset, timer, status, - disabled: status === 'counting' || needsFullReset, + disabled: status === 'counting', name, duration, }; } + +export function useMountingReset() {} diff --git a/gui/src/index.scss b/gui/src/index.scss index 493e3662d4..28c5673d6f 100644 --- a/gui/src/index.scss +++ b/gui/src/index.scss @@ -62,7 +62,7 @@ body { // overflow: hidden; -- NEVER EVER BRING THIS BACK <3 background: theme('colors.background.20'); - --navbar-w: 81px; + --navbar-w: 101px; --topbar-h: 38px; @screen mobile { diff --git a/gui/src/store/app-store.ts b/gui/src/store/app-store.ts index 4c06b20459..37f447036c 100644 --- a/gui/src/store/app-store.ts +++ b/gui/src/store/app-store.ts @@ -9,6 +9,7 @@ import { } from 'solarxr-protocol'; import { selectAtom } from 'jotai/utils'; import { isEqual } from '@react-hookz/deep-equal'; +import { FEET_BODY_PARTS, FINGER_BODY_PARTS } from '@/hooks/body-parts'; export interface FlatDeviceTracker { device?: DeviceDataT; @@ -99,3 +100,15 @@ export const trackerFromIdAtom = ({ (a) => a, isEqual ); + +export const feetAssignedTrackers = atom((get) => + get(assignedTrackersAtom).some( + (t) => t.tracker.info?.bodyPart && FEET_BODY_PARTS.includes(t.tracker.info.bodyPart) + ) +); + +export const fingerAssignedTrackers = atom((get) => + get(assignedTrackersAtom).some( + (t) => t.tracker.info?.bodyPart && FINGER_BODY_PARTS.has(t.tracker.info.bodyPart) + ) +); From 890a03a30db0af2de2329cce643d7b5907f3013d Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Mon, 22 Sep 2025 01:54:41 +0200 Subject: [PATCH 21/34] Translations --- gui/public/i18n/en/translation.ftl | 27 +++++++++++++++++- gui/src/components/MainLayout.tsx | 1 - gui/src/components/Sidebar.tsx | 4 +-- gui/src/components/Toolbar.tsx | 15 ++++++---- gui/src/components/home/HomeSettingsModal.tsx | 10 ++++--- .../settings/pages/HomeScreenSettings.tsx | 28 ++++++++----------- .../tracking-checklist/TrackingChecklist.tsx | 24 +++++++++------- .../TrackingChecklistModal.tsx | 12 ++++---- 8 files changed, 74 insertions(+), 47 deletions(-) diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index 7615680162..24249a127a 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -869,6 +869,16 @@ settings-utils-advanced-open_logs = Logs folder settings-utils-advanced-open_logs-description = Open SlimeVR's logs folder in file explorer, containing the logs of the app settings-utils-advanced-open_logs-label = Open folder +## Home Screen +settings-home-list-layout = Trackers list layout +settings-home-list-layout-desc = Select one of the possible layouts of the home screen +settings-home-list-layout-grid = Grid +settings-home-list-layout-table = Table + +## Tracking Checlist +settings-tracking_checklist-active_steps = Active Steps +settings-tracking_checklist-active_steps-desc = List all the steps that will show in the tracking checklist. You can either disable or enable ignorable steps + ## Setup/onboarding menu onboarding-skip = Skip setup onboarding-continue = Continue @@ -1294,6 +1304,8 @@ onboarding-stay_aligned-done = Done ## Home home-no_trackers = No trackers detected or assigned +home-settings = Home Page Settings +home-settings-close = Close ## Trackers Still On notification trackers_still_on-modal-title = Trackers still on @@ -1553,6 +1565,15 @@ error_collection_modal-confirm = I agree error_collection_modal-cancel = I don't want to +tracking_checklist = Tracking Checklist +tracking_checklist-settings = Tracking Checklist Settings +tracking_checklist-settings-close = Close +tracking_checklist-status-incomplete = You are not prepared to use SlimeVR! +tracking_checklist-status-partial = {$count -> + [one] You have 1 warning! + *[many] You have {$count} warnings! +} +tracking_checklist-status-complete = You are prepared to use SlimeVR! tracking_checklist-MOUNTING_CALIBRATION = Perform a mounting calibration tracking_checklist-FULL_RESET = Perform a full Reset tracking_checklist-FULL_RESET-desc = Some Trackers need a reset to be performed @@ -1585,4 +1606,8 @@ tracking_checklist-STAY_ALIGNED_CONFIGURED-open = Open Stay Aligned Wizard tracking_checklist-ignore = Ignore -sidebar-preview-mocap_mode_soon = Mocap Mode (Soonâ„¢) +preview-mocap_mode_soon = Mocap Mode (Soonâ„¢) +preview-disable_render = Disable rendering + +toolbar-mounting_calibration = Mounting Calibration +toolbar-connected_trackers = {$count} trackers connected diff --git a/gui/src/components/MainLayout.tsx b/gui/src/components/MainLayout.tsx index e3e81663fc..2fce31f130 100644 --- a/gui/src/components/MainLayout.tsx +++ b/gui/src/components/MainLayout.tsx @@ -13,7 +13,6 @@ import './MainLayout.scss'; import { Toolbar } from './Toolbar'; import { Sidebar } from './Sidebar'; import { TrackingChecklistMobile } from './tracking-checklist/TrackingChecklist'; -import { ArrowRightIcon } from './commons/icon/ArrowIcons'; import { useTrackingChecklist } from '@/hooks/tracking-checklist'; export function MainLayout({ diff --git a/gui/src/components/Sidebar.tsx b/gui/src/components/Sidebar.tsx index 81a0b26237..90689a1c24 100644 --- a/gui/src/components/Sidebar.tsx +++ b/gui/src/components/Sidebar.tsx @@ -128,7 +128,7 @@ export function PreviewControls({ open }: { open: boolean }) { content={ } preferedDirection="top" @@ -186,7 +186,7 @@ function PreviewSection({ open }: { open: boolean }) { > Disable rendering
} + content={} >
- - Mounting Calibration - +
- - {trackers.length} trackers connected - +
{showSettings && (
- Home Page Settings +
- +
diff --git a/gui/src/components/settings/pages/HomeScreenSettings.tsx b/gui/src/components/settings/pages/HomeScreenSettings.tsx index b8f1077d54..33c2753b98 100644 --- a/gui/src/components/settings/pages/HomeScreenSettings.tsx +++ b/gui/src/components/settings/pages/HomeScreenSettings.tsx @@ -65,11 +65,11 @@ export function TrackingChecklistSettings({ return (
- Active Steps - - List all the steps that will show in the tracking checklist. You can - either disable or enable ignorable steps - + +
- Trackers list layout - - Select one of the possible layouts of the home screen - + +
setLayout('default')} > @@ -158,7 +156,7 @@ export function HomeLayoutSettings() {
setLayout('table')} > @@ -179,15 +177,11 @@ export function HomeScreenSettings() {
}> - - Home Screen Settings - + }> - - Tracking Checklist - +
diff --git a/gui/src/components/tracking-checklist/TrackingChecklist.tsx b/gui/src/components/tracking-checklist/TrackingChecklist.tsx index 735444c159..bd14d20df1 100644 --- a/gui/src/components/tracking-checklist/TrackingChecklist.tsx +++ b/gui/src/components/tracking-checklist/TrackingChecklist.tsx @@ -392,7 +392,7 @@ export function TrackingChecklist({ )} >
- Tracking checklist +
{completion === 'incomplete' && ( - - You are not prepared to use SlimeVR! - + )} {completion === 'partial' && ( - - You have {warnings.length} warnings! - + )} {completion == 'complete' && ( - - You are prepared to use SlimeVR! - + )}
diff --git a/gui/src/components/tracking-checklist/TrackingChecklistModal.tsx b/gui/src/components/tracking-checklist/TrackingChecklistModal.tsx index 461ac41334..9478e260e6 100644 --- a/gui/src/components/tracking-checklist/TrackingChecklistModal.tsx +++ b/gui/src/components/tracking-checklist/TrackingChecklistModal.tsx @@ -21,14 +21,14 @@ export function TrackingChecklistModal({ }} >
- - Tracking Checklist Settings - +
- +
From 1bc20b56930475506b9e0d9e4eb22db3964a708c Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Mon, 22 Sep 2025 02:22:22 +0200 Subject: [PATCH 22/34] Cleanup + More translations --- gui/public/i18n/en/translation.ftl | 5 +++++ gui/src/components/Toolbar.tsx | 8 ++++---- gui/src/components/commons/Checkbox.tsx | 2 +- gui/src/components/home/ResetButton.tsx | 2 +- gui/src/components/widgets/SkeletonVisualizerWidget.tsx | 2 +- gui/tailwind.config.ts | 1 - 6 files changed, 12 insertions(+), 8 deletions(-) diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index 24249a127a..b05b26fca9 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -1608,6 +1608,11 @@ tracking_checklist-ignore = Ignore preview-mocap_mode_soon = Mocap Mode (Soonâ„¢) preview-disable_render = Disable rendering +preview-disabled_render = Rendering disabled toolbar-mounting_calibration = Mounting Calibration +toolbar-mounting_calibration-default = Body +toolbar-mounting_calibration-feet = Feet +toolbar-mounting_calibration-fingers = Fingers +toolbar-drift_reset = Drift Reset toolbar-connected_trackers = {$count} trackers connected diff --git a/gui/src/components/Toolbar.tsx b/gui/src/components/Toolbar.tsx index cdf0b7c5c0..f0e41dac57 100644 --- a/gui/src/components/Toolbar.tsx +++ b/gui/src/components/Toolbar.tsx @@ -134,7 +134,7 @@ export function Toolbar({ showSettings }: { showSettings: boolean }) {
- Drift Resets +
@@ -154,18 +154,18 @@ export function Toolbar({ showSettings }: { showSettings: boolean }) { {groupVisibility['fingers'] && ( )}
diff --git a/gui/src/components/commons/Checkbox.tsx b/gui/src/components/commons/Checkbox.tsx index 93e33b0894..e94be27986 100644 --- a/gui/src/components/commons/Checkbox.tsx +++ b/gui/src/components/commons/Checkbox.tsx @@ -55,7 +55,7 @@ export function CheckBox({ className={classNames( { 'rounded-lg': outlined, - 'text-background-30 ': !outlined || disabled, + 'text-background-30': !outlined || disabled, 'bg-background-60': outlined && color === 'primary', 'bg-background-70': outlined && color === 'secondary', 'bg-background-50': outlined && color === 'tertiary', diff --git a/gui/src/components/home/ResetButton.tsx b/gui/src/components/home/ResetButton.tsx index 88cc0d77dc..12d0f50efd 100644 --- a/gui/src/components/home/ResetButton.tsx +++ b/gui/src/components/home/ResetButton.tsx @@ -46,7 +46,7 @@ export function ResetButton({ icon={} onClick={triggerReset} className={classNames( - 'border-2 m-1', + 'border-2 py-[5px]', status === 'finished' ? 'border-status-success' : 'transition-[border-color] duration-500 ease-in-out border-transparent', diff --git a/gui/src/components/widgets/SkeletonVisualizerWidget.tsx b/gui/src/components/widgets/SkeletonVisualizerWidget.tsx index 9d775da47c..e81a62a232 100644 --- a/gui/src/components/widgets/SkeletonVisualizerWidget.tsx +++ b/gui/src/components/widgets/SkeletonVisualizerWidget.tsx @@ -433,7 +433,7 @@ export function SkeletonVisualizerWidget({ onClick={() => toggleDisabled?.()} > - Rendering disabled +
Date: Mon, 22 Sep 2025 08:14:52 +0200 Subject: [PATCH 23/34] Feet mounting reset --- gui/public/i18n/en/translation.ftl | 9 ++- gui/public/images/mounting/MountingFeets.webp | Bin 0 -> 30678 bytes .../images/mounting/MountingFeetsSide.webp | Bin 0 -> 52158 bytes gui/src/components/Toolbar.tsx | 7 ++- .../tracking-checklist/TrackingChecklist.tsx | 34 ++++++++++ gui/src/hooks/body-parts.ts | 59 +++++++++++++++++- gui/src/hooks/reset.ts | 39 +----------- gui/src/hooks/tracking-checklist.ts | 2 + gui/src/store/app-store.ts | 3 +- .../src/main/java/dev/slimevr/VRServer.kt | 2 +- .../tracking/processor/HumanPoseManager.kt | 22 +++---- .../processor/skeleton/HumanSkeleton.kt | 26 +++++++- .../skeleton/TapDetectionManager.java | 6 +- .../tracking/trackers/TrackerResetsHandler.kt | 7 +-- .../slimevr/tracking/trackers/TrackerUtils.kt | 13 ++++ .../TrackingChecklistManager.kt | 23 ++++++- solarxr-protocol | 2 +- 17 files changed, 181 insertions(+), 73 deletions(-) create mode 100644 gui/public/images/mounting/MountingFeets.webp create mode 100644 gui/public/images/mounting/MountingFeetsSide.webp diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index b05b26fca9..dff031b0d6 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -237,8 +237,8 @@ reset-reset_all_warning_default-v2 = reset-full = Full Reset reset-mounting = Mounting Calibration -reset-mounting-feet = Reset Feet Mounting -reset-mounting-fingers = Reset Fingers Mounting +reset-mounting-feet = Feet Calibration +reset-mounting-fingers = Fingers Calibration reset-yaw = Yaw Reset ## Serial detection stuff @@ -1133,6 +1133,10 @@ onboarding-automatic_mounting-done-restart = Try again onboarding-automatic_mounting-mounting_reset-title = Mounting Reset onboarding-automatic_mounting-mounting_reset-step-0 = 1. Squat in a "skiing" pose with your legs bent, your upper body tilted forwards, and your arms bent. onboarding-automatic_mounting-mounting_reset-step-1 = 2. Press the "Mounting calibration" button and wait for 3 seconds before the trackers' mounting orientations will reset. + +onboarding-automatic_mounting-mounting_reset-feet-step-0 = 1. Stand on your toes, both feets pointing forward. Alternatively you can do it siting on a chair. +onboarding-automatic_mounting-mounting_reset-feet-step-1 = 2. Press the "Feet calibration" button and wait for 3 seconds before the trackers' mounting orientations will reset. + onboarding-automatic_mounting-preparation-title = Preparation onboarding-automatic_mounting-preparation-v2-step-0 = 1. Press the "Full Reset" button. onboarding-automatic_mounting-preparation-v2-step-1 = 2. Stand upright with your arms to your sides. Make sure to look forward. @@ -1575,6 +1579,7 @@ tracking_checklist-status-partial = {$count -> } tracking_checklist-status-complete = You are prepared to use SlimeVR! tracking_checklist-MOUNTING_CALIBRATION = Perform a mounting calibration +tracking_checklist-FEET_MOUNTING_CALIBRATION = Perform a feet mounting calibration tracking_checklist-FULL_RESET = Perform a full Reset tracking_checklist-FULL_RESET-desc = Some Trackers need a reset to be performed tracking_checklist-STEAMVR_DISCONNECTED = SteamVR not running diff --git a/gui/public/images/mounting/MountingFeets.webp b/gui/public/images/mounting/MountingFeets.webp new file mode 100644 index 0000000000000000000000000000000000000000..e56c6fc138782175cad8c06dd3c71532fe0bf708 GIT binary patch literal 30678 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 N|9AA&OW>c^{|CXwxrYD% literal 0 HcmV?d00001 diff --git a/gui/public/images/mounting/MountingFeetsSide.webp b/gui/public/images/mounting/MountingFeetsSide.webp new file mode 100644 index 0000000000000000000000000000000000000000..3b1240e065fe9b586154e279fe46b8f108c74607 GIT binary patch literal 52158 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�=8=??EVe<^8-v^~;-R`dmgU~;O{AH| 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
); }, + [TrackingChecklistStepId.FEET_MOUNTING_CALIBRATION]: (step, { toggle }) => { + return ( +
+ + +
+
+ mounting reset ski pose +
+
+ mounting reset ski pose +
+
+
+ + {step.ignorable && ( + + )} +
+
+ ); + }, [TrackingChecklistStepId.STAY_ALIGNED_CONFIGURED]: (step, { toggle }) => { return ( <> diff --git a/gui/src/hooks/body-parts.ts b/gui/src/hooks/body-parts.ts index 9ff6a92cee..a2ac2766f0 100644 --- a/gui/src/hooks/body-parts.ts +++ b/gui/src/hooks/body-parts.ts @@ -1,7 +1,62 @@ import { BodyPart } from 'solarxr-protocol'; +export const ALL_BODY_PARTS = [ + BodyPart.NONE, + BodyPart.HEAD, + BodyPart.NECK, + BodyPart.CHEST, + BodyPart.WAIST, + BodyPart.HIP, + BodyPart.LEFT_UPPER_LEG, + BodyPart.RIGHT_UPPER_LEG, + BodyPart.LEFT_LOWER_LEG, + BodyPart.RIGHT_LOWER_LEG, + BodyPart.LEFT_FOOT, + BodyPart.RIGHT_FOOT, + BodyPart.LEFT_LOWER_ARM, + BodyPart.RIGHT_LOWER_ARM, + BodyPart.LEFT_UPPER_ARM, + BodyPart.RIGHT_UPPER_ARM, + BodyPart.LEFT_HAND, + BodyPart.RIGHT_HAND, + BodyPart.LEFT_SHOULDER, + BodyPart.RIGHT_SHOULDER, + BodyPart.UPPER_CHEST, + BodyPart.LEFT_HIP, + BodyPart.RIGHT_HIP, + BodyPart.LEFT_THUMB_METACARPAL, + BodyPart.LEFT_THUMB_PROXIMAL, + BodyPart.LEFT_THUMB_DISTAL, + BodyPart.LEFT_INDEX_PROXIMAL, + BodyPart.LEFT_INDEX_INTERMEDIATE, + BodyPart.LEFT_INDEX_DISTAL, + BodyPart.LEFT_MIDDLE_PROXIMAL, + BodyPart.LEFT_MIDDLE_INTERMEDIATE, + BodyPart.LEFT_MIDDLE_DISTAL, + BodyPart.LEFT_RING_PROXIMAL, + BodyPart.LEFT_RING_INTERMEDIATE, + BodyPart.LEFT_RING_DISTAL, + BodyPart.LEFT_LITTLE_PROXIMAL, + BodyPart.LEFT_LITTLE_INTERMEDIATE, + BodyPart.LEFT_LITTLE_DISTAL, + BodyPart.RIGHT_THUMB_METACARPAL, + BodyPart.RIGHT_THUMB_PROXIMAL, + BodyPart.RIGHT_THUMB_DISTAL, + BodyPart.RIGHT_INDEX_PROXIMAL, + BodyPart.RIGHT_INDEX_INTERMEDIATE, + BodyPart.RIGHT_INDEX_DISTAL, + BodyPart.RIGHT_MIDDLE_PROXIMAL, + BodyPart.RIGHT_MIDDLE_INTERMEDIATE, + BodyPart.RIGHT_MIDDLE_DISTAL, + BodyPart.RIGHT_RING_PROXIMAL, + BodyPart.RIGHT_RING_INTERMEDIATE, + BodyPart.RIGHT_RING_DISTAL, + BodyPart.RIGHT_LITTLE_PROXIMAL, + BodyPart.RIGHT_LITTLE_INTERMEDIATE, + BodyPart.RIGHT_LITTLE_DISTAL, +]; export const FEET_BODY_PARTS = [BodyPart.LEFT_FOOT, BodyPart.RIGHT_FOOT]; -export const FINGER_BODY_PARTS = new Set([ +export const FINGER_BODY_PARTS = [ BodyPart.LEFT_THUMB_METACARPAL, BodyPart.LEFT_THUMB_PROXIMAL, BodyPart.LEFT_THUMB_DISTAL, @@ -32,4 +87,4 @@ export const FINGER_BODY_PARTS = new Set([ BodyPart.RIGHT_LITTLE_PROXIMAL, BodyPart.RIGHT_LITTLE_INTERMEDIATE, BodyPart.RIGHT_LITTLE_DISTAL, -]); +]; diff --git a/gui/src/hooks/reset.ts b/gui/src/hooks/reset.ts index 67f0fd6548..c62938b91b 100644 --- a/gui/src/hooks/reset.ts +++ b/gui/src/hooks/reset.ts @@ -13,6 +13,7 @@ import { useWebsocketAPI } from './websocket-api'; import { useCountdown } from './countdown'; import { useAtomValue } from 'jotai'; import { assignedTrackersAtom } from '@/store/app-store'; +import { FEET_BODY_PARTS, FINGER_BODY_PARTS } from './body-parts'; export type ResetBtnStatus = 'idle' | 'counting' | 'finished'; @@ -21,44 +22,10 @@ export type UseResetOptions = | { type: ResetType.Full | ResetType.Yaw } | { type: ResetType.Mounting; group: MountingResetGroup }; -const feetBodyParts = [BodyPart.LEFT_FOOT, BodyPart.RIGHT_FOOT]; -const fingerBodyParts = [ - BodyPart.LEFT_THUMB_METACARPAL, - BodyPart.LEFT_THUMB_PROXIMAL, - BodyPart.LEFT_THUMB_DISTAL, - BodyPart.LEFT_INDEX_PROXIMAL, - BodyPart.LEFT_INDEX_INTERMEDIATE, - BodyPart.LEFT_INDEX_DISTAL, - BodyPart.LEFT_MIDDLE_PROXIMAL, - BodyPart.LEFT_MIDDLE_INTERMEDIATE, - BodyPart.LEFT_MIDDLE_DISTAL, - BodyPart.LEFT_RING_PROXIMAL, - BodyPart.LEFT_RING_INTERMEDIATE, - BodyPart.LEFT_RING_DISTAL, - BodyPart.LEFT_LITTLE_PROXIMAL, - BodyPart.LEFT_LITTLE_INTERMEDIATE, - BodyPart.LEFT_LITTLE_DISTAL, - BodyPart.RIGHT_THUMB_METACARPAL, - BodyPart.RIGHT_THUMB_PROXIMAL, - BodyPart.RIGHT_THUMB_DISTAL, - BodyPart.RIGHT_INDEX_PROXIMAL, - BodyPart.RIGHT_INDEX_INTERMEDIATE, - BodyPart.RIGHT_INDEX_DISTAL, - BodyPart.RIGHT_MIDDLE_PROXIMAL, - BodyPart.RIGHT_MIDDLE_INTERMEDIATE, - BodyPart.RIGHT_MIDDLE_DISTAL, - BodyPart.RIGHT_RING_PROXIMAL, - BodyPart.RIGHT_RING_INTERMEDIATE, - BodyPart.RIGHT_RING_DISTAL, - BodyPart.RIGHT_LITTLE_PROXIMAL, - BodyPart.RIGHT_LITTLE_INTERMEDIATE, - BodyPart.RIGHT_LITTLE_DISTAL, -]; - export const BODY_PARTS_GROUPS: Record = { default: [], - feet: feetBodyParts, - fingers: fingerBodyParts, + feet: FEET_BODY_PARTS, + fingers: FINGER_BODY_PARTS, }; export function useReset(options: UseResetOptions, onReseted?: () => void) { diff --git a/gui/src/hooks/tracking-checklist.ts b/gui/src/hooks/tracking-checklist.ts index a0aab2f7c6..16766428fa 100644 --- a/gui/src/hooks/tracking-checklist.ts +++ b/gui/src/hooks/tracking-checklist.ts @@ -24,6 +24,8 @@ export const trackingchecklistIdtoLabel: Record 'tracking_checklist-NETWORK_PROFILE_PUBLIC', [TrackingChecklistStepId.MOUNTING_CALIBRATION]: 'tracking_checklist-MOUNTING_CALIBRATION', + [TrackingChecklistStepId.FEET_MOUNTING_CALIBRATION]: + 'tracking_checklist-FEET_MOUNTING_CALIBRATION', [TrackingChecklistStepId.STAY_ALIGNED_CONFIGURED]: 'tracking_checklist-STAY_ALIGNED_CONFIGURED', }; diff --git a/gui/src/store/app-store.ts b/gui/src/store/app-store.ts index 37f447036c..5786b8cc89 100644 --- a/gui/src/store/app-store.ts +++ b/gui/src/store/app-store.ts @@ -109,6 +109,7 @@ export const feetAssignedTrackers = atom((get) => export const fingerAssignedTrackers = atom((get) => get(assignedTrackersAtom).some( - (t) => t.tracker.info?.bodyPart && FINGER_BODY_PARTS.has(t.tracker.info.bodyPart) + (t) => + t.tracker.info?.bodyPart && FINGER_BODY_PARTS.includes(t.tracker.info.bodyPart) ) ); diff --git a/server/core/src/main/java/dev/slimevr/VRServer.kt b/server/core/src/main/java/dev/slimevr/VRServer.kt index 8b6003ed15..a70117ca04 100644 --- a/server/core/src/main/java/dev/slimevr/VRServer.kt +++ b/server/core/src/main/java/dev/slimevr/VRServer.kt @@ -315,7 +315,7 @@ class VRServer @JvmOverloads constructor( queueTask { humanPoseManager.resetTrackersYaw(resetSourceName, bodyParts) } } - fun resetTrackersMounting(resetSourceName: String?, bodyParts: List = TrackerUtils.allBodyPartsButFingers) { + fun resetTrackersMounting(resetSourceName: String?, bodyParts: List? = null) { queueTask { humanPoseManager.resetTrackersMounting(resetSourceName, bodyParts) } } diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt index 8af7ece570..fba5385a0d 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt @@ -567,23 +567,19 @@ class HumanPoseManager(val server: VRServer?) { } @JvmOverloads - fun resetTrackersMounting(resetSourceName: String?, bodyParts: List = TrackerUtils.allBodyPartsButFingers) { - skeleton.resetTrackersMounting(resetSourceName, bodyParts) - //FIXME: Should prob take into consideration the different kinds of mounting - if (server != null) { - server.configManager.vrConfig.resetsConfig.preferedMountingMethod = - MountingMethods.AUTOMATIC - server.trackingChecklistManager.resetMountingCompleted = true; - server.configManager.saveConfig() - } + fun resetTrackersMounting(resetSourceName: String?, bodyParts: List? = null) { + if (server === null) return + + val finalBodyParts = bodyParts + ?: if (server.configManager.vrConfig.resetsConfig.resetMountingFeet) + TrackerUtils.allBodyPartsButFingers + else TrackerUtils.allBodyPartsButFingersAndFeets + + skeleton.resetTrackersMounting(resetSourceName, finalBodyParts) } fun clearTrackersMounting(resetSourceName: String?) { skeleton.clearTrackersMounting(resetSourceName) - if (server != null) { - server.trackingChecklistManager.resetMountingCompleted = false; - server.configManager.saveConfig() - } } @get:ThreadSafe diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt index 03c1a220a4..2de652bc61 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt @@ -1,6 +1,7 @@ package dev.slimevr.tracking.processor.skeleton import dev.slimevr.VRServer +import dev.slimevr.config.MountingMethods import dev.slimevr.config.StayAlignedConfig import dev.slimevr.tracking.processor.Bone import dev.slimevr.tracking.processor.BoneType @@ -1600,16 +1601,29 @@ class HumanSkeleton( referenceRotation = it.getRotation() } - // If onlyFeet is true, feet will be forced to be mounting reset in their reset handlers. - val onlyFeet = bodyParts.isNotEmpty() && bodyParts.all { it == BodyPart.LEFT_FOOT || it == BodyPart.RIGHT_FOOT } for (tracker in trackersToReset) { // Only reset if tracker needsMounting if (tracker != null && tracker.allowMounting && (bodyParts.isEmpty() || bodyParts.contains(tracker.trackerPosition?.bodyPart))) { - tracker.resetsHandler.resetMounting(referenceRotation, onlyFeet) + tracker.resetsHandler.resetMounting(referenceRotation) } } legTweaks.resetBuffer() localizer.reset() + + if (humanPoseManager.server != null) { + humanPoseManager.server.configManager.vrConfig.resetsConfig.preferedMountingMethod = + MountingMethods.AUTOMATIC + humanPoseManager.server.trackingChecklistManager.resetMountingCompleted = bodyParts.any { it -> + val defaultParts = if (humanPoseManager.server.configManager.vrConfig.resetsConfig.resetMountingFeet) + TrackerUtils.allBodyPartsButFingers + else TrackerUtils.allBodyPartsButFingersAndFeets + + return@any defaultParts.contains(it) + } + humanPoseManager.server.trackingChecklistManager.feetResetMountingCompleted = bodyParts.any { TrackerUtils.feetsBodyParts.contains(it) } + humanPoseManager.server.configManager.saveConfig() + } + LogManager.info("[HumanSkeleton] Reset: mounting ($resetSourceName)") } @@ -1627,6 +1641,12 @@ class HumanSkeleton( } legTweaks.resetBuffer() LogManager.info("[HumanSkeleton] Clear: mounting ($resetSourceName)") + + if (humanPoseManager.server != null) { + humanPoseManager.server.trackingChecklistManager.resetMountingCompleted = false + humanPoseManager.server.trackingChecklistManager.feetResetMountingCompleted = false + humanPoseManager.server.configManager.saveConfig() + } } fun updateTapDetectionConfig() { diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/TapDetectionManager.java b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/TapDetectionManager.java index 404f38c6a5..fc42876fe6 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/TapDetectionManager.java +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/TapDetectionManager.java @@ -221,11 +221,7 @@ private void checkMountingReset() { // However, feet being reset or not will end up being decided on a // per-tracker basis // due to the setting being in ResetsConfig.kt - skeleton - .resetTrackersMounting( - resetSourceName, - TrackerUtils.INSTANCE.getAllBodyPartsButFingers() - ); + humanPoseManager.resetTrackersMounting(resetSourceName); mountingResetDetector.resetDetector(); mountingResetAllowPlaySound = true; diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerResetsHandler.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerResetsHandler.kt index 54d52ae1f8..751d57eaeb 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerResetsHandler.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerResetsHandler.kt @@ -35,7 +35,6 @@ class TrackerResetsHandler(val tracker: Tracker) { private var compensateDrift = false private var driftPrediction = false private var driftCompensationEnabled = false - private var resetMountingFeet = false private var armsResetMode = ArmsResetModes.BACK private var yawResetSmoothTime = 0.0f var saveMountingReset = false @@ -161,7 +160,6 @@ class TrackerResetsHandler(val tracker: Tracker) { * Reads/loads reset settings from the given config */ fun readResetConfig(config: ResetsConfig) { - resetMountingFeet = config.resetMountingFeet armsResetMode = config.mode yawResetSmoothTime = config.yawResetSmoothTime saveMountingReset = config.saveMountingReset @@ -383,17 +381,14 @@ class TrackerResetsHandler(val tracker: Tracker) { /** * Perform the math to align the tracker to go forward * and stores it in mountRotFix, and adjusts yawFix - * If forceFeet is true, always reset feet regardless of resetMountingFeet's value. */ - fun resetMounting(reference: Quaternion, forceFeet: Boolean = false) { + fun resetMounting(reference: Quaternion) { if (tracker.trackerDataType == TrackerDataType.FLEX_RESISTANCE) { tracker.trackerFlexHandler.resetMax() tracker.resetFilteringQuats(reference) return } else if (tracker.trackerDataType == TrackerDataType.FLEX_ANGLE) { return - } else if (!resetMountingFeet && tracker.trackerPosition.isFoot() && !forceFeet) { - return } constraintFix = Quaternion.IDENTITY diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerUtils.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerUtils.kt index 8aa27052df..ef51fecf90 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerUtils.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerUtils.kt @@ -102,4 +102,17 @@ object TrackerUtils { BodyPart.RIGHT_HAND, BodyPart.LEFT_SHOULDER, BodyPart.RIGHT_SHOULDER, BodyPart.LEFT_FOOT, BodyPart.RIGHT_FOOT, ) + + val allBodyPartsButFingersAndFeets = listOf( + BodyPart.HEAD, BodyPart.NECK, BodyPart.UPPER_CHEST, + BodyPart.CHEST, BodyPart.WAIST, BodyPart.HIP, + BodyPart.LEFT_UPPER_LEG, BodyPart.RIGHT_UPPER_LEG, BodyPart.LEFT_LOWER_LEG, + BodyPart.RIGHT_LOWER_LEG, BodyPart.LEFT_LOWER_ARM, BodyPart.RIGHT_LOWER_ARM, + BodyPart.LEFT_UPPER_ARM, BodyPart.RIGHT_UPPER_ARM, BodyPart.LEFT_HAND, + BodyPart.RIGHT_HAND, BodyPart.LEFT_SHOULDER, BodyPart.RIGHT_SHOULDER + ) + + val feetsBodyParts = listOf( + BodyPart.LEFT_FOOT, BodyPart.RIGHT_FOOT + ) } diff --git a/server/core/src/main/java/dev/slimevr/trackingchecklist/TrackingChecklistManager.kt b/server/core/src/main/java/dev/slimevr/trackingchecklist/TrackingChecklistManager.kt index 650fdc5749..4019c44c49 100644 --- a/server/core/src/main/java/dev/slimevr/trackingchecklist/TrackingChecklistManager.kt +++ b/server/core/src/main/java/dev/slimevr/trackingchecklist/TrackingChecklistManager.kt @@ -9,6 +9,7 @@ import dev.slimevr.games.vrchat.VRCConfigValidity import dev.slimevr.games.vrchat.VRCConfigValues import dev.slimevr.tracking.trackers.Tracker import dev.slimevr.tracking.trackers.TrackerStatus +import dev.slimevr.tracking.trackers.TrackerUtils import dev.slimevr.tracking.trackers.udp.TrackerDataType import solarxr_protocol.datatypes.DeviceIdT import solarxr_protocol.datatypes.TrackerIdT @@ -31,6 +32,7 @@ class TrackingChecklistManager(private val vrServer: VRServer) : VRCConfigListen // Simple flag set to true if reset mounting was performed at least once. // This value is only runtime and never saved var resetMountingCompleted = false + var feetResetMountingCompleted = false init { vrServer.vrcConfigManager.addListener(this) @@ -125,6 +127,17 @@ class TrackingChecklistManager(private val vrServer: VRServer) : VRCConfigListen }, ) + steps.add( + TrackingChecklistStepT().apply { + id = TrackingChecklistStepId.FEET_MOUNTING_CALIBRATION + valid = false + enabled = false + optional = false + ignorable = true + visibility = TrackingChecklistStepVisibility.ALWAYS + }, + ) + steps.add( TrackingChecklistStepT().apply { id = TrackingChecklistStepId.UNASSIGNED_HMD @@ -163,7 +176,7 @@ class TrackingChecklistManager(private val vrServer: VRServer) : VRCConfigListen assignedTrackers.filter { it.isImu() && it.trackerDataType != TrackerDataType.FLEX_ANGLE } val trackersWithError = - imuTrackers.filter { it.status === TrackerStatus.ERROR } + imuTrackers.filter { it.status == TrackerStatus.ERROR } updateValidity( TrackingChecklistStepId.TRACKER_ERROR, trackersWithError.isEmpty(), @@ -279,6 +292,14 @@ class TrackingChecklistManager(private val vrServer: VRServer) : VRCConfigListen it.enabled = vrServer.configManager.vrConfig.resetsConfig.preferedMountingMethod == MountingMethods.AUTOMATIC } + updateValidity(TrackingChecklistStepId.FEET_MOUNTING_CALIBRATION, feetResetMountingCompleted) { + it.enabled = + vrServer.configManager.vrConfig.resetsConfig.preferedMountingMethod == MountingMethods.AUTOMATIC + && !vrServer.configManager.vrConfig.resetsConfig.resetMountingFeet + && imuTrackers.any { t -> TrackerUtils.feetsBodyParts.contains(t.trackerPosition?.bodyPart) } + } + + updateValidity(TrackingChecklistStepId.STAY_ALIGNED_CONFIGURED, vrServer.configManager.vrConfig.stayAlignedConfig.enabled) listeners.forEach { it.onStepsUpdate() } diff --git a/solarxr-protocol b/solarxr-protocol index c34b07b0d0..b2b791764f 160000 --- a/solarxr-protocol +++ b/solarxr-protocol @@ -1 +1 @@ -Subproject commit c34b07b0d0c768f88bfccd1bae4dc566dbbe3f39 +Subproject commit b2b791764fb9298634921b7bce46fded2dcc17e7 From 84bf6a0fee10a0aaafaca1ef365dfa41b9c85be7 Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Mon, 22 Sep 2025 20:07:08 +0200 Subject: [PATCH 24/34] Small ui changes --- gui/src/components/Toolbar.tsx | 6 +-- gui/src/components/tracker/TrackerCard.tsx | 44 +++++++++++++++---- .../processor/skeleton/HumanSkeleton.kt | 16 ++++--- 3 files changed, 48 insertions(+), 18 deletions(-) diff --git a/gui/src/components/Toolbar.tsx b/gui/src/components/Toolbar.tsx index e0010e115d..d9e4c83ca7 100644 --- a/gui/src/components/Toolbar.tsx +++ b/gui/src/components/Toolbar.tsx @@ -135,15 +135,15 @@ export function Toolbar({ showSettings }: { showSettings: boolean }) { <>
-
-
+
+
-
+
-
- +
+
+ {warning && ( +
+ +
+ )} +
+ +
-
- + +
+ {trackerName} @@ -212,8 +234,8 @@ export function TrackerCard({ 'rounded-lg overflow-hidden transition-[box-shadow] duration-200 ease-linear', interactable && 'hover:bg-background-50 cursor-pointer', outlined && 'outline outline-2 outline-accent-background-40', - warning && - 'outline outline-2 -outline-offset-2 outline-status-warning', + // warning && + // 'outline outline-2 -outline-offset-2 outline-status-warning', bg )} style={ @@ -226,7 +248,13 @@ export function TrackerCard({ : {} } > - {smol && } + {smol && ( + + )} {!smol && }
{showUpdate && diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt index 2de652bc61..c63e5112db 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt @@ -1613,14 +1613,16 @@ class HumanSkeleton( if (humanPoseManager.server != null) { humanPoseManager.server.configManager.vrConfig.resetsConfig.preferedMountingMethod = MountingMethods.AUTOMATIC - humanPoseManager.server.trackingChecklistManager.resetMountingCompleted = bodyParts.any { it -> - val defaultParts = if (humanPoseManager.server.configManager.vrConfig.resetsConfig.resetMountingFeet) - TrackerUtils.allBodyPartsButFingers - else TrackerUtils.allBodyPartsButFingersAndFeets + if (!humanPoseManager.server.trackingChecklistManager.resetMountingCompleted) + humanPoseManager.server.trackingChecklistManager.resetMountingCompleted = bodyParts.any { it -> + val defaultParts = if (humanPoseManager.server.configManager.vrConfig.resetsConfig.resetMountingFeet) + TrackerUtils.allBodyPartsButFingers + else TrackerUtils.allBodyPartsButFingersAndFeets - return@any defaultParts.contains(it) - } - humanPoseManager.server.trackingChecklistManager.feetResetMountingCompleted = bodyParts.any { TrackerUtils.feetsBodyParts.contains(it) } + return@any defaultParts.contains(it) + } + if (!humanPoseManager.server.trackingChecklistManager.feetResetMountingCompleted) + humanPoseManager.server.trackingChecklistManager.feetResetMountingCompleted = bodyParts.any { TrackerUtils.feetsBodyParts.contains(it) } humanPoseManager.server.configManager.saveConfig() } From 49ec25eb1c92b0166ea8000736ccb2e293c887cd Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Wed, 24 Sep 2025 05:24:13 +0200 Subject: [PATCH 25/34] Redone tracker table list -> Faster + easier to customize --- gui/src/components/commons/Tooltip.tsx | 31 +- gui/src/components/home/Home.tsx | 4 +- gui/src/components/tracker/TrackerCard.tsx | 32 +- gui/src/components/tracker/TrackersTable.tsx | 518 +++++++++---------- gui/src/hooks/tracking-checklist.ts | 17 +- 5 files changed, 301 insertions(+), 301 deletions(-) diff --git a/gui/src/components/commons/Tooltip.tsx b/gui/src/components/commons/Tooltip.tsx index 37d249be0c..4c15b50e84 100644 --- a/gui/src/components/commons/Tooltip.tsx +++ b/gui/src/components/commons/Tooltip.tsx @@ -8,6 +8,7 @@ import { useLayoutEffect, MutableRefObject, useMemo, + createElement, } from 'react'; import { createPortal } from 'react-dom'; import { Typography } from './Typography'; @@ -22,6 +23,8 @@ interface TooltipProps { mode?: 'corner' | 'center'; variant?: 'auto' | 'drawer' | 'floating'; disabled?: boolean; + tag?: string; + spacing?: number; } interface TooltipPos { @@ -83,10 +86,9 @@ const getFloatingTooltipPosition = ( blockedDirections: Direction[], mode: TooltipProps['mode'], childrenRect: DOMRect, - tooltipRect: DOMRect + tooltipRect: DOMRect, + spacing: number ) => { - const spacing = 10; - const getPosition = ( direction: TooltipProps['preferedDirection'] ): TooltipPos => { @@ -234,10 +236,14 @@ export function FloatingTooltip({ blockedDirections = [], mode, children, + spacing, }: { - childRef: MutableRefObject; + childRef: MutableRefObject; children: ReactNode; -} & Pick) { +} & Pick< + TooltipProps, + 'mode' | 'preferedDirection' | 'blockedDirections' | 'spacing' +>) { const tooltipRef = useRef(null); const [tooltipStyle, setTooltipStyle] = useState(); @@ -254,7 +260,8 @@ export function FloatingTooltip({ blockedDirections, mode, childrenRect, - tooltipRect + tooltipRect, + spacing ?? 20 ) ); }; @@ -321,7 +328,7 @@ export function DrawerTooltip({ childRef, }: { children: ReactNode; - childRef: MutableRefObject; + childRef: MutableRefObject; }) { const touchTimestamp = useRef(0); const touchTimeout = useRef(0); @@ -456,8 +463,10 @@ export function Tooltip({ mode = 'center', variant = 'auto', disabled = false, + tag = 'div', + spacing = 20, }: TooltipProps) { - const childRef = useRef(null); + const childRef = useRef(null); const { isMobile } = useBreakpoint('mobile'); let portal = null; @@ -470,6 +479,7 @@ export function Tooltip({ blockedDirections={blockedDirections} mode={mode} childRef={childRef} + spacing={spacing} > {content} @@ -486,6 +496,7 @@ export function Tooltip({ preferedDirection={preferedDirection} mode={mode} childRef={childRef} + spacing={spacing} > {content} @@ -493,9 +504,7 @@ export function Tooltip({ return ( <> -
- {children} -
+ {createElement(tag, { className: 'contents', ref: childRef }, children)} {!disabled && createPortal(portal, document.body)} ); diff --git a/gui/src/components/home/Home.tsx b/gui/src/components/home/Home.tsx index 5acc8ac27e..76f4a2c76d 100644 --- a/gui/src/components/home/Home.tsx +++ b/gui/src/components/home/Home.tsx @@ -59,11 +59,11 @@ export function Home() { showUpdates interactable warning={ - !!hightlightedTrackers.find( + !!hightlightedTrackers?.trackers.find( (t) => t?.deviceId?.id === tracker.trackerId?.deviceId?.id && t?.trackerNum === tracker.trackerId?.trackerNum - ) + ) && hightlightedTrackers.step } /> ))} diff --git a/gui/src/components/tracker/TrackerCard.tsx b/gui/src/components/tracker/TrackerCard.tsx index d1eaaa0230..7c8a55ce5e 100644 --- a/gui/src/components/tracker/TrackerCard.tsx +++ b/gui/src/components/tracker/TrackerCard.tsx @@ -1,9 +1,11 @@ import { useConfig } from '@/hooks/config'; -import { MouseEventHandler } from 'react'; +import { MouseEventHandler, useMemo } from 'react'; import { DeviceDataT, TrackerDataT, TrackerStatus as TrackerStatusEnum, + TrackingChecklistStep, + TrackingChecklistStepT, } from 'solarxr-protocol'; import { Typography } from '@/components/commons/Typography'; import { TrackerBattery } from './TrackerBattery'; @@ -19,6 +21,7 @@ import { Tooltip } from '@/components/commons/Tooltip'; import { Localized } from '@fluent/react'; import { checkForUpdate } from '@/hooks/firmware-update'; import { WarningIcon } from '@/components/commons/icon/WarningIcon'; +import { trackingchecklistIdtoLabel } from '@/hooks/tracking-checklist'; function UpdateIcon({ showUpdate, @@ -130,7 +133,7 @@ function TrackerSmol({ }: { tracker: TrackerDataT; device?: DeviceDataT; - warning?: boolean; + warning?: TrackingChecklistStepT | boolean; }) { const { useName } = useTracker(tracker); @@ -213,7 +216,7 @@ export function TrackerCard({ bg?: string; shakeHighlight?: boolean; onClick?: MouseEventHandler; - warning?: boolean; + warning?: TrackingChecklistStepT | boolean; showUpdates?: boolean; }) { const { currentFirmwareRelease } = useAppContext(); @@ -249,11 +252,24 @@ export function TrackerCard({ } > {smol && ( - + + + +
+ ) + } + > + + )} {!smol && }
diff --git a/gui/src/components/tracker/TrackersTable.tsx b/gui/src/components/tracker/TrackersTable.tsx index 75d6b2fbdb..97c1dbe716 100644 --- a/gui/src/components/tracker/TrackersTable.tsx +++ b/gui/src/components/tracker/TrackersTable.tsx @@ -1,11 +1,10 @@ -import { useLocalization } from '@fluent/react'; import classNames from 'classnames'; import { IPv4 } from 'ip-num/IPNumber'; -import { MouseEventHandler, ReactNode, useMemo, useState } from 'react'; +import { createContext, ReactNode, useContext, useMemo } from 'react'; import { TrackerDataT, - TrackerIdT, TrackerStatus as TrackerStatusEnum, + TrackingChecklistStepT, } from 'solarxr-protocol'; import { useConfig } from '@/hooks/config'; import { useTracker } from '@/hooks/tracker'; @@ -17,35 +16,13 @@ import { TrackerStatus } from './TrackerStatus'; import { TrackerWifi } from './TrackerWifi'; import { FlatDeviceTracker } from '@/store/app-store'; import { StayAlignedInfo } from '@/components/stay-aligned/StayAlignedInfo'; -import { useTrackingChecklist } from '@/hooks/tracking-checklist'; - -enum DisplayColumn { - NAME, - TYPE, - BATTERY, - PING, - TPS, - ROTATION, - TEMPERATURE, - LINEAR_ACCELERATION, - POSITION, - STAY_ALIGNED, - URL, -} - -const displayColumns: { [k: string]: boolean } = { - [DisplayColumn.NAME]: true, - [DisplayColumn.TYPE]: true, - [DisplayColumn.BATTERY]: true, - [DisplayColumn.PING]: true, - [DisplayColumn.TPS]: true, - [DisplayColumn.ROTATION]: true, - [DisplayColumn.TEMPERATURE]: true, - [DisplayColumn.LINEAR_ACCELERATION]: true, - [DisplayColumn.POSITION]: true, - [DisplayColumn.STAY_ALIGNED]: true, - [DisplayColumn.URL]: true, -}; +import { + HightlightedTrackers, + trackingchecklistIdtoLabel, + useTrackingChecklist, +} from '@/hooks/tracking-checklist'; +import { Tooltip } from '@/components/commons/Tooltip'; +import { WarningIcon } from '@/components/commons/icon/WarningIcon'; const isSlime = ({ device }: FlatDeviceTracker) => device?.hardwareInfo?.manufacturer === 'SlimeVR' || @@ -57,15 +34,36 @@ const getDeviceName = ({ device }: FlatDeviceTracker) => const getTrackerName = ({ tracker }: FlatDeviceTracker) => tracker?.info?.customName?.toString() || ''; -export function TrackerNameCell({ tracker }: { tracker: TrackerDataT }) { +export function TrackerNameCell({ + tracker, + warning, +}: { + tracker: TrackerDataT; + warning: TrackingChecklistStepT | boolean; +}) { const { useName } = useTracker(tracker); const name = useName(); return ( -
-
- +
+
+ {warning && ( +
+ +
+ )} +
+ +
@@ -102,60 +100,191 @@ export function TrackerRotCell({ ); } -export function RowContainer({ +function Header({ + name, + className, + first = false, + last = false, + show = true, +}: { + first?: boolean; + last?: boolean; + name: string; + className?: string; + show?: boolean; +}) { + return ( + +
+ +
+ + ); +} + +function Cell({ children, - rounded = 'none', - hover, - tracker, - onClick, - onMouseOver, - onMouseOut, - warning, + first = false, + last = false, + show = true, }: { children: ReactNode; - rounded?: 'left' | 'right' | 'none'; - hover: boolean; - tracker: TrackerDataT; - onClick?: MouseEventHandler; - onMouseOver?: MouseEventHandler; - onMouseOut?: MouseEventHandler; - warning: boolean; + first?: boolean; + last?: boolean; + show?: boolean; }) { + const { tracker } = useContext(TrackerRowProvider); const { useVelocity } = useTracker(tracker); const velocity = useVelocity(); return ( -
+
{children}
-
+ + ); +} + +const TrackerRowProvider = createContext(undefined as never); + +function Row({ + data, + hightlightedTrackers, + clickedTracker, +}: { + data: FlatDeviceTracker; + hightlightedTrackers: HightlightedTrackers | undefined; + clickedTracker: (tracker: TrackerDataT) => void; +}) { + const { config } = useConfig(); + const fontColor = config?.devSettings?.highContrast ? 'primary' : 'secondary'; + const moreInfo = config?.devSettings?.moreInfo; + + const { tracker, device } = data; + + const warning = + !!hightlightedTrackers?.trackers.find( + (t) => + t?.deviceId?.id === tracker.trackerId?.deviceId?.id && + t?.trackerNum === tracker.trackerId?.trackerNum + ) && hightlightedTrackers.step; + + return ( + + + + +
+ ) + } + tag="tr" + spacing={-5} + > + clickedTracker(tracker)}> + + + + + + {device?.hardwareInfo?.manufacturer || '--'} + + + + {device?.hardwareStatus?.batteryPctEstimate != null && ( + + )} + + + {(device?.hardwareStatus?.rssi != null || + device?.hardwareStatus?.ping != null) && ( + + )} + + + {tracker.tps && ( + {tracker.tps} + )} + + + + + + {tracker?.temp && tracker?.temp?.temp != 0 && ( + + {tracker.temp.temp.toFixed(2)} + + )} + + + {tracker.linearAcceleration && ( + + {formatVector3(tracker.linearAcceleration, 1)} + + )} + + + {tracker.position && ( + + {formatVector3(tracker.position, 2)} + + )} + + + + + + + udp:// + {IPv4.fromNumber( + device?.hardwareInfo?.ipAddress?.addr || 0 + ).toString()} + + + + + ); } @@ -166,15 +295,9 @@ export function TrackersTable({ clickedTracker: (tracker: TrackerDataT) => void; flatTrackers: FlatDeviceTracker[]; }) { - const { l10n } = useLocalization(); - const [hoverTracker, setHoverTracker] = useState(null); const { config } = useConfig(); const { hightlightedTrackers } = useTrackingChecklist(); - const trackerEqual = (id: TrackerIdT | null) => - id?.trackerNum == hoverTracker?.trackerNum && - (!id?.deviceId || id.deviceId.id == hoverTracker?.deviceId?.id); - const filteringEnabled = config?.debug && config?.devSettings?.filterSlimesAndHMD; const sortingEnabled = config?.debug && config?.devSettings?.sortByName; @@ -190,207 +313,54 @@ export function TrackersTable({ return list; }, [flatTrackers, filteringEnabled, sortingEnabled]); - const fontColor = config?.devSettings?.highContrast ? 'primary' : 'secondary'; const moreInfo = config?.devSettings?.moreInfo; - const hasTemperature = !!filteredSortedTrackers.find( - ({ tracker }) => tracker?.temp && tracker?.temp?.temp != 0 - ); - displayColumns[DisplayColumn.TEMPERATURE] = hasTemperature || false; - displayColumns[DisplayColumn.POSITION] = moreInfo || false; - displayColumns[DisplayColumn.LINEAR_ACCELERATION] = moreInfo || false; - displayColumns[DisplayColumn.STAY_ALIGNED] = moreInfo || false; - displayColumns[DisplayColumn.URL] = moreInfo || false; - const displayColumnsKeys = Object.keys(displayColumns).filter( - (k) => displayColumns[k] - ); - const firstColumnId = +displayColumnsKeys[0]; - const lastColumnId = +displayColumnsKeys[displayColumnsKeys.length - 1]; - - function column({ - id, - label, - labelClassName, - row, - }: { - id: DisplayColumn; - label: string; - labelClassName?: string; - row: (data: FlatDeviceTracker) => ReactNode | null; - }) { - let rounded: 'left' | 'right' | 'none' = 'none'; - if (firstColumnId === id) rounded = 'left'; - else if (lastColumnId === id) rounded = 'right'; - - if (!displayColumns[id]) return <>; - - return ( -
-
- {label} -
- {filteredSortedTrackers.map((data, index) => ( - clickedTracker(data.tracker)} - hover={trackerEqual(data.tracker.trackerId)} - onMouseOver={() => setHoverTracker(data.tracker.trackerId)} - onMouseOut={() => setHoverTracker(null)} - warning={ - !!hightlightedTrackers.find( - (t) => - t?.deviceId?.id === data.tracker.trackerId?.deviceId?.id && - t?.trackerNum === data.tracker.trackerId?.trackerNum - ) - } - > - {row(data) || <>} - - ))} -
- ); - } - return ( -
- {column({ - id: DisplayColumn.NAME, - label: l10n.getString('tracker-table-column-name'), - row: ({ tracker }) => ( - - ), - })} - - {column({ - id: DisplayColumn.TYPE, - label: l10n.getString('tracker-table-column-type'), - row: ({ device }) => ( - - {device?.hardwareInfo?.manufacturer || '--'} - - ), - })} - - {column({ - id: DisplayColumn.BATTERY, - label: l10n.getString('tracker-table-column-battery'), - row: ({ device, tracker }) => - device?.hardwareStatus?.batteryPctEstimate != null && ( - - ), - })} - - {column({ - id: DisplayColumn.PING, - label: l10n.getString('tracker-table-column-ping'), - row: ({ device, tracker }) => - (device?.hardwareStatus?.rssi != null || - device?.hardwareStatus?.ping != null) && ( - - ), - })} - - {column({ - id: DisplayColumn.TPS, - label: l10n.getString('tracker-table-column-tps'), - row: ({ tracker }) => ( - - {tracker?.tps != null ? <>{tracker.tps} : <>} - - ), - })} - - {column({ - id: DisplayColumn.ROTATION, - label: l10n.getString('tracker-table-column-rotation'), - labelClassName: classNames({ - 'w-44': config?.devSettings?.preciseRotation, - 'w-32': !config?.devSettings?.preciseRotation, - }), - row: ({ tracker }) => ( - - ), - })} - - {column({ - id: DisplayColumn.TEMPERATURE, - label: l10n.getString('tracker-table-column-temperature'), - row: ({ tracker }) => - tracker?.temp && - tracker?.temp?.temp != 0 && ( - - {`${tracker.temp.temp.toFixed(2)}`} - - ), - })} - - {column({ - id: DisplayColumn.LINEAR_ACCELERATION, - label: l10n.getString('tracker-table-column-linear-acceleration'), - labelClassName: 'w-36', - row: ({ tracker }) => - tracker.linearAcceleration && ( - - {formatVector3(tracker.linearAcceleration, 1)} - - ), - })} - - {column({ - id: DisplayColumn.POSITION, - label: l10n.getString('tracker-table-column-position'), - labelClassName: 'w-36', - row: ({ tracker }) => - tracker.position && ( - - {formatVector3(tracker.position, 2)} - - ), - })} - - {column({ - id: DisplayColumn.STAY_ALIGNED, - label: l10n.getString('tracker-table-column-stay_aligned'), - labelClassName: 'w-36', - row: ({ tracker }) => ( - - ), - })} - - {column({ - id: DisplayColumn.URL, - label: l10n.getString('tracker-table-column-url'), - row: ({ device }) => ( - - udp:// - {IPv4.fromNumber( - device?.hardwareInfo?.ipAddress?.addr || 0 - ).toString()} - - ), - })} +
+ + +
+
+
+
+
+
+
+
+
+
+
+ + {filteredSortedTrackers.map((data) => ( + + ))} +
); } diff --git a/gui/src/hooks/tracking-checklist.ts b/gui/src/hooks/tracking-checklist.ts index 16766428fa..c773522f65 100644 --- a/gui/src/hooks/tracking-checklist.ts +++ b/gui/src/hooks/tracking-checklist.ts @@ -6,6 +6,7 @@ import { TrackingChecklistStepVisibility, IgnoreTrackingChecklistStepRequestT, RpcMessage, + TrackerIdT, } from 'solarxr-protocol'; import { useWebsocketAPI } from './websocket-api'; import { createContext, useContext, useEffect, useMemo, useState } from 'react'; @@ -39,6 +40,10 @@ export type TrackingChecklistStep = TrackingChecklistStepT & { status: TrackingChecklistStepStatus; firstRequired: boolean; }; +export type HightlightedTrackers = { + step: TrackingChecklistStep; + trackers: Array; +}; const stepVisibility = ({ visibility, status, firstRequired }: TrackingChecklistStep) => firstRequired || @@ -124,15 +129,15 @@ export function provideTrackingChecklist() { [steps] ); - const hightlightedTrackers = useMemo(() => { - if (!firstRequired || !firstRequired.extraData) return []; + const hightlightedTrackers: HightlightedTrackers | undefined = useMemo(() => { + if (!firstRequired || !firstRequired.extraData) return undefined; if ('trackersId' in firstRequired.extraData) { - return firstRequired.extraData.trackersId; + return { step: firstRequired, trackers: firstRequired.extraData.trackersId }; } - if ('trackerId' in firstRequired.extraData) { - return [firstRequired.extraData.trackerId]; + if ('trackerId' in firstRequired.extraData && firstRequired.extraData.trackerId) { + return { step: firstRequired, trackers: [firstRequired.extraData.trackerId] }; } - return []; + return { step: firstRequired, trackers: [] }; }, [firstRequired]); const progress = useMemo(() => { From 53ca5b3bed0b89f0efdb11976087fea62d98b525 Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Wed, 24 Sep 2025 06:55:14 +0200 Subject: [PATCH 26/34] Better tooltips --- gui/src/components/Toolbar.tsx | 2 +- gui/src/components/tracker/TrackerCard.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/gui/src/components/Toolbar.tsx b/gui/src/components/Toolbar.tsx index d9e4c83ca7..c48dc254a0 100644 --- a/gui/src/components/Toolbar.tsx +++ b/gui/src/components/Toolbar.tsx @@ -135,7 +135,7 @@ export function Toolbar({ showSettings }: { showSettings: boolean }) { <>
-
+
diff --git a/gui/src/components/tracker/TrackerCard.tsx b/gui/src/components/tracker/TrackerCard.tsx index 7c8a55ce5e..1aeb38a0b0 100644 --- a/gui/src/components/tracker/TrackerCard.tsx +++ b/gui/src/components/tracker/TrackerCard.tsx @@ -253,8 +253,9 @@ export function TrackerCard({ > {smol && ( From 32ced2284f3e44cba43ba29973c4ad5128277177 Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Wed, 24 Sep 2025 07:01:19 +0200 Subject: [PATCH 27/34] Lint --- gui/src/components/home/Home.tsx | 6 +++--- .../mounting/mounting-steps/MountingReset.tsx | 1 + gui/src/components/tracker/TrackersTable.tsx | 14 +++++++------- gui/src/hooks/tracking-checklist.ts | 6 +++--- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/gui/src/components/home/Home.tsx b/gui/src/components/home/Home.tsx index 76f4a2c76d..ae6228bf5a 100644 --- a/gui/src/components/home/Home.tsx +++ b/gui/src/components/home/Home.tsx @@ -15,7 +15,7 @@ export function Home() { const { l10n } = useLocalization(); const { config } = useConfig(); const trackers = useAtomValue(flatTrackersAtom); - const { hightlightedTrackers } = useTrackingChecklist(); + const { highlightedTrackers } = useTrackingChecklist(); const navigate = useNavigate(); const sendToSettings = (tracker: TrackerDataT) => { @@ -59,11 +59,11 @@ export function Home() { showUpdates interactable warning={ - !!hightlightedTrackers?.trackers.find( + !!highlightedTrackers?.trackers.find( (t) => t?.deviceId?.id === tracker.trackerId?.deviceId?.id && t?.trackerNum === tracker.trackerId?.trackerNum - ) && hightlightedTrackers.step + ) && highlightedTrackers.step } /> ))} diff --git a/gui/src/components/onboarding/pages/mounting/mounting-steps/MountingReset.tsx b/gui/src/components/onboarding/pages/mounting/mounting-steps/MountingReset.tsx index 78bc3a293e..47340f82c8 100644 --- a/gui/src/components/onboarding/pages/mounting/mounting-steps/MountingReset.tsx +++ b/gui/src/components/onboarding/pages/mounting/mounting-steps/MountingReset.tsx @@ -59,6 +59,7 @@ export function MountingResetStep({
diff --git a/gui/src/components/tracker/TrackersTable.tsx b/gui/src/components/tracker/TrackersTable.tsx index 97c1dbe716..c7e241f944 100644 --- a/gui/src/components/tracker/TrackersTable.tsx +++ b/gui/src/components/tracker/TrackersTable.tsx @@ -17,7 +17,7 @@ import { TrackerWifi } from './TrackerWifi'; import { FlatDeviceTracker } from '@/store/app-store'; import { StayAlignedInfo } from '@/components/stay-aligned/StayAlignedInfo'; import { - HightlightedTrackers, + highlightedTrackers, trackingchecklistIdtoLabel, useTrackingChecklist, } from '@/hooks/tracking-checklist'; @@ -167,11 +167,11 @@ const TrackerRowProvider = createContext(undefined as never); function Row({ data, - hightlightedTrackers, + highlightedTrackers, clickedTracker, }: { data: FlatDeviceTracker; - hightlightedTrackers: HightlightedTrackers | undefined; + highlightedTrackers: highlightedTrackers | undefined; clickedTracker: (tracker: TrackerDataT) => void; }) { const { config } = useConfig(); @@ -181,11 +181,11 @@ function Row({ const { tracker, device } = data; const warning = - !!hightlightedTrackers?.trackers.find( + !!highlightedTrackers?.trackers.find( (t) => t?.deviceId?.id === tracker.trackerId?.deviceId?.id && t?.trackerNum === tracker.trackerId?.trackerNum - ) && hightlightedTrackers.step; + ) && highlightedTrackers.step; return ( @@ -296,7 +296,7 @@ export function TrackersTable({ flatTrackers: FlatDeviceTracker[]; }) { const { config } = useConfig(); - const { hightlightedTrackers } = useTrackingChecklist(); + const { highlightedTrackers } = useTrackingChecklist(); const filteringEnabled = config?.debug && config?.devSettings?.filterSlimesAndHMD; @@ -357,7 +357,7 @@ export function TrackersTable({ ))} diff --git a/gui/src/hooks/tracking-checklist.ts b/gui/src/hooks/tracking-checklist.ts index c773522f65..b63cae9c97 100644 --- a/gui/src/hooks/tracking-checklist.ts +++ b/gui/src/hooks/tracking-checklist.ts @@ -40,7 +40,7 @@ export type TrackingChecklistStep = TrackingChecklistStepT & { status: TrackingChecklistStepStatus; firstRequired: boolean; }; -export type HightlightedTrackers = { +export type highlightedTrackers = { step: TrackingChecklistStep; trackers: Array; }; @@ -129,7 +129,7 @@ export function provideTrackingChecklist() { [steps] ); - const hightlightedTrackers: HightlightedTrackers | undefined = useMemo(() => { + const highlightedTrackers: highlightedTrackers | undefined = useMemo(() => { if (!firstRequired || !firstRequired.extraData) return undefined; if ('trackersId' in firstRequired.extraData) { return { step: firstRequired, trackers: firstRequired.extraData.trackersId }; @@ -170,7 +170,7 @@ export function provideTrackingChecklist() { return { ...steps, firstRequired, - hightlightedTrackers, + highlightedTrackers, progress, completion, warnings, From e54b1556241edfb65cd8d36ba8422e466d9ed39f Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Wed, 24 Sep 2025 07:07:30 +0200 Subject: [PATCH 28/34] More lint --- gui/src/components/commons/Tooltip.tsx | 46 +++++++++---------- .../onboarding/pages/CalibrationTutorial.tsx | 9 ++-- .../settings/pages/InterfaceSettings.tsx | 23 ++++++---- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/gui/src/components/commons/Tooltip.tsx b/gui/src/components/commons/Tooltip.tsx index 4c15b50e84..69cdfff349 100644 --- a/gui/src/components/commons/Tooltip.tsx +++ b/gui/src/components/commons/Tooltip.tsx @@ -387,29 +387,29 @@ export function DrawerTooltip({ setDrawerStyle(undefined); }; - // useLayoutEffect(() => { - // if (childRef.current && childRef.current.children[0]) { - // const elem = childRef.current.children[0] as HTMLElement; - - // elem.addEventListener('mousedown', touchStart); // for debug on desktop - // elem.addEventListener('mouseup', touchEnd); // for debug on desktop - // elem.addEventListener('scroll', scroll); - - // elem.addEventListener('click', touchEnd); - // elem.addEventListener('touchstart', touchStart); - // elem.addEventListener('touchend', touchEnd); - - // return () => { - // elem.removeEventListener('mousedown', touchStart); // for debug on desktop - // elem.removeEventListener('mouseup', touchEnd); // for debug on desktop - // elem.removeEventListener('scroll', scroll); - - // elem.removeEventListener('touchstart', touchStart); - // elem.removeEventListener('touchend', touchEnd); - // clearTimeout(touchTimeout.current); - // }; - // } - // }, []); + useLayoutEffect(() => { + if (childRef.current && childRef.current.children[0]) { + const elem = childRef.current.children[0] as HTMLElement; + + elem.addEventListener('mousedown', touchStart); // for debug on desktop + elem.addEventListener('mouseup', touchEnd); // for debug on desktop + elem.addEventListener('scroll', scroll); + + elem.addEventListener('click', touchEnd); + elem.addEventListener('touchstart', touchStart); + elem.addEventListener('touchend', touchEnd); + + return () => { + elem.removeEventListener('mousedown', touchStart); // for debug on desktop + elem.removeEventListener('mouseup', touchEnd); // for debug on desktop + elem.removeEventListener('scroll', scroll); + + elem.removeEventListener('touchstart', touchStart); + elem.removeEventListener('touchend', touchEnd); + clearTimeout(touchTimeout.current); + }; + } + }, []); // FIXME: Completely broken not sure why. Will be solved when tooltips on mobile actually work return ( diff --git a/gui/src/components/onboarding/pages/CalibrationTutorial.tsx b/gui/src/components/onboarding/pages/CalibrationTutorial.tsx index 3cc3759fb1..395b59d3e4 100644 --- a/gui/src/components/onboarding/pages/CalibrationTutorial.tsx +++ b/gui/src/components/onboarding/pages/CalibrationTutorial.tsx @@ -9,7 +9,7 @@ import { useCountdown } from '@/hooks/countdown'; import classNames from 'classnames'; import { TaybolIcon } from '@/components/commons/icon/TaybolIcon'; import { useRestCalibrationTrackers } from '@/hooks/imu-logic'; -import { averageVector, Vector3FromVec3fT } from '@/maths/vector3'; +import { Vector3FromVec3fT } from '@/maths/vector3'; import { Vector3 } from 'three'; import { useTimeout } from '@/hooks/timeout'; import { useAtomValue } from 'jotai'; @@ -37,7 +37,10 @@ export function CalibrationTutorialPage() { const [settled, setSettled] = useState(false); const { timer, isCounting, startCountdown, abortCountdown } = useCountdown({ duration: settled ? IMU_CALIBRATION_TIME : IMU_SETTLE_TIME, - onCountdownEnd: () => settled ? setCalibrationStatus(CalibrationStatus.SUCCESS) : setSettled(true), + onCountdownEnd: () => + settled + ? setCalibrationStatus(CalibrationStatus.SUCCESS) + : setSettled(true), }); useTimeout(() => setSkipButton(true), 10000); const connectedIMUTrackers = useAtomValue(connectedIMUTrackersAtom); @@ -56,7 +59,7 @@ export function CalibrationTutorialPage() { return false; const trackerId = x.device.id.id; -// x.tracker.trackerId.trackerNum + (x.tracker.trackerId.deviceId.id << 8); + // x.tracker.trackerId.trackerNum + (x.tracker.trackerId.deviceId.id << 8); const lastValue = lastValueMap.current.get(trackerId) ?? new Vector3(); lastValueMap.current.set(trackerId, lastValue); diff --git a/gui/src/components/settings/pages/InterfaceSettings.tsx b/gui/src/components/settings/pages/InterfaceSettings.tsx index d6a627e6f8..4cfdd0d668 100644 --- a/gui/src/components/settings/pages/InterfaceSettings.tsx +++ b/gui/src/components/settings/pages/InterfaceSettings.tsx @@ -284,16 +284,19 @@ export function InterfaceSettings() { )}
-
- +
+
+ +
+ {config?.debug && }
From 120c9d7abe4814dd552ab33e88cd6df71a45af3b Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Wed, 24 Sep 2025 07:15:20 +0200 Subject: [PATCH 29/34] More lint --- gui/src/components/tracker/TrackerCard.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gui/src/components/tracker/TrackerCard.tsx b/gui/src/components/tracker/TrackerCard.tsx index 1aeb38a0b0..b3d9971fec 100644 --- a/gui/src/components/tracker/TrackerCard.tsx +++ b/gui/src/components/tracker/TrackerCard.tsx @@ -1,10 +1,9 @@ import { useConfig } from '@/hooks/config'; -import { MouseEventHandler, useMemo } from 'react'; +import { MouseEventHandler } from 'react'; import { DeviceDataT, TrackerDataT, TrackerStatus as TrackerStatusEnum, - TrackingChecklistStep, TrackingChecklistStepT, } from 'solarxr-protocol'; import { Typography } from '@/components/commons/Typography'; From 1d37912deb2a424032acabd603aa86a1de093436 Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Wed, 24 Sep 2025 07:33:54 +0200 Subject: [PATCH 30/34] More lint --- .../java/dev/slimevr/NetworkProfileChecker.kt | 4 ++-- server/core/src/main/java/dev/slimevr/VRServer.kt | 4 ++-- .../main/java/dev/slimevr/config/ResetsConfig.kt | 4 ++-- ...htListConfig.kt => TrackingChecklistConfig.kt} | 0 .../config/{VRCWarningsConfig.kt => VRCConfig.kt} | 0 .../dev/slimevr/games/vrchat/VRCConfigHandler.kt | 11 ++++++----- .../java/dev/slimevr/protocol/rpc/RPCHandler.kt | 2 +- .../protocol/rpc/games/vrchat/RPCVRChatHandler.kt | 6 +++--- .../protocol/rpc/settings/RPCSettingsHandler.kt | 1 - .../RPCTrackingChecklistHandler.kt | 2 +- .../tracking/processor/HumanPoseManager.kt | 7 ++++--- .../tracking/processor/skeleton/HumanSkeleton.kt | 12 ++++++++---- .../processor/skeleton/TapDetectionManager.java | 1 - .../dev/slimevr/tracking/trackers/TrackerUtils.kt | 5 +++-- .../trackingchecklist/TrackingChecklistManager.kt | 15 ++++++--------- .../desktop/DesktopNetworkProfileChecker.kt | 5 ++--- .../src/main/java/dev/slimevr/desktop/Main.kt | 2 -- 17 files changed, 40 insertions(+), 41 deletions(-) rename server/core/src/main/java/dev/slimevr/config/{FlightListConfig.kt => TrackingChecklistConfig.kt} (100%) rename server/core/src/main/java/dev/slimevr/config/{VRCWarningsConfig.kt => VRCConfig.kt} (100%) diff --git a/server/core/src/main/java/dev/slimevr/NetworkProfileChecker.kt b/server/core/src/main/java/dev/slimevr/NetworkProfileChecker.kt index 66e12e7716..787e634acb 100644 --- a/server/core/src/main/java/dev/slimevr/NetworkProfileChecker.kt +++ b/server/core/src/main/java/dev/slimevr/NetworkProfileChecker.kt @@ -52,9 +52,9 @@ abstract class NetworkProfileChecker { abstract val publicNetworks: List } -class NetworkProfileCheckerStub: NetworkProfileChecker() { +class NetworkProfileCheckerStub : NetworkProfileChecker() { override val isSupported: Boolean - get() = false; + get() = false override val publicNetworks: List get() = listOf() } diff --git a/server/core/src/main/java/dev/slimevr/VRServer.kt b/server/core/src/main/java/dev/slimevr/VRServer.kt index a70117ca04..283c4c792a 100644 --- a/server/core/src/main/java/dev/slimevr/VRServer.kt +++ b/server/core/src/main/java/dev/slimevr/VRServer.kt @@ -7,7 +7,6 @@ import dev.slimevr.bridge.ISteamVRBridge import dev.slimevr.config.ConfigManager import dev.slimevr.firmware.FirmwareUpdateHandler import dev.slimevr.firmware.SerialFlashingHandler -import dev.slimevr.trackingchecklist.TrackingChecklistManager import dev.slimevr.games.vrchat.VRCConfigHandler import dev.slimevr.games.vrchat.VRCConfigHandlerStub import dev.slimevr.games.vrchat.VRChatConfigManager @@ -29,6 +28,7 @@ import dev.slimevr.tracking.processor.HumanPoseManager import dev.slimevr.tracking.processor.skeleton.HumanSkeleton import dev.slimevr.tracking.trackers.* import dev.slimevr.tracking.trackers.udp.TrackersUDPServer +import dev.slimevr.trackingchecklist.TrackingChecklistManager import dev.slimevr.util.ann.VRServerThread import dev.slimevr.websocketapi.WebSocketVRBridge import io.eiren.util.ann.ThreadSafe @@ -121,7 +121,7 @@ class VRServer @JvmOverloads constructor( val trackingChecklistManager: TrackingChecklistManager - val networkProfileChecker: NetworkProfileChecker; + val networkProfileChecker: NetworkProfileChecker init { // UwU diff --git a/server/core/src/main/java/dev/slimevr/config/ResetsConfig.kt b/server/core/src/main/java/dev/slimevr/config/ResetsConfig.kt index 1621fa4c81..025cc5ece0 100644 --- a/server/core/src/main/java/dev/slimevr/config/ResetsConfig.kt +++ b/server/core/src/main/java/dev/slimevr/config/ResetsConfig.kt @@ -31,7 +31,8 @@ enum class ArmsResetModes(val id: Int) { enum class MountingMethods(val id: Int) { MANUAL(0), - AUTOMATIC(1); + AUTOMATIC(1), + ; companion object { val values = MountingMethods.entries.toTypedArray() @@ -65,7 +66,6 @@ class ResetsConfig { var preferedMountingMethod = MountingMethods.AUTOMATIC - fun updateTrackersResetsSettings() { for (t in VRServer.instance.allTrackers) { t.resetsHandler.readResetConfig(this) diff --git a/server/core/src/main/java/dev/slimevr/config/FlightListConfig.kt b/server/core/src/main/java/dev/slimevr/config/TrackingChecklistConfig.kt similarity index 100% rename from server/core/src/main/java/dev/slimevr/config/FlightListConfig.kt rename to server/core/src/main/java/dev/slimevr/config/TrackingChecklistConfig.kt diff --git a/server/core/src/main/java/dev/slimevr/config/VRCWarningsConfig.kt b/server/core/src/main/java/dev/slimevr/config/VRCConfig.kt similarity index 100% rename from server/core/src/main/java/dev/slimevr/config/VRCWarningsConfig.kt rename to server/core/src/main/java/dev/slimevr/config/VRCConfig.kt diff --git a/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt b/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt index 1852cf496d..3fddce4f89 100644 --- a/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt +++ b/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt @@ -116,12 +116,13 @@ class VRChatConfigManager(val server: VRServer, private val handler: VRCConfigHa fun toggleMuteWarning(key: String) { val keys = VRCConfigValidity::class.java.declaredFields.asSequence().map { p -> p.name } - if (!keys.contains(key)) return; + if (!keys.contains(key)) return - if (!server.configManager.vrConfig.vrcConfig.mutedWarnings.contains(key)) + if (!server.configManager.vrConfig.vrcConfig.mutedWarnings.contains(key)) { server.configManager.vrConfig.vrcConfig.mutedWarnings.add(key) - else + } else { server.configManager.vrConfig.vrcConfig.mutedWarnings.remove(key) + } server.configManager.saveConfig() @@ -133,7 +134,7 @@ class VRChatConfigManager(val server: VRServer, private val handler: VRCConfigHa validity, values, recommended, - server.configManager.vrConfig.vrcConfig.mutedWarnings + server.configManager.vrConfig.vrcConfig.mutedWarnings, ) } } @@ -195,7 +196,7 @@ class VRChatConfigManager(val server: VRServer, private val handler: VRCConfigHa fun onChange(values: VRCConfigValues) { val recommended = recommendedValues() - val validity = checkValidity(values, recommended); + val validity = checkValidity(values, recommended) currentValidity = validity currentValues = values listeners.forEach { diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt index fceacbf98b..def48d864c 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt @@ -9,7 +9,6 @@ import dev.slimevr.protocol.ProtocolHandler import dev.slimevr.protocol.datafeed.DataFeedBuilder import dev.slimevr.protocol.rpc.autobone.RPCAutoBoneHandler import dev.slimevr.protocol.rpc.firmware.RPCFirmwareUpdateHandler -import dev.slimevr.protocol.rpc.trackingchecklist.RPCTrackingChecklistHandler import dev.slimevr.protocol.rpc.games.vrchat.RPCVRChatHandler import dev.slimevr.protocol.rpc.reset.RPCResetHandler import dev.slimevr.protocol.rpc.serial.RPCProvisioningHandler @@ -20,6 +19,7 @@ import dev.slimevr.protocol.rpc.setup.RPCHandshakeHandler import dev.slimevr.protocol.rpc.setup.RPCTapSetupHandler import dev.slimevr.protocol.rpc.setup.RPCUtil.getLocalIp import dev.slimevr.protocol.rpc.status.RPCStatusHandler +import dev.slimevr.protocol.rpc.trackingchecklist.RPCTrackingChecklistHandler import dev.slimevr.protocol.rpc.trackingpause.RPCTrackingPause import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets import dev.slimevr.tracking.processor.stayaligned.poses.RelaxedPose diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/games/vrchat/RPCVRChatHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/games/vrchat/RPCVRChatHandler.kt index 5bcca22011..481e918d35 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/games/vrchat/RPCVRChatHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/games/vrchat/RPCVRChatHandler.kt @@ -37,7 +37,7 @@ class RPCVRChatHandler( validity = validity, values = values, recommended = api.server.vrcConfigManager.recommendedValues(), - muted = api.server.configManager.vrConfig.vrcConfig.mutedWarnings + muted = api.server.configManager.vrConfig.vrcConfig.mutedWarnings, ) val outbound = rpcHandler.createRPCMessage( @@ -52,7 +52,7 @@ class RPCVRChatHandler( private fun onToggleMuteRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { val req = messageHeader.message(VRCConfigSettingToggleMute()) as VRCConfigSettingToggleMute? ?: return - api.server.vrcConfigManager.toggleMuteWarning(req.key()); + api.server.vrcConfigManager.toggleMuteWarning(req.key()) } override fun onChange(validity: VRCConfigValidity, values: VRCConfigValues, recommended: VRCConfigRecommendedValues, muted: List) { @@ -64,7 +64,7 @@ class RPCVRChatHandler( validity = validity, values = values, recommended = recommended, - muted + muted, ) val outbound = rpcHandler.createRPCMessage( diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt index 0f8ed19700..74b2033d0d 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt @@ -3,7 +3,6 @@ package dev.slimevr.protocol.rpc.settings import com.google.flatbuffers.FlatBufferBuilder import dev.slimevr.bridge.ISteamVRBridge import dev.slimevr.config.ArmsResetModes -import dev.slimevr.config.MountingMethods import dev.slimevr.filtering.TrackerFilters import dev.slimevr.protocol.GenericConnection import dev.slimevr.protocol.ProtocolAPI diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingchecklist/RPCTrackingChecklistHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingchecklist/RPCTrackingChecklistHandler.kt index f99f49f99d..ca6f6b34e7 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingchecklist/RPCTrackingChecklistHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingchecklist/RPCTrackingChecklistHandler.kt @@ -1,10 +1,10 @@ package dev.slimevr.protocol.rpc.trackingchecklist import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.trackingchecklist.TrackingChecklistListener import dev.slimevr.protocol.GenericConnection import dev.slimevr.protocol.ProtocolAPI import dev.slimevr.protocol.rpc.RPCHandler +import dev.slimevr.trackingchecklist.TrackingChecklistListener import solarxr_protocol.rpc.* class RPCTrackingChecklistHandler( diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt index fba5385a0d..78c3896243 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt @@ -5,7 +5,6 @@ import dev.slimevr.VRServer import dev.slimevr.VRServer.Companion.getNextLocalTrackerId import dev.slimevr.autobone.errors.BodyProportionError import dev.slimevr.config.ConfigManager -import dev.slimevr.config.MountingMethods import dev.slimevr.tracking.processor.config.SkeletonConfigManager import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets import dev.slimevr.tracking.processor.config.SkeletonConfigToggles @@ -571,9 +570,11 @@ class HumanPoseManager(val server: VRServer?) { if (server === null) return val finalBodyParts = bodyParts - ?: if (server.configManager.vrConfig.resetsConfig.resetMountingFeet) + ?: if (server.configManager.vrConfig.resetsConfig.resetMountingFeet) { TrackerUtils.allBodyPartsButFingers - else TrackerUtils.allBodyPartsButFingersAndFeets + } else { + TrackerUtils.allBodyPartsButFingersAndFeets + } skeleton.resetTrackersMounting(resetSourceName, finalBodyParts) } diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt index c63e5112db..5dbb57f4d2 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt @@ -1613,16 +1613,20 @@ class HumanSkeleton( if (humanPoseManager.server != null) { humanPoseManager.server.configManager.vrConfig.resetsConfig.preferedMountingMethod = MountingMethods.AUTOMATIC - if (!humanPoseManager.server.trackingChecklistManager.resetMountingCompleted) + if (!humanPoseManager.server.trackingChecklistManager.resetMountingCompleted) { humanPoseManager.server.trackingChecklistManager.resetMountingCompleted = bodyParts.any { it -> - val defaultParts = if (humanPoseManager.server.configManager.vrConfig.resetsConfig.resetMountingFeet) + val defaultParts = if (humanPoseManager.server.configManager.vrConfig.resetsConfig.resetMountingFeet) { TrackerUtils.allBodyPartsButFingers - else TrackerUtils.allBodyPartsButFingersAndFeets + } else { + TrackerUtils.allBodyPartsButFingersAndFeets + } return@any defaultParts.contains(it) } - if (!humanPoseManager.server.trackingChecklistManager.feetResetMountingCompleted) + } + if (!humanPoseManager.server.trackingChecklistManager.feetResetMountingCompleted) { humanPoseManager.server.trackingChecklistManager.feetResetMountingCompleted = bodyParts.any { TrackerUtils.feetsBodyParts.contains(it) } + } humanPoseManager.server.configManager.saveConfig() } diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/TapDetectionManager.java b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/TapDetectionManager.java index fc42876fe6..2d345fea16 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/TapDetectionManager.java +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/TapDetectionManager.java @@ -7,7 +7,6 @@ import dev.slimevr.setup.TapSetupHandler; import dev.slimevr.tracking.processor.HumanPoseManager; import dev.slimevr.tracking.trackers.Tracker; -import dev.slimevr.tracking.trackers.TrackerUtils; import solarxr_protocol.rpc.ResetType; import solarxr_protocol.rpc.StatusData; diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerUtils.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerUtils.kt index ef51fecf90..f3e73b3eee 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerUtils.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerUtils.kt @@ -109,10 +109,11 @@ object TrackerUtils { BodyPart.LEFT_UPPER_LEG, BodyPart.RIGHT_UPPER_LEG, BodyPart.LEFT_LOWER_LEG, BodyPart.RIGHT_LOWER_LEG, BodyPart.LEFT_LOWER_ARM, BodyPart.RIGHT_LOWER_ARM, BodyPart.LEFT_UPPER_ARM, BodyPart.RIGHT_UPPER_ARM, BodyPart.LEFT_HAND, - BodyPart.RIGHT_HAND, BodyPart.LEFT_SHOULDER, BodyPart.RIGHT_SHOULDER + BodyPart.RIGHT_HAND, BodyPart.LEFT_SHOULDER, BodyPart.RIGHT_SHOULDER, ) val feetsBodyParts = listOf( - BodyPart.LEFT_FOOT, BodyPart.RIGHT_FOOT + BodyPart.LEFT_FOOT, + BodyPart.RIGHT_FOOT, ) } diff --git a/server/core/src/main/java/dev/slimevr/trackingchecklist/TrackingChecklistManager.kt b/server/core/src/main/java/dev/slimevr/trackingchecklist/TrackingChecklistManager.kt index 4019c44c49..a1181f42b7 100644 --- a/server/core/src/main/java/dev/slimevr/trackingchecklist/TrackingChecklistManager.kt +++ b/server/core/src/main/java/dev/slimevr/trackingchecklist/TrackingChecklistManager.kt @@ -283,7 +283,7 @@ class TrackingChecklistManager(private val vrServer: VRServer) : VRCConfigListen } } } else { - it.extraData = null; + it.extraData = null } } } @@ -294,12 +294,11 @@ class TrackingChecklistManager(private val vrServer: VRServer) : VRCConfigListen updateValidity(TrackingChecklistStepId.FEET_MOUNTING_CALIBRATION, feetResetMountingCompleted) { it.enabled = - vrServer.configManager.vrConfig.resetsConfig.preferedMountingMethod == MountingMethods.AUTOMATIC - && !vrServer.configManager.vrConfig.resetsConfig.resetMountingFeet - && imuTrackers.any { t -> TrackerUtils.feetsBodyParts.contains(t.trackerPosition?.bodyPart) } + vrServer.configManager.vrConfig.resetsConfig.preferedMountingMethod == MountingMethods.AUTOMATIC && + !vrServer.configManager.vrConfig.resetsConfig.resetMountingFeet && + imuTrackers.any { t -> TrackerUtils.feetsBodyParts.contains(t.trackerPosition?.bodyPart) } } - updateValidity(TrackingChecklistStepId.STAY_ALIGNED_CONFIGURED, vrServer.configManager.vrConfig.stayAlignedConfig.enabled) listeners.forEach { it.onStepsUpdate() } @@ -316,12 +315,11 @@ class TrackingChecklistManager(private val vrServer: VRServer) : VRCConfigListen } } - override fun onChange( validity: VRCConfigValidity, values: VRCConfigValues, recommended: VRCConfigRecommendedValues, - muted: List + muted: List, ) { updateValidity( TrackingChecklistStepId.VRCHAT_SETTINGS, @@ -334,7 +332,7 @@ class TrackingChecklistManager(private val vrServer: VRServer) : VRCConfigListen } fun ignoreStep(step: TrackingChecklistStepT, ignore: Boolean) { - if (!step.ignorable) return; + if (!step.ignorable) return val ignoredSteps = vrServer.configManager.vrConfig.trackingChecklist.ignoredStepsIds if (ignore && !ignoredSteps.contains(step.id)) { ignoredSteps.add(step.id) @@ -343,5 +341,4 @@ class TrackingChecklistManager(private val vrServer: VRServer) : VRCConfigListen } vrServer.configManager.saveConfig() } - } diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/DesktopNetworkProfileChecker.kt b/server/desktop/src/main/java/dev/slimevr/desktop/DesktopNetworkProfileChecker.kt index 74344cdf6b..3fbf796673 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/DesktopNetworkProfileChecker.kt +++ b/server/desktop/src/main/java/dev/slimevr/desktop/DesktopNetworkProfileChecker.kt @@ -263,8 +263,7 @@ fun enumerateNetworks(): List? { return null } -class DesktopNetworkProfileChecker(private val server: VRServer): - NetworkProfileChecker() { +class DesktopNetworkProfileChecker(private val server: VRServer) : NetworkProfileChecker() { private val updateTickTimer = Timer("NetworkProfileCheck") private var publicNetworksLocal: List = listOf() @@ -272,7 +271,7 @@ class DesktopNetworkProfileChecker(private val server: VRServer): get() = OperatingSystem.currentPlatform == OperatingSystem.WINDOWS override val publicNetworks: List - get() = publicNetworksLocal; + get() = publicNetworksLocal init { if (OperatingSystem.currentPlatform == OperatingSystem.WINDOWS) { diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt b/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt index 200c6f4884..088c670b0f 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt +++ b/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt @@ -3,7 +3,6 @@ package dev.slimevr.desktop import dev.slimevr.Keybinding -import dev.slimevr.NetworkProfileChecker import dev.slimevr.SLIMEVR_IDENTIFIER import dev.slimevr.VRServer import dev.slimevr.bridge.Bridge @@ -131,7 +130,6 @@ fun main(args: Array) { ) vrServer.start() - // Start service for USB HID trackers DesktopHIDManager( "Sensors HID service", From 615df56a702e5c8faccf3b4dbc0ea6bbe97c97d0 Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Wed, 24 Sep 2025 07:41:33 +0200 Subject: [PATCH 31/34] Fix case with no server for unit tests --- .../java/dev/slimevr/tracking/processor/HumanPoseManager.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt index 78c3896243..e261d4e48c 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt @@ -567,10 +567,8 @@ class HumanPoseManager(val server: VRServer?) { @JvmOverloads fun resetTrackersMounting(resetSourceName: String?, bodyParts: List? = null) { - if (server === null) return - val finalBodyParts = bodyParts - ?: if (server.configManager.vrConfig.resetsConfig.resetMountingFeet) { + ?: if (server?.configManager?.vrConfig?.resetsConfig?.resetMountingFeet == true) { TrackerUtils.allBodyPartsButFingers } else { TrackerUtils.allBodyPartsButFingersAndFeets From 5c262d54571cb3678d704b566ceefe27ebf3d195 Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Wed, 24 Sep 2025 23:16:11 +0200 Subject: [PATCH 32/34] Fix font truncate --- gui/src/components/commons/Typography.tsx | 2 +- gui/src/components/tracker/TrackerCard.tsx | 2 +- solarxr-protocol | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gui/src/components/commons/Typography.tsx b/gui/src/components/commons/Typography.tsx index eb6d78a686..03de1ea738 100644 --- a/gui/src/components/commons/Typography.tsx +++ b/gui/src/components/commons/Typography.tsx @@ -77,7 +77,7 @@ export function Typography({ whitespace, textAlign, italic && 'italic', - truncate && 'leading-3 text-ellipsis', + truncate && 'leading-[1.2rem] text-ellipsis', truncate && (config?.textSize ?? 12) > 12 && 'line-clamp-1', truncate && (config?.textSize ?? 12) <= 12 && 'line-clamp-2', sentryMask && 'sentry-mask', diff --git a/gui/src/components/tracker/TrackerCard.tsx b/gui/src/components/tracker/TrackerCard.tsx index b3d9971fec..90e4867ba1 100644 --- a/gui/src/components/tracker/TrackerCard.tsx +++ b/gui/src/components/tracker/TrackerCard.tsx @@ -162,7 +162,7 @@ function TrackerSmol({
-
+
{trackerName} diff --git a/solarxr-protocol b/solarxr-protocol index b2b791764f..7e6cc3b425 160000 --- a/solarxr-protocol +++ b/solarxr-protocol @@ -1 +1 @@ -Subproject commit b2b791764fb9298634921b7bce46fded2dcc17e7 +Subproject commit 7e6cc3b42593fdd7022af11ad3eb2e8a29f3c42f From 92155da831d90890379dfd4c4458701ff3fdad0b Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Thu, 25 Sep 2025 01:39:18 +0200 Subject: [PATCH 33/34] Small fixes --- .../tracking-checklist/TrackingChecklist.tsx | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/gui/src/components/tracking-checklist/TrackingChecklist.tsx b/gui/src/components/tracking-checklist/TrackingChecklist.tsx index 46e96bfbee..7245369f25 100644 --- a/gui/src/components/tracking-checklist/TrackingChecklist.tsx +++ b/gui/src/components/tracking-checklist/TrackingChecklist.tsx @@ -130,18 +130,9 @@ const stepContentLookup: Record<
- - - + + +
@@ -250,7 +241,7 @@ const stepContentLookup: Record< <>
-
+