mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-07-11 05:41:55 +00:00
Add api for showing toast notifications
This commit is contained in:
+4
-2
@@ -62,7 +62,7 @@ class Updater:
|
|||||||
"updatable": self.localVer != None
|
"updatable": self.localVer != None
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return {"current": "unknown", "updatable": False}
|
return {"current": "unknown", "remote": self.remoteVer, "updatable": False}
|
||||||
|
|
||||||
async def check_for_updates(self):
|
async def check_for_updates(self):
|
||||||
async with ClientSession() as web:
|
async with ClientSession() as web:
|
||||||
@@ -70,6 +70,8 @@ class Updater:
|
|||||||
remoteVersions = await res.json()
|
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)
|
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")
|
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()
|
return await self.get_version()
|
||||||
|
|
||||||
async def version_reloader(self):
|
async def version_reloader(self):
|
||||||
@@ -79,7 +81,7 @@ class Updater:
|
|||||||
await self.check_for_updates()
|
await self.check_for_updates()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
await sleep(60 * 60) # 1 hour
|
await sleep(60 * 60 * 6) # 6 hours
|
||||||
|
|
||||||
async def do_update(self):
|
async def do_update(self):
|
||||||
version = self.remoteVer["tag_name"]
|
version = self.remoteVer["tag_name"]
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"decky-frontend-lib": "^1.2.4",
|
"decky-frontend-lib": "^1.5.0",
|
||||||
"react-icons": "^4.4.0"
|
"react-icons": "^4.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+4
-4
@@ -9,7 +9,7 @@ specifiers:
|
|||||||
'@types/react': 16.14.0
|
'@types/react': 16.14.0
|
||||||
'@types/react-router': 5.1.18
|
'@types/react-router': 5.1.18
|
||||||
'@types/webpack': ^5.28.0
|
'@types/webpack': ^5.28.0
|
||||||
decky-frontend-lib: ^1.2.4
|
decky-frontend-lib: ^1.5.0
|
||||||
husky: ^8.0.1
|
husky: ^8.0.1
|
||||||
import-sort-style-module: ^6.0.0
|
import-sort-style-module: ^6.0.0
|
||||||
inquirer: ^8.2.4
|
inquirer: ^8.2.4
|
||||||
@@ -23,7 +23,7 @@ specifiers:
|
|||||||
typescript: ^4.7.4
|
typescript: ^4.7.4
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
decky-frontend-lib: 1.2.4
|
decky-frontend-lib: 1.5.0
|
||||||
react-icons: 4.4.0_react@16.14.0
|
react-icons: 4.4.0_react@16.14.0
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
@@ -806,8 +806,8 @@ packages:
|
|||||||
ms: 2.1.2
|
ms: 2.1.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/decky-frontend-lib/1.2.4:
|
/decky-frontend-lib/1.5.0:
|
||||||
resolution: {integrity: sha512-r3mLEey9KUkF68geJVSjNlOz/Fg4vpMKUzoutSceyd8o/J5l+QR+Vf0b3gwK3UN9Sp4Pj4XQ1eB82+/W0ApsFg==}
|
resolution: {integrity: sha512-BT/txV7Q1NJfbnT99jt0OdUE2Qv4Gm1jhrevctx84D+pp7TfpCYau+Khnq2B9agEQQo7CWh5hOJW/SX5n2dKnQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/deepmerge/4.2.2:
|
/deepmerge/4.2.2:
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -2,23 +2,7 @@ import { DialogButton, Field, ProgressBarWithInfo, Spinner } from 'decky-fronten
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { FaArrowDown } from 'react-icons/fa';
|
import { FaArrowDown } from 'react-icons/fa';
|
||||||
|
|
||||||
import { callUpdaterMethod, finishUpdate } from '../../../../updater';
|
import { VerInfo, 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function UpdaterSettings() {
|
export default function UpdaterSettings() {
|
||||||
const [versionInfo, setVersionInfo] = useState<VerInfo | null>(null);
|
const [versionInfo, setVersionInfo] = useState<VerInfo | null>(null);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { sleep } from 'decky-frontend-lib';
|
||||||
|
|
||||||
import PluginLoader from './plugin-loader';
|
import PluginLoader from './plugin-loader';
|
||||||
import { DeckyUpdater } from './updater';
|
import { DeckyUpdater } from './updater';
|
||||||
|
|
||||||
@@ -12,6 +14,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
(async () => {
|
(async () => {
|
||||||
|
await sleep(1000);
|
||||||
window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text());
|
window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text());
|
||||||
|
|
||||||
window.DeckyPluginLoader?.dismountAll();
|
window.DeckyPluginLoader?.dismountAll();
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import Logger from './logger';
|
|||||||
import { Plugin } from './plugin';
|
import { Plugin } from './plugin';
|
||||||
import RouterHook from './router-hook';
|
import RouterHook from './router-hook';
|
||||||
import TabsHook from './tabs-hook';
|
import TabsHook from './tabs-hook';
|
||||||
|
import Toaster from './toaster';
|
||||||
|
import { VerInfo, callUpdaterMethod } from './updater';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {}
|
interface Window {}
|
||||||
@@ -22,6 +24,7 @@ class PluginLoader extends Logger {
|
|||||||
private tabsHook: TabsHook = new TabsHook();
|
private tabsHook: TabsHook = new TabsHook();
|
||||||
// private windowHook: WindowHook = new WindowHook();
|
// private windowHook: WindowHook = new WindowHook();
|
||||||
private routerHook: RouterHook = new RouterHook();
|
private routerHook: RouterHook = new RouterHook();
|
||||||
|
private toaster: Toaster = new Toaster();
|
||||||
private deckyState: DeckyState = new DeckyState();
|
private deckyState: DeckyState = new DeckyState();
|
||||||
|
|
||||||
private reloadLock: boolean = false;
|
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) {
|
public addPluginInstallPrompt(artifact: string, version: string, request_id: string, hash: string) {
|
||||||
showModal(
|
showModal(
|
||||||
<PluginInstallModal
|
<PluginInstallModal
|
||||||
@@ -189,6 +202,7 @@ class PluginLoader extends Logger {
|
|||||||
createPluginAPI(pluginName: string) {
|
createPluginAPI(pluginName: string) {
|
||||||
return {
|
return {
|
||||||
routerHook: this.routerHook,
|
routerHook: this.routerHook,
|
||||||
|
toaster: this.toaster,
|
||||||
callServerMethod: this.callServerMethod,
|
callServerMethod: this.callServerMethod,
|
||||||
async callPluginMethod(methodName: string, args = {}) {
|
async callPluginMethod(methodName: string, args = {}) {
|
||||||
const response = await fetch(`http://127.0.0.1:1337/plugins/${pluginName}/methods/${methodName}`, {
|
const response = await fetch(`http://127.0.0.1:1337/plugins/${pluginName}/methods/${methodName}`, {
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -11,6 +11,22 @@ export interface DeckyUpdater {
|
|||||||
finish: () => void;
|
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 = {}) {
|
export async function callUpdaterMethod(methodName: string, args = {}) {
|
||||||
const response = await fetch(`http://127.0.0.1:1337/updater/${methodName}`, {
|
const response = await fetch(`http://127.0.0.1:1337/updater/${methodName}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
Reference in New Issue
Block a user