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>
This commit is contained in:
TrainDoctor
2022-10-30 10:32:05 -07:00
committed by GitHub
parent f5fc205384
commit bace5143d2
9 changed files with 391 additions and 248 deletions

View File

@@ -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:

View File

@@ -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",

View File

@@ -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'}

View File

@@ -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>;
};

View File

@@ -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>
);

View File

@@ -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 = () => {

View File

@@ -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;

View File

@@ -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;
}
// @ts-ignore
return oFilter.call(this, ...args);
if (
typeof currentNode?.memoizedProps?.visible == 'boolean' &&
currentNode?.type?.toString()?.includes('QuickAccessMenuBrowserView')
) {
this.log(`QAM root was found in ${iters} recursion cycles`);
return currentNode;
}
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;
}
if (!currentNode) return null;
if (currentNode.sibling) {
let node = await findQAMRoot(currentNode.sibling, iters + 1);
if (node !== null) return node;
}
return await findQAMRoot(currentNode, iters + 1);
}
(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);
(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 (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;
}
} catch (e) {
this.error('Error patching 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>
),
});
}
}

View File

@@ -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');
}
}