mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-13 12:15:09 +03:00
implement new toaster hook
this also supports the notification list and probably also desktop toasts (UI wip, read location enum prop from toast component probably)
This commit is contained in:
@@ -69,13 +69,13 @@ class ErrorBoundaryHook extends Logger {
|
||||
});
|
||||
|
||||
if (!ErrorBoundary) {
|
||||
this.error('could not find ValveErrorBoundary');
|
||||
this.error('@decky/ui could not find ErrorBoundary, skipping patch');
|
||||
return;
|
||||
}
|
||||
|
||||
this.errorBoundaryPatch = replacePatch(ErrorBoundary.prototype, 'render', function (this: any) {
|
||||
if (this.state._deckyForceRerender) {
|
||||
const stateClone = {...this.state, _deckyForceRerender: null};
|
||||
const stateClone = { ...this.state, _deckyForceRerender: null };
|
||||
this.setState(stateClone);
|
||||
return null;
|
||||
}
|
||||
@@ -93,9 +93,9 @@ class ErrorBoundaryHook extends Logger {
|
||||
return callOriginal;
|
||||
});
|
||||
// Small hack that gives us a lot more flexibility to force rerenders.
|
||||
ValveErrorBoundary.prototype._deckyForceRerender = function (this: any) {
|
||||
this.setState({...this.state, _deckyForceRerender: true});
|
||||
}
|
||||
ErrorBoundary.prototype._deckyForceRerender = function (this: any) {
|
||||
this.setState({ ...this.state, _deckyForceRerender: true });
|
||||
};
|
||||
}
|
||||
|
||||
public temporarilyDisableReporting() {
|
||||
|
||||
@@ -36,7 +36,6 @@ import Toaster from './toaster';
|
||||
import { getVersionInfo } from './updater';
|
||||
import { getSetting, setSetting } from './utils/settings';
|
||||
import TranslationHelper, { TranslationClass } from './utils/TranslationHelper';
|
||||
import AppHook from './app-hook';
|
||||
|
||||
const StorePage = lazy(() => import('./components/store/Store'));
|
||||
const SettingsPage = lazy(() => import('./components/settings'));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ErrorBoundary, Focusable, Patch, afterPatch, beforePatch, findInReactTree, findModuleByExport, findModuleExport, getReactRoot, sleep } from '@decky/ui';
|
||||
import { FC, ReactElement, ReactNode, cloneElement, createElement, memo } from 'react';
|
||||
import { ErrorBoundary, Patch, afterPatch, findInReactTree, getReactRoot, sleep } from '@decky/ui';
|
||||
import { FC, ReactElement, ReactNode, cloneElement, createElement } from 'react';
|
||||
import type { Route } from 'react-router';
|
||||
|
||||
import {
|
||||
@@ -32,6 +32,7 @@ class RouterHook extends Logger {
|
||||
private DeckyWrapper = this.routerWrapper.bind(this);
|
||||
private DeckyGlobalComponentsWrapper = this.globalComponentsWrapper.bind(this);
|
||||
private toReplace = new Map<string, ReactNode>();
|
||||
private routerPatch?: Patch;
|
||||
public routes?: any[];
|
||||
|
||||
constructor() {
|
||||
@@ -41,22 +42,25 @@ class RouterHook extends Logger {
|
||||
window.__ROUTER_HOOK_INSTANCE?.deinit?.();
|
||||
window.__ROUTER_HOOK_INSTANCE = this;
|
||||
|
||||
(async()=> {
|
||||
(async () => {
|
||||
const root = getReactRoot(document.getElementById('root') as any);
|
||||
// TODO be more specific, this is horrible and very very slow
|
||||
const findRouterNode = () =>findInReactTree(root, node => typeof node?.pendingProps?.loggedIn == "undefined" && node?.type?.toString().includes("Settings.Root()"));
|
||||
const findRouterNode = () =>
|
||||
findInReactTree(
|
||||
root,
|
||||
(node) =>
|
||||
typeof node?.pendingProps?.loggedIn == 'undefined' && node?.type?.toString().includes('Settings.Root()'),
|
||||
);
|
||||
let routerNode = findRouterNode();
|
||||
while (!routerNode) {
|
||||
this.warn(
|
||||
'Failed to find Router node, reattempting in 5 seconds.',
|
||||
);
|
||||
this.warn('Failed to find Router node, reattempting in 5 seconds.');
|
||||
await sleep(5000);
|
||||
routerNode = findRouterNode();
|
||||
}
|
||||
if (routerNode) {
|
||||
this.debug("routerNode", routerNode);
|
||||
this.debug('routerNode', routerNode);
|
||||
// Patch the component globally
|
||||
afterPatch(routerNode.elementType, "type", this.handleRouterRender.bind(this));
|
||||
this.routerPatch = afterPatch(routerNode.elementType, 'type', this.handleRouterRender.bind(this));
|
||||
// Swap out the current instance
|
||||
routerNode.type = routerNode.elementType.type;
|
||||
if (routerNode?.alternate) {
|
||||
@@ -91,14 +95,14 @@ class RouterHook extends Logger {
|
||||
return returnVal;
|
||||
}
|
||||
|
||||
private globalComponentsWrapper () {
|
||||
private globalComponentsWrapper() {
|
||||
const { components } = useDeckyGlobalComponentsState();
|
||||
if (this.renderedComponents.length != components.size) {
|
||||
this.debug('Rerendering global components');
|
||||
this.renderedComponents = Array.from(components.values()).map((GComponent) => <GComponent />);
|
||||
}
|
||||
return <>{this.renderedComponents}</>;
|
||||
};
|
||||
}
|
||||
|
||||
private routerWrapper({ children }: { children: ReactElement }) {
|
||||
// Used to store the new replicated routes we create to allow routes to be unpatched.
|
||||
@@ -106,7 +110,7 @@ class RouterHook extends Logger {
|
||||
const { routes, routePatches } = useDeckyRouterState();
|
||||
// TODO make more redundant
|
||||
if (!children?.props?.children?.[0]?.props?.children) {
|
||||
console.log("routerWrapper wrong component?", children)
|
||||
console.log('routerWrapper wrong component?', children);
|
||||
return children;
|
||||
}
|
||||
const mainRouteList = children.props.children[0].props.children;
|
||||
@@ -116,7 +120,7 @@ class RouterHook extends Logger {
|
||||
|
||||
this.debug('Rerendered routes list');
|
||||
return children;
|
||||
};
|
||||
}
|
||||
|
||||
private processList(
|
||||
routeList: any[],
|
||||
@@ -167,7 +171,7 @@ class RouterHook extends Logger {
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props'] = {}) {
|
||||
this.routerState.addRoute(path, component, props);
|
||||
@@ -194,8 +198,7 @@ class RouterHook extends Logger {
|
||||
}
|
||||
|
||||
deinit() {
|
||||
// this.wrapperPatch.unpatch();
|
||||
// this.routerPatch?.unpatch();
|
||||
this.routerPatch?.unpatch();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+26
-15
@@ -1,5 +1,14 @@
|
||||
// TabsHook for versions after the Desktop merge
|
||||
import { ErrorBoundary, Patch, QuickAccessTab, afterPatch, createReactTreePatcher, findInReactTree, findModuleByExport, getReactRoot, setReactPatcherLoggingEnabled, sleep } from '@decky/ui';
|
||||
import {
|
||||
ErrorBoundary,
|
||||
Patch,
|
||||
QuickAccessTab,
|
||||
afterPatch,
|
||||
createReactTreePatcher,
|
||||
findInReactTree,
|
||||
findModuleByExport,
|
||||
getReactRoot,
|
||||
} from '@decky/ui';
|
||||
|
||||
import { QuickAccessVisibleStateProvider } from './components/QuickAccessVisibleState';
|
||||
import Logger from './logger';
|
||||
@@ -20,9 +29,7 @@ interface Tab {
|
||||
class TabsHook extends Logger {
|
||||
// private keys = 7;
|
||||
tabs: Tab[] = [];
|
||||
private qAMRoot?: any;
|
||||
private qamPatch?: Patch;
|
||||
private cachedTabs: any;
|
||||
|
||||
constructor() {
|
||||
super('TabsHook');
|
||||
@@ -34,25 +41,29 @@ class TabsHook extends Logger {
|
||||
|
||||
init() {
|
||||
// TODO patch the "embedded" renderer in this module too (seems to be for VR? unsure)
|
||||
const qamModule = findModuleByExport(e => e?.type?.toString()?.includes("QuickAccessMenuBrowserView"));
|
||||
const qamRenderer = Object.values(qamModule).find((e: any) => e?.type?.toString()?.includes("QuickAccessMenuBrowserView"))
|
||||
const qamModule = findModuleByExport((e) => e?.type?.toString()?.includes('QuickAccessMenuBrowserView'));
|
||||
const qamRenderer = Object.values(qamModule).find((e: any) =>
|
||||
e?.type?.toString()?.includes('QuickAccessMenuBrowserView'),
|
||||
);
|
||||
|
||||
const patchHandler = createReactTreePatcher([
|
||||
tree => findInReactTree(tree, node => node?.props?.onFocusNavDeactivated)
|
||||
], (args, ret) => {
|
||||
this.log("qam render", args, ret);
|
||||
const tabs = findInReactTree(ret, (x) => x?.props?.tabs);
|
||||
this.render(tabs.props.tabs, args[0].visible);
|
||||
return ret;
|
||||
}, "TabsHook");
|
||||
const patchHandler = createReactTreePatcher(
|
||||
[(tree) => findInReactTree(tree, (node) => node?.props?.onFocusNavDeactivated)],
|
||||
(args, ret) => {
|
||||
this.log('qam render', args, ret);
|
||||
const tabs = findInReactTree(ret, (x) => x?.props?.tabs);
|
||||
this.render(tabs.props.tabs, args[0].visible);
|
||||
return ret;
|
||||
},
|
||||
'TabsHook',
|
||||
);
|
||||
|
||||
this.qamPatch = afterPatch(qamRenderer, "type", patchHandler);
|
||||
this.qamPatch = afterPatch(qamRenderer, 'type', patchHandler);
|
||||
|
||||
// Patch already rendered qam
|
||||
const root = getReactRoot(document.getElementById('root') as any);
|
||||
const qamNode = root && findInReactTree(root, (n: any) => n.elementType == qamRenderer); // need elementType, because type is actually mobx wrapper
|
||||
if (qamNode) {
|
||||
this.debug("qamNode", qamNode);
|
||||
this.debug('qamNode', qamNode);
|
||||
// Only affects this fiber node so we don't need to unpatch here
|
||||
qamNode.type = qamNode.elementType.type;
|
||||
if (qamNode?.alternate) {
|
||||
|
||||
+36
-136
@@ -1,19 +1,16 @@
|
||||
import type { ToastData } from '@decky/api';
|
||||
import {
|
||||
Export,
|
||||
Patch,
|
||||
afterPatch,
|
||||
findClassByName,
|
||||
findInReactTree,
|
||||
findModuleExport,
|
||||
getReactRoot,
|
||||
sleep,
|
||||
} from '@decky/ui';
|
||||
import { ReactNode } from 'react';
|
||||
import { Patch, callOriginal, findModuleExport, injectFCTrampoline, replacePatch } from '@decky/ui';
|
||||
|
||||
import Toast from './components/Toast';
|
||||
import Logger from './logger';
|
||||
|
||||
// TODO export
|
||||
enum ToastType {
|
||||
New,
|
||||
Update,
|
||||
Remove,
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__TOASTER_INSTANCE: any;
|
||||
@@ -23,18 +20,12 @@ declare global {
|
||||
}
|
||||
|
||||
class Toaster extends Logger {
|
||||
// private routerHook: RouterHook;
|
||||
// private toasterState: DeckyToasterState = new DeckyToasterState();
|
||||
private node: any;
|
||||
private rNode: any;
|
||||
private audioModule: any;
|
||||
private finishStartup?: () => void;
|
||||
private ready: Promise<void> = new Promise((res) => (this.finishStartup = res));
|
||||
private toasterPatch?: Patch;
|
||||
private toastPatch?: Patch;
|
||||
|
||||
constructor() {
|
||||
super('Toaster');
|
||||
// this.routerHook = routerHook;
|
||||
|
||||
window.__TOASTER_INSTANCE?.deinit?.();
|
||||
window.__TOASTER_INSTANCE = this;
|
||||
@@ -42,115 +33,18 @@ class Toaster extends Logger {
|
||||
}
|
||||
|
||||
async init() {
|
||||
// this.routerHook.addGlobalComponent('DeckyToaster', () => (
|
||||
// <DeckyToasterStateContextProvider deckyToasterState={this.toasterState}>
|
||||
// <DeckyToaster />
|
||||
// </DeckyToasterStateContextProvider>
|
||||
// ));
|
||||
let instance: any;
|
||||
const tree = getReactRoot(document.getElementById('root') as any);
|
||||
const toasterClass1 = findClassByName('GamepadToastPlaceholder');
|
||||
const toasterClass2 = findClassByName('ToastPlaceholder');
|
||||
const toasterClass3 = findClassByName('ToastPopup');
|
||||
const toasterClass4 = findClassByName('GamepadToastPopup');
|
||||
const findToasterRoot = (currentNode: any, iters: number): any => {
|
||||
if (iters >= 80) {
|
||||
// currently 66
|
||||
return null;
|
||||
const ToastRenderer = findModuleExport((e) => e?.toString()?.includes(`controller:"notification",method:`));
|
||||
this.debug('toastrenderer', ToastRenderer);
|
||||
// TODO find a way to undo this if possible?
|
||||
const patchedRenderer = injectFCTrampoline(ToastRenderer);
|
||||
this.toastPatch = replacePatch(patchedRenderer, 'component', (args: any[]) => {
|
||||
this.debug('render toast', args);
|
||||
if (args?.[0]?.group?.decky || args?.[0]?.group?.notifications?.[0]?.decky) {
|
||||
this.debug('rendering decky toast');
|
||||
return args[0].group.notifications.map((notification: any) => <Toast toast={notification.data} />);
|
||||
}
|
||||
if (
|
||||
currentNode?.memoizedProps?.className?.startsWith?.(toasterClass1) ||
|
||||
currentNode?.memoizedProps?.className?.startsWith?.(toasterClass2) ||
|
||||
currentNode?.memoizedProps?.className?.startsWith?.(toasterClass3) ||
|
||||
currentNode?.memoizedProps?.className?.startsWith?.(toasterClass4)
|
||||
) {
|
||||
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.warn(
|
||||
'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 = findInReactTree(
|
||||
this.node.return.return,
|
||||
(node) => node?.stateNode && node.type?.InstallErrorReportingStore,
|
||||
);
|
||||
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 = Object.getPrototypeOf(this.rNode.stateNode).render;
|
||||
let int: number | undefined;
|
||||
this.rNode.stateNode.render = (...args: any[]) => {
|
||||
const ret = oRender.call(this.rNode.stateNode, ...args);
|
||||
if (ret && !this?.node?.return?.return) {
|
||||
int && 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.audioModule = findModuleExport((e: Export) => e.PlayNavSound && e.RegisterCallbackOnPlaySound);
|
||||
return callOriginal;
|
||||
});
|
||||
|
||||
this.log('Initialized');
|
||||
this.finishStartup?.();
|
||||
@@ -168,8 +62,6 @@ class Toaster extends Logger {
|
||||
data: toast,
|
||||
decky: true,
|
||||
};
|
||||
// @ts-ignore
|
||||
toastData.data.appid = () => 0;
|
||||
if (toast.sound === undefined) toast.sound = 6;
|
||||
if (toast.playSound === undefined) toast.playSound = true;
|
||||
if (toast.showToast === undefined) toast.showToast = true;
|
||||
@@ -180,19 +72,27 @@ class Toaster extends Logger {
|
||||
window.NotificationStore.BIsUserInGame())
|
||||
)
|
||||
return;
|
||||
if (toast.playSound) this.audioModule?.PlayNavSound(toast.sound);
|
||||
if (toast.showToast) {
|
||||
window.NotificationStore.m_rgNotificationToasts.push(toastData);
|
||||
window.NotificationStore.DispatchNextToast();
|
||||
function fnTray(toast: any, tray: any) {
|
||||
let group = {
|
||||
eType: toast.eType,
|
||||
notifications: [toast],
|
||||
};
|
||||
tray.unshift(group);
|
||||
}
|
||||
const info = {
|
||||
showToast: toast.showToast,
|
||||
sound: toast.sound,
|
||||
eFeature: 0,
|
||||
toastDurationMS: toastData.nToastDurationMS,
|
||||
fnTray,
|
||||
};
|
||||
window.NotificationStore.ProcessNotification(info, toastData, ToastType.New);
|
||||
}
|
||||
}
|
||||
|
||||
deinit() {
|
||||
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');
|
||||
this.toastPatch?.unpatch();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user