Add plugin updater, notification badge, fixes

This commit is contained in:
AAGaming
2022-08-21 16:41:25 -04:00
parent 1930400032
commit 8b3f569a09
17 changed files with 300 additions and 161 deletions
+4 -4
View File
@@ -88,7 +88,7 @@ class Loader:
async def get_plugins(self, request): async def get_plugins(self, request):
plugins = list(self.plugins.values()) plugins = list(self.plugins.values())
return web.json_response([str(i) if not i.legacy else "$LEGACY_"+str(i) for i in plugins]) return web.json_response([{"name": str(i) if not i.legacy else "$LEGACY_"+str(i), "version": i.version} for i in plugins])
def handle_frontend_assets(self, request): def handle_frontend_assets(self, request):
plugin = self.plugins[request.match_info["plugin_name"]] plugin = self.plugins[request.match_info["plugin_name"]]
@@ -116,13 +116,13 @@ class Loader:
self.logger.info(f"Plugin {plugin.name} is passive") self.logger.info(f"Plugin {plugin.name} is passive")
self.plugins[plugin.name] = plugin.start() self.plugins[plugin.name] = plugin.start()
self.logger.info(f"Loaded {plugin.name}") self.logger.info(f"Loaded {plugin.name}")
self.loop.create_task(self.dispatch_plugin(plugin.name if not plugin.legacy else "$LEGACY_" + plugin.name)) self.loop.create_task(self.dispatch_plugin(plugin.name if not plugin.legacy else "$LEGACY_" + plugin.name, plugin.version))
except Exception as e: except Exception as e:
self.logger.error(f"Could not load {file}. {e}") self.logger.error(f"Could not load {file}. {e}")
print_exc() print_exc()
async def dispatch_plugin(self, name): async def dispatch_plugin(self, name, version):
await inject_to_tab("SP", f"window.importDeckyPlugin('{name}')") await inject_to_tab("SP", f"window.importDeckyPlugin('{name}', '{version}')")
def import_plugins(self): def import_plugins(self):
self.logger.info(f"import plugins from {self.plugin_path}") self.logger.info(f"import plugins from {self.plugin_path}")
+1 -1
View File
@@ -113,7 +113,7 @@ class PluginManager:
async def inject_javascript(self, request=None): async def inject_javascript(self, request=None):
try: try:
await inject_to_tab("SP", "try{" + open(path.join(path.dirname(__file__), "./static/plugin-loader.iife.js"), "r").read() + "}catch(e){console.error(e)}", True) await inject_to_tab("SP", "try{window.deckyHasLoaded = true;(async()=>{while(!window.SP_REACT){await new Promise(r => setTimeout(r, 10))};" + open(path.join(path.dirname(__file__), "./static/plugin-loader.iife.js"), "r").read() + "})();}catch(e){console.error(e)}", True)
except: except:
logger.info("Failed to inject JavaScript into tab") logger.info("Failed to inject JavaScript into tab")
pass pass
+6
View File
@@ -21,7 +21,13 @@ class PluginWrapper:
self.socket_addr = f"/tmp/plugin_socket_{time()}" self.socket_addr = f"/tmp/plugin_socket_{time()}"
self.method_call_lock = Lock() self.method_call_lock = Lock()
self.version = None
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r")) json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r"))
if path.isfile(path.join(plugin_path, plugin_directory, "package.json")):
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r"))
self.version = package_json["version"]
self.legacy = False self.legacy = False
self.main_view_html = json["main_view_html"] if "main_view_html" in json else "" self.main_view_html = json["main_view_html"] if "main_view_html" in json else ""
-4
View File
@@ -1255,10 +1255,6 @@ packages:
brace-expansion: 1.1.11 brace-expansion: 1.1.11
dev: true dev: true
/minimist/1.2.6:
resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==}
dev: false
/ms/2.1.2: /ms/2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true dev: true
+21 -1
View File
@@ -1,20 +1,30 @@
import { FC, createContext, useContext, useEffect, useState } from 'react'; import { FC, createContext, useContext, useEffect, useState } from 'react';
import { Plugin } from '../plugin'; import { Plugin } from '../plugin';
import { PluginUpdateMapping } from '../store';
interface PublicDeckyState { interface PublicDeckyState {
plugins: Plugin[]; plugins: Plugin[];
activePlugin: Plugin | null; activePlugin: Plugin | null;
updates: PluginUpdateMapping | null;
hasLoaderUpdate?: boolean;
} }
export class DeckyState { export class DeckyState {
private _plugins: Plugin[] = []; private _plugins: Plugin[] = [];
private _activePlugin: Plugin | null = null; private _activePlugin: Plugin | null = null;
private _updates: PluginUpdateMapping | null = null;
private _hasLoaderUpdate: boolean = false;
public eventBus = new EventTarget(); public eventBus = new EventTarget();
publicState(): PublicDeckyState { publicState(): PublicDeckyState {
return { plugins: this._plugins, activePlugin: this._activePlugin }; return {
plugins: this._plugins,
activePlugin: this._activePlugin,
updates: this._updates,
hasLoaderUpdate: this._hasLoaderUpdate,
};
} }
setPlugins(plugins: Plugin[]) { setPlugins(plugins: Plugin[]) {
@@ -32,6 +42,16 @@ export class DeckyState {
this.notifyUpdate(); this.notifyUpdate();
} }
setUpdates(updates: PluginUpdateMapping) {
this._updates = updates;
this.notifyUpdate();
}
setHasLoaderUpdate(hasUpdate: boolean) {
this._hasLoaderUpdate = hasUpdate;
this.notifyUpdate();
}
private notifyUpdate() { private notifyUpdate() {
this.eventBus.dispatchEvent(new Event('update')); this.eventBus.dispatchEvent(new Event('update'));
} }
@@ -0,0 +1,25 @@
import { CSSProperties, FunctionComponent } from 'react';
interface NotificationBadgeProps {
show?: boolean;
style?: CSSProperties;
}
const NotificationBadge: FunctionComponent<NotificationBadgeProps> = ({ show, style }) => {
return show ? (
<div
style={{
position: 'absolute',
top: '8px',
right: '8px',
height: '10px',
width: '10px',
background: 'orange',
borderRadius: '50%',
...style,
}}
/>
) : null;
};
export default NotificationBadge;
+3 -2
View File
@@ -9,9 +9,10 @@ import {
import { VFC } from 'react'; import { VFC } from 'react';
import { useDeckyState } from './DeckyState'; import { useDeckyState } from './DeckyState';
import NotificationBadge from './NotificationBadge';
const PluginView: VFC = () => { const PluginView: VFC = () => {
const { plugins, activePlugin, setActivePlugin } = useDeckyState(); const { plugins, updates, activePlugin, setActivePlugin } = useDeckyState();
if (activePlugin) { if (activePlugin) {
return ( return (
@@ -23,7 +24,6 @@ const PluginView: VFC = () => {
</div> </div>
); );
} }
return ( return (
<div className={joinClassNames(staticClasses.TabGroupPanel, scrollClasses.ScrollPanel, scrollClasses.ScrollY)}> <div className={joinClassNames(staticClasses.TabGroupPanel, scrollClasses.ScrollPanel, scrollClasses.ScrollY)}>
<PanelSection> <PanelSection>
@@ -35,6 +35,7 @@ const PluginView: VFC = () => {
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>{icon}</div> <div>{icon}</div>
<div>{name}</div> <div>{name}</div>
<NotificationBadge show={updates?.has(name)} style={{ top: '-5px', right: '-5px' }} />
</div> </div>
</ButtonItem> </ButtonItem>
</PanelSectionRow> </PanelSectionRow>
@@ -20,9 +20,7 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, ha
onOK={async () => { onOK={async () => {
setLoading(true); setLoading(true);
await onOK(); await onOK();
Router.NavigateBackOrOpenMenu(); setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
await sleep(250);
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000);
}} }}
onCancel={async () => { onCancel={async () => {
await onCancel(); await onCancel();
@@ -1,4 +1,4 @@
import { Field, ToggleField } from 'decky-frontend-lib'; import { Field, Toggle } from 'decky-frontend-lib';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { FaBug } from 'react-icons/fa'; import { FaBug } from 'react-icons/fa';
@@ -21,8 +21,8 @@ export default function RemoteDebuggingSettings() {
} }
icon={<FaBug style={{ display: 'block' }} />} icon={<FaBug style={{ display: 'block' }} />}
> >
<ToggleField <Toggle
checked={allowRemoteDebugging} value={allowRemoteDebugging}
onChange={(toggleValue) => { onChange={(toggleValue) => {
setAllowRemoteDebugging(toggleValue); setAllowRemoteDebugging(toggleValue);
if (toggleValue) window.DeckyPluginLoader.callServerMethod('allow_remote_debugging'); if (toggleValue) window.DeckyPluginLoader.callServerMethod('allow_remote_debugging');
@@ -2,7 +2,7 @@ import { DialogButton, Field, TextField } from 'decky-frontend-lib';
import { useState } from 'react'; import { useState } from 'react';
import { FaShapes } from 'react-icons/fa'; import { FaShapes } from 'react-icons/fa';
import { installFromURL } from '../../../store/Store'; import { installFromURL } from '../../../../store';
import RemoteDebuggingSettings from './RemoteDebugging'; import RemoteDebuggingSettings from './RemoteDebugging';
import UpdaterSettings from './Updater'; import UpdaterSettings from './Updater';
@@ -1,10 +1,16 @@
import { DialogButton, Menu, MenuItem, showContextMenu, staticClasses } from 'decky-frontend-lib'; import { DialogButton, Focusable, Menu, MenuItem, showContextMenu } from 'decky-frontend-lib';
import { FaEllipsisH } from 'react-icons/fa'; import { useEffect } from 'react';
import { FaDownload, FaEllipsisH } from 'react-icons/fa';
import { requestPluginInstall } from '../../../../store';
import { useDeckyState } from '../../../DeckyState'; import { useDeckyState } from '../../../DeckyState';
export default function PluginList() { export default function PluginList() {
const { plugins } = useDeckyState(); const { plugins, updates } = useDeckyState();
useEffect(() => {
window.DeckyPluginLoader.checkPluginUpdates();
}, []);
if (plugins.length === 0) { if (plugins.length === 0) {
return ( return (
@@ -16,27 +22,45 @@ export default function PluginList() {
return ( return (
<ul style={{ listStyleType: 'none' }}> <ul style={{ listStyleType: 'none' }}>
{plugins.map(({ name }) => ( {plugins.map(({ name, version }) => {
<li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}> const update = updates?.get(name);
<span>{name}</span> return (
<div className={staticClasses.Title} style={{ marginLeft: 'auto', boxShadow: 'none' }}> <li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', paddingBottom: '10px' }}>
<DialogButton <span>
style={{ height: '40px', width: '40px', padding: '10px 12px' }} {name} {version}
onClick={(e: MouseEvent) => </span>
showContextMenu( <Focusable style={{ marginLeft: 'auto', boxShadow: 'none', display: 'flex', justifyContent: 'right' }}>
<Menu label="Plugin Actions"> {update && (
<MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(name)}>Reload</MenuItem> <DialogButton
<MenuItem onSelected={() => window.DeckyPluginLoader.uninstallPlugin(name)}>Uninstall</MenuItem> style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
</Menu>, onClick={() => requestPluginInstall(name, update)}
e.currentTarget ?? window, >
) <div style={{ display: 'flex', flexDirection: 'row' }}>
} Update to {update.name}
> <FaDownload style={{ paddingLeft: '2rem' }} />
<FaEllipsisH /> </div>
</DialogButton> </DialogButton>
</div> )}
</li> <DialogButton
))} style={{ height: '40px', width: '40px', padding: '10px 12px', minWidth: '40px' }}
onClick={(e: MouseEvent) =>
showContextMenu(
<Menu label="Plugin Actions">
<MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(name, version)}>
Reload
</MenuItem>
<MenuItem onSelected={() => window.DeckyPluginLoader.uninstallPlugin(name)}>Uninstall</MenuItem>
</Menu>,
e.currentTarget ?? window,
)
}
>
<FaEllipsisH />
</DialogButton>
</Focusable>
</li>
);
})}
</ul> </ul>
); );
} }
+9 -9
View File
@@ -15,18 +15,15 @@ import {
LegacyStorePlugin, LegacyStorePlugin,
StorePlugin, StorePlugin,
StorePluginVersion, StorePluginVersion,
isLegacyPlugin,
requestLegacyPluginInstall, requestLegacyPluginInstall,
requestPluginInstall, requestPluginInstall,
} from './Store'; } from '../../store';
interface PluginCardProps { interface PluginCardProps {
plugin: StorePlugin | LegacyStorePlugin; plugin: StorePlugin | LegacyStorePlugin;
} }
function isLegacyPlugin(plugin: LegacyStorePlugin | StorePlugin): plugin is LegacyStorePlugin {
return 'artifact' in plugin;
}
const PluginCard: FC<PluginCardProps> = ({ plugin }) => { const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
const [selectedOption, setSelectedOption] = useState<number>(0); const [selectedOption, setSelectedOption] = useState<number>(0);
const buttonRef = useRef<HTMLDivElement>(null); const buttonRef = useRef<HTMLDivElement>(null);
@@ -119,13 +116,16 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
<p className={joinClassNames(staticClasses.PanelSectionRow)}> <p className={joinClassNames(staticClasses.PanelSectionRow)}>
<span>Author: {plugin.author}</span> <span>Author: {plugin.author}</span>
</p> </p>
<p className={joinClassNames('deckyStoreCardTagsContainer', staticClasses.PanelSectionRow)} style={{ <p
className={joinClassNames('deckyStoreCardTagsContainer', staticClasses.PanelSectionRow)}
style={{
padding: '0 16px', padding: '0 16px',
display: 'flex', display: 'flex',
flexWrap: 'wrap', flexWrap: 'wrap',
gap: '5px 10px', gap: '5px 10px',
}}> }}
<span style={{padding: '5px 0'}}>Tags:</span> >
<span style={{ padding: '5px 0' }}>Tags:</span>
{plugin.tags.map((tag: string) => ( {plugin.tags.map((tag: string) => (
<span <span
className="deckyStoreCardTag" className="deckyStoreCardTag"
@@ -183,7 +183,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
onClick={() => onClick={() =>
isLegacyPlugin(plugin) isLegacyPlugin(plugin)
? requestLegacyPluginInstall(plugin, Object.keys(plugin.versions)[selectedOption]) ? requestLegacyPluginInstall(plugin, Object.keys(plugin.versions)[selectedOption])
: requestPluginInstall(plugin, plugin.versions[selectedOption]) : requestPluginInstall(plugin.name, plugin.versions[selectedOption])
} }
> >
Install Install
+5 -95
View File
@@ -1,111 +1,21 @@
import { ModalRoot, SteamSpinner, showModal, staticClasses } from 'decky-frontend-lib'; import { SteamSpinner } from 'decky-frontend-lib';
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { LegacyStorePlugin, StorePlugin, getLegacyPluginList, getPluginList } from '../../store';
import PluginCard from './PluginCard'; import PluginCard from './PluginCard';
export interface StorePluginVersion {
name: string;
hash: string;
}
export interface StorePlugin {
id: number;
name: string;
versions: StorePluginVersion[];
author: string;
description: string;
tags: string[];
}
export interface LegacyStorePlugin {
artifact: string;
versions: {
[version: string]: string;
};
author: string;
description: string;
tags: string[];
}
export async function installFromURL(url: string) {
const formData = new FormData();
const splitURL = url.split('/');
formData.append('name', splitURL[splitURL.length - 1].replace('.zip', ''));
formData.append('artifact', url);
await fetch('http://localhost:1337/browser/install_plugin', {
method: 'POST',
body: formData,
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
});
}
export function requestLegacyPluginInstall(plugin: LegacyStorePlugin, selectedVer: string) {
showModal(
<ModalRoot
onOK={() => {
const formData = new FormData();
formData.append('name', plugin.artifact);
formData.append('artifact', `https://github.com/${plugin.artifact}/archive/refs/tags/${selectedVer}.zip`);
formData.append('version', selectedVer);
formData.append('hash', plugin.versions[selectedVer]);
fetch('http://localhost:1337/browser/install_plugin', {
method: 'POST',
body: formData,
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
});
}}
onCancel={() => {
// do nothing
}}
>
<div className={staticClasses.Title} style={{ flexDirection: 'column', boxShadow: 'unset' }}>
Using legacy plugins
</div>
You are currently installing a <b>legacy</b> plugin. Legacy plugins are no longer supported and may have issues.
Legacy plugins do not support gamepad input. To interact with a legacy plugin, you will need to use the
touchscreen.
</ModalRoot>,
);
}
export async function requestPluginInstall(plugin: StorePlugin, selectedVer: StorePluginVersion) {
const formData = new FormData();
formData.append('name', plugin.name);
formData.append('artifact', `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`);
formData.append('version', selectedVer.name);
formData.append('hash', selectedVer.hash);
await fetch('http://localhost:1337/browser/install_plugin', {
method: 'POST',
body: formData,
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
});
}
const StorePage: FC<{}> = () => { const StorePage: FC<{}> = () => {
const [data, setData] = useState<StorePlugin[] | null>(null); const [data, setData] = useState<StorePlugin[] | null>(null);
const [legacyData, setLegacyData] = useState<LegacyStorePlugin[] | null>(null); const [legacyData, setLegacyData] = useState<LegacyStorePlugin[] | null>(null);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
const res = await fetch('https://beta.deckbrew.xyz/plugins', { const res = await getPluginList();
method: 'GET',
}).then((r) => r.json());
console.log(res); console.log(res);
setData(res.filter((x: StorePlugin) => x.name !== 'Example Plugin')); setData(res);
})(); })();
(async () => { (async () => {
const res = await fetch('https://plugins.deckbrew.xyz/get_plugins', { const res = await getLegacyPluginList();
method: 'GET',
}).then((r) => r.json());
console.log(res); console.log(res);
setLegacyData(res); setLegacyData(res);
})(); })();
+5 -4
View File
@@ -38,15 +38,14 @@ if (!window.webpackJsonp || window.webpackJsonp.deckyShimmed) {
} }
(async () => { (async () => {
window.deckyHasLoaded = true;
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();
window.DeckyPluginLoader?.deinit(); window.DeckyPluginLoader?.deinit();
window.DeckyPluginLoader = new PluginLoader(); window.DeckyPluginLoader = new PluginLoader();
window.importDeckyPlugin = function (name: string) { window.importDeckyPlugin = function (name: string, version: string) {
window.DeckyPluginLoader?.importPlugin(name); window.DeckyPluginLoader?.importPlugin(name, version);
}; };
window.syncDeckyPlugins = async function () { window.syncDeckyPlugins = async function () {
@@ -57,8 +56,10 @@ if (!window.webpackJsonp || window.webpackJsonp.deckyShimmed) {
}) })
).json(); ).json();
for (const plugin of plugins) { for (const plugin of plugins) {
if (!window.DeckyPluginLoader.hasPlugin(plugin)) window.DeckyPluginLoader?.importPlugin(plugin); if (!window.DeckyPluginLoader.hasPlugin(plugin.name))
window.DeckyPluginLoader?.importPlugin(plugin.name, plugin.version);
} }
window.DeckyPluginLoader.checkPluginUpdates();
}; };
setTimeout(() => window.syncDeckyPlugins(), 5000); setTimeout(() => window.syncDeckyPlugins(), 5000);
+46 -10
View File
@@ -1,9 +1,10 @@
import { ModalRoot, QuickAccessTab, showModal, staticClasses } from 'decky-frontend-lib'; import { ModalRoot, QuickAccessTab, Router, showModal, sleep, staticClasses } from 'decky-frontend-lib';
import { FaPlug } from 'react-icons/fa'; import { FaPlug } from 'react-icons/fa';
import { DeckyState, DeckyStateContextProvider } from './components/DeckyState'; import { DeckyState, DeckyStateContextProvider, useDeckyState } from './components/DeckyState';
import LegacyPlugin from './components/LegacyPlugin'; import LegacyPlugin from './components/LegacyPlugin';
import PluginInstallModal from './components/modals/PluginInstallModal'; import PluginInstallModal from './components/modals/PluginInstallModal';
import NotificationBadge from './components/NotificationBadge';
import PluginView from './components/PluginView'; import PluginView from './components/PluginView';
import SettingsPage from './components/settings'; import SettingsPage from './components/settings';
import StorePage from './components/store/Store'; import StorePage from './components/store/Store';
@@ -11,6 +12,7 @@ import TitleView from './components/TitleView';
import Logger from './logger'; import Logger from './logger';
import { Plugin } from './plugin'; import { Plugin } from './plugin';
import RouterHook from './router-hook'; import RouterHook from './router-hook';
import { checkForUpdates } from './store';
import TabsHook from './tabs-hook'; import TabsHook from './tabs-hook';
import Toaster from './toaster'; import Toaster from './toaster';
import { VerInfo, callUpdaterMethod } from './updater'; import { VerInfo, callUpdaterMethod } from './updater';
@@ -29,12 +31,17 @@ class PluginLoader extends Logger {
private reloadLock: boolean = false; private reloadLock: boolean = false;
// stores a list of plugin names which requested to be reloaded // stores a list of plugin names which requested to be reloaded
private pluginReloadQueue: string[] = []; private pluginReloadQueue: { name: string; version?: string }[] = [];
constructor() { constructor() {
super(PluginLoader.name); super(PluginLoader.name);
this.log('Initialized'); this.log('Initialized');
const TabIcon = () => {
const { updates, hasLoaderUpdate } = useDeckyState();
return <NotificationBadge show={(updates && updates.size > 0) || hasLoaderUpdate} />;
};
this.tabsHook.add({ this.tabsHook.add({
id: QuickAccessTab.Decky, id: QuickAccessTab.Decky,
title: null, title: null,
@@ -44,7 +51,14 @@ class PluginLoader extends Logger {
<PluginView /> <PluginView />
</DeckyStateContextProvider> </DeckyStateContextProvider>
), ),
icon: <FaPlug />, icon: (
<DeckyStateContextProvider deckyState={this.deckyState}>
<>
<FaPlug />
<TabIcon />
</>
</DeckyStateContextProvider>
),
}); });
this.routerHook.addRoute('/decky/store', () => <StorePage />); this.routerHook.addRoute('/decky/store', () => <StorePage />);
@@ -62,7 +76,28 @@ class PluginLoader extends Logger {
if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) { if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) {
this.toaster.toast({ this.toaster.toast({
title: 'Decky', title: 'Decky',
body: `Update to ${versionInfo?.remote?.tag_name} availiable!`, body: `Update to ${versionInfo?.remote?.tag_name} available!`,
onClick: () => Router.Navigate('/decky/settings'),
});
this.deckyState.setHasLoaderUpdate(true);
}
await sleep(7000);
await this.notifyPluginUpdates();
}
public async checkPluginUpdates() {
const updates = await checkForUpdates(this.plugins);
this.deckyState.setUpdates(updates);
return updates;
}
public async notifyPluginUpdates() {
const updates = await this.checkPluginUpdates();
if (updates?.size > 0) {
this.toaster.toast({
title: 'Decky',
body: `Updates available for ${updates.size} plugin${updates.size > 1 ? 's' : ''}!`,
onClick: () => Router.Navigate('/decky/settings/plugins'),
}); });
} }
} }
@@ -128,10 +163,10 @@ class PluginLoader extends Logger {
this.deckyState.setPlugins(this.plugins); this.deckyState.setPlugins(this.plugins);
} }
public async importPlugin(name: string) { public async importPlugin(name: string, version?: string | undefined) {
if (this.reloadLock) { if (this.reloadLock) {
this.log('Reload currently in progress, adding to queue', name); this.log('Reload currently in progress, adding to queue', name);
this.pluginReloadQueue.push(name); this.pluginReloadQueue.push({ name, version: version });
return; return;
} }
@@ -144,7 +179,7 @@ class PluginLoader extends Logger {
if (name.startsWith('$LEGACY_')) { if (name.startsWith('$LEGACY_')) {
await this.importLegacyPlugin(name.replace('$LEGACY_', '')); await this.importLegacyPlugin(name.replace('$LEGACY_', ''));
} else { } else {
await this.importReactPlugin(name); await this.importReactPlugin(name, version);
} }
this.deckyState.setPlugins(this.plugins); this.deckyState.setPlugins(this.plugins);
@@ -155,12 +190,12 @@ class PluginLoader extends Logger {
this.reloadLock = false; this.reloadLock = false;
const nextPlugin = this.pluginReloadQueue.shift(); const nextPlugin = this.pluginReloadQueue.shift();
if (nextPlugin) { if (nextPlugin) {
this.importPlugin(nextPlugin); this.importPlugin(nextPlugin.name, nextPlugin.version);
} }
} }
} }
private async importReactPlugin(name: string) { private async importReactPlugin(name: string, version?: string) {
let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, { let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, {
credentials: 'include', credentials: 'include',
headers: { headers: {
@@ -172,6 +207,7 @@ class PluginLoader extends Logger {
this.plugins.push({ this.plugins.push({
...plugin, ...plugin,
name: name, name: name,
version: version,
}); });
} else throw new Error(`${name} frontend_bundle not OK`); } else throw new Error(`${name} frontend_bundle not OK`);
} }
+1
View File
@@ -1,5 +1,6 @@
export interface Plugin { export interface Plugin {
name: string; name: string;
version?: string;
icon: JSX.Element; icon: JSX.Element;
content?: JSX.Element; content?: JSX.Element;
onDismount?(): void; onDismount?(): void;
+121
View File
@@ -0,0 +1,121 @@
import { ModalRoot, showModal, staticClasses } from 'decky-frontend-lib';
import { Plugin } from './plugin';
export interface StorePluginVersion {
name: string;
hash: string;
}
export interface StorePlugin {
id: number;
name: string;
versions: StorePluginVersion[];
author: string;
description: string;
tags: string[];
}
export interface LegacyStorePlugin {
artifact: string;
versions: {
[version: string]: string;
};
author: string;
description: string;
tags: string[];
}
// name: version
export type PluginUpdateMapping = Map<string, StorePluginVersion>;
export function getPluginList(): Promise<StorePlugin[]> {
return fetch('https://beta.deckbrew.xyz/plugins', {
method: 'GET',
}).then((r) => r.json());
}
export function getLegacyPluginList(): Promise<LegacyStorePlugin[]> {
return fetch('https://plugins.deckbrew.xyz/get_plugins', {
method: 'GET',
}).then((r) => r.json());
}
export async function installFromURL(url: string) {
const formData = new FormData();
const splitURL = url.split('/');
formData.append('name', splitURL[splitURL.length - 1].replace('.zip', ''));
formData.append('artifact', url);
await fetch('http://localhost:1337/browser/install_plugin', {
method: 'POST',
body: formData,
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
});
}
export function requestLegacyPluginInstall(plugin: LegacyStorePlugin, selectedVer: string) {
showModal(
<ModalRoot
onOK={() => {
const formData = new FormData();
formData.append('name', plugin.artifact);
formData.append('artifact', `https://github.com/${plugin.artifact}/archive/refs/tags/${selectedVer}.zip`);
formData.append('version', selectedVer);
formData.append('hash', plugin.versions[selectedVer]);
fetch('http://localhost:1337/browser/install_plugin', {
method: 'POST',
body: formData,
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
});
}}
onCancel={() => {
// do nothing
}}
>
<div className={staticClasses.Title} style={{ flexDirection: 'column', boxShadow: 'unset' }}>
Using legacy plugins
</div>
You are currently installing a <b>legacy</b> plugin. Legacy plugins are no longer supported and may have issues.
Legacy plugins do not support gamepad input. To interact with a legacy plugin, you will need to use the
touchscreen.
</ModalRoot>,
);
}
export async function requestPluginInstall(plugin: string, selectedVer: StorePluginVersion) {
const formData = new FormData();
formData.append('name', plugin);
formData.append('artifact', `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`);
formData.append('version', selectedVer.name);
formData.append('hash', selectedVer.hash);
await fetch('http://localhost:1337/browser/install_plugin', {
method: 'POST',
body: formData,
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
});
}
export async function checkForUpdates(plugins: Plugin[]): Promise<PluginUpdateMapping> {
const serverData = await getPluginList();
const updateMap = new Map<string, StorePluginVersion>();
for (let plugin of plugins) {
const remotePlugin = serverData?.find((x) => x.name == plugin.name);
if (remotePlugin && remotePlugin.versions?.length > 0 && plugin.version != remotePlugin?.versions?.[0]?.name) {
updateMap.set(plugin.name, remotePlugin.versions[0]);
}
}
return updateMap;
}
export function isLegacyPlugin(plugin: LegacyStorePlugin | StorePlugin): plugin is LegacyStorePlugin {
return 'artifact' in plugin;
}