From 44897b3e7f5cf12b03d7be71f0615619bddff9b8 Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:41:33 +0200 Subject: [PATCH] feat(ui): better downloading information --- packages/oneclient_app/src/bridge/runtime.rs | 16 +++++++- .../src/view/onboarding/downloading/mod.rs | 20 ++++++++-- .../oneclient_core/src/bundles/install.rs | 15 +++++--- .../oneclient_core/src/bundles/updates.rs | 38 ++++++++++++++++++- .../src/packages/modpack/mrpack.rs | 3 +- .../src/packages/provider/http.rs | 9 ++++- .../src/packages/store/download.rs | 14 +++++-- .../oneclient_core/src/packages/store/mod.rs | 8 +++- 8 files changed, 103 insertions(+), 20 deletions(-) diff --git a/packages/oneclient_app/src/bridge/runtime.rs b/packages/oneclient_app/src/bridge/runtime.rs index 067aa53b..7a8b622c 100644 --- a/packages/oneclient_app/src/bridge/runtime.rs +++ b/packages/oneclient_app/src/bridge/runtime.rs @@ -804,16 +804,28 @@ async fn install_package( .get_version(project_id, version_id, &state.services) .await?; - PackageStore::install_to_cluster( + let session = oneclient_core::notification::GroupedProgressSession::start( + &state.services.notifier, + format!("Installing {}", project.name), + ); + let size = version.primary_file().map(|f| f.size).unwrap_or(0); + let child = session.child(project.name.clone(), size); + + let result = PackageStore::install_to_cluster( provider, &project, &version, cluster_id, false, false, + Some(&child), &state.services, ) - .await?; + .await; + + child.finish(); + session.finish(); + result?; Ok(()) } diff --git a/packages/oneclient_app/src/view/onboarding/downloading/mod.rs b/packages/oneclient_app/src/view/onboarding/downloading/mod.rs index 09b49b8b..aa2414e5 100644 --- a/packages/oneclient_app/src/view/onboarding/downloading/mod.rs +++ b/packages/oneclient_app/src/view/onboarding/downloading/mod.rs @@ -57,6 +57,7 @@ struct TaskLine { struct GroupedAgg { children: HashMap, done_units: u64, + carried: u64, } impl GroupedAgg { @@ -82,7 +83,8 @@ impl GroupedAgg { } fn downloaded_bytes(&self) -> u64 { - self.done_units + self.carried + + self.done_units + self .children .values() @@ -260,8 +262,12 @@ impl Component for OnboardingDownloading { let is_running = *running.read(); let is_complete = *complete.read(); - let global = if total == 0 { - if is_complete { 100.0 } else { 0.0 } + let global = if is_complete { + 100.0 + } else if total == 0 { + 0.0 + } else if total_estimate_now > 0 { + (agg_now.downloaded_bytes() as f32 / total_estimate_now as f32 * 100.0).clamp(0.0, 99.0) } else { ((done as f32 + agg_now.fraction()) / total as f32 * 100.0).clamp(0.0, 100.0) }; @@ -531,7 +537,7 @@ fn run_install_batch(plans: Vec, predownload: bool, handles: Instal let downloaded = agg.peek().downloaded_bytes(); let delta = downloaded.saturating_sub(prev); - let inst = delta as f64 / 0.5; + let inst = delta as f64; speed = if speed <= 0.0 { inst @@ -755,6 +761,12 @@ fn apply_grouped(agg: &mut GroupedAgg, event: GroupedProgressEvent) { } } GroupedProgressEvent::End { .. } => { + agg.carried += agg.done_units + + agg + .children + .values() + .map(|t| t.current.min(t.total)) + .sum::(); agg.children.clear(); agg.done_units = 0; } diff --git a/packages/oneclient_core/src/bundles/install.rs b/packages/oneclient_core/src/bundles/install.rs index 4b2f9921..a92c0397 100644 --- a/packages/oneclient_core/src/bundles/install.rs +++ b/packages/oneclient_core/src/bundles/install.rs @@ -10,7 +10,7 @@ use crate::bundles::error::BundleError; use crate::bundles::manager::BundlesManager; use crate::bundles::overrides; use crate::bundles::types::{BundleArchive, BundleFile, BundleFileKind}; -use crate::notification::{GroupedProgressSession, TaskPhase}; +use crate::notification::{GroupedProgressChild, GroupedProgressSession, TaskPhase}; use crate::packages::domain::{ContentType, GameLoader}; use crate::packages::error::PackageError; use crate::packages::store::{PackageStore, unlink_cluster_file}; @@ -26,6 +26,7 @@ pub async fn install_package_from_bundle( cluster_id: i64, bundle_name: &str, skip_compatibility: bool, + child: Option<&GroupedProgressChild>, services: &LauncherServices, ) -> LauncherResult { let cluster = PackageStore::get_cluster(cluster_id, services).await?; @@ -63,13 +64,14 @@ pub async fn install_package_from_bundle( cluster_id, skip_compatibility, false, + child, services, ) .await?; artifact.hash } BundleFileKind::External(ext) => { - install_external(ext, &cluster, skip_compatibility, services).await? + install_external(ext, &cluster, skip_compatibility, child, services).await? } }; @@ -90,9 +92,10 @@ async fn install_external( ext: &ExternalFile, cluster: &ClusterRow, skip_compatibility: bool, + child: Option<&GroupedProgressChild>, services: &LauncherServices, ) -> LauncherResult { - let artifact = crate::packages::store::download_external(ext, false, services).await?; + let artifact = crate::packages::store::download_external(ext, false, child, services).await?; PackageStore::link_artifact(&artifact, cluster, Some(&ext.name), services).await?; let _ = skip_compatibility; @@ -197,8 +200,8 @@ pub async fn install_enabled_bundle_files( let bundle_name = &bundle_name; let results = futures_util::stream::iter(to_install.into_iter().map(|file| async move { let child = progress.map(|p| { - let c = p.child(format!("Mod {}", file.display_name()), 1); - c.set_phase(TaskPhase::Installing); + let c = p.child(format!("Mod {}", file.display_name()), file.size.max(1)); + c.set_phase(TaskPhase::Downloading); c }); @@ -207,11 +210,13 @@ pub async fn install_enabled_bundle_files( cluster_id, bundle_name, skip_compatibility, + child.as_ref(), services, ) .await; if let Some(child) = child { + child.set_phase(TaskPhase::Installing); child.finish(); } (file.display_name(), result) diff --git a/packages/oneclient_core/src/bundles/updates.rs b/packages/oneclient_core/src/bundles/updates.rs index c568cf00..6464371e 100644 --- a/packages/oneclient_core/src/bundles/updates.rs +++ b/packages/oneclient_core/src/bundles/updates.rs @@ -328,8 +328,21 @@ pub async fn apply_bundle_updates( } } + let session = (!check.updates_available.is_empty() || !check.additions_available.is_empty()) + .then(|| { + crate::notification::GroupedProgressSession::start( + &services.notifier, + "Updating bundle content", + ) + }); + for update in check.updates_available { - match apply_single_update(&update, &overrides, services).await { + let child = session.as_ref().map(|s| { + let c = s.child(update.new_file.display_name(), update.new_file.size.max(1)); + c.set_phase(crate::notification::TaskPhase::Downloading); + c + }); + match apply_single_update(&update, &overrides, child.as_ref(), services).await { Ok(_) => result.updates_applied.push(update), Err(err) => result.updates_failed.push(err.to_string()), } @@ -337,12 +350,21 @@ pub async fn apply_bundle_updates( for addition in check.additions_available { let file_id = addition.new_file.kind.package_id(); - match apply_single_addition(&addition, &overrides, services).await { + let child = session.as_ref().map(|s| { + let c = s.child(addition.new_file.display_name(), addition.new_file.size.max(1)); + c.set_phase(crate::notification::TaskPhase::Downloading); + c + }); + match apply_single_addition(&addition, &overrides, child.as_ref(), services).await { Ok(_) => result.additions_applied.push(addition), Err(err) => result.additions_failed.push(format!("{file_id}: {err:#}")), } } + if let Some(session) = session { + session.finish(); + } + let has_changes = !result.updates_applied.is_empty() || !result.removals_applied.is_empty() || !result.additions_applied.is_empty(); @@ -389,6 +411,7 @@ pub async fn apply_bundle_updates( async fn apply_single_update( update: &BundlePackageUpdate, overrides: &[ClusterBundleOverrideRow], + child: Option<&crate::notification::GroupedProgressChild>, services: &LauncherServices, ) -> LauncherResult<()> { let hash = install_package_from_bundle( @@ -396,9 +419,14 @@ async fn apply_single_update( update.cluster_id, &update.bundle_name, true, + child, services, ) .await?; + if let Some(child) = child { + child.set_phase(crate::notification::TaskPhase::Installing); + child.finish(); + } let file_id = update.new_file.kind.package_id(); if should_be_disabled(&update.bundle_name, &file_id, overrides) { @@ -415,6 +443,7 @@ async fn apply_single_update( async fn apply_single_addition( addition: &BundlePackageAddition, overrides: &[ClusterBundleOverrideRow], + child: Option<&crate::notification::GroupedProgressChild>, services: &LauncherServices, ) -> LauncherResult<()> { let hash = install_package_from_bundle( @@ -422,9 +451,14 @@ async fn apply_single_addition( addition.cluster_id, &addition.bundle_name, true, + child, services, ) .await?; + if let Some(child) = child { + child.set_phase(crate::notification::TaskPhase::Installing); + child.finish(); + } let file_id = addition.new_file.kind.package_id(); if should_be_disabled(&addition.bundle_name, &file_id, overrides) { diff --git a/packages/oneclient_core/src/packages/modpack/mrpack.rs b/packages/oneclient_core/src/packages/modpack/mrpack.rs index 30574bbd..f13c1cf5 100644 --- a/packages/oneclient_core/src/packages/modpack/mrpack.rs +++ b/packages/oneclient_core/src/packages/modpack/mrpack.rs @@ -168,6 +168,7 @@ async fn install_mrpack_file( &project, &version, false, + None, services, ) .await?; @@ -189,7 +190,7 @@ async fn install_mrpack_file( content_type, }; - let artifact = store::download_external(&external, false, services).await?; + let artifact = store::download_external(&external, false, None, services).await?; PackageStore::link_artifact(&artifact, cluster, Some(&file_name), services).await?; Ok(()) diff --git a/packages/oneclient_core/src/packages/provider/http.rs b/packages/oneclient_core/src/packages/provider/http.rs index b1844f60..88f3547b 100644 --- a/packages/oneclient_core/src/packages/provider/http.rs +++ b/packages/oneclient_core/src/packages/provider/http.rs @@ -1,7 +1,8 @@ use reqwest::Method; use serde::de::DeserializeOwned; -use crate::http::{RequestClient, RequestError, ResponseOptions}; +use crate::http::{RequestClient, RequestError, ResponseNotifyOptions, ResponseOptions}; +use crate::notification::GroupedProgressChild; use crate::state::LauncherServices; pub async fn fetch_json( @@ -33,10 +34,14 @@ pub async fn download_url( client: &RequestClient, url: &str, dest: impl AsRef + Send, + child: Option<&GroupedProgressChild>, services: &LauncherServices, ) -> Result<(), RequestError> { let request = reqwest::Request::new(Method::GET, url.parse()?); + let options = ResponseOptions { + notify: child.map(|c| ResponseNotifyOptions::grouped(c.clone())), + }; client - .download_file(request, dest, ResponseOptions::default(), services) + .download_file(request, dest, options, services) .await } diff --git a/packages/oneclient_core/src/packages/store/download.rs b/packages/oneclient_core/src/packages/store/download.rs index 8a408996..e257e422 100644 --- a/packages/oneclient_core/src/packages/store/download.rs +++ b/packages/oneclient_core/src/packages/store/download.rs @@ -6,6 +6,7 @@ use crate::LauncherResult; use crate::crypto::{normalize_hash, sha1_file}; use crate::packages::domain::{ContentType, ProviderId}; use crate::packages::error::PackageError; +use crate::notification::{GroupedProgressChild, TaskPhase}; use crate::packages::provider::http::download_url; use crate::packages::types::{ExternalFile, VersionDetail, VersionFile}; use crate::state::LauncherServices; @@ -14,6 +15,7 @@ pub async fn ensure_artifact_file( hash: &str, url: &str, dest: &std::path::Path, + child: Option<&GroupedProgressChild>, services: &LauncherServices, ) -> LauncherResult { polyio::create_dir_all(dest.parent().unwrap_or(dest)).await?; @@ -26,8 +28,11 @@ pub async fn ensure_artifact_file( tokio::fs::remove_file(dest).await?; } - download_url(&services.requester, url, dest, services).await?; + download_url(&services.requester, url, dest, child, services).await?; + if let Some(child) = child { + child.set_phase(TaskPhase::Verifying); + } let actual = sha1_file(dest).await?; if normalize_hash(&actual) != normalize_hash(hash) { return Err(PackageError::HashMismatch { @@ -40,6 +45,7 @@ pub async fn ensure_artifact_file( Ok(tokio::fs::metadata(dest).await?.len()) } +#[allow(clippy::too_many_arguments)] pub async fn download_version_file( provider: ProviderId, project_id: &str, @@ -47,6 +53,7 @@ pub async fn download_version_file( content_type: ContentType, file: &VersionFile, force: bool, + child: Option<&GroupedProgressChild>, services: &LauncherServices, ) -> LauncherResult { let hash = normalize_hash(&file.sha1); @@ -68,7 +75,7 @@ pub async fn download_version_file( &file.file_name, )?; - let size = ensure_artifact_file(&hash, &file.url, &dest, services).await?; + let size = ensure_artifact_file(&hash, &file.url, &dest, child, services).await?; let stored_path = relative_cache_path(&dest)?; let published_at = version.published.to_rfc3339(); @@ -103,6 +110,7 @@ pub async fn download_version_file( pub async fn download_external( file: &ExternalFile, force: bool, + child: Option<&GroupedProgressChild>, services: &LauncherServices, ) -> LauncherResult { let hash = normalize_hash(&file.sha1); @@ -120,7 +128,7 @@ pub async fn download_external( &hash[..hash.len().min(16)], &file.name, )?; - let size = ensure_artifact_file(&hash, &file.url, &dest, services).await?; + let size = ensure_artifact_file(&hash, &file.url, &dest, child, services).await?; let stored_path = relative_cache_path(&dest)?; artifact_dao::insert_artifact( diff --git a/packages/oneclient_core/src/packages/store/mod.rs b/packages/oneclient_core/src/packages/store/mod.rs index e4495016..1f0a3262 100644 --- a/packages/oneclient_core/src/packages/store/mod.rs +++ b/packages/oneclient_core/src/packages/store/mod.rs @@ -13,6 +13,7 @@ use super::domain::{ContentType, GameLoader, ProviderId}; use super::error::PackageError; use super::types::{CachedArtifact, ProjectDetail, ProviderReleaseInfo, VersionDetail, LinkedArtifactInfo}; use crate::crypto::{normalize_hash, sha1_file}; +use crate::notification::GroupedProgressChild; use crate::state::LauncherServices; use crate::{LauncherError, LauncherResult}; use std::path::Path; @@ -45,6 +46,7 @@ impl PackageStore { project: &ProjectDetail, version: &VersionDetail, force: bool, + child: Option<&GroupedProgressChild>, services: &LauncherServices, ) -> LauncherResult { let file = version.primary_file().ok_or(PackageError::NoPrimaryFile)?; @@ -56,11 +58,13 @@ impl PackageStore { project.content_type, file, force, + child, services, ) .await } + #[allow(clippy::too_many_arguments)] pub async fn install_to_cluster( provider_id: ProviderId, project: &ProjectDetail, @@ -68,6 +72,7 @@ impl PackageStore { cluster_id: i64, skip_compatibility: bool, force_download: bool, + child: Option<&GroupedProgressChild>, services: &LauncherServices, ) -> LauncherResult { let cluster = Self::get_cluster(cluster_id, services).await?; @@ -81,6 +86,7 @@ impl PackageStore { project, version, force_download, + child, services, ) .await?; @@ -286,7 +292,7 @@ impl PackageStore { let project = provider.get_project(project_id, services).await?; - Self::download_and_cache(provider_id, &project, &version, false, services).await + Self::download_and_cache(provider_id, &project, &version, false, None, services).await } }