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
+4 -3
View File
@@ -121,7 +121,7 @@ class Loader:
with open(path.join(self.plugin_path, plugin.plugin_directory, "dist/index.js"), 'r') as bundle: 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") 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: try:
plugin = PluginWrapper(file, plugin_directory, self.plugin_path) plugin = PluginWrapper(file, plugin_directory, self.plugin_path)
if plugin.name in self.plugins: if plugin.name in self.plugins:
@@ -135,7 +135,8 @@ class Loader:
self.logger.info(f"Plugin {plugin.name} is passive") self.logger.info(f"Plugin {plugin.name} is passive")
self.plugins[plugin.name] = plugin.start() self.plugins[plugin.name] = plugin.start()
self.logger.info(f"Loaded {plugin.name}") 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: except Exception as e:
self.logger.error(f"Could not load {file}. {e}") self.logger.error(f"Could not load {file}. {e}")
print_exc() 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"))] 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: for directory in directories:
self.logger.info(f"found plugin: {directory}") 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): async def handle_reloads(self):
while True: while True:
+1 -1
View File
@@ -41,7 +41,7 @@
} }
}, },
"dependencies": { "dependencies": {
"decky-frontend-lib": "^3.7.3", "decky-frontend-lib": "^3.7.11",
"react-file-icon": "^1.2.0", "react-file-icon": "^1.2.0",
"react-icons": "^4.4.0", "react-icons": "^4.4.0",
"react-markdown": "^8.0.3", "react-markdown": "^8.0.3",
+4 -10
View File
@@ -10,7 +10,7 @@ specifiers:
'@types/react-file-icon': ^1.0.1 '@types/react-file-icon': ^1.0.1
'@types/react-router': 5.1.18 '@types/react-router': 5.1.18
'@types/webpack': ^5.28.0 '@types/webpack': ^5.28.0
decky-frontend-lib: ^3.7.3 decky-frontend-lib: ^3.7.11
husky: ^8.0.1 husky: ^8.0.1
import-sort-style-module: ^6.0.0 import-sort-style-module: ^6.0.0
inquirer: ^8.2.4 inquirer: ^8.2.4
@@ -30,7 +30,7 @@ specifiers:
typescript: ^4.7.4 typescript: ^4.7.4
dependencies: dependencies:
decky-frontend-lib: 3.7.3 decky-frontend-lib: 3.7.11
react-file-icon: 1.2.0_wcqkhtmu7mswc6yz4uyexck3ty react-file-icon: 1.2.0_wcqkhtmu7mswc6yz4uyexck3ty
react-icons: 4.4.0_react@16.14.0 react-icons: 4.4.0_react@16.14.0
react-markdown: 8.0.3_vshvapmxg47tngu7tvrsqpq55u react-markdown: 8.0.3_vshvapmxg47tngu7tvrsqpq55u
@@ -944,10 +944,8 @@ packages:
dependencies: dependencies:
ms: 2.1.2 ms: 2.1.2
/decky-frontend-lib/3.7.3: /decky-frontend-lib/3.7.11:
resolution: {integrity: sha512-HFHI19zr3gzOXDBF0DE9W+ZSx+mtjc/XqCYANoVfpMaDX1ITZpk2lMzBGuh9QvtHZ4LygtYEPIWDlrJDs8rGKA==} resolution: {integrity: sha512-c5/kXqCLYhCl0zC+kPJ2gTUjTp6N0zUFKzTQKVKTuQ3U+01fHAU6sUsDqudbdTNdjXiofGujMmeJqKaU2vQoXQ==}
dependencies:
minimist: 1.2.7
dev: false dev: false
/decode-named-character-reference/1.0.2: /decode-named-character-reference/1.0.2:
@@ -1944,10 +1942,6 @@ packages:
brace-expansion: 1.1.11 brace-expansion: 1.1.11
dev: true dev: true
/minimist/1.2.7:
resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==}
dev: false
/mri/1.2.0: /mri/1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -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); const QuickAccessVisibleState = createContext<boolean>(true);
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState); export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
export const QuickAccessVisibleStateProvider: FC<{}> = ({ children }) => { export const QuickAccessVisibleStateProvider: FC<{ initial: boolean; setter: ((val: boolean) => {}[]) | never[] }> = ({
const divRef = useRef<HTMLDivElement>(null); children,
const [visible, setVisible] = useState<boolean>(false); initial,
useEffect(() => { setter,
const doc: Document | void | null = divRef?.current?.ownerDocument; }) => {
if (!doc) return; const [visible, setVisible] = useState<boolean>(initial);
setVisible(doc.visibilityState == 'visible'); const [prev, setPrev] = useState<boolean>(initial);
const onChange = (e: Event) => { // hack to use an array as a "pointer" to pass the setter up the tree
setVisible(doc.visibilityState == 'visible'); setter[0] = setVisible;
}; if (initial != prev) {
doc.addEventListener('visibilitychange', onChange); setPrev(initial);
return () => { setVisible(initial);
doc.removeEventListener('visibilitychange', onChange); }
}; return <QuickAccessVisibleState.Provider value={visible}>{children}</QuickAccessVisibleState.Provider>;
}, [divRef]);
return (
<div ref={divRef}>
<QuickAccessVisibleState.Provider value={visible}>{children}</QuickAccessVisibleState.Provider>
</div>
);
}; };
+11 -13
View File
@@ -27,20 +27,18 @@ const templateClasses = findModule((mod) => {
const Toast: FunctionComponent<ToastProps> = ({ toast }) => { const Toast: FunctionComponent<ToastProps> = ({ toast }) => {
return ( return (
<div className={toastClasses.ToastPopup}> <div
<div style={{ '--toast-duration': `${toast.duration}ms` } as React.CSSProperties}
style={{ '--toast-duration': `${toast.duration}ms` } as React.CSSProperties} onClick={toast.onClick}
onClick={toast.onClick} className={joinClassNames(templateClasses.ShortTemplate, toast.className || '')}
className={joinClassNames(templateClasses.ShortTemplate, toast.className || '')} >
> {toast.logo && <div className={templateClasses.StandardLogoDimensions}>{toast.logo}</div>}
{toast.logo && <div className={templateClasses.StandardLogoDimensions}>{toast.logo}</div>} <div className={joinClassNames(templateClasses.Content, toast.contentClassName || '')}>
<div className={joinClassNames(templateClasses.Content, toast.contentClassName || '')}> <div className={templateClasses.Header}>
<div className={templateClasses.Header}> {toast.icon && <div className={templateClasses.Icon}>{toast.icon}</div>}
{toast.icon && <div className={templateClasses.Icon}>{toast.icon}</div>} <div className={templateClasses.Title}>{toast.title}</div>
<div className={templateClasses.Title}>{toast.title}</div>
</div>
<div className={templateClasses.Body}>{toast.body}</div>
</div> </div>
<div className={templateClasses.Body}>{toast.body}</div>
</div> </div>
</div> </div>
); );
+4 -2
View File
@@ -23,6 +23,7 @@ import { Plugin } from './plugin';
import RouterHook from './router-hook'; import RouterHook from './router-hook';
import { checkForUpdates } from './store'; import { checkForUpdates } from './store';
import TabsHook from './tabs-hook'; import TabsHook from './tabs-hook';
import OldTabsHook from './tabs-hook.old';
import Toaster from './toaster'; import Toaster from './toaster';
import { VerInfo, callUpdaterMethod } from './updater'; import { VerInfo, callUpdaterMethod } from './updater';
import { getSetting } from './utils/settings'; import { getSetting } from './utils/settings';
@@ -38,10 +39,10 @@ declare global {
class PluginLoader extends Logger { class PluginLoader extends Logger {
private plugins: Plugin[] = []; 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 windowHook: WindowHook = new WindowHook();
private routerHook: RouterHook = new RouterHook(); private routerHook: RouterHook = new RouterHook();
public toaster: Toaster = new Toaster(this.routerHook); public toaster: Toaster = new Toaster();
private deckyState: DeckyState = new DeckyState(); private deckyState: DeckyState = new DeckyState();
private reloadLock: boolean = false; private reloadLock: boolean = false;
@@ -52,6 +53,7 @@ class PluginLoader extends Logger {
constructor() { constructor() {
super(PluginLoader.name); super(PluginLoader.name);
this.tabsHook.init();
this.log('Initialized'); this.log('Initialized');
const TabBadge = () => { const TabBadge = () => {
+119
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;
+98 -64
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 { QuickAccessVisibleStateProvider } from './components/QuickAccessVisibleState';
import Logger from './logger'; import Logger from './logger';
import { findSP } from './utils/windows';
declare global { declare global {
interface Window { interface Window {
__TABS_HOOK_INSTANCE: any; __TABS_HOOK_INSTANCE: any;
} securitystore: any;
interface Array<T> {
__filter: any;
} }
} }
const isTabsArray = (tabs: any) => {
const length = tabs.length;
return length >= 7 && tabs[length - 1]?.tab;
};
interface Tab { interface Tab {
id: QuickAccessTab | number; id: QuickAccessTab | number;
title: any; title: any;
@@ -27,7 +23,9 @@ interface Tab {
class TabsHook extends Logger { class TabsHook extends Logger {
// private keys = 7; // private keys = 7;
tabs: Tab[] = []; tabs: Tab[] = [];
private oFilter: (...args: any[]) => any; private qAMRoot?: any;
private qamPatch?: Patch;
private unsubscribeSecurity?: () => void;
constructor() { constructor() {
super('TabsHook'); super('TabsHook');
@@ -35,65 +33,90 @@ class TabsHook extends Logger {
this.log('Initialized'); this.log('Initialized');
window.__TABS_HOOK_INSTANCE?.deinit?.(); window.__TABS_HOOK_INSTANCE?.deinit?.();
window.__TABS_HOOK_INSTANCE = this; window.__TABS_HOOK_INSTANCE = this;
}
const self = this; init() {
const oFilter = (this.oFilter = Array.prototype.filter); const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current;
Array.prototype.filter = function patchedFilter(...args: any[]) { let qAMRoot: any;
if (isTabsArray(this)) { const findQAMRoot = (currentNode: any, iters: number): any => {
self.render(this); if (iters >= 55) {
// currently 45
return null;
} }
// @ts-ignore if (
return oFilter.call(this, ...args); 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;
}; };
(async () => {
if (document.title != 'SP') qAMRoot = findQAMRoot(tree, 0);
try { while (!qAMRoot) {
const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current; this.error(
let qAMRoot: any; 'Failed to find QAM root node, reattempting in 5 seconds. A developer may need to increase the recursion limit.',
async function findQAMRoot(currentNode: any, iters: number): Promise<any> { );
if (iters >= 60) { await sleep(5000);
// currently 44 qAMRoot = findQAMRoot(tree, 0);
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);
} }
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() { deinit() {
Array.prototype.filter = this.oFilter; this.qamPatch?.unpatch();
this.qAMRoot.return.alternate.type = this.qAMRoot.return.type;
this.unsubscribeSecurity?.();
} }
add(tab: Tab) { add(tab: Tab) {
@@ -106,14 +129,25 @@ class TabsHook extends Logger {
this.tabs = this.tabs.filter((tab) => tab.id !== id); 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) { for (const { title, icon, content, id } of this.tabs) {
existingTabs.push({ existingTabs.push({
key: id, key: id,
title, title,
tab: icon, tab: icon,
decky: true, decky: true,
panel: <QuickAccessVisibleStateProvider>{content}</QuickAccessVisibleStateProvider>, panel: (
<QuickAccessVisibleStateProvider initial={visible} setter={[]}>
{content}
</QuickAccessVisibleStateProvider>
),
}); });
} }
} }
+135 -134
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 Toast from './components/Toast';
import Logger from './logger'; import Logger from './logger';
import RouterHook from './router-hook';
declare global { declare global {
interface Window { interface Window {
@@ -14,16 +12,18 @@ declare global {
} }
class Toaster extends Logger { class Toaster extends Logger {
private instanceRetPatch?: Patch; // private routerHook: RouterHook;
private routerHook: RouterHook; // private toasterState: DeckyToasterState = new DeckyToasterState();
private toasterState: DeckyToasterState = new DeckyToasterState();
private node: any; private node: any;
private rNode: any;
private settingsModule: 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'); super('Toaster');
this.routerHook = routerHook; // this.routerHook = routerHook;
window.__TOASTER_INSTANCE?.deinit?.(); window.__TOASTER_INSTANCE?.deinit?.();
window.__TOASTER_INSTANCE = this; window.__TOASTER_INSTANCE = this;
@@ -31,135 +31,136 @@ class Toaster extends Logger {
} }
async init() { async init() {
this.routerHook.addGlobalComponent('DeckyToaster', () => ( // this.routerHook.addGlobalComponent('DeckyToaster', () => (
<DeckyToasterStateContextProvider deckyToasterState={this.toasterState}> // <DeckyToasterStateContextProvider deckyToasterState={this.toasterState}>
<DeckyToaster /> // <DeckyToaster />
</DeckyToasterStateContextProvider> // </DeckyToasterStateContextProvider>
)); // ));
// let instance: any; let instance: any;
// while (true) { const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current;
// instance = findInReactTree( const findToasterRoot = (currentNode: any, iters: number): any => {
// (document.getElementById('root') as any)._reactRootContainer._internalRoot.current, if (iters >= 50) {
// (x) => x?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPlaceholder'), // currently 40
// ); return null;
// if (instance) break; }
// this.debug('finding instance'); if (currentNode?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPlaceholder')) {
// await sleep(2000); this.log(`Toaster root was found in ${iters} recursion cycles`);
// } return currentNode;
// // const windowManager = findModuleChild((m) => { }
// // if (typeof m !== 'object') return false; if (currentNode.sibling) {
// // for (let prop in m) { let node = findToasterRoot(currentNode.sibling, iters + 1);
// // if (m[prop]?.prototype?.GetRenderElement) return m[prop]; if (node !== null) return node;
// // } }
// // return false; if (currentNode.child) {
// // }); let node = findToasterRoot(currentNode.child, iters + 1);
// this.node = instance.return.return; if (node !== null) return node;
// let toast: any; }
// let renderedToast: ReactNode = null; return null;
// console.log(instance, this.node); };
// // replacePatch(window.SteamClient.BrowserView, "Destroy", (args: any[]) => { instance = findToasterRoot(tree, 0);
// // console.debug("destroy", args) while (!instance) {
// // return callOriginal; this.error(
// // }) 'Failed to find Toaster root node, reattempting in 5 seconds. A developer may need to increase the recursion limit.',
// // let node = this.node.child.updateQueue.lastEffect; );
// // while (node.next && !node.deckyPatched) { await sleep(5000);
// // node = node.next; instance = findToasterRoot(tree, 0);
// // if (node.deps[1] == "notificationtoasts") { }
// // console.log("Deleting destroy"); this.node = instance.return;
// // node.deckyPatched = true; this.rNode = this.node.return;
// // node.create = () => {console.debug("VVVVVVVVVVV")}; let toast: any;
// // node.destroy = () => {console.debug("AAAAAAAAAAAAAAAAaaaaaaaaaaaaaaa")}; let renderedToast: ReactNode = null;
// // } let innerPatched: any;
// // } const repatch = () => {
// this.node.stateNode.render = (...args: any[]) => { if (this.node && !this.node.type.decky) {
// const ret = this.node.stateNode.__proto__.render.call(this.node.stateNode, ...args); this.toasterPatch = afterPatch(this.node, 'type', (_: any, ret: any) => {
// console.log('toast', ret); const inner = findInReactTree(ret.props.children, (x) => x?.props?.onDismiss);
// if (ret) { if (innerPatched) {
// console.log(ret) inner.type = innerPatched;
// // this.instanceRetPatch = replacePatch(ret, 'type', (innerArgs: any) => { } else {
// // console.log("inner toast", innerArgs) afterPatch(inner, 'type', (innerArgs: any, ret: any) => {
// // // @ts-ignore const currentToast = innerArgs[0]?.notification;
// // const oldEffect = window.SP_REACT.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current.useEffect; if (currentToast?.decky) {
// // // @ts-ignore if (currentToast == toast) {
// // window.SP_REACT.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current.useEffect = (effect, deps) => { ret.props.children = renderedToast;
// // console.log(effect, deps) } else {
// // if (deps?.[1] == "notificationtoasts") { toast = currentToast;
// // console.log("run") renderedToast = <Toast toast={toast.data} />;
// // effect(); ret.props.children = renderedToast;
// // } }
// // return oldEffect(effect, deps); } else {
// // } toast = null;
// // const ret = this.instanceRetPatch?.original(...args); renderedToast = null;
// // console.log("inner ret", ret) }
// // // @ts-ignore return ret;
// // window.SP_REACT.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current.useEffect = oldEffect; });
// // return ret innerPatched = inner.type;
// // }); }
// } return ret;
// // console.log("toast ret", ret) });
// // if (ret?.props?.children[1]?.children?.props) { this.node.type.decky = true;
// // const currentToast = ret.props.children[1].children.props.notification; this.node.alternate.type = this.node.type;
// // if (currentToast?.decky) { }
// // if (currentToast == toast) { };
// // ret.props.children[1].children = renderedToast; const oRender = this.rNode.stateNode.__proto__.render;
// // } else { let int: NodeJS.Timer | undefined;
// // toast = currentToast; this.rNode.stateNode.render = (...args: any[]) => {
// // renderedToast = <Toast toast={toast} />; const ret = oRender.call(this.rNode.stateNode, ...args);
// // ret.props.children[1].children = renderedToast; if (ret && !this?.node?.return?.return) {
// // } clearInterval(int);
// // } else { int = setInterval(() => {
// // toast = null; const n = findToasterRoot(tree, 0);
// // renderedToast = null; if (n?.return) {
// // } clearInterval(int);
// // } this.node = n.return;
// // return ret; this.rNode = this.node.return;
// // }); repatch();
// // } } else {
// return ret; this.error('Failed to re-grab Toaster node, trying again...');
// }; }
// this.settingsModule = findModuleChild((m) => { }, 1200);
// if (typeof m !== 'object') return undefined; }
// for (let prop in m) { repatch();
// if (typeof m[prop]?.settings && m[prop]?.communityPreferences) return m[prop]; return ret;
// } };
// });
// // const idx = FocusNavController.m_ActiveContext.m_rgGamepadNavigationTrees.findIndex((x: any) => x.m_ID == "ToastContainer"); this.rNode.stateNode.shouldComponentUpdate = () => true;
// // if (idx > -1) { this.rNode.stateNode.forceUpdate();
// // FocusNavController.m_ActiveContext.m_rgGamepadNavigationTrees.splice(idx, 1) delete this.rNode.stateNode.shouldComponentUpdate;
// // }
// this.node.stateNode.forceUpdate(); this.log('Initialized');
// this.node.stateNode.shouldComponentUpdate = () => { this.finishStartup?.();
// return false;
// };
// this.log('Initialized');
// this.ready = true;
} }
toast(toast: ToastData) { async toast(toast: ToastData) {
toast.duration = toast.duration || 5e3; // toast.duration = toast.duration || 5e3;
this.toasterState.addToast(toast); // this.toasterState.addToast(toast);
// const settings = this.settingsModule?.settings; await this.ready;
// let toastData = { const settings = this.settingsModule?.settings;
// nNotificationID: window.NotificationStore.m_nNextTestNotificationID++, let toastData = {
// rtCreated: Date.now(), nNotificationID: window.NotificationStore.m_nNextTestNotificationID++,
// eType: 15, rtCreated: Date.now(),
// nToastDurationMS: toast.duration || 5e3, eType: 15,
// data: toast, nToastDurationMS: toast.duration || (toast.duration = 5e3),
// decky: true, data: toast,
// }; decky: true,
// // @ts-ignore };
// toastData.data.appid = () => 0; // @ts-ignore
// if ( toastData.data.appid = () => 0;
// (settings?.bDisableAllToasts && !toast.critical) || if (
// (settings?.bDisableToastsInGame && !toast.critical && window.NotificationStore.BIsUserInGame()) (settings?.bDisableAllToasts && !toast.critical) ||
// ) (settings?.bDisableToastsInGame && !toast.critical && window.NotificationStore.BIsUserInGame())
// return; )
// window.NotificationStore.m_rgNotificationToasts.push(toastData); return;
// window.NotificationStore.DispatchNextToast(); window.NotificationStore.m_rgNotificationToasts.push(toastData);
window.NotificationStore.DispatchNextToast();
} }
deinit() { 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');
} }
} }