summaryrefslogtreecommitdiff
path: root/frontend
diff options
context:
space:
mode:
authorTrainDoctor <traindoctor@protonmail.com>2022-10-30 10:32:05 -0700
committerGitHub <noreply@github.com>2022-10-30 10:32:05 -0700
commitbace5143d28c42ffcc83509b7fcdf02b6cae6934 (patch)
tree5a39a5980a84136df5a6781ba1e200d151112073 /frontend
parentf5fc2053847d3054d36d3348d21e7de060342698 (diff)
downloaddecky-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>
Diffstat (limited to 'frontend')
-rw-r--r--frontend/package.json2
-rw-r--r--frontend/pnpm-lock.yaml14
-rw-r--r--frontend/src/components/QuickAccessVisibleState.tsx36
-rw-r--r--frontend/src/components/Toast.tsx24
-rw-r--r--frontend/src/plugin-loader.tsx6
-rw-r--r--frontend/src/tabs-hook.old.tsx119
-rw-r--r--frontend/src/tabs-hook.tsx154
-rw-r--r--frontend/src/toaster.tsx269
8 files changed, 383 insertions, 241 deletions
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');
}
}