summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--backend/decky_loader/injector.py4
-rw-r--r--backend/decky_loader/utilities.py2
-rw-r--r--frontend/pnpm-lock.yaml12
-rw-r--r--frontend/pnpm-workspace.yaml7
-rw-r--r--frontend/src/components/NotificationBadge.tsx55
-rw-r--r--frontend/src/components/PluginView.tsx40
-rw-r--r--frontend/src/components/QuickAccessVisibleState.tsx17
-rw-r--r--frontend/src/components/settings/pages/developer/index.tsx33
-rw-r--r--frontend/src/developer.tsx42
-rw-r--r--frontend/src/hooks/router.tsx (renamed from frontend/src/router-hook.tsx)8
-rw-r--r--frontend/src/hooks/tabs.tsx (renamed from frontend/src/tabs-hook.tsx)4
-rw-r--r--frontend/src/hooks/toaster.tsx (renamed from frontend/src/toaster.tsx)25
-rw-r--r--frontend/src/hooks/vr-dashboard-bar.tsx105
-rw-r--r--frontend/src/plugin-loader.tsx56
-rwxr-xr-xscripts/deckdebug.sh2
16 files changed, 303 insertions, 110 deletions
diff --git a/.gitignore b/.gitignore
index f9a954e0..bfb7a211 100644
--- a/.gitignore
+++ b/.gitignore
@@ -158,6 +158,7 @@ backend/decky_loader/static
# ignore settings.json
# prevents leaking login details
.vscode/settings.json
+.vscode/settings.*.json
# plugins folder for local launches
/plugins/*
diff --git a/backend/decky_loader/injector.py b/backend/decky_loader/injector.py
index ded58193..26d8f8fe 100644
--- a/backend/decky_loader/injector.py
+++ b/backend/decky_loader/injector.py
@@ -168,7 +168,7 @@ class Tab:
# Page finishes loading when breakpoint hits
for _ in range(20):
- # this works around 1/5 of the time, so just send it 8 times.
+ # this works around 1/5 of the time, so just send it 20 times.
# the js accounts for being injected multiple times allowing only one instance to run at a time anyway
await self._send_devtools_cmd({
"method": "Runtime.evaluate",
@@ -435,4 +435,4 @@ async def close_old_tabs():
if not t.title or (t.title not in SHARED_CTX_NAMES and any(url in t.url for url in CLOSEABLE_URLS) and not any(url in t.url for url in DO_NOT_CLOSE_URLS)):
logger.debug("Closing tab: " + getattr(t, "title", "Untitled"))
await t.close()
- await sleep(0.5)
+ # await sleep(0.5)
diff --git a/backend/decky_loader/utilities.py b/backend/decky_loader/utilities.py
index 99a1b019..7f1ac09d 100644
--- a/backend/decky_loader/utilities.py
+++ b/backend/decky_loader/utilities.py
@@ -442,7 +442,7 @@ class Utilities:
enumerable: true,
configurable: true,
get: function() {
- return (GamepadNavTree?.m_context?.m_controller || FocusNavController)?.m_ActiveContext?.ActiveWindow || window;
+ return (window?.GamepadNavTree?.m_context?.m_controller || window?.FocusNavController)?.m_ActiveContext?.ActiveWindow || window;
}
});
""" + await res.text() + "\n}"
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index 57e28d59..37d7ff8e 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -4,13 +4,16 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+overrides:
+ '@decky/ui': link:../../lib
+
importers:
.:
dependencies:
'@decky/ui':
- specifier: ^4.15.1
- version: 4.15.1
+ specifier: link:../../lib
+ version: link:../../lib
compare-versions:
specifier: ^6.1.1
version: 6.1.1
@@ -222,9 +225,6 @@ packages:
'@decky/api@1.1.3':
resolution: {integrity: sha512-XsPCZxfxk5I1UtylIUN3qaWQI31siQbKfbLIskkI5innEatY1m4NQqBv/6hwPaO9mKMbdqYpnh5PSJDeMEOOBA==}
- '@decky/ui@4.15.1':
- resolution: {integrity: sha512-pIOpN9cm+90oaiQvLpAao8GfuflTC1tbZh1erMUKZ7IHeFYb/vrEhpI/v/avNWOySWFxGl0iVsDALvwIu2l1kg==}
-
'@esbuild/aix-ppc64@0.20.2':
resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==}
engines: {node: '>=12'}
@@ -2311,8 +2311,6 @@ snapshots:
'@decky/api@1.1.3': {}
- '@decky/ui@4.15.1': {}
-
'@esbuild/aix-ppc64@0.20.2':
optional: true
diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml
index a6e847b3..80cb4cd7 100644
--- a/frontend/pnpm-workspace.yaml
+++ b/frontend/pnpm-workspace.yaml
@@ -1,5 +1,6 @@
-minimumReleaseAgeExclude:
-- "@decky/api"
-- "@decky/ui"
allowBuilds:
esbuild: true
+
+minimumReleaseAgeExclude:
+ - '@decky/api'
+ - '@decky/ui'
diff --git a/frontend/src/components/NotificationBadge.tsx b/frontend/src/components/NotificationBadge.tsx
index 3c5d8215..1d14d576 100644
--- a/frontend/src/components/NotificationBadge.tsx
+++ b/frontend/src/components/NotificationBadge.tsx
@@ -2,23 +2,52 @@ import { CSSProperties, FunctionComponent } from 'react';
interface NotificationBadgeProps {
show?: boolean;
+ vr?: boolean;
style?: CSSProperties;
}
-const NotificationBadge: FunctionComponent<NotificationBadgeProps> = ({ show, style }) => {
+const NotificationBadge: FunctionComponent<NotificationBadgeProps> = ({ show, style, vr }) => {
return show ? (
- <div
- style={{
- position: 'absolute',
- top: '8px',
- right: '8px',
- height: '10px',
- width: '10px',
- background: 'orange',
- borderRadius: '50%',
- ...style,
- }}
- />
+ vr ? (
+ // whoever made transform:scale completely break absolute positioning goes straight to super turbo hell this took me an hour
+ <div
+ style={{
+ top: 0,
+ left: 0,
+ marginLeft: 'auto',
+ width: 0,
+ height: 0,
+ overflow: 'visible',
+ transform: 'translate(0px, 0px)',
+ }}
+ >
+ <div
+ style={{
+ position: 'fixed',
+ top: '-18px',
+ right: '-6px',
+ width: '10px',
+ height: '10px',
+ background: 'orange',
+ borderRadius: '50%',
+ ...style,
+ }}
+ />
+ </div>
+ ) : (
+ <div
+ style={{
+ position: 'absolute',
+ top: '8px',
+ right: '8px',
+ height: '10px',
+ width: '10px',
+ background: 'orange',
+ borderRadius: '50%',
+ ...style,
+ }}
+ />
+ )
) : null;
};
diff --git a/frontend/src/components/PluginView.tsx b/frontend/src/components/PluginView.tsx
index fbafaf05..27c1f2f1 100644
--- a/frontend/src/components/PluginView.tsx
+++ b/frontend/src/components/PluginView.tsx
@@ -1,4 +1,12 @@
-import { ButtonItem, ErrorBoundary, Focusable, PanelSection, PanelSectionRow } from '@decky/ui';
+import {
+ ButtonItem,
+ ErrorBoundary,
+ Focusable,
+ PanelSection,
+ PanelSectionRow,
+ joinClassNames,
+ vrDashboardClasses,
+} from '@decky/ui';
import { FC, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaBan, FaEyeSlash } from 'react-icons/fa';
@@ -8,7 +16,7 @@ import NotificationBadge from './NotificationBadge';
import { useQuickAccessVisible } from './QuickAccessVisibleState';
import TitleView from './TitleView';
-const PluginView: FC = () => {
+const PluginView: FC<{ vr?: boolean }> = ({ vr }) => {
const {
plugins,
disabledPlugins,
@@ -40,20 +48,38 @@ const PluginView: FC = () => {
if (activePlugin) {
return (
- <Focusable onCancelButton={closeActivePlugin}>
+ <Focusable
+ onCancelButton={closeActivePlugin}
+ className={
+ (vr && joinClassNames(vrDashboardClasses.QuickAccessPopupPanel, vrDashboardClasses.BottomFade)) || ''
+ }
+ style={{
+ display: 'flex',
+ flexDirection: 'column',
+ paddingBottom: '20px', // override bottomfade, there is prolly a better way
+ }}
+ >
<TitleView />
- <div style={{ height: '100%', paddingTop: '16px' }}>
+ <div style={{ paddingTop: '16px', overflowY: vr ? 'auto' : undefined }}>
<ErrorBoundary>{(visible || activePlugin.alwaysRender) && activePlugin.content}</ErrorBoundary>
</div>
</Focusable>
);
}
return (
- <>
+ <div
+ className={(vr && joinClassNames(vrDashboardClasses.QuickAccessPopupPanel, vrDashboardClasses.BottomFade)) || ''}
+ style={{
+ display: 'flex',
+ flexDirection: 'column',
+ paddingBottom: '20px', // override bottomfade, there is prolly a better way
+ }}
+ >
<TitleView />
<div
style={{
- paddingTop: '16px',
+ paddingTop: vr ? '16px' : '16px',
+ overflowY: vr ? 'auto' : undefined,
}}
>
<PanelSection>
@@ -92,7 +118,7 @@ const PluginView: FC = () => {
</div>
</PanelSection>
</div>
- </>
+ </div>
);
};
diff --git a/frontend/src/components/QuickAccessVisibleState.tsx b/frontend/src/components/QuickAccessVisibleState.tsx
index f90c24b4..83e45177 100644
--- a/frontend/src/components/QuickAccessVisibleState.tsx
+++ b/frontend/src/components/QuickAccessVisibleState.tsx
@@ -1,16 +1,21 @@
import { FC, PropsWithChildren, createContext, useContext, useState } from 'react';
-const QuickAccessVisibleState = createContext<boolean>(false);
+export const QuickAccessVisibleState = createContext<boolean>(false);
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
-export const QuickAccessVisibleStateProvider: FC<PropsWithChildren<{ tab: any }>> = ({ children, tab }) => {
- const initial = tab.initialVisibility;
+export const QuickAccessVisibleStateProvider: FC<PropsWithChildren<{ tab?: any; initialVal?: boolean }>> = ({
+ children,
+ tab,
+ initialVal,
+}) => {
+ const initial = initialVal || tab?.initialVisibility;
const [visible, setVisible] = useState<boolean>(initial);
// HACK but i can't think of a better way to do this
- tab.qAMVisibilitySetter = (val: boolean) => {
- if (val != visible) setVisible(val);
- };
+ if (tab)
+ tab.qAMVisibilitySetter = (val: boolean) => {
+ if (val != visible) setVisible(val);
+ };
return <QuickAccessVisibleState.Provider value={visible}>{children}</QuickAccessVisibleState.Provider>;
};
diff --git a/frontend/src/components/settings/pages/developer/index.tsx b/frontend/src/components/settings/pages/developer/index.tsx
index 17187675..b6a337f0 100644
--- a/frontend/src/components/settings/pages/developer/index.tsx
+++ b/frontend/src/components/settings/pages/developer/index.tsx
@@ -9,7 +9,7 @@ import {
Toggle,
showModal,
} from '@decky/ui';
-import { useRef, useState } from 'react';
+import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaFileArchive, FaLink, FaReact, FaSteamSymbol, FaTerminal } from 'react-icons/fa';
@@ -49,7 +49,6 @@ export default function DeveloperSettings() {
const [acceptedWarning, setAcceptedWarning] = useSetting<boolean>('developer.warn.third_party', false);
const waitTime = acceptedWarning ? 0 : 5;
const [pluginURL, setPluginURL] = useState('');
- const textRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
return (
@@ -82,13 +81,6 @@ export default function DeveloperSettings() {
</Field>
<Field
label={t('SettingsDeveloperIndex.third_party_plugins.label_url')}
- description={
- <TextField
- label={t('SettingsDeveloperIndex.third_party_plugins.label_desc')}
- value={pluginURL}
- onChange={(e) => setPluginURL(e?.target.value)}
- />
- }
icon={<FaLink style={{ display: 'block' }} />}
>
<DialogButton
@@ -114,6 +106,11 @@ export default function DeveloperSettings() {
{t('SettingsDeveloperIndex.third_party_plugins.button_install')}
</DialogButton>
</Field>
+ <TextField
+ label={t('SettingsDeveloperIndex.third_party_plugins.label_desc')}
+ value={pluginURL}
+ onChange={(e) => setPluginURL(e?.target.value)}
+ />
</DialogControlsSection>
<DialogControlsSection>
<DialogControlsSectionHeader>{t('SettingsDeveloperIndex.header')}</DialogControlsSectionHeader>
@@ -160,18 +157,7 @@ export default function DeveloperSettings() {
<Field
label={t('SettingsDeveloperIndex.react_devtools.label')}
description={
- <>
- <span style={{ whiteSpace: 'pre-line' }}>{t('SettingsDeveloperIndex.react_devtools.desc')}</span>
- <br />
- <br />
- <div ref={textRef}>
- <TextField
- label={t('SettingsDeveloperIndex.react_devtools.ip_label')}
- value={reactDevtoolsIP}
- onChange={(e) => setReactDevtoolsIP(e?.target.value)}
- />
- </div>
- </>
+ <span style={{ whiteSpace: 'pre-line' }}>{t('SettingsDeveloperIndex.react_devtools.desc')}</span>
}
icon={<FaReact style={{ display: 'block' }} />}
>
@@ -184,6 +170,11 @@ export default function DeveloperSettings() {
}}
/>
</Field>
+ <TextField
+ label={t('SettingsDeveloperIndex.react_devtools.ip_label')}
+ value={reactDevtoolsIP}
+ onChange={(e) => setReactDevtoolsIP(e?.target.value)}
+ />
</DialogControlsSection>
</DialogBody>
);
diff --git a/frontend/src/developer.tsx b/frontend/src/developer.tsx
index 5df6de07..6e1b9599 100644
--- a/frontend/src/developer.tsx
+++ b/frontend/src/developer.tsx
@@ -9,20 +9,13 @@ const logger = new Logger('DeveloperMode');
let removeSettingsObserver: () => void = () => {};
-declare global {
- interface Window {
- settingsStore: any;
- }
-}
-
export async function setShowValveInternal(show: boolean) {
if (show) {
- const mobx =
- window.settingsStore[
- Object.getOwnPropertySymbols(window.settingsStore).find(
- (x) => x.toString() == 'Symbol(mobx administration)',
- ) as any
- ];
+ const mobx = (window.settingsStore as any)[
+ Object.getOwnPropertySymbols(window.settingsStore).find(
+ (x) => x.toString() == 'Symbol(mobx administration)',
+ ) as any
+ ];
removeSettingsObserver = (mobx.observe_ || mobx.observe).call(mobx, (e: any) => {
e.newValue.bIsValveEmail = true;
@@ -37,17 +30,18 @@ export async function setShowValveInternal(show: boolean) {
}
}
-export async function setShouldConnectToReactDevTools(enable: boolean) {
- DeckyPluginLoader.toaster.toast({
- title: enable ? (
- <TranslationHelper transClass={TranslationClass.DEVELOPER} transText={'enabling'} />
- ) : (
- <TranslationHelper transClass={TranslationClass.DEVELOPER} transText={'disabling'} />
- ),
- body: <TranslationHelper transClass={TranslationClass.DEVELOPER} transText={'5secreload'} />,
- icon: <FaReact />,
- });
- await sleep(5000);
+export async function setShouldConnectToReactDevTools(enable: boolean, skipWait?: boolean) {
+ skipWait ||
+ DeckyPluginLoader.toaster.toast({
+ title: enable ? (
+ <TranslationHelper transClass={TranslationClass.DEVELOPER} transText={'enabling'} />
+ ) : (
+ <TranslationHelper transClass={TranslationClass.DEVELOPER} transText={'disabling'} />
+ ),
+ body: <TranslationHelper transClass={TranslationClass.DEVELOPER} transText={'5secreload'} />,
+ icon: <FaReact />,
+ });
+ skipWait || (await sleep(5000));
return enable ? DeckyBackend.call('utilities/enable_rdt') : DeckyBackend.call('utilities/disable_rdt');
}
@@ -58,5 +52,5 @@ export async function startup() {
if (isValveInternalEnabled) setShowValveInternal(isValveInternalEnabled);
if ((isRDTEnabled && !window.deckyHasConnectedRDT) || (!isRDTEnabled && window.deckyHasConnectedRDT))
- setShouldConnectToReactDevTools(isRDTEnabled);
+ setShouldConnectToReactDevTools(isRDTEnabled, true);
}
diff --git a/frontend/src/router-hook.tsx b/frontend/src/hooks/router.tsx
index a7b6ec92..670ff8b4 100644
--- a/frontend/src/router-hook.tsx
+++ b/frontend/src/hooks/router.tsx
@@ -16,15 +16,15 @@ import {
DeckyGlobalComponentsState,
DeckyGlobalComponentsStateContextProvider,
useDeckyGlobalComponentsState,
-} from './components/DeckyGlobalComponentsState';
+} from '../components/DeckyGlobalComponentsState';
import {
DeckyRouterState,
DeckyRouterStateContextProvider,
RoutePatch,
RouterEntry,
useDeckyRouterState,
-} from './components/DeckyRouterState';
-import Logger from './logger';
+} from '../components/DeckyRouterState';
+import Logger from '../logger';
declare global {
interface Window {
@@ -83,7 +83,7 @@ class RouterHook extends Logger {
this.patchGamepadRouter();
break;
// Not fully implemented yet
- // case UIMode.Desktop:
+ // case EUIMode.Desktop:
// this.debug("Patching desktop router");
// this.patchDesktopRouter();
// break;
diff --git a/frontend/src/tabs-hook.tsx b/frontend/src/hooks/tabs.tsx
index ecb60f30..4d917b6c 100644
--- a/frontend/src/tabs-hook.tsx
+++ b/frontend/src/hooks/tabs.tsx
@@ -10,8 +10,8 @@ import {
getReactRoot,
} from '@decky/ui';
-import { QuickAccessVisibleStateProvider } from './components/QuickAccessVisibleState';
-import Logger from './logger';
+import { QuickAccessVisibleStateProvider } from '../components/QuickAccessVisibleState';
+import Logger from '../logger';
declare global {
interface Window {
diff --git a/frontend/src/toaster.tsx b/frontend/src/hooks/toaster.tsx
index 99f8023e..9108d9ec 100644
--- a/frontend/src/toaster.tsx
+++ b/frontend/src/hooks/toaster.tsx
@@ -1,21 +1,20 @@
import type { ToastData, ToastNotification } from '@decky/api';
-import { ErrorBoundary, Patch, callOriginal, findModuleExport, injectFCTrampoline, replacePatch } from '@decky/ui';
+import {
+ ESteamNotificationAction,
+ ErrorBoundary,
+ Patch,
+ callOriginal,
+ findModuleExport,
+ injectFCTrampoline,
+ replacePatch,
+} from '@decky/ui';
-import Toast from './components/Toast';
-import Logger from './logger';
-
-// TODO export
-enum ToastType {
- New,
- Update,
- Remove,
-}
+import Toast from '../components/Toast';
+import Logger from '../logger';
declare global {
interface Window {
__TOASTER_INSTANCE: any;
- settingsStore: any;
- NotificationStore: any;
}
}
@@ -111,7 +110,7 @@ class Toaster extends Logger {
}, toast.expiration);
}
try {
- window.NotificationStore.ProcessNotification(info, toastData, ToastType.New);
+ window.NotificationStore.ProcessNotification(info, toastData, ESteamNotificationAction.New);
} catch (e) {
this.error('Error while sending toast:', e);
}
diff --git a/frontend/src/hooks/vr-dashboard-bar.tsx b/frontend/src/hooks/vr-dashboard-bar.tsx
new file mode 100644
index 00000000..905aee03
--- /dev/null
+++ b/frontend/src/hooks/vr-dashboard-bar.tsx
@@ -0,0 +1,105 @@
+// Hook the SteamVR Hotbar
+import { ErrorBoundary, Patch, afterPatch, findInReactTree, findModuleExport, getReactRoot } from '@decky/ui';
+import { ReactNode, createElement } from 'react';
+
+import Logger from '../logger';
+
+declare global {
+ interface Window {
+ __VR_DASHBAORD_BAR_HOOK_INSTANCE: any;
+ }
+}
+
+// TODO kinda dumb
+enum VRDashboardBarButtonID {
+ Decky = 999,
+}
+
+export interface VRDashboardBarButton {
+ id: VRDashboardBarButtonID | number;
+ active?: boolean;
+ children?: ReactNode;
+ tooltip?: ReactNode;
+ popupContents?: ReactNode;
+ onActivate?: () => void;
+ refBarPopupHandle?: any; // TODO idk
+ onPopupVisibilityChanged?: (visible: boolean) => void;
+}
+
+class VRDashboardBarHook extends Logger {
+ rightSideButtons: VRDashboardBarButton[] = [];
+ private rendererdRightSideButtons: ReactNode[] = [];
+ private barPatch?: Patch;
+ private barButton?: any;
+
+ constructor() {
+ super('VRDashboardBarHook');
+
+ this.log('Initialized');
+ window.__VR_DASHBAORD_BAR_HOOK_INSTANCE?.deinit?.();
+ window.__VR_DASHBAORD_BAR_HOOK_INSTANCE = this;
+ }
+
+ init() {
+ this.barButton = findModuleExport((e) => e?.render?.toString()?.includes('VRDashboardBarSmallButtonActive'));
+ const barComponent = findModuleExport((e) => e?.type?.toString()?.includes('vrLocation=="dashboard-bar"'));
+
+ this.barPatch = afterPatch(barComponent, 'type', (_, ret) => {
+ const nodes = findInReactTree(ret, (x) => x?.length > 6);
+ if (!window?.securitystore?.IsLockScreenActive?.()) nodes.splice(4, 0, this.rendererdRightSideButtons);
+ return ret;
+ });
+
+ // Patch already rendered bar
+ const root = getReactRoot(document.getElementById('root') as any);
+ const barNode =
+ root &&
+ findInReactTree(
+ root,
+ (n: any) => n.elementType == barComponent || (barComponent != null && n.elementType == barComponent),
+ ); // need elementType, because case where type is actually mobx wrapper
+ if (barNode) {
+ // Only affects this fiber node so we don't need to unpatch here
+ barNode.type = barNode.elementType.type;
+ if (barNode?.alternate) {
+ barNode.alternate.type = barNode.type;
+ }
+ }
+ }
+
+ deinit() {
+ this.barPatch?.unpatch();
+ }
+
+ addRightSideButton(button: VRDashboardBarButton) {
+ this.debug('Adding RSB', button.id, 'to render array');
+ this.rightSideButtons.push(button);
+ this.renderRightSideButtons();
+ }
+
+ removeRightSideButton(id: number) {
+ this.debug('Removing RSB', id);
+ this.rightSideButtons = this.rightSideButtons.filter((button) => button.id !== id);
+ this.renderRightSideButtons();
+ }
+
+ renderRightSideButtons() {
+ const arr = [];
+ for (const button of this.rightSideButtons) {
+ arr.push(
+ createElement(this.barButton, {
+ active: button.active,
+ children: <ErrorBoundary>{button.children}</ErrorBoundary>,
+ tooltip: button.tooltip,
+ popupContents: <ErrorBoundary>{button.popupContents}</ErrorBoundary>,
+ onActivate: button.onActivate,
+ refBarPopupHandle: button.refBarPopupHandle,
+ onPopupVisibilityChanged: button.onPopupVisibilityChanged,
+ }),
+ );
+ }
+ this.rendererdRightSideButtons = arr;
+ }
+}
+
+export default VRDashboardBarHook;
diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx
index 81b96dcc..0edcb5f7 100644
--- a/frontend/src/plugin-loader.tsx
+++ b/frontend/src/plugin-loader.tsx
@@ -7,9 +7,11 @@ import {
PanelSectionRow,
QuickAccessTab,
findSP,
+ joinClassNames,
quickAccessMenuClasses,
showModal,
sleep,
+ vrDashboardClasses,
} from '@decky/ui';
import { FC, lazy } from 'react';
import { FaDownload, FaExclamationCircle, FaExclamationTriangle, FaPlug } from 'react-icons/fa';
@@ -24,19 +26,20 @@ import PluginInstallModal from './components/modals/PluginInstallModal';
import PluginUninstallModal from './components/modals/PluginUninstallModal';
import NotificationBadge from './components/NotificationBadge';
import PluginView from './components/PluginView';
-import { useQuickAccessVisible } from './components/QuickAccessVisibleState';
+import { QuickAccessVisibleStateProvider, useQuickAccessVisible } from './components/QuickAccessVisibleState';
import WithSuspense from './components/WithSuspense';
import ErrorBoundaryHook from './errorboundary-hook';
import { FrozenPluginService } from './frozen-plugins-service';
import { HiddenPluginsService } from './hidden-plugins-service';
+import RouterHook from './hooks/router';
+import TabsHook from './hooks/tabs';
+import Toaster from './hooks/toaster';
+import VRDashboardBarHook from './hooks/vr-dashboard-bar';
import Logger from './logger';
import { NotificationService } from './notification-service';
import { DisabledPlugin, InstallType, Plugin, PluginLoadType } from './plugin';
-import RouterHook from './router-hook';
import { deinitSteamFixes, initSteamFixes } from './steamfixes';
import { PluginUpdateMapping, checkForPluginUpdates } from './store';
-import TabsHook from './tabs-hook';
-import Toaster from './toaster';
import { getVersionInfo } from './updater';
import { getPluginDisplayName } from './utils/pluginHelpers';
import { getSetting, setSetting } from './utils/settings';
@@ -70,6 +73,7 @@ class PluginLoader extends Logger {
private plugins: Plugin[] = [];
public errorBoundaryHook: ErrorBoundaryHook = new ErrorBoundaryHook();
private tabsHook: TabsHook = new TabsHook();
+ private vrDashboardBarHook: VRDashboardBarHook = new VRDashboardBarHook(); // TODO public? unsure
public routerHook: RouterHook = new RouterHook();
public toaster: Toaster = new Toaster();
private deckyState: DeckyState = new DeckyState();
@@ -105,10 +109,11 @@ class PluginLoader extends Logger {
DeckyBackend.addEventListener(`loader/plugin_event`, this.pluginEventListener);
this.tabsHook.init();
+ this.vrDashboardBarHook.init();
- const TabBadge = () => {
+ const TabBadge = ({ vr }: { vr?: boolean }) => {
const { updates, hasLoaderUpdate } = useDeckyState();
- return <NotificationBadge show={(updates && updates.size > 0) || hasLoaderUpdate} />;
+ return <NotificationBadge vr={vr} show={(updates && updates.size > 0) || hasLoaderUpdate} />;
};
this.tabsHook.add({
@@ -127,6 +132,44 @@ class PluginLoader extends Logger {
),
});
+ this.vrDashboardBarHook.addRightSideButton({
+ id: 999,
+ tooltip: 'Decky',
+ popupContents: (
+ <QuickAccessVisibleStateProvider initialVal={true}>
+ {' '}
+ {/*for compat only, these don't render when inactive*/}
+ <DeckyStateContextProvider deckyState={this.deckyState}>
+ <div className={joinClassNames(vrDashboardClasses.DashboardBarPopupContents)}>
+ <PluginView vr={true} />
+ </div>
+ </DeckyStateContextProvider>
+ </QuickAccessVisibleStateProvider>
+ ),
+ children: (
+ <DeckyStateContextProvider deckyState={this.deckyState}>
+ <div
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ }}
+ className={'deckyVRIcon'}
+ >
+ <FaPlug style={{ marginRight: '0px' }} />
+ <TabBadge vr={true} />
+ <style>
+ {`
+ div:has(>.deckyVRIcon) {
+ overflow: visible !important;
+ }
+ `}
+ </style>
+ </div>
+ </DeckyStateContextProvider>
+ ),
+ });
+
this.routerHook.addRoute('/decky/store', () => (
<DeckyStateContextProvider deckyState={this.deckyState}>
<WithSuspense route={true}>
@@ -399,6 +442,7 @@ class PluginLoader extends Logger {
deinitFilepickerPatches();
this.routerHook.deinit();
this.tabsHook.deinit();
+ this.vrDashboardBarHook.deinit();
this.toaster.deinit();
this.errorBoundaryHook.deinit();
}
diff --git a/scripts/deckdebug.sh b/scripts/deckdebug.sh
index 9ecf63fe..d67fd236 100755
--- a/scripts/deckdebug.sh
+++ b/scripts/deckdebug.sh
@@ -10,7 +10,7 @@ fi
# https://jackson.dev/post/a-portable-nix-shell-shebang/
-if [ -z "$INSIDE_NIX_RANDOMSTRING" ] && command -v nix &> /dev/null; then
+if [ -z "$INSIDE_NIX_RANDOMSTRING" ] && command -v nix &> /dev/null && test -e /etc/NIXOS; then
# If the user has nix, relaunch in nix shell with dependencies added
INSIDE_NIX_RANDOMSTRING=1 nix shell \
nixpkgs#websocat \