Skip to content

Commit

Permalink
Add tutorials for Virtual Display and SBS modes
Browse files Browse the repository at this point in the history
  • Loading branch information
wheaney committed Dec 24, 2023
1 parent 5549a16 commit 87b9fd9
Show file tree
Hide file tree
Showing 9 changed files with 276 additions and 17 deletions.
Binary file added assets/tutorials/common/display-resolution.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/tutorials/sbs/scaling-mode-stretch.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
DRIVER_STATE_FILE_PATH = '/dev/shm/xr_driver_state'

INSTALLED_VERSION_SETTING_KEY = "installed_from_plugin_version"
DONT_SHOW_AGAIN_SETTING_KEY = "dont_show_again"
CONTROL_FLAGS = ['recenter_screen', 'recalibrate', 'sbs_mode']
SBS_MODE_VALUES = ['unset', 'enable', 'disable']

Expand Down Expand Up @@ -163,6 +164,19 @@ async def retrieve_driver_state(self):

return state

async def retrieve_dont_show_again_keys(self):
return [key for key in settings.getSetting(DONT_SHOW_AGAIN_SETTING_KEY, "").split(",") if key]

async def set_dont_show_again(self, key):
try:
dont_show_again_keys = await self.retrieve_dont_show_again_keys(self)
dont_show_again_keys.append(key)
settings.setSetting(DONT_SHOW_AGAIN_SETTING_KEY, ",".join(dont_show_again_keys))
return True
except Exception as e:
decky_plugin.logger.error(f"Error setting dont_show_again {e}")
return False

async def is_driver_running(self):
try:
output = subprocess.check_output(['systemctl', 'is-active', 'xreal-air-driver'], stderr=subprocess.STDOUT)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "decky-xrealAir",
"version": "0.6.0",
"version": "0.6.0.1",
"description": "Virtual display and head-tracking modes for the XREAL Air glasses",
"scripts": {
"build": "shx rm -rf dist && rollup -c",
Expand Down
67 changes: 51 additions & 16 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
ToggleField
} from "decky-frontend-lib";
// @ts-ignore
import React, {Fragment, useEffect, useState, VFC} from "react";
import React, {Dispatch, Fragment, SetStateAction, useEffect, useState, VFC} from "react";
import {FaGlasses} from "react-icons/fa";
import {BiMessageError} from "react-icons/bi";
import { PiPlugsConnected } from "react-icons/pi";
Expand All @@ -26,6 +26,8 @@ import {LuHelpCircle} from 'react-icons/lu';
import QrButton from "./QrButton";
import beam from "../assets/beam.png";
import ButtonFieldSmall from "./ButtonFieldSmall";
import {onChangeTutorial} from "./tutorials";
import {useStableState} from "./stableState";

