mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-07-11 20:51:57 +00:00
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
This commit is contained in:
@@ -151,6 +151,8 @@ class PluginBrowser:
|
|||||||
self.loader.watcher.disabled = False
|
self.loader.watcher.disabled = False
|
||||||
|
|
||||||
async def _install(self, artifact: str, name: str, version: str, hash: str):
|
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
|
# Will be set later in code
|
||||||
res_zip = None
|
res_zip = None
|
||||||
|
|
||||||
@@ -164,12 +166,17 @@ class PluginBrowser:
|
|||||||
# Check if the file is a local file or a URL
|
# Check if the file is a local file or a URL
|
||||||
if artifact.startswith("file://"):
|
if artifact.startswith("file://"):
|
||||||
logger.info(f"Installing {name} from local ZIP file (Version: {version})")
|
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())
|
res_zip = BytesIO(open(artifact[7:], "rb").read())
|
||||||
else:
|
else:
|
||||||
logger.info(f"Installing {name} from URL (Version: {version})")
|
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:
|
async with ClientSession() as client:
|
||||||
logger.debug(f"Fetching {artifact}")
|
logger.debug(f"Fetching {artifact}")
|
||||||
res = await client.get(artifact, ssl=get_ssl_context())
|
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:
|
if res.status == 200:
|
||||||
logger.debug("Got 200. Reading...")
|
logger.debug("Got 200. Reading...")
|
||||||
data = await res.read()
|
data = await res.read()
|
||||||
@@ -178,6 +185,7 @@ class PluginBrowser:
|
|||||||
else:
|
else:
|
||||||
logger.fatal(f"Could not fetch from URL. {await res.text()}")
|
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 = ""
|
storeUrl = ""
|
||||||
match self.settings.getSetting("store", 0):
|
match self.settings.getSetting("store", 0):
|
||||||
case 0: storeUrl = "https://plugins.deckbrew.xyz/plugins" # default
|
case 0: storeUrl = "https://plugins.deckbrew.xyz/plugins" # default
|
||||||
@@ -190,6 +198,7 @@ class PluginBrowser:
|
|||||||
if res.status != 200:
|
if res.status != 200:
|
||||||
logger.error(f"Server did not accept install count increment request. code: {res.status}")
|
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":
|
if res_zip and version == "dev":
|
||||||
with ZipFile(res_zip) as plugin_zip:
|
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]
|
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 plugin is installed, uninstall it
|
||||||
if isInstalled:
|
if isInstalled:
|
||||||
|
await self.loader.ws.emit("loader/plugin_download_info", 90, "Store.download_progress_info.uninstalling_previous")
|
||||||
try:
|
try:
|
||||||
logger.debug("Uninstalling existing plugin...")
|
logger.debug("Uninstalling existing plugin...")
|
||||||
await self.uninstall_plugin(name)
|
await self.uninstall_plugin(name)
|
||||||
except:
|
except:
|
||||||
logger.error(f"Plugin {name} could not be uninstalled.")
|
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
|
# Install the plugin
|
||||||
logger.debug("Unzipping...")
|
logger.debug("Unzipping...")
|
||||||
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
|
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
|
||||||
@@ -232,6 +244,7 @@ class PluginBrowser:
|
|||||||
plugin_folder = self.find_plugin_folder(name)
|
plugin_folder = self.find_plugin_folder(name)
|
||||||
assert plugin_folder is not None
|
assert plugin_folder is not None
|
||||||
plugin_dir = path.join(self.plugin_path, plugin_folder)
|
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)
|
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
|
||||||
if ret:
|
if ret:
|
||||||
logger.info(f"Installed {name} (Version: {version})")
|
logger.info(f"Installed {name} (Version: {version})")
|
||||||
@@ -251,6 +264,7 @@ class PluginBrowser:
|
|||||||
logger.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
|
logger.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
|
||||||
if self.loader.watcher:
|
if self.loader.watcher:
|
||||||
self.loader.watcher.disabled = False
|
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):
|
async def request_plugin_install(self, artifact: str, name: str, version: str, hash: str, install_type: PluginInstallType):
|
||||||
request_id = str(time())
|
request_id = str(time())
|
||||||
|
|||||||
@@ -231,6 +231,15 @@
|
|||||||
"store_testing_warning": {
|
"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.",
|
"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"
|
"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": {
|
"StoreSelect": {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ConfirmModal, Navigation, QuickAccessTab } from '@decky/ui';
|
import { ConfirmModal, Navigation, ProgressBarWithInfo, QuickAccessTab } from '@decky/ui';
|
||||||
import { FC, useMemo, useState } from 'react';
|
import { FC, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FaCheck, FaDownload } from 'react-icons/fa';
|
||||||
|
|
||||||
import { InstallType } from '../../plugin';
|
import { InstallType } from '../../plugin';
|
||||||
|
|
||||||
@@ -27,8 +28,42 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
|
|||||||
closeModal,
|
closeModal,
|
||||||
}) => {
|
}) => {
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
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();
|
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
|
// 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
|
// 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 => {
|
const installTypeGrouped = useMemo((): TitleTranslationMapping => {
|
||||||
@@ -66,7 +101,10 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={i} style={{ display: 'flex', flexDirection: 'column' }}>
|
<li key={i} style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
<div>{description}</div>
|
<span>
|
||||||
|
{description}{' '}
|
||||||
|
{(pluginsCompleted.includes(name) && <FaCheck />) || (name === pluginInProgress && <FaDownload />)}
|
||||||
|
</span>
|
||||||
{hash === 'False' && (
|
{hash === 'False' && (
|
||||||
<div style={{ color: 'red', paddingLeft: '10px' }}>{t('PluginInstallModal.no_hash')}</div>
|
<div style={{ color: 'red', paddingLeft: '10px' }}>{t('PluginInstallModal.no_hash')}</div>
|
||||||
)}
|
)}
|
||||||
@@ -74,6 +112,17 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</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>
|
</div>
|
||||||
</ConfirmModal>
|
</ConfirmModal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ConfirmModal, Navigation, QuickAccessTab } from '@decky/ui';
|
import { ConfirmModal, Navigation, ProgressBarWithInfo, QuickAccessTab } from '@decky/ui';
|
||||||
import { FC, useState } from 'react';
|
import { FC, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import TranslationHelper, { TranslationClass } from '../../utils/TranslationHelper';
|
import TranslationHelper, { TranslationClass } from '../../utils/TranslationHelper';
|
||||||
@@ -24,8 +24,26 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
|
|||||||
closeModal,
|
closeModal,
|
||||||
}) => {
|
}) => {
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [percentage, setPercentage] = useState<number>(0);
|
||||||
|
const [downloadInfo, setDownloadInfo] = useState<string | null>(null);
|
||||||
const { t } = useTranslation();
|
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 (
|
return (
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
bOKDisabled={loading}
|
bOKDisabled={loading}
|
||||||
@@ -80,6 +98,14 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
|
|||||||
install_type={installType}
|
install_type={installType}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{loading && (
|
||||||
|
<ProgressBarWithInfo
|
||||||
|
layout="inline"
|
||||||
|
bottomSeparator="none"
|
||||||
|
nProgress={percentage}
|
||||||
|
sOperationText={downloadInfo}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>}
|
{hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>}
|
||||||
</ConfirmModal>
|
</ConfirmModal>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user