summaryrefslogtreecommitdiff
path: root/frontend/src/hooks/toaster.tsx
diff options
context:
space:
mode:
authorAAGaming <aagaming@riseup.net>2026-10-26 23:50:59 -0400
committerAAGaming <aagaming@riseup.net>2026-10-26 23:50:59 -0400
commite82a55e9582fbd6d84f3f4510b04480c35e41fe4 (patch)
tree1fe05c2296bbedbf301137a67a28ef542804fd69 /frontend/src/hooks/toaster.tsx
parent6301508037b30489c42ee9ea8acee97cdea76297 (diff)
downloaddecky-loader-e82a55e9582fbd6d84f3f4510b04480c35e41fe4.tar.gz
decky-loader-e82a55e9582fbd6d84f3f4510b04480c35e41fe4.zip
feat: initial VERY VERY incomplete steam frame support
Diffstat (limited to 'frontend/src/hooks/toaster.tsx')
-rw-r--r--frontend/src/hooks/toaster.tsx125
1 files changed, 125 insertions, 0 deletions
diff --git a/frontend/src/hooks/toaster.tsx b/frontend/src/hooks/toaster.tsx
new file mode 100644
index 00000000..9108d9ec
--- /dev/null
+++ b/frontend/src/hooks/toaster.tsx
@@ -0,0 +1,125 @@
+import type { ToastData, ToastNotification } from '@decky/api';
+import {
+ ESteamNotificationAction,
+ ErrorBoundary,
+ Patch,
+ callOriginal,
+ findModuleExport,
+ injectFCTrampoline,
+ replacePatch,
+} from '@decky/ui';
+
+import Toast from '../components/Toast';
+import Logger from '../logger';
+
+declare global {
+ interface Window {
+ __TOASTER_INSTANCE: any;
+ }
+}
+
+class Toaster extends Logger {
+ private toastPatch?: Patch;
+
+ constructor() {
+ super('Toaster');
+
+ window.__TOASTER_INSTANCE?.deinit?.();
+ window.__TOASTER_INSTANCE = this;
+
+ 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) => (
+ <ErrorBoundary>
+ <Toast toast={notification.data} newIndicator={notification.bNewIndicator} location={args?.[0]?.location} />
+ </ErrorBoundary>
+ ));
+ }
+ return callOriginal;
+ });
+
+ this.log('Initialized');
+ }
+
+ 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 || 31,
+ eSource: 1, // Client
+ nToastDurationMS: toast.duration || (toast.duration = 5e3),
+ data: toast,
+ decky: true,
+ };
+ 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,
+ playSound: toast.playSound,
+ 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);
+ }
+ try {
+ window.NotificationStore.ProcessNotification(info, toastData, ESteamNotificationAction.New);
+ } catch (e) {
+ this.error('Error while sending toast:', e);
+ }
+ return toastResult;
+ }
+
+ deinit() {
+ this.toastPatch?.unpatch();
+ }
+}
+
+export default Toaster;