diff options
Diffstat (limited to 'frontend/src')
| -rw-r--r-- | frontend/src/errorboundary-hook.tsx | 10 | ||||
| -rw-r--r-- | frontend/src/plugin-loader.tsx | 1 | ||||
| -rw-r--r-- | frontend/src/router-hook.tsx | 35 | ||||
| -rw-r--r-- | frontend/src/tabs-hook.tsx | 41 | ||||
| -rw-r--r-- | frontend/src/toaster.tsx | 172 |
5 files changed, 86 insertions, 173 deletions
diff --git a/frontend/src/errorboundary-hook.tsx b/frontend/src/errorboundary-hook.tsx index 072c96ca..95be77ab 100644 --- a/frontend/src/errorboundary-hook.tsx +++ b/frontend/src/errorboundary-hook.tsx @@ -69,13 +69,13 @@ class ErrorBoundaryHook extends Logger { }); if (!ErrorBoundary) { - this.error('could not find ValveErrorBoundary'); + this.error('@decky/ui could not find ErrorBoundary, skipping patch'); return; } this.errorBoundaryPatch = replacePatch(ErrorBoundary.prototype, 'render', function (this: any) { if (this.state._deckyForceRerender) { - const stateClone = {...this.state, _deckyForceRerender: null}; + const stateClone = { ...this.state, _deckyForceRerender: null }; this.setState(stateClone); return null; } @@ -93,9 +93,9 @@ class ErrorBoundaryHook extends Logger { return callOriginal; }); // Small hack that gives us a lot more flexibility to force rerenders. - ValveErrorBoundary.prototype._deckyForceRerender = function (this: any) { - this.setState({...this.state, _deckyForceRerender: true}); - } + ErrorBoundary.prototype._deckyForceRerender = function (this: any) { + this.setState({ ...this.state, _deckyForceRerender: true }); + }; } public temporarilyDisableReporting() { diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx index f5ff71b5..8187116e 100644 --- a/frontend/src/plugin-loader.tsx +++ b/frontend/src/plugin-loader.tsx @@ -36,7 +36,6 @@ import Toaster from './toaster'; import { getVersionInfo } from './updater'; import { getSetting, setSetting } from './utils/settings'; import TranslationHelper, { TranslationClass } from './utils/TranslationHelper'; -import AppHook from './app-hook'; const StorePage = lazy(() => import('./components/store/Store')); const SettingsPage = lazy(() => import('./components/settings')); diff --git a/frontend/src/router-hook.tsx b/frontend/src/router-hook.tsx index 9aba497e..4255f257 100644 --- a/frontend/src/router-hook.tsx +++ b/frontend/src/router-hook.tsx @@ -1,5 +1,5 @@ -import { ErrorBoundary, Focusable, Patch, afterPatch, beforePatch, findInReactTree, findModuleByExport, findModuleExport, getReactRoot, sleep } from '@decky/ui'; -import { FC, ReactElement, ReactNode, cloneElement, createElement, memo } from 'react'; +import { ErrorBoundary, Patch, afterPatch, findInReactTree, getReactRoot, sleep } from '@decky/ui'; +import { FC, ReactElement, ReactNode, cloneElement, createElement } from 'react'; import type { Route } from 'react-router'; import { @@ -32,6 +32,7 @@ class RouterHook extends Logger { private DeckyWrapper = this.routerWrapper.bind(this); private DeckyGlobalComponentsWrapper = this.globalComponentsWrapper.bind(this); private toReplace = new Map<string, ReactNode>(); + private routerPatch?: Patch; public routes?: any[]; constructor() { @@ -41,22 +42,25 @@ class RouterHook extends Logger { window.__ROUTER_HOOK_INSTANCE?.deinit?.(); window.__ROUTER_HOOK_INSTANCE = this; - (async()=> { + (async () => { const root = getReactRoot(document.getElementById('root') as any); // TODO be more specific, this is horrible and very very slow - const findRouterNode = () =>findInReactTree(root, node => typeof node?.pendingProps?.loggedIn == "undefined" && node?.type?.toString().includes("Settings.Root()")); + const findRouterNode = () => + findInReactTree( + root, + (node) => + typeof node?.pendingProps?.loggedIn == 'undefined' && node?.type?.toString().includes('Settings.Root()'), + ); let routerNode = findRouterNode(); while (!routerNode) { - this.warn( - 'Failed to find Router node, reattempting in 5 seconds.', - ); + this.warn('Failed to find Router node, reattempting in 5 seconds.'); await sleep(5000); routerNode = findRouterNode(); } if (routerNode) { - this.debug("routerNode", routerNode); + this.debug('routerNode', routerNode); // Patch the component globally - afterPatch(routerNode.elementType, "type", this.handleRouterRender.bind(this)); + this.routerPatch = afterPatch(routerNode.elementType, 'type', this.handleRouterRender.bind(this)); // Swap out the current instance routerNode.type = routerNode.elementType.type; if (routerNode?.alternate) { @@ -91,14 +95,14 @@ class RouterHook extends Logger { return returnVal; } - private globalComponentsWrapper () { + private globalComponentsWrapper() { const { components } = useDeckyGlobalComponentsState(); if (this.renderedComponents.length != components.size) { this.debug('Rerendering global components'); this.renderedComponents = Array.from(components.values()).map((GComponent) => <GComponent />); } return <>{this.renderedComponents}</>; - }; + } private routerWrapper({ children }: { children: ReactElement }) { // Used to store the new replicated routes we create to allow routes to be unpatched. @@ -106,7 +110,7 @@ class RouterHook extends Logger { const { routes, routePatches } = useDeckyRouterState(); // TODO make more redundant if (!children?.props?.children?.[0]?.props?.children) { - console.log("routerWrapper wrong component?", children) + console.log('routerWrapper wrong component?', children); return children; } const mainRouteList = children.props.children[0].props.children; @@ -116,7 +120,7 @@ class RouterHook extends Logger { this.debug('Rerendered routes list'); return children; - }; + } private processList( routeList: any[], @@ -167,7 +171,7 @@ class RouterHook extends Logger { }); } }); - }; + } addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props'] = {}) { this.routerState.addRoute(path, component, props); @@ -194,8 +198,7 @@ class RouterHook extends Logger { } deinit() { - // this.wrapperPatch.unpatch(); - // this.routerPatch?.unpatch(); + this.routerPatch?.unpatch(); } } diff --git a/frontend/src/tabs-hook.tsx b/frontend/src/tabs-hook.tsx index dbf6ca35..34b5f592 100644 --- a/frontend/src/tabs-hook.tsx +++ b/frontend/src/tabs-hook.tsx @@ -1,5 +1,14 @@ // TabsHook for versions after the Desktop merge -import { ErrorBoundary, Patch, QuickAccessTab, afterPatch, createReactTreePatcher, findInReactTree, findModuleByExport, getReactRoot, setReactPatcherLoggingEnabled, sleep } from '@decky/ui'; +import { + ErrorBoundary, + Patch, + QuickAccessTab, + afterPatch, + createReactTreePatcher, + findInReactTree, + findModuleByExport, + getReactRoot, +} from '@decky/ui'; import { QuickAccessVisibleStateProvider } from './components/QuickAccessVisibleState'; import Logger from './logger'; @@ -20,9 +29,7 @@ interface Tab { class TabsHook extends Logger { // private keys = 7; tabs: Tab[] = []; - private qAMRoot?: any; private qamPatch?: Patch; - private cachedTabs: any; constructor() { super('TabsHook'); @@ -34,25 +41,29 @@ class TabsHook extends Logger { init() { // TODO patch the "embedded" renderer in this module too (seems to be for VR? unsure) - const qamModule = findModuleByExport(e => e?.type?.toString()?.includes("QuickAccessMenuBrowserView")); - const qamRenderer = Object.values(qamModule).find((e: any) => e?.type?.toString()?.includes("QuickAccessMenuBrowserView")) + const qamModule = findModuleByExport((e) => e?.type?.toString()?.includes('QuickAccessMenuBrowserView')); + const qamRenderer = Object.values(qamModule).find((e: any) => + e?.type?.toString()?.includes('QuickAccessMenuBrowserView'), + ); - const patchHandler = createReactTreePatcher([ - tree => findInReactTree(tree, node => node?.props?.onFocusNavDeactivated) - ], (args, ret) => { - this.log("qam render", args, ret); - const tabs = findInReactTree(ret, (x) => x?.props?.tabs); - this.render(tabs.props.tabs, args[0].visible); - return ret; - }, "TabsHook"); + const patchHandler = createReactTreePatcher( + [(tree) => findInReactTree(tree, (node) => node?.props?.onFocusNavDeactivated)], + (args, ret) => { + this.log('qam render', args, ret); + const tabs = findInReactTree(ret, (x) => x?.props?.tabs); + this.render(tabs.props.tabs, args[0].visible); + return ret; + }, + 'TabsHook', + ); - this.qamPatch = afterPatch(qamRenderer, "type", patchHandler); + this.qamPatch = afterPatch(qamRenderer, 'type', patchHandler); // Patch already rendered qam const root = getReactRoot(document.getElementById('root') as any); const qamNode = root && findInReactTree(root, (n: any) => n.elementType == qamRenderer); // need elementType, because type is actually mobx wrapper if (qamNode) { - this.debug("qamNode", qamNode); + this.debug('qamNode', qamNode); // Only affects this fiber node so we don't need to unpatch here qamNode.type = qamNode.elementType.type; if (qamNode?.alternate) { diff --git a/frontend/src/toaster.tsx b/frontend/src/toaster.tsx index 4bc08772..611806d2 100644 --- a/frontend/src/toaster.tsx +++ b/frontend/src/toaster.tsx @@ -1,19 +1,16 @@ import type { ToastData } from '@decky/api'; -import { - Export, - Patch, - afterPatch, - findClassByName, - findInReactTree, - findModuleExport, - getReactRoot, - sleep, -} from '@decky/ui'; -import { ReactNode } from 'react'; +import { Patch, callOriginal, findModuleExport, injectFCTrampoline, replacePatch } from '@decky/ui'; import Toast from './components/Toast'; import Logger from './logger'; +// TODO export +enum ToastType { + New, + Update, + Remove, +} + declare global { interface Window { __TOASTER_INSTANCE: any; @@ -23,18 +20,12 @@ declare global { } class Toaster extends Logger { - // private routerHook: RouterHook; - // private toasterState: DeckyToasterState = new DeckyToasterState(); - private node: any; - private rNode: any; - private audioModule: any; private finishStartup?: () => void; private ready: Promise<void> = new Promise((res) => (this.finishStartup = res)); - private toasterPatch?: Patch; + private toastPatch?: Patch; constructor() { super('Toaster'); - // this.routerHook = routerHook; window.__TOASTER_INSTANCE?.deinit?.(); window.__TOASTER_INSTANCE = this; @@ -42,115 +33,18 @@ class Toaster extends Logger { } async init() { - // this.routerHook.addGlobalComponent('DeckyToaster', () => ( - // <DeckyToasterStateContextProvider deckyToasterState={this.toasterState}> - // <DeckyToaster /> - // </DeckyToasterStateContextProvider> - // )); - let instance: any; - const tree = getReactRoot(document.getElementById('root') as any); - const toasterClass1 = findClassByName('GamepadToastPlaceholder'); - const toasterClass2 = findClassByName('ToastPlaceholder'); - const toasterClass3 = findClassByName('ToastPopup'); - const toasterClass4 = findClassByName('GamepadToastPopup'); - const findToasterRoot = (currentNode: any, iters: number): any => { - if (iters >= 80) { - // currently 66 - return null; - } - if ( - currentNode?.memoizedProps?.className?.startsWith?.(toasterClass1) || - currentNode?.memoizedProps?.className?.startsWith?.(toasterClass2) || - currentNode?.memoizedProps?.className?.startsWith?.(toasterClass3) || - currentNode?.memoizedProps?.className?.startsWith?.(toasterClass4) - ) { - this.log(`Toaster root was found in ${iters} recursion cycles`); - return currentNode; - } - if (currentNode.sibling) { - let node = findToasterRoot(currentNode.sibling, iters + 1); - if (node !== null) return node; - } - if (currentNode.child) { - let node = findToasterRoot(currentNode.child, iters + 1); - if (node !== null) return node; - } - return null; - }; - instance = findToasterRoot(tree, 0); - while (!instance) { - this.warn( - 'Failed to find Toaster root node, reattempting in 5 seconds. A developer may need to increase the recursion limit.', - ); - await sleep(5000); - instance = findToasterRoot(tree, 0); - } - this.node = instance.return; - this.rNode = findInReactTree( - this.node.return.return, - (node) => node?.stateNode && node.type?.InstallErrorReportingStore, - ); - let toast: any; - let renderedToast: ReactNode = null; - let innerPatched: any; - const repatch = () => { - if (this.node && !this.node.type.decky) { - this.toasterPatch = afterPatch(this.node, 'type', (_: any, ret: any) => { - const inner = findInReactTree(ret.props.children, (x) => x?.props?.onDismiss); - if (innerPatched) { - inner.type = innerPatched; - } else { - afterPatch(inner, 'type', (innerArgs: any, ret: any) => { - const currentToast = innerArgs[0]?.notification; - if (currentToast?.decky) { - if (currentToast == toast) { - ret.props.children = renderedToast; - } else { - toast = currentToast; - renderedToast = <Toast toast={toast.data} />; - ret.props.children = renderedToast; - } - } else { - toast = null; - renderedToast = null; - } - return ret; - }); - innerPatched = inner.type; - } - return ret; - }); - this.node.type.decky = true; - this.node.alternate.type = this.node.type; - } - }; - const oRender = Object.getPrototypeOf(this.rNode.stateNode).render; - let int: number | undefined; - this.rNode.stateNode.render = (...args: any[]) => { - const ret = oRender.call(this.rNode.stateNode, ...args); - if (ret && !this?.node?.return?.return) { - int && clearInterval(int); - int = setInterval(() => { - const n = findToasterRoot(tree, 0); - if (n?.return) { - clearInterval(int); - this.node = n.return; - this.rNode = this.node.return; - repatch(); - } else { - this.error('Failed to re-grab Toaster node, trying again...'); - } - }, 1200); + const ToastRenderer = findModuleExport((e) => e?.toString()?.includes(`controller:"notification",method:`)); + this.debug('toastrenderer', ToastRenderer); + // TODO find a way to undo this if possible? + const patchedRenderer = injectFCTrampoline(ToastRenderer); + this.toastPatch = replacePatch(patchedRenderer, 'component', (args: any[]) => { + this.debug('render toast', args); + if (args?.[0]?.group?.decky || args?.[0]?.group?.notifications?.[0]?.decky) { + this.debug('rendering decky toast'); + return args[0].group.notifications.map((notification: any) => <Toast toast={notification.data} />); } - repatch(); - return ret; - }; - - this.rNode.stateNode.shouldComponentUpdate = () => true; - this.rNode.stateNode.forceUpdate(); - delete this.rNode.stateNode.shouldComponentUpdate; - - this.audioModule = findModuleExport((e: Export) => e.PlayNavSound && e.RegisterCallbackOnPlaySound); + return callOriginal; + }); this.log('Initialized'); this.finishStartup?.(); @@ -168,8 +62,6 @@ class Toaster extends Logger { data: toast, decky: true, }; - // @ts-ignore - toastData.data.appid = () => 0; if (toast.sound === undefined) toast.sound = 6; if (toast.playSound === undefined) toast.playSound = true; if (toast.showToast === undefined) toast.showToast = true; @@ -180,19 +72,27 @@ class Toaster extends Logger { window.NotificationStore.BIsUserInGame()) ) return; - if (toast.playSound) this.audioModule?.PlayNavSound(toast.sound); if (toast.showToast) { - window.NotificationStore.m_rgNotificationToasts.push(toastData); - window.NotificationStore.DispatchNextToast(); + function fnTray(toast: any, tray: any) { + let group = { + eType: toast.eType, + notifications: [toast], + }; + tray.unshift(group); + } + const info = { + showToast: toast.showToast, + sound: toast.sound, + eFeature: 0, + toastDurationMS: toastData.nToastDurationMS, + fnTray, + }; + window.NotificationStore.ProcessNotification(info, toastData, ToastType.New); } } deinit() { - this.toasterPatch?.unpatch(); - this.node.alternate.type = this.node.type; - delete this.rNode.stateNode.render; - this.ready = new Promise((res) => (this.finishStartup = res)); - // this.routerHook.removeGlobalComponent('DeckyToaster'); + this.toastPatch?.unpatch(); } } |
