diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | backend/decky_loader/injector.py | 4 | ||||
| -rw-r--r-- | backend/decky_loader/utilities.py | 2 | ||||
| -rw-r--r-- | frontend/pnpm-lock.yaml | 12 | ||||
| -rw-r--r-- | frontend/pnpm-workspace.yaml | 7 | ||||
| -rw-r--r-- | frontend/src/components/NotificationBadge.tsx | 55 | ||||
| -rw-r--r-- | frontend/src/components/PluginView.tsx | 40 | ||||
| -rw-r--r-- | frontend/src/components/QuickAccessVisibleState.tsx | 17 | ||||
| -rw-r--r-- | frontend/src/components/settings/pages/developer/index.tsx | 33 | ||||
| -rw-r--r-- | frontend/src/developer.tsx | 42 | ||||
| -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.tsx | 105 | ||||
| -rw-r--r-- | frontend/src/plugin-loader.tsx | 56 | ||||
| -rwxr-xr-x | scripts/deckdebug.sh | 2 |
16 files changed, 303 insertions, 110 deletions
@@ -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 \ |
