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 --- backend/updater.py | 6 +- frontend/package.json | 2 +- frontend/pnpm-lock.yaml | 8 +- 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 ++++ 9 files changed, 190 insertions(+), 24 deletions(-) create mode 100644 frontend/src/components/Toast.tsx create mode 100644 frontend/src/toaster.tsx diff --git a/backend/updater.py b/backend/updater.py index 4c3cd715..4fca0496 100644 --- a/backend/updater.py +++ b/backend/updater.py @@ -62,7 +62,7 @@ class Updater: "updatable": self.localVer != None } else: - return {"current": "unknown", "updatable": False} + return {"current": "unknown", "remote": self.remoteVer, "updatable": False} async def check_for_updates(self): async with ClientSession() as web: @@ -70,6 +70,8 @@ class Updater: remoteVersions = await res.json() self.remoteVer = next(filter(lambda ver: ver["prerelease"] and ver["tag_name"].startswith("v") and ver["tag_name"].endswith("-pre"), remoteVersions), None) logger.info("Updated remote version information") + tab = await get_tab("SP") + await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False) return await self.get_version() async def version_reloader(self): @@ -79,7 +81,7 @@ class Updater: await self.check_for_updates() except: pass - await sleep(60 * 60) # 1 hour + await sleep(60 * 60 * 6) # 6 hours async def do_update(self): version = self.remoteVer["tag_name"] diff --git a/frontend/package.json b/frontend/package.json index 422ba3c3..3765e94b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,7 +37,7 @@ } }, "dependencies": { - "decky-frontend-lib": "^1.2.4", + "decky-frontend-lib": "^1.5.0", "react-icons": "^4.4.0" } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 932f5e18..85cf84e0 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -9,7 +9,7 @@ specifiers: '@types/react': 16.14.0 '@types/react-router': 5.1.18 '@types/webpack': ^5.28.0 - decky-frontend-lib: ^1.2.4 + decky-frontend-lib: ^1.5.0 husky: ^8.0.1 import-sort-style-module: ^6.0.0 inquirer: ^8.2.4 @@ -23,7 +23,7 @@ specifiers: typescript: ^4.7.4 dependencies: - decky-frontend-lib: 1.2.4 + decky-frontend-lib: 1.5.0 react-icons: 4.4.0_react@16.14.0 devDependencies: @@ -806,8 +806,8 @@ packages: ms: 2.1.2 dev: true - /decky-frontend-lib/1.2.4: - resolution: {integrity: sha512-r3mLEey9KUkF68geJVSjNlOz/Fg4vpMKUzoutSceyd8o/J5l+QR+Vf0b3gwK3UN9Sp4Pj4XQ1eB82+/W0ApsFg==} + /decky-frontend-lib/1.5.0: + resolution: {integrity: sha512-BT/txV7Q1NJfbnT99jt0OdUE2Qv4Gm1jhrevctx84D+pp7TfpCYau+Khnq2B9agEQQo7CWh5hOJW/SX5n2dKnQ==} dev: false /deepmerge/4.2.2: 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