diff options
| -rw-r--r-- | backend/decky_loader/browser.py | 14 | ||||
| -rw-r--r-- | backend/locales/en-US.json | 9 | ||||
| -rw-r--r-- | frontend/src/components/modals/MultiplePluginsInstallModal.tsx | 55 | ||||
| -rw-r--r-- | frontend/src/components/modals/PluginInstallModal.tsx | 30 |
4 files changed, 103 insertions, 5 deletions
diff --git a/backend/decky_loader/browser.py b/backend/decky_loader/browser.py index 6fa71bb2..ea036ba8 100644 --- a/backend/decky_loader/browser.py +++ b/backend/decky_loader/browser.py @@ -151,6 +151,8 @@ class PluginBrowser: self.loader.watcher.disabled = False async def _install(self, artifact: str, name: str, version: str, hash: str): + await self.loader.ws.emit("loader/plugin_download_start", name) + await self.loader.ws.emit("loader/plugin_download_info", 5, "Store.download_progress_info.start") # Will be set later in code res_zip = None @@ -164,12 +166,17 @@ class PluginBrowser: # Check if the file is a local file or a URL if artifact.startswith("file://"): logger.info(f"Installing {name} from local ZIP file (Version: {version})") + await self.loader.ws.emit("loader/plugin_download_info", 10, "Store.download_progress_info.open_zip") res_zip = BytesIO(open(artifact[7:], "rb").read()) else: logger.info(f"Installing {name} from URL (Version: {version})") + await self.loader.ws.emit("loader/plugin_download_info", 10, "Store.download_progress_info.download_zip") + async with ClientSession() as client: logger.debug(f"Fetching {artifact}") res = await client.get(artifact, ssl=get_ssl_context()) + #TODO track progress of this download in chunks like with decky updates + #TODO but squish with min 15 and max 75 if res.status == 200: logger.debug("Got 200. Reading...") data = await res.read() @@ -178,6 +185,7 @@ class PluginBrowser: else: logger.fatal(f"Could not fetch from URL. {await res.text()}") + await self.loader.ws.emit("loader/plugin_download_info", 80, "Store.download_progress_info.increment_count") storeUrl = "" match self.settings.getSetting("store", 0): case 0: storeUrl = "https://plugins.deckbrew.xyz/plugins" # default @@ -190,6 +198,7 @@ class PluginBrowser: if res.status != 200: logger.error(f"Server did not accept install count increment request. code: {res.status}") + await self.loader.ws.emit("loader/plugin_download_info", 85, "Store.download_progress_info.parse_zip") if res_zip and version == "dev": with ZipFile(res_zip) as plugin_zip: plugin_json_list = [file for file in plugin_zip.namelist() if file.endswith("/plugin.json") and file.count("/") == 1] @@ -219,12 +228,15 @@ class PluginBrowser: # If plugin is installed, uninstall it if isInstalled: + await self.loader.ws.emit("loader/plugin_download_info", 90, "Store.download_progress_info.uninstalling_previous") try: logger.debug("Uninstalling existing plugin...") await self.uninstall_plugin(name) except: logger.error(f"Plugin {name} could not be uninstalled.") + + await self.loader.ws.emit("loader/plugin_download_info", 95, "Store.download_progress_info.installing_plugin") # Install the plugin logger.debug("Unzipping...") ret = self._unzip_to_plugin_dir(res_zip, name, hash) @@ -232,6 +244,7 @@ class PluginBrowser: plugin_folder = self.find_plugin_folder(name) assert plugin_folder is not None plugin_dir = path.join(self.plugin_path, plugin_folder) + #TODO count again from 0% to 100% quickly for this one if it does anything ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir) if ret: logger.info(f"Installed {name} (Version: {version})") @@ -251,6 +264,7 @@ class PluginBrowser: logger.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})") if self.loader.watcher: self.loader.watcher.disabled = False + await self.loader.ws.emit("loader/plugin_download_finish", name) async def request_plugin_install(self, artifact: str, name: str, version: str, hash: str, install_type: PluginInstallType): request_id = str(time()) diff --git a/backend/locales/en-US.json b/backend/locales/en-US.json index 1898b29b..94e1ed3d 100644 --- a/backend/locales/en-US.json +++ b/backend/locales/en-US.json @@ -231,6 +231,15 @@ "store_testing_warning": { "desc": "You can use this store channel to test bleeding-edge plugin versions. Be sure to leave feedback on GitHub so the plugin can be updated for all users.", "label": "Welcome to the Testing Store Channel" + }, + "download_progress_info": { + "start": "Initializing", + "open_zip": "Opening zip file", + "download_zip": "Downloading plugin", + "increment_count": "Incrementing download count", + "parse_zip": "Parsing zip file", + "uninstalling_previous": "Uninstalling previous copy", + "installing_plugin": "Installing plugin" } }, "StoreSelect": { diff --git a/frontend/src/components/modals/MultiplePluginsInstallModal.tsx b/frontend/src/components/modals/MultiplePluginsInstallModal.tsx index 73b8acb1..ba49ba92 100644 --- a/frontend/src/components/modals/MultiplePluginsInstallModal.tsx +++ b/frontend/src/components/modals/MultiplePluginsInstallModal.tsx @@ -1,6 +1,7 @@ -import { ConfirmModal, Navigation, QuickAccessTab } from '@decky/ui'; -import { FC, useMemo, useState } from 'react'; +import { ConfirmModal, Navigation, ProgressBarWithInfo, QuickAccessTab } from '@decky/ui'; +import { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { FaCheck, FaDownload } from 'react-icons/fa'; import { InstallType } from '../../plugin'; @@ -27,8 +28,42 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({ closeModal, }) => { const [loading, setLoading] = useState<boolean>(false); + const [percentage, setPercentage] = useState<number>(0); + const [pluginsCompleted, setPluginsCompleted] = useState<string[]>([]); + const [pluginInProgress, setInProgress] = useState<string | null>(); + const [downloadInfo, setDownloadInfo] = useState<string | null>(null); const { t } = useTranslation(); + function updateDownloadState(percent: number, trans_text: string | undefined, trans_info: Record<string, string>) { + setPercentage(percent); + if (trans_text === undefined) { + setDownloadInfo(null); + } else { + setDownloadInfo(t(trans_text, trans_info)); + } + } + + function startDownload(name: string) { + setInProgress(name); + setPercentage(0); + } + + function finishDownload(name: string) { + setPluginsCompleted((list) => [...list, name]); + } + + useEffect(() => { + DeckyBackend.addEventListener('loader/plugin_download_info', updateDownloadState); + DeckyBackend.addEventListener('loader/plugin_download_start', startDownload); + DeckyBackend.addEventListener('loader/plugin_download_finish', finishDownload); + + return () => { + DeckyBackend.removeEventListener('loader/plugin_download_info', updateDownloadState); + DeckyBackend.removeEventListener('loader/plugin_download_start', startDownload); + DeckyBackend.removeEventListener('loader/plugin_download_finish', finishDownload); + }; + }, []); + // 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 => { @@ -66,7 +101,10 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({ return ( <li key={i} style={{ display: 'flex', flexDirection: 'column' }}> - <div>{description}</div> + <span> + {description}{' '} + {(pluginsCompleted.includes(name) && <FaCheck />) || (name === pluginInProgress && <FaDownload />)} + </span> {hash === 'False' && ( <div style={{ color: 'red', paddingLeft: '10px' }}>{t('PluginInstallModal.no_hash')}</div> )} @@ -74,6 +112,17 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({ ); })} </ul> + {/* TODO: center the progress bar and make it 80% width */} + {loading && ( + <ProgressBarWithInfo + // when the key changes, react considers this a new component so resets the progress without the smoothing animation + key={pluginInProgress} + bottomSeparator="none" + focusable={false} + nProgress={percentage} + sOperationText={downloadInfo} + /> + )} </div> </ConfirmModal> ); diff --git a/frontend/src/components/modals/PluginInstallModal.tsx b/frontend/src/components/modals/PluginInstallModal.tsx index 1d149b2a..8b3128a1 100644 --- a/frontend/src/components/modals/PluginInstallModal.tsx +++ b/frontend/src/components/modals/PluginInstallModal.tsx @@ -1,5 +1,5 @@ -import { ConfirmModal, Navigation, QuickAccessTab } from '@decky/ui'; -import { FC, useState } from 'react'; +import { ConfirmModal, Navigation, ProgressBarWithInfo, QuickAccessTab } from '@decky/ui'; +import { FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import TranslationHelper, { TranslationClass } from '../../utils/TranslationHelper'; @@ -24,8 +24,26 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ closeModal, }) => { const [loading, setLoading] = useState<boolean>(false); + const [percentage, setPercentage] = useState<number>(0); + const [downloadInfo, setDownloadInfo] = useState<string | null>(null); const { t } = useTranslation(); + function updateDownloadState(percent: number, trans_text: string | undefined, trans_info: Record<string, string>) { + setPercentage(percent); + if (trans_text === undefined) { + setDownloadInfo(null); + } else { + setDownloadInfo(t(trans_text, trans_info)); + } + } + + useEffect(() => { + DeckyBackend.addEventListener('loader/plugin_download_info', updateDownloadState); + return () => { + DeckyBackend.removeEventListener('loader/plugin_download_info', updateDownloadState); + }; + }, []); + return ( <ConfirmModal bOKDisabled={loading} @@ -80,6 +98,14 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ install_type={installType} /> </div> + {loading && ( + <ProgressBarWithInfo + layout="inline" + bottomSeparator="none" + nProgress={percentage} + sOperationText={downloadInfo} + /> + )} {hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>} </ConfirmModal> ); |
