summaryrefslogtreecommitdiff
path: root/frontend/src/toaster.tsx
diff options
context:
space:
mode:
authorAAGaming <aagaming@riseup.net>2024-08-05 14:07:10 -0400
committerGitHub <noreply@github.com>2024-08-05 14:07:10 -0400
commit131f0961ff451ec47376483178e092c8d7403b27 (patch)
tree4d2ea34e8220e14c4b820cc1ad38face7193f6fe /frontend/src/toaster.tsx
parent75aa1e4851445646994ba3a61ff41325403359fb (diff)
downloaddecky-loader-131f0961ff451ec47376483178e092c8d7403b27.tar.gz
decky-loader-131f0961ff451ec47376483178e092c8d7403b27.zip
Rewrite router/tabs/toaster hooks (#661)
Diffstat (limited to 'frontend/src/toaster.tsx')
-rw-r--r--frontend/src/toaster.tsx238
1 files changed, 79 insertions, 159 deletions
diff --git a/frontend/src/toaster.tsx b/frontend/src/toaster.tsx
index 4bc08772..e45b14a4 100644
--- a/frontend/src/toaster.tsx
+++ b/frontend/src/toaster.tsx
@@ -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 type { ToastData, ToastNotification } from '@decky/api';
+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,176 +20,99 @@ 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;
- this.init();
- }
- 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;
- }
- 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;
+ const ValveToastRenderer = findModuleExport((e) => e?.toString()?.includes(`controller:"notification",method:`));
+ // TODO find a way to undo this if possible?
+ const patchedRenderer = injectFCTrampoline(ValveToastRenderer);
+ this.toastPatch = replacePatch(patchedRenderer, 'component', (args: any[]) => {
+ if (args?.[0]?.group?.decky || args?.[0]?.group?.notifications?.[0]?.decky) {
+ return args[0].group.notifications.map((notification: any) => (
+ <Toast toast={notification.data} newIndicator={notification.bNewIndicator} location={args?.[0]?.location} />
+ ));
}
- 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?.();
}
- async toast(toast: ToastData) {
- // toast.duration = toast.duration || 5e3;
- // this.toasterState.addToast(toast);
- await this.ready;
+ toast(toast: ToastData): ToastNotification {
+ if (toast.sound === undefined) toast.sound = 6;
+ if (toast.playSound === undefined) toast.playSound = true;
+ if (toast.showToast === undefined) toast.showToast = true;
+ if (toast.timestamp === undefined) toast.timestamp = new Date();
+ if (toast.showNewIndicator === undefined) toast.showNewIndicator = true;
+ /* eType 13
+ 13: {
+ proto: m.mu,
+ fnTray: null,
+ showToast: !0,
+ sound: f.PN.ToastMisc,
+ eFeature: l.uX
+ }
+ */
let toastData = {
nNotificationID: window.NotificationStore.m_nNextTestNotificationID++,
+ bNewIndicator: toast.showNewIndicator,
rtCreated: Date.now(),
- eType: toast.eType || 11,
+ eType: toast.eType || 13,
+ eSource: 1, // Client
nToastDurationMS: toast.duration || (toast.duration = 5e3),
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;
- if (
- (window.settingsStore.settings.bDisableAllToasts && !toast.critical) ||
- (window.settingsStore.settings.bDisableToastsInGame &&
- !toast.critical &&
- window.NotificationStore.BIsUserInGame())
- )
- return;
- if (toast.playSound) this.audioModule?.PlayNavSound(toast.sound);
- if (toast.showToast) {
- window.NotificationStore.m_rgNotificationToasts.push(toastData);
- window.NotificationStore.DispatchNextToast();
+ let group: any;
+ function fnTray(toast: any, tray: any) {
+ group = {
+ eType: toast.eType,
+ notifications: [toast],
+ };
+ tray.unshift(group);
+ }
+ const info = {
+ showToast: toast.showToast,
+ sound: toast.sound,
+ eFeature: 0,
+ toastDurationMS: toastData.nToastDurationMS,
+ bCritical: toast.critical,
+ fnTray,
+ };
+ const self = this;
+ let expirationTimeout: number;
+ const toastResult: ToastNotification = {
+ data: toast,
+ dismiss() {
+ // it checks against the id of notifications[0]
+ try {
+ expirationTimeout && clearTimeout(expirationTimeout);
+ group && window.NotificationStore.RemoveGroupFromTray(group);
+ } catch (e) {
+ self.error('Error while dismissing toast:', e);
+ }
+ },
+ };
+ if (toast.expiration) {
+ expirationTimeout = setTimeout(() => {
+ try {
+ group && window.NotificationStore.RemoveGroupFromTray(group);
+ } catch (e) {
+ this.error('Error while dismissing expired toast:', e);
+ }
+ }, toast.expiration);
}
+ window.NotificationStore.ProcessNotification(info, toastData, ToastType.New);
+ return toastResult;
}
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();
}
}