diff options
| author | AAGaming <aa@mail.catvibers.me> | 2022-08-21 16:41:25 -0400 |
|---|---|---|
| committer | AAGaming <aa@mail.catvibers.me> | 2022-08-21 16:41:25 -0400 |
| commit | 8b3f569a09db9daf7748426f916a66591159928f (patch) | |
| tree | 237cc3711c7098b30a7e7cda97db9e406b0f7db0 /frontend | |
| parent | 1930400032a850b833f5f71523008e326f40547a (diff) | |
| download | decky-loader-8b3f569a09db9daf7748426f916a66591159928f.tar.gz decky-loader-8b3f569a09db9daf7748426f916a66591159928f.zip | |
Add plugin updater, notification badge, fixesv2.0.5-pre15
Diffstat (limited to 'frontend')
| -rw-r--r-- | frontend/pnpm-lock.yaml | 4 | ||||
| -rw-r--r-- | frontend/src/components/DeckyState.tsx | 22 | ||||
| -rw-r--r-- | frontend/src/components/NotificationBadge.tsx | 25 | ||||
| -rw-r--r-- | frontend/src/components/PluginView.tsx | 5 | ||||
| -rw-r--r-- | frontend/src/components/modals/PluginInstallModal.tsx | 4 | ||||
| -rw-r--r-- | frontend/src/components/settings/pages/general/RemoteDebugging.tsx | 6 | ||||
| -rw-r--r-- | frontend/src/components/settings/pages/general/index.tsx | 2 | ||||
| -rw-r--r-- | frontend/src/components/settings/pages/plugin_list/index.tsx | 72 | ||||
| -rw-r--r-- | frontend/src/components/store/PluginCard.tsx | 18 | ||||
| -rw-r--r-- | frontend/src/components/store/Store.tsx | 100 | ||||
| -rw-r--r-- | frontend/src/index.tsx | 9 | ||||
| -rw-r--r-- | frontend/src/plugin-loader.tsx | 56 | ||||
| -rw-r--r-- | frontend/src/plugin.ts | 1 | ||||
| -rw-r--r-- | frontend/src/store.tsx | 121 |
14 files changed, 289 insertions, 156 deletions
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; +} |
