summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAAGaming <aa@mail.catvibers.me>2022-08-09 21:52:03 -0400
committerAAGaming <aa@mail.catvibers.me>2022-08-09 21:52:03 -0400
commit67426af3ef73e788d99b6d2e0c730c270daea273 (patch)
tree30c1f4b33e63d38d8d5cc26f26af655f1b5a44ba
parent0dbdb4a143f6e4f2b08c5a38a597d5a1c49a109c (diff)
downloaddecky-loader-67426af3ef73e788d99b6d2e0c730c270daea273.tar.gz
decky-loader-67426af3ef73e788d99b6d2e0c730c270daea273.zip
Add api for showing toast notificationsv2.0.4-67426af-pre
-rw-r--r--backend/updater.py6
-rw-r--r--frontend/package.json2
-rw-r--r--frontend/pnpm-lock.yaml8
-rw-r--r--frontend/src/components/Toast.tsx54
-rw-r--r--frontend/src/components/settings/pages/general/Updater.tsx18
-rw-r--r--frontend/src/index.tsx3
-rw-r--r--frontend/src/plugin-loader.tsx14
-rw-r--r--frontend/src/toaster.tsx93
-rw-r--r--frontend/src/updater.ts16
9 files changed, 190 insertions, 24 deletions
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<ToastProps> = ({ toast }) => {
+ return (
+ <div
+ style={{ '--toast-duration': `${toast.nToastDurationMS}ms` } as React.CSSProperties}
+ className={joinClassNames(toastClasses.ToastPopup, toastClasses.toastEnter)}
+ >
+ <div
+ onClick={toast.data.onClick}
+ className={joinClassNames(templateClasses.ShortTemplate, toast.data.className || '')}
+ >
+ {toast.data.logo && <div className={templateClasses.StandardLogoDimensions}>{toast.data.logo}</div>}
+ <div className={joinClassNames(templateClasses.Content, toast.data.contentClassName || '')}>
+ <div className={templateClasses.Header}>
+ {toast.data.icon && <div className={templateClasses.Icon}>{toast.data.icon}</div>}
+ <div className={templateClasses.Title}>{toast.data.title}</div>
+ </div>
+ <div className={templateClasses.Body}>{toast.data.body}</div>
+ </div>
+ </div>
+ </div>
+ );
+};
+
+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<VerInfo | null>(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(
<PluginInstallModal
@@ -189,6 +202,7 @@ class PluginLoader extends Logger {
createPluginAPI(pluginName: string) {
return {
routerHook: this.routerHook,
+ toaster: this.toaster,
callServerMethod: this.callServerMethod,
async callPluginMethod(methodName: string, args = {}) {
const response = await fetch(`http://127.0.0.1:1337/plugins/${pluginName}/methods/${methodName}`, {
diff --git a/frontend/src/toaster.tsx b/frontend/src/toaster.tsx
new file mode 100644
index 00000000..b6901ed2
--- /dev/null
+++ b/frontend/src/toaster.tsx
@@ -0,0 +1,93 @@
+import { ToastData, afterPatch, findInReactTree, findModuleChild, sleep, unpatch } from 'decky-frontend-lib';
+
+import Toast from './components/Toast';
+import Logger from './logger';
+
+declare global {
+ interface Window {
+ __TOASTER_INSTANCE: any;
+ NotificationStore: any;
+ }
+}
+
+class Toaster extends Logger {
+ private instanceRet: any;
+ private node: any;
+ private settingsModule: any;
+
+ constructor() {
+ super('Toaster');
+
+ window.__TOASTER_INSTANCE?.deinit?.();
+ window.__TOASTER_INSTANCE = this;
+ this.init();
+ }
+
+ async init() {
+ this.settingsModule = findModuleChild((m) => {
+ 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 = () => <Toast toast={toast} />;
+ }
+ 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',