mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-07-11 06:21:58 +00:00
Add update all button to plugin list (#466)
This commit is contained in:
+16
-4
@@ -63,10 +63,8 @@ class PluginBrowser:
|
|||||||
# create bin directory if needed.
|
# create bin directory if needed.
|
||||||
chmod(pluginBasePath, 777)
|
chmod(pluginBasePath, 777)
|
||||||
if access(pluginBasePath, W_OK):
|
if access(pluginBasePath, W_OK):
|
||||||
|
|
||||||
if not path.exists(pluginBinPath):
|
if not path.exists(pluginBinPath):
|
||||||
mkdir(pluginBinPath)
|
mkdir(pluginBinPath)
|
||||||
|
|
||||||
if not access(pluginBinPath, W_OK):
|
if not access(pluginBinPath, W_OK):
|
||||||
chmod(pluginBinPath, 777)
|
chmod(pluginBinPath, 777)
|
||||||
|
|
||||||
@@ -216,9 +214,23 @@ class PluginBrowser:
|
|||||||
await tab.open_websocket()
|
await tab.open_websocket()
|
||||||
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}', '{request_id}', '{hash}', {install_type})")
|
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}', '{request_id}', '{hash}', {install_type})")
|
||||||
|
|
||||||
|
async def request_multiple_plugin_installs(self, requests):
|
||||||
|
request_id = str(time())
|
||||||
|
self.install_requests[request_id] = [PluginInstallContext(req['artifact'], req['name'], req['version'], req['hash']) for req in requests]
|
||||||
|
js_requests_parameter = ','.join([
|
||||||
|
f"{{ name: '{req['name']}', version: '{req['version']}', hash: '{req['hash']}', install_type: {req['install_type']}}}" for req in requests
|
||||||
|
])
|
||||||
|
|
||||||
|
tab = await get_gamepadui_tab()
|
||||||
|
await tab.open_websocket()
|
||||||
|
await tab.evaluate_js(f"DeckyPluginLoader.addMultiplePluginsInstallPrompt('{request_id}', [{js_requests_parameter}])")
|
||||||
|
|
||||||
async def confirm_plugin_install(self, request_id):
|
async def confirm_plugin_install(self, request_id):
|
||||||
request = self.install_requests.pop(request_id)
|
requestOrRequests = self.install_requests.pop(request_id)
|
||||||
await self._install(request.artifact, request.name, request.version, request.hash)
|
if isinstance(requestOrRequests, list):
|
||||||
|
[await self._install(req.artifact, req.name, req.version, req.hash) for req in requestOrRequests]
|
||||||
|
else:
|
||||||
|
await self._install(requestOrRequests.artifact, requestOrRequests.name, requestOrRequests.version, requestOrRequests.hash)
|
||||||
|
|
||||||
def cancel_plugin_install(self, request_id):
|
def cancel_plugin_install(self, request_id):
|
||||||
self.install_requests.pop(request_id)
|
self.install_requests.pop(request_id)
|
||||||
|
|||||||
@@ -44,13 +44,37 @@
|
|||||||
"title": "Update {{artifact}}"
|
"title": "Update {{artifact}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"MultiplePluginsInstallModal": {
|
||||||
|
"title": {
|
||||||
|
"mixed_one": "Modify 1 plugin",
|
||||||
|
"mixed_other": "Modify {{count}} plugins",
|
||||||
|
"update_one": "Update 1 plugin",
|
||||||
|
"update_other": "Update {{count}} plugins",
|
||||||
|
"reinstall_one": "Reinstall 1 plugin",
|
||||||
|
"reinstall_other": "Reinstall {{count}} plugins",
|
||||||
|
"install_one": "Install 1 plugin",
|
||||||
|
"install_other": "Install {{count}} plugins"
|
||||||
|
},
|
||||||
|
"ok_button": {
|
||||||
|
"idle": "Confirm",
|
||||||
|
"loading": "Working"
|
||||||
|
},
|
||||||
|
"confirm": "Are you sure you want to make the following modifications?",
|
||||||
|
"description": {
|
||||||
|
"install": "Install {{name}} {{version}}",
|
||||||
|
"update": "Update {{name}} to {{version}}",
|
||||||
|
"reinstall": "Reinstall {{name}} {{version}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
"PluginListIndex": {
|
"PluginListIndex": {
|
||||||
"no_plugin": "No plugins installed!",
|
"no_plugin": "No plugins installed!",
|
||||||
"plugin_actions": "Plugin Actions",
|
"plugin_actions": "Plugin Actions",
|
||||||
"reinstall": "Reinstall",
|
"reinstall": "Reinstall",
|
||||||
"reload": "Reload",
|
"reload": "Reload",
|
||||||
"uninstall": "Uninstall",
|
"uninstall": "Uninstall",
|
||||||
"update_to": "Update to {{name}}"
|
"update_to": "Update to {{name}}",
|
||||||
|
"update_all_one": "Update 1 plugin",
|
||||||
|
"update_all_other": "Update {{count}} plugins"
|
||||||
},
|
},
|
||||||
"PluginLoader": {
|
"PluginLoader": {
|
||||||
"decky_title": "Decky",
|
"decky_title": "Decky",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class Utilities:
|
|||||||
"ping": self.ping,
|
"ping": self.ping,
|
||||||
"http_request": self.http_request,
|
"http_request": self.http_request,
|
||||||
"install_plugin": self.install_plugin,
|
"install_plugin": self.install_plugin,
|
||||||
|
"install_plugins": self.install_plugins,
|
||||||
"cancel_plugin_install": self.cancel_plugin_install,
|
"cancel_plugin_install": self.cancel_plugin_install,
|
||||||
"confirm_plugin_install": self.confirm_plugin_install,
|
"confirm_plugin_install": self.confirm_plugin_install,
|
||||||
"uninstall_plugin": self.uninstall_plugin,
|
"uninstall_plugin": self.uninstall_plugin,
|
||||||
@@ -70,6 +71,11 @@ class Utilities:
|
|||||||
install_type=install_type
|
install_type=install_type
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def install_plugins(self, requests):
|
||||||
|
return await self.context.plugin_browser.request_multiple_plugin_installs(
|
||||||
|
requests=requests
|
||||||
|
)
|
||||||
|
|
||||||
async def confirm_plugin_install(self, request_id):
|
async def confirm_plugin_install(self, request_id):
|
||||||
return await self.context.plugin_browser.confirm_plugin_install(request_id)
|
return await self.context.plugin_browser.confirm_plugin_install(request_id)
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -14,7 +14,12 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { FaDownload, FaEllipsisH, FaRecycle } from 'react-icons/fa';
|
import { FaDownload, FaEllipsisH, FaRecycle } from 'react-icons/fa';
|
||||||
|
|
||||||
import { InstallType } from '../../../../plugin';
|
import { InstallType } from '../../../../plugin';
|
||||||
import { StorePluginVersion, getPluginList, requestPluginInstall } from '../../../../store';
|
import {
|
||||||
|
StorePluginVersion,
|
||||||
|
getPluginList,
|
||||||
|
requestMultiplePluginInstalls,
|
||||||
|
requestPluginInstall,
|
||||||
|
} from '../../../../store';
|
||||||
import { useSetting } from '../../../../utils/hooks/useSetting';
|
import { useSetting } from '../../../../utils/hooks/useSetting';
|
||||||
import { useDeckyState } from '../../../DeckyState';
|
import { useDeckyState } from '../../../DeckyState';
|
||||||
|
|
||||||
@@ -67,9 +72,9 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
|
|||||||
onClick={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion, InstallType.UPDATE)}
|
onClick={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion, InstallType.UPDATE)}
|
||||||
onOKButton={() => 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 })}
|
{t('PluginListIndex.update_to', { name: data?.update?.name })}
|
||||||
<FaDownload style={{ paddingLeft: '2rem' }} />
|
<FaDownload style={{ paddingLeft: '1rem' }} />
|
||||||
</div>
|
</div>
|
||||||
</DialogButton>
|
</DialogButton>
|
||||||
) : (
|
) : (
|
||||||
@@ -78,14 +83,22 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
|
|||||||
onClick={() => reinstallPlugin(pluginName, data?.version)}
|
onClick={() => reinstallPlugin(pluginName, data?.version)}
|
||||||
onOKButton={() => 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')}
|
{t('PluginListIndex.reinstall')}
|
||||||
<FaRecycle style={{ paddingLeft: '5.3rem' }} />
|
<FaRecycle style={{ paddingLeft: '1rem' }} />
|
||||||
</div>
|
</div>
|
||||||
</DialogButton>
|
</DialogButton>
|
||||||
)}
|
)}
|
||||||
<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}
|
onClick={showCtxMenu}
|
||||||
onOKButton={showCtxMenu}
|
onOKButton={showCtxMenu}
|
||||||
>
|
>
|
||||||
@@ -146,6 +159,30 @@ export default function PluginList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogBody>
|
<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>
|
<DialogControlsSection>
|
||||||
<ReorderableList<PluginData> entries={pluginEntries} onSave={onSave} interactables={PluginInteractables} />
|
<ReorderableList<PluginData> entries={pluginEntries} onSave={onSave} interactables={PluginInteractables} />
|
||||||
</DialogControlsSection>
|
</DialogControlsSection>
|
||||||
|
|||||||
@@ -16,12 +16,13 @@ import { FaExclamationCircle, FaPlug } from 'react-icons/fa';
|
|||||||
import { DeckyState, DeckyStateContextProvider, useDeckyState } from './components/DeckyState';
|
import { DeckyState, DeckyStateContextProvider, useDeckyState } from './components/DeckyState';
|
||||||
import LegacyPlugin from './components/LegacyPlugin';
|
import LegacyPlugin from './components/LegacyPlugin';
|
||||||
import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches';
|
import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches';
|
||||||
|
import MultiplePluginsInstallModal from './components/modals/MultiplePluginsInstallModal';
|
||||||
import PluginInstallModal from './components/modals/PluginInstallModal';
|
import PluginInstallModal from './components/modals/PluginInstallModal';
|
||||||
import NotificationBadge from './components/NotificationBadge';
|
import NotificationBadge from './components/NotificationBadge';
|
||||||
import PluginView from './components/PluginView';
|
import PluginView from './components/PluginView';
|
||||||
import WithSuspense from './components/WithSuspense';
|
import WithSuspense from './components/WithSuspense';
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
import { Plugin } from './plugin';
|
import { InstallType, Plugin } from './plugin';
|
||||||
import RouterHook from './router-hook';
|
import RouterHook from './router-hook';
|
||||||
import { deinitSteamFixes, initSteamFixes } from './steamfixes';
|
import { deinitSteamFixes, initSteamFixes } from './steamfixes';
|
||||||
import { checkForUpdates } from './store';
|
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) {
|
public uninstallPlugin(name: string, title: string, button_text: string, description: string) {
|
||||||
showModal(
|
showModal(
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
|
|||||||
+23
-2
@@ -23,6 +23,12 @@ export interface StorePlugin {
|
|||||||
image_url: string;
|
image_url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PluginInstallRequest {
|
||||||
|
plugin: string;
|
||||||
|
selectedVer: StorePluginVersion;
|
||||||
|
installType: InstallType;
|
||||||
|
}
|
||||||
|
|
||||||
// name: version
|
// name: version
|
||||||
export type PluginUpdateMapping = Map<string, StorePluginVersion>;
|
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) {
|
export async function requestPluginInstall(plugin: string, selectedVer: StorePluginVersion, installType: InstallType) {
|
||||||
const artifactUrl =
|
const artifactUrl = selectedVer.artifact ?? pluginUrl(selectedVer.hash);
|
||||||
selectedVer.artifact ?? `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`;
|
|
||||||
await window.DeckyPluginLoader.callServerMethod('install_plugin', {
|
await window.DeckyPluginLoader.callServerMethod('install_plugin', {
|
||||||
name: plugin,
|
name: plugin,
|
||||||
artifact: artifactUrl,
|
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> {
|
export async function checkForUpdates(plugins: Plugin[]): Promise<PluginUpdateMapping> {
|
||||||
const serverData = await getPluginList();
|
const serverData = await getPluginList();
|
||||||
const updateMap = new Map<string, StorePluginVersion>();
|
const updateMap = new Map<string, StorePluginVersion>();
|
||||||
@@ -96,3 +113,7 @@ export async function checkForUpdates(plugins: Plugin[]): Promise<PluginUpdateMa
|
|||||||
}
|
}
|
||||||
return updateMap;
|
return updateMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pluginUrl(hash: string) {
|
||||||
|
return `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${hash}.zip`;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user