mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-15 18:13:40 +03:00
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:
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
14
frontend/pnpm-lock.yaml
generated
14
frontend/pnpm-lock.yaml
generated
@@ -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'}
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
119
frontend/src/tabs-hook.old.tsx
Normal file
119
frontend/src/tabs-hook.old.tsx
Normal 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;
|
||||
@@ -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>
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user