interface Config {
disabled: boolean;
Expand Down Expand Up @@ -93,7 +95,7 @@ function headsetModeToConfig(headsetMode: HeadsetModeOption, joystickMode: boole
case "virtual_display":
return { disabled: false, output_mode: "external_only", external_mode: "virtual_display" }
case "vr_lite":
return { disabled: false, output_mode: joystickMode ? "joystick" : "mouse" }
return { disabled: false, output_mode: joystickMode ? "joystick" : "mouse", external_mode: "none" }
case "sideview":
return { disabled: false, output_mode: "external_only", external_mode: "sideview" }
case "disabled":
Expand All @@ -107,6 +109,8 @@ function configToHeadsetMode(config?: Config): HeadsetModeOption {
return "vr_lite"
}

const HeadsetModeConfirmationTimeoutMs = 1000

const ModeNotchLabels: NotchLabel[] = [
{
label: "Virtual display",
Expand Down Expand Up @@ -180,6 +184,8 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({serverAPI}) => {
const [installationStatus, setInstallationStatus] = useState<InstallationStatus>("checking");
const [showAdvanced, setShowAdvanced] = useState<boolean>(false);
const [error, setError] = useState<string>();
const [dontShowAgainKeys, setDontShowAgainKeys] = useState<string[]>([]);
const [dirtyHeadsetMode, stableHeadsetMode, setDirtyHeadsetMode] = useStableState<HeadsetModeOption | undefined>(undefined, HeadsetModeConfirmationTimeoutMs);

async function retrieveConfig() {
const configRes: ServerResponse<Config> = await serverAPI.callPluginMethod<{}, Config>("retrieve_config", {});
Expand All @@ -201,6 +207,15 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({serverAPI}) => {
}, 1000);
}

async function retrieveDontShowAgainKeys() {
const dontShowAgainKeysRes: ServerResponse<string[]> = await serverAPI.callPluginMethod<{}, string[]>("retrieve_dont_show_again_keys", {});
if (dontShowAgainKeysRes.success) {
setDontShowAgainKeys(dontShowAgainKeysRes.result);
} else {
setError(dontShowAgainKeysRes.result);
}
}

async function checkInstallation() {
const installedRes: ServerResponse<boolean> = await serverAPI.callPluginMethod<{}, boolean>("is_driver_installed", {});
if (installedRes.success) {
Expand Down Expand Up @@ -233,11 +248,21 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({serverAPI}) => {
res.success ? setDirtyControlFlags({...flags, last_updated: Date.now()}) : setError(res.result);
}

async function setDontShowAgain(key: string) {
const res = await serverAPI.callPluginMethod<{ key: string }, void>("set_dont_show_again", { key });
if (res.success) {
setDontShowAgainKeys([...dontShowAgainKeys, key]);
} else {
setError(res.result);
}
}

// these asynchronous calls should execute ONLY one time, hence the empty array as the second argument
useEffect(() => {
retrieveConfig().catch((err) => setError(err));
checkInstallation().catch((err) => setError(err));
retrieveDriverState().catch((err) => setError(err));
retrieveDontShowAgainKeys().catch((err) => setError(err));
}, []);

useEffect(() => {
Expand All @@ -253,9 +278,21 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({serverAPI}) => {
}
}, [driverState])

// this effect will be triggered after headsetMode has been stable for a certain period of time
useEffect(() => {
if (stableHeadsetMode&& config) {
onChangeTutorial(`headset_mode_${stableHeadsetMode}`, () => {
updateConfig({
...config,
...headsetModeToConfig(stableHeadsetMode, isJoystickMode)
}).catch(e => setError(e))
}, dontShowAgainKeys, setDontShowAgain);
}
}, [stableHeadsetMode])

const deviceConnected = !!driverState?.connected_device_brand && !!driverState?.connected_device_model
const deviceName = deviceConnected ? `${driverState?.connected_device_brand} ${driverState?.connected_device_model}` : "No device connected"
const headsetMode: HeadsetModeOption = configToHeadsetMode(config)
const headsetMode: HeadsetModeOption = dirtyHeadsetMode ?? configToHeadsetMode(config)
const isDisabled = !deviceConnected || headsetMode == "disabled"
const isVirtualDisplayMode = !isDisabled && headsetMode == "virtual_display"
const isSideviewMode = !isDisabled && headsetMode == "sideview"
Expand All @@ -269,11 +306,16 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({serverAPI}) => {
checked={sbsModeEnabled}
label={"Enable side-by-side mode"}
description={!driverState?.sbs_mode_enabled && "Adjust virtual display depth. View 3D content."}
onChange={(sbs_mode_enabled) => writeControlFlags(
{
sbs_mode: sbs_mode_enabled ? 'enable' : 'disable'
}
)}/>
onChange={(sbs_mode_enabled) => {
onChangeTutorial(`sbs_mode_enabled_${sbs_mode_enabled}`, () => {
writeControlFlags(
{
sbs_mode: sbs_mode_enabled ? 'enable' : 'disable'
}
)}, dontShowAgainKeys, setDontShowAgain
)
}}
/>
</PanelSectionRow>;

const joystickModeButton = <PanelSectionRow>
Expand Down Expand Up @@ -385,14 +427,7 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({serverAPI}) => {
min={0} max={HeadsetModeOptions.length-1}
notchLabels={ModeNotchLabels}
notchCount={HeadsetModeOptions.length}
onChange={(newMode) => {
if (config) {
updateConfig({
...config,
...headsetModeToConfig(HeadsetModeOptions[newMode], isJoystickMode)
}).catch(e => setError(e))
}
}}
onChange={(newMode) => setDirtyHeadsetMode(HeadsetModeOptions[newMode])}
/>
</PanelSectionRow>}
{isVrLiteMode && isJoystickMode && joystickModeButton}
Expand Down
17 changes: 17 additions & 0 deletions src/stableState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {Dispatch, SetStateAction, useEffect, useState} from "react";

// allows for setting a dirty state that doesn't take effect (as a stable state) until a delay has passed without change
export function useStableState<T>(initialState: T, delay: number) : [T, T, Dispatch<SetStateAction<T>>] {
const [dirtyState, setDirtyState] = useState(initialState);
const [stableState, setStableState] = useState(initialState);

useEffect(() => {
const timeoutId = setTimeout(() => {
setStableState(dirtyState);
}, delay);

return () => clearTimeout(timeoutId);
}, [dirtyState, delay]);

return [dirtyState, stableState, setDirtyState];
}
Loading

0 comments on commit 87b9fd9

Please sign in to comment.