mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-07-11 19:21:59 +00:00
Add plugin updater, notification badge, fixes
This commit is contained in:
+4
-4
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
Generated
-4
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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,16 +22,33 @@ 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' }}>
|
||||||
|
<span>
|
||||||
|
{name} {version}
|
||||||
|
</span>
|
||||||
|
<Focusable style={{ marginLeft: 'auto', boxShadow: 'none', display: 'flex', justifyContent: 'right' }}>
|
||||||
|
{update && (
|
||||||
<DialogButton
|
<DialogButton
|
||||||
style={{ height: '40px', width: '40px', padding: '10px 12px' }}
|
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) =>
|
onClick={(e: MouseEvent) =>
|
||||||
showContextMenu(
|
showContextMenu(
|
||||||
<Menu label="Plugin Actions">
|
<Menu label="Plugin Actions">
|
||||||
<MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(name)}>Reload</MenuItem>
|
<MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(name, version)}>
|
||||||
|
Reload
|
||||||
|
</MenuItem>
|
||||||
<MenuItem onSelected={() => window.DeckyPluginLoader.uninstallPlugin(name)}>Uninstall</MenuItem>
|
<MenuItem onSelected={() => window.DeckyPluginLoader.uninstallPlugin(name)}>Uninstall</MenuItem>
|
||||||
</Menu>,
|
</Menu>,
|
||||||
e.currentTarget ?? window,
|
e.currentTarget ?? window,
|
||||||
@@ -34,9 +57,10 @@ export default function PluginList() {
|
|||||||
>
|
>
|
||||||
<FaEllipsisH />
|
<FaEllipsisH />
|
||||||
</DialogButton>
|
</DialogButton>
|
||||||
</div>
|
</Focusable>
|
||||||
</li>
|
</li>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user