summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--frontend/src/components/DeckyIcon.tsx2
-rw-r--r--frontend/src/components/Toast.tsx39
-rw-r--r--frontend/src/components/settings/pages/testing/index.tsx3
-rw-r--r--frontend/src/index.ts3
-rw-r--r--frontend/src/plugin-loader.tsx28
-rw-r--r--frontend/src/toaster.tsx65
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() {