From 8b3f569a09db9daf7748426f916a66591159928f Mon Sep 17 00:00:00 2001 From: AAGaming Date: Sun, 21 Aug 2022 16:41:25 -0400 Subject: Add plugin updater, notification badge, fixes --- frontend/src/components/DeckyState.tsx | 22 ++++- frontend/src/components/NotificationBadge.tsx | 25 ++++++ frontend/src/components/PluginView.tsx | 5 +- .../src/components/modals/PluginInstallModal.tsx | 4 +- .../settings/pages/general/RemoteDebugging.tsx | 6 +- .../components/settings/pages/general/index.tsx | 2 +- .../settings/pages/plugin_list/index.tsx | 72 ++++++++++----- frontend/src/components/store/PluginCard.tsx | 18 ++-- frontend/src/components/store/Store.tsx | 100 ++------------------- 9 files changed, 116 insertions(+), 138 deletions(-) create mode 100644 frontend/src/components/NotificationBadge.tsx (limited to 'frontend/src/components') 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 = ({ show, style }) => { + return show ? ( +
+ ) : 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 = () => {
); } - return (
@@ -35,6 +35,7 @@ const PluginView: VFC = () => {
{icon}
{name}
+
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 = ({ 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={} > - { 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 (
    - {plugins.map(({ name }) => ( -
  • - {name} -
    - - showContextMenu( - - window.DeckyPluginLoader.importPlugin(name)}>Reload - window.DeckyPluginLoader.uninstallPlugin(name)}>Uninstall - , - e.currentTarget ?? window, - ) - } - > - - -
    -
  • - ))} + {plugins.map(({ name, version }) => { + const update = updates?.get(name); + return ( +
  • + + {name} {version} + + + {update && ( + requestPluginInstall(name, update)} + > +
    + Update to {update.name} + +
    +
    + )} + + showContextMenu( + + window.DeckyPluginLoader.importPlugin(name, version)}> + Reload + + window.DeckyPluginLoader.uninstallPlugin(name)}>Uninstall + , + e.currentTarget ?? window, + ) + } + > + + +
    +
  • + ); + })}
); } 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 = ({ plugin }) => { const [selectedOption, setSelectedOption] = useState(0); const buttonRef = useRef(null); @@ -119,13 +116,16 @@ const PluginCard: FC = ({ plugin }) => {

Author: {plugin.author}

-

- Tags: + }} + > + Tags: {plugin.tags.map((tag: string) => ( = ({ 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( - { - 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 - }} - > -

- Using legacy plugins -
- You are currently installing a legacy 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. - , - ); -} - -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(null); const [legacyData, setLegacyData] = useState(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); })(); -- cgit v1.2.3