diff options
| author | AAGaming <aagaming@riseup.net> | 2024-07-28 18:19:55 -0400 |
|---|---|---|
| committer | AAGaming <aagaming@riseup.net> | 2024-08-03 14:04:20 -0400 |
| commit | 4cf80595ad61107a4edb2041e63983329f23ccb7 (patch) | |
| tree | f6353008999c12f0ee49e3647a54c47275c79c73 | |
| parent | 4c23549748e71c061ea2fc1f8fa9262f15018cfc (diff) | |
| download | decky-loader-4cf80595ad61107a4edb2041e63983329f23ccb7.tar.gz decky-loader-4cf80595ad61107a4edb2041e63983329f23ccb7.zip | |
feat(toaster):add support for dismissing toasts and new indicator
| -rw-r--r-- | frontend/src/components/DeckyIcon.tsx | 2 | ||||
| -rw-r--r-- | frontend/src/components/Toast.tsx | 39 | ||||
| -rw-r--r-- | frontend/src/components/settings/pages/testing/index.tsx | 3 | ||||
| -rw-r--r-- | frontend/src/index.ts | 3 | ||||
| -rw-r--r-- | frontend/src/plugin-loader.tsx | 28 | ||||
| -rw-r--r-- | frontend/src/toaster.tsx | 65 |
6 files changed, 85 insertions, 55 deletions
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<SVGAttributes<SVGElement>> = (props) => ( - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 456" width="512" height="456" {...props}> + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 456" {...props}> <g> <path style={{ fill: 'none' }} diff --git a/frontend/src/components/Toast.tsx b/frontend/src/components/Toast.tsx index e40d1d22..e86e9337 100644 --- a/frontend/src/components/Toast.tsx +++ b/frontend/src/components/Toast.tsx @@ -3,7 +3,6 @@ import { Focusable, Navigation, findClassModule, joinClassNames } from '@decky/u import { FC, memo } from 'react'; import Logger from '../logger'; -import TranslationHelper, { TranslationClass } from '../utils/TranslationHelper'; const logger = new Logger('ToastRenderer'); @@ -17,6 +16,7 @@ export enum ToastLocation { interface ToastProps { toast: ToastData; + newIndicator?: boolean; } interface ToastRendererProps extends ToastProps { @@ -27,7 +27,7 @@ const templateClasses = findClassModule((m) => m.ShortTemplate) || {}; // These are memoized as they like to randomly rerender -const GamepadUIPopupToast: FC<ToastProps> = memo(({ toast }) => { +const GamepadUIPopupToast: FC<Omit<ToastProps, 'newIndicator'>> = memo(({ toast }) => { return ( <div style={{ '--toast-duration': `${toast.duration}ms` } as React.CSSProperties} @@ -46,13 +46,13 @@ const GamepadUIPopupToast: FC<ToastProps> = memo(({ toast }) => { ); }); -const GamepadUIQAMToast: FC<ToastProps> = memo(({ toast }) => { +const GamepadUIQAMToast: FC<ToastProps> = memo(({ toast, newIndicator }) => { // The fields aren't mismatched, the logic for these is just a bit weird. return ( <Focusable onActivate={() => { - Navigation.CloseSideMenus(); toast.onClick?.(); + Navigation.CloseSideMenus(); }} className={joinClassNames( templateClasses.StandardTemplateContainer, @@ -65,11 +65,7 @@ const GamepadUIQAMToast: FC<ToastProps> = memo(({ toast }) => { <div className={joinClassNames(templateClasses.Content, toast.contentClassName || '')}> <div className={templateClasses.Header}> {toast.icon && <div className={templateClasses.Icon}>{toast.icon}</div>} - <div className={templateClasses.Title}> - {toast.header || ( - <TranslationHelper transClass={TranslationClass.PLUGIN_LOADER} transText="decky_title" /> - )} - </div> + {toast.title && <div className={templateClasses.Title}>{toast.title}</div>} {/* timestamp should always be defined by toaster */} {/* TODO check how valve does this */} {toast.timestamp && ( @@ -78,29 +74,30 @@ const GamepadUIQAMToast: FC<ToastProps> = memo(({ toast }) => { </div> )} </div> - <div className={templateClasses.StandardNotificationDescription}> - {toast.fullTemplateTitle || toast.title} - </div> - <div className={templateClasses.StandardNotificationSubText}>{toast.body}</div> + {toast.body && <div className={templateClasses.StandardNotificationDescription}>{toast.body}</div>} + {toast.subtext && <div className={templateClasses.StandardNotificationSubText}>{toast.subtext}</div>} </div> - {/* TODO support NewIndicator */} - {/* <div className={templateClasses.NewIndicator}><svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" - viewBox="0 0 50 50" fill="none"> - <circle fill="currentColor" cx="25" cy="25" r="25"></circle> - </svg></div> */} + {newIndicator && ( + <div className={templateClasses.NewIndicator}> + <svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 50 50" fill="none"> + <circle fill="currentColor" cx="25" cy="25" r="25"></circle> + </svg> + </div> + )} </div> </Focusable> ); }); -export const ToastRenderer: FC<ToastRendererProps> = memo(({ toast, location }) => { +export const ToastRenderer: FC<ToastRendererProps> = 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 <GamepadUIQAMToast toast={toast} newIndicator={false} />; case ToastLocation.GAMEPADUI_POPUP: return <GamepadUIPopupToast toast={toast} />; case ToastLocation.GAMEPADUI_QAM: - return <GamepadUIQAMToast toast={toast} />; + return <GamepadUIQAMToast toast={toast} newIndicator={newIndicator} />; } }); 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() { <DialogButton style={{ height: '40px', minWidth: '60px', marginRight: '10px' }} onClick={async () => { - DeckyPluginLoader.toaster.toast({ + const downloadToast = DeckyPluginLoader.toaster.toast({ title: t('Testing.start_download_toast', { id: version.id }), body: null, icon: <FaFlask />, }); 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: <TranslationHelper transClass={TranslationClass.PLUGIN_LOADER} transText="decky_title" />, body: ( <TranslationHelper @@ -219,8 +220,9 @@ class PluginLoader extends Logger { i18nArgs={{ tag_name: versionInfo?.remote?.tag_name }} /> ), - icon: <DeckyIcon />, - onClick: () => Router.Navigate('/decky/settings'), + logo: <DeckyIcon />, + icon: <FaDownload />, + 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: <TranslationHelper transClass={TranslationClass.PLUGIN_LOADER} transText="decky_title" />, body: ( <TranslationHelper @@ -248,8 +251,9 @@ class PluginLoader extends Logger { i18nArgs={{ count: updates.size }} /> ), - icon: <DeckyIcon />, - onClick: () => Router.Navigate('/decky/settings/plugins'), + logo: <DeckyIcon />, + icon: <FaDownload />, + 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<void> = 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) => ( - <Toast toast={notification.data} location={args?.[0]?.location} /> + <Toast toast={notification.data} newIndicator={notification.bNewIndicator} location={args?.[0]?.location} /> )); } 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() { |
