diff options
| author | TrainDoctor <traindoctor@protonmail.com> | 2022-10-30 10:32:05 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-10-30 10:32:05 -0700 |
| commit | bace5143d28c42ffcc83509b7fcdf02b6cae6934 (patch) | |
| tree | 5a39a5980a84136df5a6781ba1e200d151112073 | |
| parent | f5fc2053847d3054d36d3348d21e7de060342698 (diff) | |
| download | decky-loader-bace5143d28c42ffcc83509b7fcdf02b6cae6934.tar.gz decky-loader-bace5143d28c42ffcc83509b7fcdf02b6cae6934.zip | |
Merge Tabs and Injection Fixes, bring back native Valve toaster (#238)
* Bring back component patch-based tabshook
* better injection point
* finally fix dumb loading error
* fix QAM injection breaking after lock
* shut up typescript
* fix lock screen focusing issues
* Bring back the Valve toaster!
* Add support for stable steamos
* fix focus bug on lock screen but actually
* oops: remove extra console log
* shut up typescript again
* better fix for lockscreen bug
* better probably
* actually fix focus issues (WTF)
Co-authored-by: AAGaming <aa@mail.catvibers.me>
| -rw-r--r-- | backend/loader.py | 7 | ||||
| -rw-r--r-- | frontend/package.json | 2 | ||||
| -rw-r--r-- | frontend/pnpm-lock.yaml | 14 | ||||
| -rw-r--r-- | frontend/src/components/QuickAccessVisibleState.tsx | 36 | ||||
| -rw-r--r-- | frontend/src/components/Toast.tsx | 24 | ||||
| -rw-r--r-- | frontend/src/plugin-loader.tsx | 6 | ||||
| -rw-r--r-- | frontend/src/tabs-hook.old.tsx | 119 | ||||
| -rw-r--r-- | frontend/src/tabs-hook.tsx | 154 | ||||
| -rw-r--r-- | frontend/src/toaster.tsx | 269 |
9 files changed, 387 insertions, 244 deletions
diff --git a/backend/loader.py b/backend/loader.py index 8c48c7ae..e756ba5e 100644 --- a/backend/loader.py +++ b/backend/loader.py @@ -121,7 +121,7 @@ class Loader: with open(path.join(self.plugin_path, plugin.plugin_directory, "dist/index.js"), 'r') as bundle: return web.Response(text=bundle.read(), content_type="application/javascript") - def import_plugin(self, file, plugin_directory, refresh=False): + def import_plugin(self, file, plugin_directory, refresh=False, batch=False): try: plugin = PluginWrapper(file, plugin_directory, self.plugin_path) if plugin.name in self.plugins: @@ -135,7 +135,8 @@ class Loader: self.logger.info(f"Plugin {plugin.name} is passive") self.plugins[plugin.name] = plugin.start() self.logger.info(f"Loaded {plugin.name}") - self.loop.create_task(self.dispatch_plugin(plugin.name if not plugin.legacy else "$LEGACY_" + plugin.name, plugin.version)) + if not batch: + self.loop.create_task(self.dispatch_plugin(plugin.name if not plugin.legacy else "$LEGACY_" + plugin.name, plugin.version)) except Exception as e: self.logger.error(f"Could not load {file}. {e}") print_exc() @@ -150,7 +151,7 @@ class Loader: directories = [i for i in listdir(self.plugin_path) if path.isdir(path.join(self.plugin_path, i)) and path.isfile(path.join(self.plugin_path, i, "plugin.json"))] for directory in directories: self.logger.info(f"found plugin: {directory}") - self.import_plugin(path.join(self.plugin_path, directory, "main.py"), directory) + self.import_plugin(path.join(self.plugin_path, directory, "main.py"), directory, False, True) async def handle_reloads(self): while True: diff --git a/frontend/package.json b/frontend/package.json index 316e4c78..ec09b0c2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,7 +41,7 @@ } }, "dependencies": { - "decky-frontend-lib": "^3.7.3", + "decky-frontend-lib": "^3.7.11", "react-file-icon": "^1.2.0", "react-icons": "^4.4.0", "react-markdown": "^8.0.3", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 0fc03765..ab6e2391 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -10,7 +10,7 @@ specifiers: '@types/react-file-icon': ^1.0.1 '@types/react-router': 5.1.18 '@types/webpack': ^5.28.0 - decky-frontend-lib: ^3.7.3 + decky-frontend-lib: ^3.7.11 husky: ^8.0.1 import-sort-style-module: ^6.0.0 inquirer: ^8.2.4 @@ -30,7 +30,7 @@ specifiers: typescript: ^4.7.4 dependencies: - decky-frontend-lib: 3.7.3 + decky-frontend-lib: 3.7.11 react-file-icon: 1.2.0_wcqkhtmu7mswc6yz4uyexck3ty react-icons: 4.4.0_react@16.14.0 react-markdown: 8.0.3_vshvapmxg47tngu7tvrsqpq55u @@ -944,10 +944,8 @@ packages: dependencies: ms: 2.1.2 - /decky-frontend-lib/3.7.3: - resolution: {integrity: sha512-HFHI19zr3gzOXDBF0DE9W+ZSx+mtjc/XqCYANoVfpMaDX1ITZpk2lMzBGuh9QvtHZ4LygtYEPIWDlrJDs8rGKA==} - dependencies: - minimist: 1.2.7 + /decky-frontend-lib/3.7.11: + resolution: {integrity: sha512-c5/kXqCLYhCl0zC+kPJ2gTUjTp6N0zUFKzTQKVKTuQ3U+01fHAU6sUsDqudbdTNdjXiofGujMmeJqKaU2vQoXQ==} dev: false /decode-named-character-reference/1.0.2: @@ -1944,10 +1942,6 @@ packages: brace-expansion: 1.1.11 dev: true - /minimist/1.2.7: - resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} - dev: false - /mri/1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} diff --git a/frontend/src/components/QuickAccessVisibleState.tsx b/frontend/src/components/QuickAccessVisibleState.tsx index 4df7e1a1..09babe84 100644 --- a/frontend/src/components/QuickAccessVisibleState.tsx +++ b/frontend/src/components/QuickAccessVisibleState.tsx @@ -1,27 +1,21 @@ -import { FC, createContext, useContext, useEffect, useRef, useState } from 'react'; +import { FC, createContext, useContext, useState } from 'react'; const QuickAccessVisibleState = createContext<boolean>(true); export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState); -export const QuickAccessVisibleStateProvider: FC<{}> = ({ children }) => { - const divRef = useRef<HTMLDivElement>(null); - const [visible, setVisible] = useState<boolean>(false); - useEffect(() => { - const doc: Document | void | null = divRef?.current?.ownerDocument; - if (!doc) return; - setVisible(doc.visibilityState == 'visible'); - const onChange = (e: Event) => { - setVisible(doc.visibilityState == 'visible'); - }; - doc.addEventListener('visibilitychange', onChange); - return () => { - doc.removeEventListener('visibilitychange', onChange); - }; - }, [divRef]); - return ( - <div ref={divRef}> - <QuickAccessVisibleState.Provider value={visible}>{children}</QuickAccessVisibleState.Provider> - </div> - ); +export const QuickAccessVisibleStateProvider: FC<{ initial: boolean; setter: ((val: boolean) => {}[]) | never[] }> = ({ + children, + initial, + setter, +}) => { + const [visible, setVisible] = useState<boolean>(initial); + const [prev, setPrev] = useState<boolean>(initial); + // hack to use an array as a "pointer" to pass the setter up the tree + setter[0] = setVisible; + if (initial != prev) { + setPrev(initial); + setVisible(initial); + } + return <QuickAccessVisibleState.Provider value={visible}>{children}</QuickAccessVisibleState.Provider>; }; diff --git a/frontend/src/components/Toast.tsx b/frontend/src/components/Toast.tsx index e7a220c2..78fb60aa 100644 --- a/frontend/src/components/Toast.tsx +++ b/frontend/src/components/Toast.tsx @@ -27,20 +27,18 @@ const templateClasses = findModule((mod) => { const Toast: FunctionComponent<ToastProps> = ({ toast }) => { return ( - <div className={toastClasses.ToastPopup}> - <div - style={{ '--toast-duration': `${toast.duration}ms` } as React.CSSProperties} - onClick={toast.onClick} - className={joinClassNames(templateClasses.ShortTemplate, toast.className || '')} - > - {toast.logo && <div className={templateClasses.StandardLogoDimensions}>{toast.logo}</div>} - <div className={joinClassNames(templateClasses.Content, toast.contentClassName || '')}> - <div className={templateClasses.Header}> - {toast.icon && <div className={templateClasses.Icon}>{toast.icon}</div>} - <div className={templateClasses.Title}>{toast.title}</div> - </div> - <div className={templateClasses.Body}>{toast.body}</div> + <div + style={{ '--toast-duration': `${toast.duration}ms` } as React.CSSProperties} + onClick={toast.onClick} + className={joinClassNames(templateClasses.ShortTemplate, toast.className || '')} + > + {toast.logo && <div className={templateClasses.StandardLogoDimensions}>{toast.logo}</div>} + <div className={joinClassNames(templateClasses.Content, toast.contentClassName || '')}> + <div className={templateClasses.Header}> + {toast.icon && <div className={templateClasses.Icon}>{toast.icon}</div>} + <div className={templateClasses.Title}>{toast.title}</div> </div> + <div className={templateClasses.Body}>{toast.body}</div> </div> </div> ); diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx index 92c634c9..f24a9605 100644 --- a/frontend/src/plugin-loader.tsx +++ b/frontend/src/plugin-loader.tsx @@ -23,6 +23,7 @@ import { Plugin } from './plugin'; import RouterHook from './router-hook'; import { checkForUpdates } from './store'; import TabsHook from './tabs-hook'; +import OldTabsHook from './tabs-hook.old'; import Toaster from './toaster'; import { VerInfo, callUpdaterMethod } from './updater'; import { getSetting } from './utils/settings'; @@ -38,10 +39,10 @@ declare global { class PluginLoader extends Logger { private plugins: Plugin[] = []; - private tabsHook: TabsHook = new TabsHook(); + private tabsHook: TabsHook | OldTabsHook = document.title == 'SP' ? new OldTabsHook() : new TabsHook(); // private windowHook: WindowHook = new WindowHook(); private routerHook: RouterHook = new RouterHook(); - public toaster: Toaster = new Toaster(this.routerHook); + public toaster: Toaster = new Toaster(); private deckyState: DeckyState = new DeckyState(); private reloadLock: boolean = false; @@ -52,6 +53,7 @@ class PluginLoader extends Logger { constructor() { super(PluginLoader.name); + this.tabsHook.init(); this.log('Initialized'); const TabBadge = () => { diff --git a/frontend/src/tabs-hook.old.tsx b/frontend/src/tabs-hook.old.tsx new file mode 100644 index 00000000..5b511596 --- /dev/null +++ b/frontend/src/tabs-hook.old.tsx @@ -0,0 +1,119 @@ +// TabsHook for versions before the Desktop merge +import { Patch, afterPatch, sleep } from 'decky-frontend-lib'; +import { memo } from 'react'; + +import NewTabsHook from './tabs-hook'; + +declare global { + interface Array<T> { + __filter: any; + } +} + +const isTabsArray = (tabs: any) => { + const length = tabs.length; + return length >= 7 && tabs[length - 1]?.tab; +}; + +class TabsHook extends NewTabsHook { + // private keys = 7; + private quickAccess: any; + private tabRenderer: any; + private memoizedQuickAccess: any; + private cNode: any; + + private qAPTree: any; + private rendererTree: any; + + private cNodePatch?: Patch; + + constructor() { + super(); + + this.log('Initialized stable TabsHook'); + } + + init() { + const self = this; + const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current; + let scrollRoot: any; + async function findScrollRoot(currentNode: any, iters: number): Promise<any> { + if (iters >= 30) { + self.error( + 'Scroll root was not found before hitting the recursion limit, a developer will need to increase the limit.', + ); + return null; + } + currentNode = currentNode?.child; + if (currentNode?.type?.prototype?.RemoveSmartScrollContainer) { + self.log(`Scroll root was found in ${iters} recursion cycles`); + return currentNode; + } + if (!currentNode) return null; + if (currentNode.sibling) { + let node = await findScrollRoot(currentNode.sibling, iters + 1); + if (node !== null) return node; + } + return await findScrollRoot(currentNode, iters + 1); + } + (async () => { + scrollRoot = await findScrollRoot(tree, 0); + while (!scrollRoot) { + this.log('Failed to find scroll root node, reattempting in 5 seconds'); + await sleep(5000); + scrollRoot = await findScrollRoot(tree, 0); + } + let newQA: any; + let newQATabRenderer: any; + this.cNodePatch = afterPatch(scrollRoot.stateNode, 'render', (_: any, ret: any) => { + if (!this.quickAccess && ret.props.children.props.children[4]) { + this.quickAccess = ret?.props?.children?.props?.children[4].type; + newQA = (...args: any) => { + const ret = this.quickAccess.type(...args); + if (ret) { + if (!newQATabRenderer) { + this.tabRenderer = ret.props.children[1].children.type; + newQATabRenderer = (...qamArgs: any[]) => { + const oFilter = Array.prototype.filter; + Array.prototype.filter = function (...args: any[]) { + if (isTabsArray(this)) { + self.render(this, qamArgs[0].visible); + } + // @ts-ignore + return oFilter.call(this, ...args); + }; + // TODO remove array hack entirely and use this instead const tabs = ret.props.children.props.children[0].props.children[1].props.children[0].props.children[0].props.tabs + const ret = this.tabRenderer(...qamArgs); + Array.prototype.filter = oFilter; + return ret; + }; + } + this.rendererTree = ret.props.children[1].children; + ret.props.children[1].children.type = newQATabRenderer; + } + return ret; + }; + this.memoizedQuickAccess = memo(newQA); + this.memoizedQuickAccess.isDeckyQuickAccess = true; + } + if (ret.props.children.props.children[4]) { + this.qAPTree = ret.props.children.props.children[4]; + ret.props.children.props.children[4].type = this.memoizedQuickAccess; + } + return ret; + }); + this.cNode = scrollRoot; + this.cNode.stateNode.forceUpdate(); + this.log('Finished initial injection'); + })(); + } + + deinit() { + this.cNodePatch?.unpatch(); + if (this.qAPTree) this.qAPTree.type = this.quickAccess; + if (this.rendererTree) this.rendererTree.type = this.tabRenderer; + if (this.cNode) this.cNode.stateNode.forceUpdate(); + } +} + +export default TabsHook; diff --git a/frontend/src/tabs-hook.tsx b/frontend/src/tabs-hook.tsx index 5929b8a0..c7790f7e 100644 --- a/frontend/src/tabs-hook.tsx +++ b/frontend/src/tabs-hook.tsx @@ -1,22 +1,18 @@ -import { QuickAccessTab, quickAccessMenuClasses, sleep } from 'decky-frontend-lib'; +// TabsHook for versions after the Desktop merge +import { Patch, QuickAccessTab, afterPatch, findInReactTree, findModule, sleep } from 'decky-frontend-lib'; +import { memo } from 'react'; import { QuickAccessVisibleStateProvider } from './components/QuickAccessVisibleState'; import Logger from './logger'; +import { findSP } from './utils/windows'; declare global { interface Window { __TABS_HOOK_INSTANCE: any; - } - interface Array<T> { - __filter: any; + securitystore: any; } } -const isTabsArray = (tabs: any) => { - const length = tabs.length; - return length >= 7 && tabs[length - 1]?.tab; -}; - interface Tab { id: QuickAccessTab | number; title: any; @@ -27,7 +23,9 @@ interface Tab { class TabsHook extends Logger { // private keys = 7; tabs: Tab[] = []; - private oFilter: (...args: any[]) => any; + private qAMRoot?: any; + private qamPatch?: Patch; + private unsubscribeSecurity?: () => void; constructor() { super('TabsHook'); @@ -35,65 +33,90 @@ class TabsHook extends Logger { this.log('Initialized'); window.__TABS_HOOK_INSTANCE?.deinit?.(); window.__TABS_HOOK_INSTANCE = this; + } - const self = this; - const oFilter = (this.oFilter = Array.prototype.filter); - Array.prototype.filter = function patchedFilter(...args: any[]) { - if (isTabsArray(this)) { - self.render(this); + init() { + const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current; + let qAMRoot: any; + const findQAMRoot = (currentNode: any, iters: number): any => { + if (iters >= 55) { + // currently 45 + return null; + } + if ( + typeof currentNode?.memoizedProps?.visible == 'boolean' && + currentNode?.type?.toString()?.includes('QuickAccessMenuBrowserView') + ) { + this.log(`QAM root was found in ${iters} recursion cycles`); + return currentNode; } - // @ts-ignore - return oFilter.call(this, ...args); + if (currentNode.child) { + let node = findQAMRoot(currentNode.child, iters + 1); + if (node !== null) return node; + } + if (currentNode.sibling) { + let node = findQAMRoot(currentNode.sibling, iters + 1); + if (node !== null) return node; + } + return null; }; - - if (document.title != 'SP') - try { - const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current; - let qAMRoot: any; - async function findQAMRoot(currentNode: any, iters: number): Promise<any> { - if (iters >= 60) { - // currently 44 - return null; - } - currentNode = currentNode?.child; - if ( - currentNode?.memoizedProps?.className && - currentNode?.memoizedProps?.className.startsWith(quickAccessMenuClasses.ViewPlaceholder) - ) { - self.log(`QAM root was found in ${iters} recursion cycles`); - return currentNode; + (async () => { + qAMRoot = findQAMRoot(tree, 0); + while (!qAMRoot) { + this.error( + 'Failed to find QAM root node, reattempting in 5 seconds. A developer may need to increase the recursion limit.', + ); + await sleep(5000); + qAMRoot = findQAMRoot(tree, 0); + } + this.qAMRoot = qAMRoot; + let patchedInnerQAM: any; + this.qamPatch = afterPatch(qAMRoot.return, 'type', (_: any, ret: any) => { + try { + if (!qAMRoot?.child) { + qAMRoot = findQAMRoot(tree, 0); + this.qAMRoot = qAMRoot; } - if (!currentNode) return null; - if (currentNode.sibling) { - let node = await findQAMRoot(currentNode.sibling, iters + 1); - if (node !== null) return node; + if (qAMRoot?.child && !qAMRoot?.child?.type?.decky) { + afterPatch(qAMRoot.child, 'type', (_: any, ret: any) => { + try { + const qamTabsRenderer = findInReactTree(ret, (x) => x?.props?.onFocusNavDeactivated); + if (patchedInnerQAM) { + qamTabsRenderer.type = patchedInnerQAM; + } else { + afterPatch(qamTabsRenderer, 'type', (innerArgs: any, ret: any) => { + const tabs = findInReactTree(ret, (x) => x?.props?.tabs); + this.render(tabs.props.tabs, innerArgs[0].visible); + return ret; + }); + patchedInnerQAM = qamTabsRenderer.type; + } + } catch (e) { + this.error('Error patching QAM inner', e); + } + return ret; + }); + qAMRoot.child.type.decky = true; + qAMRoot.child.alternate.type = qAMRoot.child.type; } - return await findQAMRoot(currentNode, iters + 1); + } catch (e) { + this.error('Error patching QAM', e); } - (async () => { - qAMRoot = await findQAMRoot(tree, 0); - while (!qAMRoot) { - this.error( - 'Failed to find QAM root node, reattempting in 5 seconds. A developer may need to increase the recursion limit.', - ); - await sleep(5000); - qAMRoot = await findQAMRoot(tree, 0); - } - while (!qAMRoot?.stateNode?.forceUpdate) { - qAMRoot = qAMRoot.return; - } - qAMRoot.stateNode.shouldComponentUpdate = () => true; - qAMRoot.stateNode.forceUpdate(); - delete qAMRoot.stateNode.shouldComponentUpdate; - })(); - } catch (e) { - this.log('Failed to rerender QAM', e); + return ret; + }); + + if (qAMRoot.return.alternate) { + qAMRoot.return.alternate.type = qAMRoot.return.type; } + this.log('Finished initial injection'); + })(); } deinit() { - Array.prototype.filter = this.oFilter; + this.qamPatch?.unpatch(); + this.qAMRoot.return.alternate.type = this.qAMRoot.return.type; + this.unsubscribeSecurity?.(); } add(tab: Tab) { @@ -106,14 +129,25 @@ class TabsHook extends Logger { this.tabs = this.tabs.filter((tab) => tab.id !== id); } - render(existingTabs: any[]) { + render(existingTabs: any[], visible: boolean) { + let deckyTabAmount = existingTabs.reduce((prev: any, cur: any) => (cur.decky ? prev + 1 : prev), 0); + if (deckyTabAmount == this.tabs.length) { + for (let tab of existingTabs) { + if (tab?.decky) tab.panel.props.setter[0](visible); + } + return; + } for (const { title, icon, content, id } of this.tabs) { existingTabs.push({ key: id, title, tab: icon, decky: true, - panel: <QuickAccessVisibleStateProvider>{content}</QuickAccessVisibleStateProvider>, + panel: ( + <QuickAccessVisibleStateProvider initial={visible} setter={[]}> + {content} + </QuickAccessVisibleStateProvider> + ), }); } } diff --git a/frontend/src/toaster.tsx b/frontend/src/toaster.tsx index 94b08d70..728bbdb8 100644 --- a/frontend/src/toaster.tsx +++ b/frontend/src/toaster.tsx @@ -1,10 +1,8 @@ -import { Patch, ToastData, sleep } from 'decky-frontend-lib'; +import { Patch, ToastData, afterPatch, findInReactTree, sleep } from 'decky-frontend-lib'; +import { ReactNode } from 'react'; -import DeckyToaster from './components/DeckyToaster'; -import { DeckyToasterState, DeckyToasterStateContextProvider } from './components/DeckyToasterState'; import Toast from './components/Toast'; import Logger from './logger'; -import RouterHook from './router-hook'; declare global { interface Window { @@ -14,16 +12,18 @@ declare global { } class Toaster extends Logger { - private instanceRetPatch?: Patch; - private routerHook: RouterHook; - private toasterState: DeckyToasterState = new DeckyToasterState(); + // private routerHook: RouterHook; + // private toasterState: DeckyToasterState = new DeckyToasterState(); private node: any; + private rNode: any; private settingsModule: any; - private ready: boolean = false; + private finishStartup?: () => void; + private ready: Promise<void> = new Promise((res) => (this.finishStartup = res)); + private toasterPatch?: Patch; - constructor(routerHook: RouterHook) { + constructor() { super('Toaster'); - this.routerHook = routerHook; + // this.routerHook = routerHook; window.__TOASTER_INSTANCE?.deinit?.(); window.__TOASTER_INSTANCE = this; @@ -31,135 +31,136 @@ class Toaster extends Logger { } async init() { - this.routerHook.addGlobalComponent('DeckyToaster', () => ( - <DeckyToasterStateContextProvider deckyToasterState={this.toasterState}> - <DeckyToaster /> - </DeckyToasterStateContextProvider> - )); - // let instance: any; - // while (true) { - // instance = findInReactTree( - // (document.getElementById('root') as any)._reactRootContainer._internalRoot.current, - // (x) => x?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPlaceholder'), - // ); - // if (instance) break; - // this.debug('finding instance'); - // await sleep(2000); - // } - // // const windowManager = findModuleChild((m) => { - // // if (typeof m !== 'object') return false; - // // for (let prop in m) { - // // if (m[prop]?.prototype?.GetRenderElement) return m[prop]; - // // } - // // return false; - // // }); - // this.node = instance.return.return; - // let toast: any; - // let renderedToast: ReactNode = null; - // console.log(instance, this.node); - // // replacePatch(window.SteamClient.BrowserView, "Destroy", (args: any[]) => { - // // console.debug("destroy", args) - // // return callOriginal; - // // }) - // // let node = this.node.child.updateQueue.lastEffect; - // // while (node.next && !node.deckyPatched) { - // // node = node.next; - // // if (node.deps[1] == "notificationtoasts") { - // // console.log("Deleting destroy"); - // // node.deckyPatched = true; - // // node.create = () => {console.debug("VVVVVVVVVVV")}; - // // node.destroy = () => {console.debug("AAAAAAAAAAAAAAAAaaaaaaaaaaaaaaa")}; - // // } - // // } - // this.node.stateNode.render = (...args: any[]) => { - // const ret = this.node.stateNode.__proto__.render.call(this.node.stateNode, ...args); - // console.log('toast', ret); - // if (ret) { - // console.log(ret) - // // this.instanceRetPatch = replacePatch(ret, 'type', (innerArgs: any) => { - // // console.log("inner toast", innerArgs) - // // // @ts-ignore - // // const oldEffect = window.SP_REACT.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current.useEffect; - // // // @ts-ignore - // // window.SP_REACT.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current.useEffect = (effect, deps) => { - // // console.log(effect, deps) - // // if (deps?.[1] == "notificationtoasts") { - // // console.log("run") - // // effect(); - // // } - // // return oldEffect(effect, deps); - // // } - // // const ret = this.instanceRetPatch?.original(...args); - // // console.log("inner ret", ret) - // // // @ts-ignore - // // window.SP_REACT.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current.useEffect = oldEffect; - // // return ret - // // }); - // } - // // console.log("toast ret", ret) - // // if (ret?.props?.children[1]?.children?.props) { - // // const currentToast = ret.props.children[1].children.props.notification; - // // if (currentToast?.decky) { - // // if (currentToast == toast) { - // // ret.props.children[1].children = renderedToast; - // // } else { - // // toast = currentToast; - // // renderedToast = <Toast toast={toast} />; - // // ret.props.children[1].children = renderedToast; - // // } - // // } else { - // // toast = null; - // // renderedToast = null; - // // } - // // } - // // return ret; - // // }); - // // } - // return ret; - // }; - // this.settingsModule = findModuleChild((m) => { - // if (typeof m !== 'object') return undefined; - // for (let prop in m) { - // if (typeof m[prop]?.settings && m[prop]?.communityPreferences) return m[prop]; - // } - // }); - // // const idx = FocusNavController.m_ActiveContext.m_rgGamepadNavigationTrees.findIndex((x: any) => x.m_ID == "ToastContainer"); - // // if (idx > -1) { - // // FocusNavController.m_ActiveContext.m_rgGamepadNavigationTrees.splice(idx, 1) - // // } - // this.node.stateNode.forceUpdate(); - // this.node.stateNode.shouldComponentUpdate = () => { - // return false; - // }; - // this.log('Initialized'); - // this.ready = true; + // this.routerHook.addGlobalComponent('DeckyToaster', () => ( + // <DeckyToasterStateContextProvider deckyToasterState={this.toasterState}> + // <DeckyToaster /> + // </DeckyToasterStateContextProvider> + // )); + let instance: any; + const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current; + const findToasterRoot = (currentNode: any, iters: number): any => { + if (iters >= 50) { + // currently 40 + return null; + } + if (currentNode?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPlaceholder')) { + 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.error( + '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 = this.node.return; + 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 = this.rNode.stateNode.__proto__.render; + let int: NodeJS.Timer | undefined; + this.rNode.stateNode.render = (...args: any[]) => { + const ret = oRender.call(this.rNode.stateNode, ...args); + if (ret && !this?.node?.return?.return) { + 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); + } + repatch(); + return ret; + }; + + this.rNode.stateNode.shouldComponentUpdate = () => true; + this.rNode.stateNode.forceUpdate(); + delete this.rNode.stateNode.shouldComponentUpdate; + + this.log('Initialized'); + this.finishStartup?.(); } - toast(toast: ToastData) { - toast.duration = toast.duration || 5e3; - this.toasterState.addToast(toast); - // const settings = this.settingsModule?.settings; - // let toastData = { - // nNotificationID: window.NotificationStore.m_nNextTestNotificationID++, - // rtCreated: Date.now(), - // eType: 15, - // nToastDurationMS: toast.duration || 5e3, - // data: toast, - // decky: true, - // }; - // // @ts-ignore - // toastData.data.appid = () => 0; - // if ( - // (settings?.bDisableAllToasts && !toast.critical) || - // (settings?.bDisableToastsInGame && !toast.critical && window.NotificationStore.BIsUserInGame()) - // ) - // return; - // window.NotificationStore.m_rgNotificationToasts.push(toastData); - // window.NotificationStore.DispatchNextToast(); + async toast(toast: ToastData) { + // toast.duration = toast.duration || 5e3; + // this.toasterState.addToast(toast); + await this.ready; + const settings = this.settingsModule?.settings; + let toastData = { + nNotificationID: window.NotificationStore.m_nNextTestNotificationID++, + rtCreated: Date.now(), + eType: 15, + nToastDurationMS: toast.duration || (toast.duration = 5e3), + data: toast, + decky: true, + }; + // @ts-ignore + toastData.data.appid = () => 0; + if ( + (settings?.bDisableAllToasts && !toast.critical) || + (settings?.bDisableToastsInGame && !toast.critical && window.NotificationStore.BIsUserInGame()) + ) + return; + window.NotificationStore.m_rgNotificationToasts.push(toastData); + window.NotificationStore.DispatchNextToast(); } deinit() { - this.routerHook.removeGlobalComponent('DeckyToaster'); + 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'); } } |
