diff options
| author | Jonas Dellinger <jonas@dellinger.dev> | 2023-05-29 18:29:36 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-05-29 09:29:36 -0700 |
| commit | 010feddf36646fb9695106bd64eab41e47e962fe (patch) | |
| tree | 4619a5c0fc1b2c9ca475ce644cce18464c77ca3e /frontend/src | |
| parent | 5114bb57112bf8bbad30768ffd26803d464b19a2 (diff) | |
| download | decky-loader-010feddf36646fb9695106bd64eab41e47e962fe.tar.gz decky-loader-010feddf36646fb9695106bd64eab41e47e962fe.zip | |
Add update all button to plugin list (#466)
Diffstat (limited to 'frontend/src')
| -rw-r--r-- | frontend/src/components/modals/MultiplePluginsInstallModal.tsx | 82 | ||||
| -rw-r--r-- | frontend/src/components/settings/pages/plugin_list/index.tsx | 49 | ||||
| -rw-r--r-- | frontend/src/plugin-loader.tsx | 16 | ||||
| -rw-r--r-- | frontend/src/store.tsx | 25 |
4 files changed, 163 insertions, 9 deletions
diff --git a/frontend/src/components/modals/MultiplePluginsInstallModal.tsx b/frontend/src/components/modals/MultiplePluginsInstallModal.tsx new file mode 100644 index 00000000..febdc38d --- /dev/null +++ b/frontend/src/components/modals/MultiplePluginsInstallModal.tsx @@ -0,0 +1,82 @@ +import { ConfirmModal, Navigation, QuickAccessTab } from 'decky-frontend-lib'; +import { FC, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { InstallType } from '../../plugin'; + +interface MultiplePluginsInstallModalProps { + requests: { name: string; version: string; hash: string; install_type: InstallType }[]; + onOK(): void | Promise<void>; + onCancel(): void | Promise<void>; + closeModal?(): void; +} + +// values are the JSON keys used in the translation file +const InstallTypeTranslationMapping = { + [InstallType.INSTALL]: 'install', + [InstallType.REINSTALL]: 'reinstall', + [InstallType.UPDATE]: 'update', +} as const satisfies Record<InstallType, string>; + +type TitleTranslationMapping = 'mixed' | (typeof InstallTypeTranslationMapping)[InstallType]; + +const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({ + requests, + onOK, + onCancel, + closeModal, +}) => { + const [loading, setLoading] = useState<boolean>(false); + const { t } = useTranslation(); + + // used as part of the title translation + // if we know all operations are of a specific type, we can show so in the title to make decision easier + const installTypeGrouped = useMemo((): TitleTranslationMapping => { + if (requests.every(({ install_type }) => install_type === InstallType.INSTALL)) return 'install'; + if (requests.every(({ install_type }) => install_type === InstallType.REINSTALL)) return 'reinstall'; + if (requests.every(({ install_type }) => install_type === InstallType.UPDATE)) return 'update'; + return 'mixed'; + }, [requests]); + + return ( + <ConfirmModal + bOKDisabled={loading} + closeModal={closeModal} + onOK={async () => { + setLoading(true); + await onOK(); + setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 250); + setTimeout(() => window.DeckyPluginLoader.checkPluginUpdates(), 1000); + }} + onCancel={async () => { + await onCancel(); + }} + strTitle={<div>{t(`MultiplePluginsInstallModal.title.${installTypeGrouped}`, { count: requests.length })}</div>} + strOKButtonText={t(`MultiplePluginsInstallModal.ok_button.${loading ? 'loading' : 'idle'}`)} + > + <div> + {t('MultiplePluginsInstallModal.confirm')} + <ul style={{ listStyle: 'none', display: 'flex', flexDirection: 'column', gap: '4px' }}> + {requests.map(({ name, version, install_type, hash }, i) => { + const installTypeStr = InstallTypeTranslationMapping[install_type]; + const description = t(`MultiplePluginsInstallModal.description.${installTypeStr}`, { + name, + version, + }); + + return ( + <li key={i} style={{ display: 'flex', flexDirection: 'column' }}> + <div>{description}</div> + {hash === 'False' && ( + <div style={{ color: 'red', paddingLeft: '10px' }}>{t('PluginInstallModal.no_hash')}</div> + )} + </li> + ); + })} + </ul> + </div> + </ConfirmModal> + ); +}; + +export default MultiplePluginsInstallModal; diff --git a/frontend/src/components/settings/pages/plugin_list/index.tsx b/frontend/src/components/settings/pages/plugin_list/index.tsx index d7ff7bd9..ce349393 100644 --- a/frontend/src/components/settings/pages/plugin_list/index.tsx +++ b/frontend/src/components/settings/pages/plugin_list/index.tsx @@ -14,7 +14,12 @@ import { useTranslation } from 'react-i18next'; import { FaDownload, FaEllipsisH, FaRecycle } from 'react-icons/fa'; import { InstallType } from '../../../../plugin'; -import { StorePluginVersion, getPluginList, requestPluginInstall } from '../../../../store'; +import { + StorePluginVersion, + getPluginList, + requestMultiplePluginInstalls, + requestPluginInstall, +} from '../../../../store'; import { useSetting } from '../../../../utils/hooks/useSetting'; import { useDeckyState } from '../../../DeckyState'; @@ -67,9 +72,9 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) { onClick={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion, InstallType.UPDATE)} onOKButton={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion, InstallType.UPDATE)} > - <div style={{ display: 'flex', flexDirection: 'row' }}> + <div style={{ display: 'flex', minWidth: '180px', justifyContent: 'space-between', alignItems: 'center' }}> {t('PluginListIndex.update_to', { name: data?.update?.name })} - <FaDownload style={{ paddingLeft: '2rem' }} /> + <FaDownload style={{ paddingLeft: '1rem' }} /> </div> </DialogButton> ) : ( @@ -78,14 +83,22 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) { onClick={() => reinstallPlugin(pluginName, data?.version)} onOKButton={() => reinstallPlugin(pluginName, data?.version)} > - <div style={{ display: 'flex', flexDirection: 'row' }}> + <div style={{ display: 'flex', minWidth: '180px', justifyContent: 'space-between', alignItems: 'center' }}> {t('PluginListIndex.reinstall')} - <FaRecycle style={{ paddingLeft: '5.3rem' }} /> + <FaRecycle style={{ paddingLeft: '1rem' }} /> </div> </DialogButton> )} <DialogButton - style={{ height: '40px', width: '40px', padding: '10px 12px', minWidth: '40px' }} + style={{ + height: '40px', + width: '40px', + padding: '10px 12px', + minWidth: '40px', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + }} onClick={showCtxMenu} onOKButton={showCtxMenu} > @@ -146,6 +159,30 @@ export default function PluginList() { return ( <DialogBody> + {updates && updates.size > 0 && ( + <DialogButton + onClick={() => + requestMultiplePluginInstalls( + [...updates.entries()].map(([plugin, selectedVer]) => ({ + installType: InstallType.UPDATE, + plugin, + selectedVer, + })), + ) + } + style={{ + position: 'absolute', + top: '57px', + right: '2.8vw', + width: 'auto', + display: 'flex', + alignItems: 'center', + }} + > + {t('PluginListIndex.update_all', { count: updates.size })} + <FaDownload style={{ paddingLeft: '1rem' }} /> + </DialogButton> + )} <DialogControlsSection> <ReorderableList<PluginData> entries={pluginEntries} onSave={onSave} interactables={PluginInteractables} /> </DialogControlsSection> diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx index 15a0ca36..57483293 100644 --- a/frontend/src/plugin-loader.tsx +++ b/frontend/src/plugin-loader.tsx @@ -16,12 +16,13 @@ import { FaExclamationCircle, FaPlug } from 'react-icons/fa'; import { DeckyState, DeckyStateContextProvider, useDeckyState } from './components/DeckyState'; import LegacyPlugin from './components/LegacyPlugin'; import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches'; +import MultiplePluginsInstallModal from './components/modals/MultiplePluginsInstallModal'; import PluginInstallModal from './components/modals/PluginInstallModal'; import NotificationBadge from './components/NotificationBadge'; import PluginView from './components/PluginView'; import WithSuspense from './components/WithSuspense'; import Logger from './logger'; -import { Plugin } from './plugin'; +import { InstallType, Plugin } from './plugin'; import RouterHook from './router-hook'; import { deinitSteamFixes, initSteamFixes } from './steamfixes'; import { checkForUpdates } from './store'; @@ -168,6 +169,19 @@ class PluginLoader extends Logger { ); } + public addMultiplePluginsInstallPrompt( + request_id: string, + requests: { name: string; version: string; hash: string; install_type: InstallType }[], + ) { + showModal( + <MultiplePluginsInstallModal + requests={requests} + onOK={() => this.callServerMethod('confirm_plugin_install', { request_id })} + onCancel={() => this.callServerMethod('cancel_plugin_install', { request_id })} + />, + ); + } + public uninstallPlugin(name: string, title: string, button_text: string, description: string) { showModal( <ConfirmModal diff --git a/frontend/src/store.tsx b/frontend/src/store.tsx index 80485252..f0ad0c1b 100644 --- a/frontend/src/store.tsx +++ b/frontend/src/store.tsx @@ -23,6 +23,12 @@ export interface StorePlugin { image_url: string; } +export interface PluginInstallRequest { + plugin: string; + selectedVer: StorePluginVersion; + installType: InstallType; +} + // name: version export type PluginUpdateMapping = Map<string, StorePluginVersion>; @@ -74,8 +80,7 @@ export async function installFromURL(url: string) { } export async function requestPluginInstall(plugin: string, selectedVer: StorePluginVersion, installType: InstallType) { - const artifactUrl = - selectedVer.artifact ?? `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`; + const artifactUrl = selectedVer.artifact ?? pluginUrl(selectedVer.hash); await window.DeckyPluginLoader.callServerMethod('install_plugin', { name: plugin, artifact: artifactUrl, @@ -85,6 +90,18 @@ export async function requestPluginInstall(plugin: string, selectedVer: StorePlu }); } +export async function requestMultiplePluginInstalls(requests: PluginInstallRequest[]) { + await window.DeckyPluginLoader.callServerMethod('install_plugins', { + requests: requests.map(({ plugin, installType, selectedVer }) => ({ + name: plugin, + artifact: selectedVer.artifact ?? pluginUrl(selectedVer.hash), + version: selectedVer.name, + hash: selectedVer.hash, + install_type: installType, + })), + }); +} + export async function checkForUpdates(plugins: Plugin[]): Promise<PluginUpdateMapping> { const serverData = await getPluginList(); const updateMap = new Map<string, StorePluginVersion>(); @@ -96,3 +113,7 @@ export async function checkForUpdates(plugins: Plugin[]): Promise<PluginUpdateMa } return updateMap; } + +function pluginUrl(hash: string) { + return `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${hash}.zip`; +} |
