From 67426af3ef73e788d99b6d2e0c730c270daea273 Mon Sep 17 00:00:00 2001 From: AAGaming Date: Tue, 9 Aug 2022 21:52:03 -0400 Subject: Add api for showing toast notifications --- frontend/src/components/Toast.tsx | 54 +++++++++++++ .../components/settings/pages/general/Updater.tsx | 18 +---- frontend/src/index.tsx | 3 + frontend/src/plugin-loader.tsx | 14 ++++ frontend/src/toaster.tsx | 93 ++++++++++++++++++++++ frontend/src/updater.ts | 16 ++++ 6 files changed, 181 insertions(+), 17 deletions(-) create mode 100644 frontend/src/components/Toast.tsx create mode 100644 frontend/src/toaster.tsx (limited to 'frontend/src') diff --git a/frontend/src/components/Toast.tsx b/frontend/src/components/Toast.tsx new file mode 100644 index 00000000..559c37c6 --- /dev/null +++ b/frontend/src/components/Toast.tsx @@ -0,0 +1,54 @@ +import { ToastData, findModule, joinClassNames } from 'decky-frontend-lib'; +import { FunctionComponent } from 'react'; + +interface ToastProps { + toast: { + data: ToastData; + nToastDurationMS: number; + }; +} + +const toastClasses = findModule((mod) => { + if (typeof mod !== 'object') return false; + + if (mod.ToastPlaceholder) { + return true; + } + + return false; +}); + +const templateClasses = findModule((mod) => { + if (typeof mod !== 'object') return false; + + if (mod.ShortTemplate) { + return true; + } + + return false; +}); + +const Toast: FunctionComponent = ({ toast }) => { + return ( +
+
+ {toast.data.logo &&
{toast.data.logo}
} +
+
+ {toast.data.icon &&
{toast.data.icon}
} +
{toast.data.title}
+
+
{toast.data.body}
+
+
+
+ ); +}; + +export default Toast; diff --git a/frontend/src/components/settings/pages/general/Updater.tsx b/frontend/src/components/settings/pages/general/Updater.tsx index 106af6f3..3d137d7a 100644 --- a/frontend/src/components/settings/pages/general/Updater.tsx +++ b/frontend/src/components/settings/pages/general/Updater.tsx @@ -2,23 +2,7 @@ import { DialogButton, Field, ProgressBarWithInfo, Spinner } from 'decky-fronten import { useEffect, useState } from 'react'; import { FaArrowDown } from 'react-icons/fa'; -import { callUpdaterMethod, finishUpdate } from '../../../../updater'; - -interface VerInfo { - current: string; - remote: { - assets: { - browser_download_url: string; - created_at: string; - }[]; - name: string; - body: string; - prerelease: boolean; - published_at: string; - tag_name: string; - } | null; - updatable: boolean; -} +import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater'; export default function UpdaterSettings() { const [versionInfo, setVersionInfo] = useState(null); diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 20f71766..3df18093 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,3 +1,5 @@ +import { sleep } from 'decky-frontend-lib'; + import PluginLoader from './plugin-loader'; import { DeckyUpdater } from './updater'; @@ -12,6 +14,7 @@ declare global { } } (async () => { + await sleep(1000); window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text()); window.DeckyPluginLoader?.dismountAll(); diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx index 29ca326f..df3b220a 100644 --- a/frontend/src/plugin-loader.tsx +++ b/frontend/src/plugin-loader.tsx @@ -12,6 +12,8 @@ import Logger from './logger'; import { Plugin } from './plugin'; import RouterHook from './router-hook'; import TabsHook from './tabs-hook'; +import Toaster from './toaster'; +import { VerInfo, callUpdaterMethod } from './updater'; declare global { interface Window {} @@ -22,6 +24,7 @@ class PluginLoader extends Logger { private tabsHook: TabsHook = new TabsHook(); // private windowHook: WindowHook = new WindowHook(); private routerHook: RouterHook = new RouterHook(); + private toaster: Toaster = new Toaster(); private deckyState: DeckyState = new DeckyState(); private reloadLock: boolean = false; @@ -54,6 +57,16 @@ class PluginLoader extends Logger { }); } + public async notifyUpdates() { + const versionInfo = (await callUpdaterMethod('get_version')).result as VerInfo; + if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) { + this.toaster.toast({ + title: 'Decky', + body: `Update to ${versionInfo?.remote?.tag_name} availiable!`, + }); + } + } + public addPluginInstallPrompt(artifact: string, version: string, request_id: string, hash: string) { showModal( { + if (typeof m !== 'object') return undefined; + for (let prop in m) { + if (typeof m[prop]?.settings?.bDisableToastsInGame !== 'undefined') return m[prop]; + } + }); + + let instance: any; + while (true) { + instance = findInReactTree( + (document.getElementById('root') as any)._reactRootContainer._internalRoot.current, + (x) => x?.memoizedProps?.className?.startsWith('toastmanager_ToastPlaceholder'), + ); + if (instance) break; + this.debug('finding instance'); + await sleep(2000); + } + + this.node = instance.return.return; + this.node.stateNode.render = (...args: any[]) => { + const ret = this.node.stateNode.__proto__.render.call(this.node.stateNode, ...args); + if (ret) { + this.instanceRet = ret; + afterPatch(ret, 'type', (_: any, ret: any) => { + if (ret?.props?.children[1]?.children?.props?.notification?.decky) { + const toast = ret.props.children[1].children.props.notification; + ret.props.children[1].children.type = () => ; + } + return ret; + }); + } + return ret; + }; + this.node.stateNode.forceUpdate(); + this.log('Initialized'); + } + + toast(toast: ToastData) { + const settings = this.settingsModule.settings; + let toastData = { + nNotificationID: window.NotificationStore.m_nNextTestNotificationID++, + rtCreated: Date.now(), + eType: 15, + nToastDurationMS: toast.duration || 5e3, + data: toast, + decky: true, + }; + // @ts-ignore + toastData.data.appid = () => 0; + if ( + (settings.bDisableAllToasts && !toast.critical) || + (settings.bDisableToastsInGame && !toast.critical && window.NotificationStore.BIsUserInGame()) + ) + return; + window.NotificationStore.m_rgNotificationToasts.push(toastData); + window.NotificationStore.DispatchNextToast(); + window.NotificationStore.m_rgNotificationToasts.pop(); + } + + deinit() { + unpatch(this.instanceRet, 'type'); + delete this.node.stateNode.render; + this.node.stateNode.forceUpdate(); + } +} + +export default Toaster; diff --git a/frontend/src/updater.ts b/frontend/src/updater.ts index f499d030..dd37f0b4 100644 --- a/frontend/src/updater.ts +++ b/frontend/src/updater.ts @@ -11,6 +11,22 @@ export interface DeckyUpdater { finish: () => void; } +export interface VerInfo { + current: string; + remote: { + assets: { + browser_download_url: string; + created_at: string; + }[]; + name: string; + body: string; + prerelease: boolean; + published_at: string; + tag_name: string; + } | null; + updatable: boolean; +} + export async function callUpdaterMethod(methodName: string, args = {}) { const response = await fetch(`http://127.0.0.1:1337/updater/${methodName}`, { method: 'POST', -- cgit v1.2.3