summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAAGaming <aa@mail.catvibers.me>2022-08-21 16:41:25 -0400
committerAAGaming <aa@mail.catvibers.me>2022-08-21 16:41:25 -0400
commit8b3f569a09db9daf7748426f916a66591159928f (patch)
tree237cc3711c7098b30a7e7cda97db9e406b0f7db0
parent1930400032a850b833f5f71523008e326f40547a (diff)
downloaddecky-loader-8b3f569a09db9daf7748426f916a66591159928f.tar.gz
decky-loader-8b3f569a09db9daf7748426f916a66591159928f.zip
Add plugin updater, notification badge, fixesv2.0.5-pre15
-rw-r--r--backend/loader.py8
-rw-r--r--backend/main.py2
-rw-r--r--backend/plugin.py6
-rw-r--r--frontend/pnpm-lock.yaml4
-rw-r--r--frontend/src/components/DeckyState.tsx22
-rw-r--r--frontend/src/components/NotificationBadge.tsx25
-rw-r--r--frontend/src/components/PluginView.tsx5
-rw-r--r--frontend/src/components/modals/PluginInstallModal.tsx4
-rw-r--r--frontend/src/components/settings/pages/general/RemoteDebugging.tsx6
-rw-r--r--frontend/src/components/settings/pages/general/index.tsx2
-rw-r--r--frontend/src/components/settings/pages/plugin_list/index.tsx72
-rw-r--r--frontend/src/components/store/PluginCard.tsx18
-rw-r--r--frontend/src/components/store/Store.tsx100
-rw-r--r--frontend/src/index.tsx9
-rw-r--r--frontend/src/plugin-loader.tsx56
-rw-r--r--frontend/src/plugin.ts1
-rw-r--r--frontend/src/store.tsx121
17 files changed, 300 insertions, 161 deletions
diff --git a/backend/loader.py b/backend/loader.py
index 3f5d1ab8..4fe104d1 100644
--- a/backend/loader.py
+++ b/backend/loader.py
@@ -88,7 +88,7 @@ class Loader:
async def get_plugins(self, request):
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):
plugin = self.plugins[request.match_info["plugin_name"]]
@@ -116,13 +116,13 @@ class Loader:
self.logger.info(f"Plugin {plugin.name} is passive")
self.plugins[plugin.name] = plugin.start()
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:
self.logger.error(f"Could not load {file}. {e}")
print_exc()
- async def dispatch_plugin(self, name):
- await inject_to_tab("SP", f"window.importDeckyPlugin('{name}')")
+ async def dispatch_plugin(self, name, version):
+ await inject_to_tab("SP", f"window.importDeckyPlugin('{name}', '{version}')")
def import_plugins(self):
self.logger.info(f"import plugins from {self.plugin_path}")
diff --git a/backend/main.py b/backend/main.py
index 07e82d92..a5730fbd 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -113,7 +113,7 @@ class PluginManager:
async def inject_javascript(self, request=None):
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:
logger.info("Failed to inject JavaScript into tab")
pass
diff --git a/backend/plugin.py b/backend/plugin.py
index 0a8a2901..f55e398c 100644
--- a/backend/plugin.py
+++ b/backend/plugin.py
@@ -21,7 +21,13 @@ class PluginWrapper:
self.socket_addr = f"/tmp/plugin_socket_{time()}"
self.method_call_lock = Lock()
+ self.version = None
+
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.main_view_html = json["main_view_html"] if "main_view_html" in json else ""
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index afbe1010..1a449431 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -1255,10 +1255,6 @@ packages:
brace-expansion: 1.1.11
dev: true
- /minimist/1.2.6:
- resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==}
- dev: false
-
/ms/2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true
diff --git a/frontend/src/components/DeckyState.tsx b/frontend/src/components/DeckyState.tsx
index cbeeb5b4..6f13a007 100644
--- a/frontend/src/components/DeckyState.tsx
+++ b/frontend/src/components/DeckyState.tsx
@@ -1,20 +1,30 @@
import { FC, createContext, useContext, useEffect, useState } from 'react';
import { Plugin } from '../plugin';
+import { PluginUpdateMapping } from '../store';
interface PublicDeckyState {
plugins: Plugin[];
activePlugin: Plugin | null;
+ updates: PluginUpdateMapping | null;
+ hasLoaderUpdate?: boolean;
}
export class DeckyState {
private _plugins: Plugin[] = [];
private _activePlugin: Plugin | null = null;
+ private _updates: PluginUpdateMapping | null = null;
+ private _hasLoaderUpdate: boolean = false;
public eventBus = new EventTarget();
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[]) {
@@ -32,6 +42,16 @@ export class DeckyState {
this.notifyUpdate();
}
+ setUpdates(updates: PluginUpdateMapping) {
+ this._updates = updates;
+ this.notifyUpdate();
+ }
+
+ setHasLoaderUpdate(hasUpdate: boolean) {
+ this._hasLoaderUpdate = hasUpdate;
+ this.notifyUpdate();
+ }
+
private notifyUpdate() {
this.eventBus.dispatchEvent(new Event('update'));
}
diff --git a/frontend/src/components/NotificationBadge.tsx b/frontend/src/components/NotificationBadge.tsx
new file mode 100644
index 00000000..3c5d8215
--- /dev/null
+++ b/frontend/src/components/NotificationBadge.tsx
@@ -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;
diff --git a/frontend/src/components/PluginView.tsx b/frontend/src/components/PluginView.tsx
index 28a1540e..67a203c9 100644
--- a/frontend/src/components/PluginView.tsx
+++ b/frontend/src/components/PluginView.tsx
@@ -9,9 +9,10 @@ import {
import { VFC } from 'react';
import { useDeckyState } from './DeckyState';
+import NotificationBadge from './NotificationBadge';
const PluginView: VFC = () => {
- const { plugins, activePlugin, setActivePlugin } = useDeckyState();
+ const { plugins, updates, activePlugin, setActivePlugin } = useDeckyState();
if (activePlugin) {
return (
@@ -23,7 +24,6 @@ const PluginView: VFC = () => {
</div>
);
}
-
return (
<div className={joinClassNames(staticClasses.TabGroupPanel, scrollClasses.ScrollPanel, scrollClasses.ScrollY)}>
<PanelSection>
@@ -35,6 +35,7 @@ const PluginView: VFC = () => {
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>{icon}</div>
<div>{name}</div>
+ <NotificationBadge show={updates?.has(name)} style={{ top: '-5px', right: '-5px' }} />
</div>
</ButtonItem>
</PanelSectionRow>
diff --git a/frontend/src/components/modals/PluginInstallModal.tsx b/frontend/src/components/modals/PluginInstallModal.tsx
index d89fb5d3..b479243c 100644
--- a/frontend/src/components/modals/PluginInstallModal.tsx
+++ b/frontend/src/components/modals/PluginInstallModal.tsx
@@ -20,9 +20,7 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, ha
onOK={async () => {
setLoading(true);
await onOK();
- Router.NavigateBackOrOpenMenu();
- await sleep(250);
- setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000);
+ setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
}}
onCancel={async () => {
await onCancel();
diff --git a/frontend/src/components/settings/pages/general/RemoteDebugging.tsx b/frontend/src/components/settings/pages/general/RemoteDebugging.tsx
index 1310263f..3fea0513 100644
--- a/frontend/src/components/settings/pages/general/RemoteDebugging.tsx
+++ b/frontend/src/components/settings/pages/general/RemoteDebugging.tsx
@@ -1,4 +1,4 @@
-import { Field, ToggleField } from 'decky-frontend-lib';
+import { Field, Toggle } from 'decky-frontend-lib';
import { useEffect, useState } from 'react';
import { FaBug } from 'react-icons/fa';
@@ -21,8 +21,8 @@ export default function RemoteDebuggingSettings() {
}
icon={<FaBug style={{ display: 'block' }} />}
>
- <ToggleField
- checked={allowRemoteDebugging}
+ <Toggle
+ value={allowRemoteDebugging}
onChange={(toggleValue) => {
setAllowRemoteDebugging(toggleValue);
if (toggleValue) window.DeckyPluginLoader.callServerMethod('allow_remote_debugging');
diff --git a/frontend/src/components/settings/pages/general/index.tsx b/frontend/src/components/settings/pages/general/index.tsx
index 16add6bc..c6680026 100644
--- a/frontend/src/components/settings/pages/general/index.tsx
+++ b/frontend/src/components/settings/pages/general/index.tsx
@@ -2,7 +2,7 @@ import { DialogButton, Field, TextField } from 'decky-frontend-lib';
import { useState } from 'react';
import { FaShapes } from 'react-icons/fa';
-import { installFromURL } from '../../../store/Store';
+import { installFromURL } from '../../../../store';
import RemoteDebuggingSettings from './RemoteDebugging';
import UpdaterSettings from './Updater';
diff --git a/frontend/src/components/settings/pages/plugin_list/index.tsx b/frontend/src/components/settings/pages/plugin_list/index.tsx
index 3888a52d..4eb89615 100644
--- a/frontend/src/components/settings/pages/plugin_list/index.tsx
+++ b/frontend/src/components/settings/pages/plugin_list/index.tsx
@@ -1,10 +1,16 @@
-import { DialogButton, Menu, MenuItem, showContextMenu, staticClasses } from 'decky-frontend-lib';
-import { FaEllipsisH } from 'react-icons/fa';
+import { DialogButton, Focusable, Menu, MenuItem, showContextMenu } from 'decky-frontend-lib';
+import { useEffect } from 'react';
+import { FaDownload, FaEllipsisH } from 'react-icons/fa';
+import { requestPluginInstall } from '../../../../store';
import { useDeckyState } from '../../../DeckyState';
export default function PluginList() {
- const { plugins } = useDeckyState();
+ const { plugins, updates } = useDeckyState();
+
+ useEffect(() => {
+ window.DeckyPluginLoader.checkPluginUpdates();
+ }, []);
if (plugins.length === 0) {
return (
@@ -16,27 +22,45 @@ export default function PluginList() {
return (
<ul style={{ listStyleType: 'none' }}>
- {plugins.map(({ name }) => (
- <li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
- <span>{name}</span>
- <div className={staticClasses.Title} style={{ marginLeft: 'auto', boxShadow: 'none' }}>
- <DialogButton
- style={{ height: '40px', width: '40px', padding: '10px 12px' }}
- onClick={(e: MouseEvent) =>
- showContextMenu(
- <Menu label="Plugin Actions">
- <MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(name)}>Reload</MenuItem>
- <MenuItem onSelected={() => window.DeckyPluginLoader.uninstallPlugin(name)}>Uninstall</MenuItem>
- </Menu>,
- e.currentTarget ?? window,
- )
- }
- >
- <FaEllipsisH />
- </DialogButton>
- </div>
- </li>
- ))}
+ {plugins.map(({ name, version }) => {
+ const update = updates?.get(name);
+ return (
+ <li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', paddingBottom: '10px' }}>
+ <span>
+ {name} {version}
+ </span>
+ <Focusable style={{ marginLeft: 'auto', boxShadow: 'none', display: 'flex', justifyContent: 'right' }}>
+ {update && (
+ <DialogButton
+ style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
+ onClick={() => requestPluginInstall(name, update)}
+ >
+ <div style={{ display: 'flex', flexDirection: 'row' }}>
+ Update to {update.name}
+ <FaDownload style={{ paddingLeft: '2rem' }} />
+ </div>
+ </DialogButton>
+ )}
+ <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>
);
}
diff --git a/frontend/src/components/store/PluginCard.tsx b/frontend/src/components/store/PluginCard.tsx
index 5a0c34ec..a6e9458a 100644
--- a/frontend/src/components/store/PluginCard.tsx
+++ b/frontend/src/components/store/PluginCard.tsx
@@ -15,18 +15,15 @@ import {
LegacyStorePlugin,
StorePlugin,
StorePluginVersion,
+ isLegacyPlugin,
requestLegacyPluginInstall,
requestPluginInstall,
-} from './Store';
+} from '../../store';
interface PluginCardProps {
plugin: StorePlugin | LegacyStorePlugin;
}
-function isLegacyPlugin(plugin: LegacyStorePlugin | StorePlugin): plugin is LegacyStorePlugin {
- return 'artifact' in plugin;
-}
-
const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
const [selectedOption, setSelectedOption] = useState<number>(0);
const buttonRef = useRef<HTMLDivElement>(null);
@@ -119,13 +116,16 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
<p className={joinClassNames(staticClasses.PanelSectionRow)}>
<span>Author: {plugin.author}</span>
</p>
- <p className={joinClassNames('deckyStoreCardTagsContainer', staticClasses.PanelSectionRow)} style={{
+ <p
+ className={joinClassNames('deckyStoreCardTagsContainer', staticClasses.PanelSectionRow)}
+ style={{
padding: '0 16px',
display: 'flex',
flexWrap: 'wrap',
gap: '5px 10px',
- }}>
- <span style={{padding: '5px 0'}}>Tags:</span>
+ }}
+ >
+ <span style={{ padding: '5px 0' }}>Tags:</span>
{plugin.tags.map((tag: string) => (
<span
className="deckyStoreCardTag"
@@ -183,7 +183,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
onClick={() =>
isLegacyPlugin(plugin)
? requestLegacyPluginInstall(plugin, Object.keys(plugin.versions)[selectedOption])
- : requestPluginInstall(plugin, plugin.versions[selectedOption])
+ : requestPluginInstall(plugin.name, plugin.versions[selectedOption])
}
>
Install
diff --git a/frontend/src/components/store/Store.tsx b/frontend/src/components/store/Store.tsx
index 16e6994f..fd582edd 100644
--- a/frontend/src/components/store/Store.tsx
+++ b/frontend/src/components/store/Store.tsx
@@ -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 { LegacyStorePlugin, StorePlugin, getLegacyPluginList, getPluginList } from '../../store';
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 [data, setData] = useState<StorePlugin[] | null>(null);
const [legacyData, setLegacyData] = useState<LegacyStorePlugin[] | null>(null);
useEffect(() => {
(async () => {
- const res = await fetch('https://beta.deckbrew.xyz/plugins', {
- method: 'GET',
- }).then((r) => r.json());
+ const res = await getPluginList();
console.log(res);
- setData(res.filter((x: StorePlugin) => x.name !== 'Example Plugin'));
+ setData(res);
})();
(async () => {
- const res = await fetch('https://plugins.deckbrew.xyz/get_plugins', {
- method: 'GET',
- }).then((r) => r.json());
+ const res = await getLegacyPluginList();
console.log(res);
setLegacyData(res);
})();
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index 11aabc9f..08b37d15 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -38,15 +38,14 @@ if (!window.webpackJsonp || window.webpackJsonp.deckyShimmed) {
}
(async () => {
- window.deckyHasLoaded = true;
window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text());
window.DeckyPluginLoader?.dismountAll();
window.DeckyPluginLoader?.deinit();
window.DeckyPluginLoader = new PluginLoader();
- window.importDeckyPlugin = function (name: string) {
- window.DeckyPluginLoader?.importPlugin(name);
+ window.importDeckyPlugin = function (name: string, version: string) {
+ window.DeckyPluginLoader?.importPlugin(name, version);
};
window.syncDeckyPlugins = async function () {
@@ -57,8 +56,10 @@ if (!window.webpackJsonp || window.webpackJsonp.deckyShimmed) {
})
).json();
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);
diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx
index df3b220a..c0fc3d00 100644
--- a/frontend/src/plugin-loader.tsx
+++ b/frontend/src/plugin-loader.tsx
@@ -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 { DeckyState, DeckyStateContextProvider } from './components/DeckyState';
+import { DeckyState, DeckyStateContextProvider, useDeckyState } from './components/DeckyState';
import LegacyPlugin from './components/LegacyPlugin';
import PluginInstallModal from './components/modals/PluginInstallModal';
+import NotificationBadge from './components/NotificationBadge';
import PluginView from './components/PluginView';
import SettingsPage from './components/settings';
import StorePage from './components/store/Store';
@@ -11,6 +12,7 @@ import TitleView from './components/TitleView';
import Logger from './logger';
import { Plugin } from './plugin';
import RouterHook from './router-hook';
+import { checkForUpdates } from './store';
import TabsHook from './tabs-hook';
import Toaster from './toaster';
import { VerInfo, callUpdaterMethod } from './updater';
@@ -29,12 +31,17 @@ class PluginLoader extends Logger {
private reloadLock: boolean = false;
// stores a list of plugin names which requested to be reloaded
- private pluginReloadQueue: string[] = [];
+ private pluginReloadQueue: { name: string; version?: string }[] = [];
constructor() {
super(PluginLoader.name);
this.log('Initialized');
+ const TabIcon = () => {
+ const { updates, hasLoaderUpdate } = useDeckyState();
+ return <NotificationBadge show={(updates && updates.size > 0) || hasLoaderUpdate} />;
+ };
+
this.tabsHook.add({
id: QuickAccessTab.Decky,
title: null,
@@ -44,7 +51,14 @@ class PluginLoader extends Logger {
<PluginView />
</DeckyStateContextProvider>
),
- icon: <FaPlug />,
+ icon: (
+ <DeckyStateContextProvider deckyState={this.deckyState}>
+ <>
+ <FaPlug />
+ <TabIcon />
+ </>
+ </DeckyStateContextProvider>
+ ),
});
this.routerHook.addRoute('/decky/store', () => <StorePage />);
@@ -62,7 +76,28 @@ class PluginLoader extends Logger {
if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) {
this.toaster.toast({
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);
}
- public async importPlugin(name: string) {
+ public async importPlugin(name: string, version?: string | undefined) {
if (this.reloadLock) {
this.log('Reload currently in progress, adding to queue', name);
- this.pluginReloadQueue.push(name);
+ this.pluginReloadQueue.push({ name, version: version });
return;
}
@@ -144,7 +179,7 @@ class PluginLoader extends Logger {
if (name.startsWith('$LEGACY_')) {
await this.importLegacyPlugin(name.replace('$LEGACY_', ''));
} else {
- await this.importReactPlugin(name);
+ await this.importReactPlugin(name, version);
}
this.deckyState.setPlugins(this.plugins);
@@ -155,12 +190,12 @@ class PluginLoader extends Logger {
this.reloadLock = false;
const nextPlugin = this.pluginReloadQueue.shift();
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`, {
credentials: 'include',
headers: {
@@ -172,6 +207,7 @@ class PluginLoader extends Logger {
this.plugins.push({
...plugin,
name: name,
+ version: version,
});
} else throw new Error(`${name} frontend_bundle not OK`);
}
diff --git a/frontend/src/plugin.ts b/frontend/src/plugin.ts
index ca36e75d..630f9775 100644
--- a/frontend/src/plugin.ts
+++ b/frontend/src/plugin.ts
@@ -1,5 +1,6 @@
export interface Plugin {
name: string;
+ version?: string;
icon: JSX.Element;
content?: JSX.Element;
onDismount?(): void;
diff --git a/frontend/src/store.tsx b/frontend/src/store.tsx
new file mode 100644
index 00000000..3e9d6823
--- /dev/null
+++ b/frontend/src/store.tsx
@@ -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;
+}