summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorParty Wumpus <48649272+PartyWumpus@users.noreply.github.com>2024-05-13 14:42:55 +0100
committerGitHub <noreply@github.com>2024-05-13 14:42:55 +0100
commit372771a228c9fbb31ec4a943dd6cfa9915741c6c (patch)
tree5cd9cc5bd5cd2814e5d588232342b754324747e3
parent675b6d5ef8c3f84e5758d675ae0ff94ebb601de4 (diff)
downloaddecky-loader-372771a228c9fbb31ec4a943dd6cfa9915741c6c.tar.gz
decky-loader-372771a228c9fbb31ec4a943dd6cfa9915741c6c.zip
plugin install progress (#614)
* Frontend progress bars * Backend bit * closure is stale i think so no closure for you * Fix formatting of the progress svgs * Reset progress bar when new plugin starts downloading
-rw-r--r--backend/decky_loader/browser.py14
-rw-r--r--backend/locales/en-US.json9
-rw-r--r--frontend/src/components/modals/MultiplePluginsInstallModal.tsx55
-rw-r--r--frontend/src/components/modals/PluginInstallModal.tsx30
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>
);