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
+21 -1
View File
@@ -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'));
}
@@ -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 { 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>
@@ -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();
@@ -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');
@@ -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';
@@ -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>
);
}
+9 -9
View File
@@ -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
+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 { 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);
})();