From 4cf80595ad61107a4edb2041e63983329f23ccb7 Mon Sep 17 00:00:00 2001 From: AAGaming Date: Sun, 28 Jul 2024 18:19:55 -0400 Subject: feat(toaster):add support for dismissing toasts and new indicator --- frontend/src/components/DeckyIcon.tsx | 2 +- frontend/src/components/Toast.tsx | 39 ++++++------- .../components/settings/pages/testing/index.tsx | 3 +- frontend/src/index.ts | 3 +- frontend/src/plugin-loader.tsx | 28 ++++++---- frontend/src/toaster.tsx | 65 +++++++++++++++------- 6 files changed, 85 insertions(+), 55 deletions(-) (limited to 'frontend/src') diff --git a/frontend/src/components/DeckyIcon.tsx b/frontend/src/components/DeckyIcon.tsx index fce249e3..85cc2ad7 100644 --- a/frontend/src/components/DeckyIcon.tsx +++ b/frontend/src/components/DeckyIcon.tsx @@ -1,7 +1,7 @@ import { FC, SVGAttributes } from 'react'; const DeckyIcon: FC> = (props) => ( - + m.ShortTemplate) || {}; // These are memoized as they like to randomly rerender -const GamepadUIPopupToast: FC = memo(({ toast }) => { +const GamepadUIPopupToast: FC> = memo(({ toast }) => { return (
= memo(({ toast }) => { ); }); -const GamepadUIQAMToast: FC = memo(({ toast }) => { +const GamepadUIQAMToast: FC = memo(({ toast, newIndicator }) => { // The fields aren't mismatched, the logic for these is just a bit weird. return ( { - Navigation.CloseSideMenus(); toast.onClick?.(); + Navigation.CloseSideMenus(); }} className={joinClassNames( templateClasses.StandardTemplateContainer, @@ -65,11 +65,7 @@ const GamepadUIQAMToast: FC = memo(({ toast }) => {
{toast.icon &&
{toast.icon}
} -
- {toast.header || ( - - )} -
+ {toast.title &&
{toast.title}
} {/* timestamp should always be defined by toaster */} {/* TODO check how valve does this */} {toast.timestamp && ( @@ -78,29 +74,30 @@ const GamepadUIQAMToast: FC = memo(({ toast }) => {
)}
-
- {toast.fullTemplateTitle || toast.title} -
-
{toast.body}
+ {toast.body &&
{toast.body}
} + {toast.subtext &&
{toast.subtext}
}
- {/* TODO support NewIndicator */} - {/*
- -
*/} + {newIndicator && ( +
+ + + +
+ )} ); }); -export const ToastRenderer: FC = memo(({ toast, location }) => { +export const ToastRenderer: FC = memo(({ toast, location, newIndicator }) => { switch (location) { default: - logger.warn(`Toast UI not implemented for location ${location}! Falling back to GamepadUIPopupToast.`); + logger.warn(`Toast UI not implemented for location ${location}! Falling back to GamepadUIQAMToast.`); + return ; case ToastLocation.GAMEPADUI_POPUP: return ; case ToastLocation.GAMEPADUI_QAM: - return ; + return ; } }); diff --git a/frontend/src/components/settings/pages/testing/index.tsx b/frontend/src/components/settings/pages/testing/index.tsx index 6f52afe3..8f02c207 100644 --- a/frontend/src/components/settings/pages/testing/index.tsx +++ b/frontend/src/components/settings/pages/testing/index.tsx @@ -91,13 +91,14 @@ export default function TestingVersionList() { { - DeckyPluginLoader.toaster.toast({ + const downloadToast = DeckyPluginLoader.toaster.toast({ title: t('Testing.start_download_toast', { id: version.id }), body: null, icon: , }); try { await downloadTestingVersion(version.id, version.head_sha); + downloadToast.dismiss(); } catch (e) { if (e instanceof Error) { DeckyPluginLoader.toaster.toast({ diff --git a/frontend/src/index.ts b/frontend/src/index.ts index 4ea22318..c962375b 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -6,12 +6,13 @@ interface Window { (async () => { // Wait for react to definitely be loaded + console.debug('[Decky:Boot] Waiting for React chunk...'); while (!window.webpackChunksteamui || window.webpackChunksteamui <= 3) { await new Promise((r) => setTimeout(r, 10)); // Can't use DFL sleep here. } if (!window.SP_REACT) { - console.debug('[Decky:Boot] Setting up React globals...'); + console.debug('[Decky:Boot] Setting up Webpack & React globals...'); // deliberate partial import const DFLWebpack = await import('@decky/ui/dist/webpack'); window.SP_REACT = DFLWebpack.findModule((m) => m.Component && m.PureComponent && m.useLayoutEffect); diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx index f9032909..b1af76fc 100644 --- a/frontend/src/plugin-loader.tsx +++ b/frontend/src/plugin-loader.tsx @@ -1,16 +1,17 @@ +import { ToastNotification } from '@decky/api'; import { ModalRoot, + Navigation, PanelSection, PanelSectionRow, QuickAccessTab, - Router, findSP, quickAccessMenuClasses, showModal, sleep, } from '@decky/ui'; import { FC, lazy } from 'react'; -import { FaExclamationCircle, FaPlug } from 'react-icons/fa'; +import { FaDownload, FaExclamationCircle, FaPlug } from 'react-icons/fa'; import DeckyIcon from './components/DeckyIcon'; import { DeckyState, DeckyStateContextProvider, UserInfo, useDeckyState } from './components/DeckyState'; @@ -80,6 +81,9 @@ class PluginLoader extends Logger { // stores a list of plugin names which requested to be reloaded private pluginReloadQueue: { name: string; version?: string }[] = []; + private loaderUpdateToast?: ToastNotification; + private pluginUpdateToast?: ToastNotification; + constructor() { super(PluginLoader.name); @@ -174,10 +178,6 @@ class PluginLoader extends Logger { >('loader/get_plugins'); private async loadPlugins() { - // wait for SP window to exist before loading plugins - while (!findSP()) { - await sleep(100); - } const plugins = await this.getPluginsFromBackend(); const pluginLoadPromises = []; const loadStart = performance.now(); @@ -210,7 +210,8 @@ class PluginLoader extends Logger { if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) { this.deckyState.setHasLoaderUpdate(true); if (this.notificationService.shouldNotify('deckyUpdates')) { - this.toaster.toast({ + this.loaderUpdateToast && this.loaderUpdateToast.dismiss(); + this.loaderUpdateToast = this.toaster.toast({ title: , body: ( ), - icon: , - onClick: () => Router.Navigate('/decky/settings'), + logo: , + icon: , + onClick: () => Navigation.Navigate('/decky/settings'), }); } } @@ -239,7 +241,8 @@ class PluginLoader extends Logger { public async notifyPluginUpdates() { const updates = await this.checkPluginUpdates(); if (updates?.size > 0 && this.notificationService.shouldNotify('pluginUpdates')) { - this.toaster.toast({ + this.pluginUpdateToast && this.pluginUpdateToast.dismiss(); + this.pluginUpdateToast = this.toaster.toast({ title: , body: ( ), - icon: , - onClick: () => Router.Navigate('/decky/settings/plugins'), + logo: , + icon: , + onClick: () => Navigation.Navigate('/decky/settings/plugins'), }); } } diff --git a/frontend/src/toaster.tsx b/frontend/src/toaster.tsx index 06150b9e..3f83e967 100644 --- a/frontend/src/toaster.tsx +++ b/frontend/src/toaster.tsx @@ -1,4 +1,4 @@ -import type { ToastData } from '@decky/api'; +import type { ToastData, ToastNotification } from '@decky/api'; import { Patch, callOriginal, findModuleExport, injectFCTrampoline, replacePatch } from '@decky/ui'; import Toast from './components/Toast'; @@ -20,8 +20,6 @@ declare global { } class Toaster extends Logger { - private finishStartup?: () => void; - private ready: Promise = new Promise((res) => (this.finishStartup = res)); private toastPatch?: Patch; constructor() { @@ -29,11 +27,7 @@ class Toaster extends Logger { window.__TOASTER_INSTANCE?.deinit?.(); window.__TOASTER_INSTANCE = this; - this.init(); - } - // TODO maybe move to constructor lol - async init() { const ValveToastRenderer = findModuleExport((e) => e?.toString()?.includes(`controller:"notification",method:`)); // TODO find a way to undo this if possible? const patchedRenderer = injectFCTrampoline(ValveToastRenderer); @@ -41,34 +35,43 @@ class Toaster extends Logger { this.debug('render toast', args); if (args?.[0]?.group?.decky || args?.[0]?.group?.notifications?.[0]?.decky) { return args[0].group.notifications.map((notification: any) => ( - + )); } 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, }; - 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(); + let group: any; function fnTray(toast: any, tray: any) { - let group = { + group = { eType: toast.eType, notifications: [toast], }; @@ -83,7 +86,31 @@ class Toaster extends Logger { 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() { -- cgit v1.2.3