Add update all button to plugin list (#466)

This commit is contained in:
Jonas Dellinger
2023-05-29 18:29:36 +02:00
committed by GitHub
parent 5114bb5711
commit 010feddf36
7 changed files with 215 additions and 19 deletions

View File

@@ -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;

View File

@@ -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>

View File

@@ -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

View File

@@ -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`;
}