diff options
| author | AAGaming <aagaming@riseup.net> | 2024-08-05 14:07:10 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-08-05 14:07:10 -0400 |
| commit | 131f0961ff451ec47376483178e092c8d7403b27 (patch) | |
| tree | 4d2ea34e8220e14c4b820cc1ad38face7193f6fe /frontend/src/toaster.tsx | |
| parent | 75aa1e4851445646994ba3a61ff41325403359fb (diff) | |
| download | decky-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.tsx | 238 |
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(); } } |
