From 3d24ac8b576b180a9f1f3482b826f5a2e7afe041 Mon Sep 17 00:00:00 2001 From: Fiery Imp Date: Thu, 24 Apr 2025 14:57:50 -0400 Subject: [PATCH] Claude: Add estimated remaining time for extraction --- rust/src/app.rs | 3 +- rust/src/installer/extractor.rs | 92 +++++++++++++++++++++++++--- rust/src/ui/download_progress.rs | 11 +--- rust/src/ui/installation_progress.rs | 23 +++++-- rust/src/ui/mod.rs | 17 ++++- 5 files changed, 120 insertions(+), 26 deletions(-) diff --git a/rust/src/app.rs b/rust/src/app.rs index 66a1bd9..0d0868c 100644 --- a/rust/src/app.rs +++ b/rust/src/app.rs @@ -297,9 +297,10 @@ impl SinfarInstallerApp<> { } // Create extraction manager with progress callback - let extractor = crate::installer::extractor::ExtractionManager::new(move |progress: f32, _filename: &str| { + let extractor = crate::installer::extractor::ExtractionManager::new(move |progress: f32, _filename: &str, remaining: std::time::Duration| { if let Ok(mut app_state) = progress_state.lock() { app_state.progress = progress; + app_state.estimated_remaining = remaining; eframe_ctx.request_repaint(); } }); diff --git a/rust/src/installer/extractor.rs b/rust/src/installer/extractor.rs index 7e87cd0..07e9791 100644 --- a/rust/src/installer/extractor.rs +++ b/rust/src/installer/extractor.rs @@ -10,19 +10,49 @@ use std::io::{Read, Seek}; use crate::app::GameType; pub struct ExtractionManager { - progress_callback: Arc>>, + progress_callback: Arc>>, + last_update: Arc>, + last_processed: Arc>, } impl ExtractionManager { pub fn new(progress_callback: F) -> Self where - F: Fn(f32, &str) + Send + Sync + 'static + F: Fn(f32, &str, std::time::Duration) + Send + Sync + 'static { Self { progress_callback: Arc::new(Mutex::new(Box::new(progress_callback))), + last_update: Arc::new(Mutex::new(std::time::Instant::now())), + last_processed: Arc::new(Mutex::new(0)), } } + // Helper function to calculate estimated time remaining + async fn update_progress(&self, processed: u64, total: u64, filename: &str) { + let progress = processed as f32 / total as f32; + let now = std::time::Instant::now(); + + let mut last_update = self.last_update.lock().await; + let mut last_processed = self.last_processed.lock().await; + + let elapsed = now.duration_since(*last_update).as_secs_f64(); + let bytes_since_last = processed - *last_processed; + let speed = bytes_since_last as f64 / elapsed; + + let remaining_bytes = total - processed; + let estimated_remaining = if speed > 0.0 { + std::time::Duration::from_secs_f64(remaining_bytes as f64 / speed) + } else { + std::time::Duration::from_secs(0) + }; + + *last_update = now; + *last_processed = processed; + + let callback = self.progress_callback.lock().await; + callback(progress, filename, estimated_remaining); + } + /// Extract a 7z archive with progress reporting pub async fn extract_7z(&self, archive_path: &Path, output_dir: &Path) -> Result<()> { info!("Starting extraction of 7z archive"); @@ -61,6 +91,8 @@ impl ExtractionManager { let archive_path = archive_path.to_path_buf(); let output_dir = output_dir.to_path_buf(); let progress_callback = self.progress_callback.clone(); + let last_update = self.last_update.clone(); + let last_processed = self.last_processed.clone(); info!("Starting blocking extraction task"); tokio::task::spawn_blocking(move || -> Result<()> { @@ -73,7 +105,9 @@ impl ExtractionManager { bytes_read: u64, total_size: u64, last_progress: f32, - progress_callback: Arc>>, + progress_callback: Arc>>, + last_update: Arc>, + last_processed: Arc>, } impl Read for ProgressReader { @@ -88,9 +122,29 @@ impl ExtractionManager { if progress - self.last_progress >= 0.01 { self.last_progress = progress; let progress_callback_clone = self.progress_callback.clone(); + let last_update_clone = self.last_update.clone(); + let last_processed_clone = self.last_processed.clone(); tokio::runtime::Handle::current().block_on(async move { let callback = progress_callback_clone.lock().await; - callback(progress, "Extracting..."); + let now = std::time::Instant::now(); + let mut last_update = last_update_clone.lock().await; + let mut last_processed = last_processed_clone.lock().await; + + let elapsed = now.duration_since(*last_update).as_secs_f64(); + let bytes_since_last = self.bytes_read - *last_processed; + let speed = bytes_since_last as f64 / elapsed; + + let remaining_bytes = self.total_size - self.bytes_read; + let estimated_remaining = if speed > 0.0 { + std::time::Duration::from_secs_f64(remaining_bytes as f64 / speed) + } else { + std::time::Duration::from_secs(0) + }; + + *last_update = now; + *last_processed = self.bytes_read; + + callback(progress, "Extracting...", estimated_remaining); }); } @@ -110,6 +164,8 @@ impl ExtractionManager { total_size: archive_size, last_progress: 0.0, progress_callback: progress_callback.clone(), + last_update: last_update.clone(), + last_processed: last_processed.clone(), }; info!("Starting decompression to: {}", output_dir.display()); @@ -127,7 +183,7 @@ impl ExtractionManager { let progress_callback_clone = progress_callback.clone(); tokio::runtime::Handle::current().block_on(async move { let callback = progress_callback_clone.lock().await; - callback(1.0, "Complete"); + callback(1.0, "Complete", std::time::Duration::from_secs(0)); }); info!("Extraction task completed successfully"); @@ -151,6 +207,8 @@ impl ExtractionManager { let archive_path = archive_path.to_path_buf(); let output_dir = output_dir.to_path_buf(); let progress_callback = self.progress_callback.clone(); + let last_update = self.last_update.clone(); + let last_processed = self.last_processed.clone(); tokio::task::spawn_blocking(move || -> Result<()> { let file = fs::File::open(&archive_path)?; @@ -217,9 +275,29 @@ impl ExtractionManager { // Update progress less frequently to reduce overhead if i % 10 == 0 || progress >= 1.0 { let progress_callback_clone = progress_callback.clone(); + let last_update_clone = last_update.clone(); + let last_processed_clone = last_processed.clone(); tokio::runtime::Handle::current().block_on(async move { let callback = progress_callback_clone.lock().await; - callback(progress, filename); + let now = std::time::Instant::now(); + let mut last_update = last_update_clone.lock().await; + let mut last_processed = last_processed_clone.lock().await; + + let elapsed = now.duration_since(*last_update).as_secs_f64(); + let bytes_since_last = processed_bytes - *last_processed; + let speed = bytes_since_last as f64 / elapsed; + + let remaining_bytes = total_size - processed_bytes; + let estimated_remaining = if speed > 0.0 { + std::time::Duration::from_secs_f64(remaining_bytes as f64 / speed) + } else { + std::time::Duration::from_secs(0) + }; + + *last_update = now; + *last_processed = processed_bytes; + + callback(progress, filename, estimated_remaining); }); } } @@ -238,7 +316,7 @@ impl ExtractionManager { let progress_callback_clone = progress_callback.clone(); tokio::runtime::Handle::current().block_on(async move { let callback = progress_callback_clone.lock().await; - callback(1.0, "Complete"); + callback(1.0, "Complete", std::time::Duration::from_secs(0)); }); info!("ZIP extraction completed successfully"); diff --git a/rust/src/ui/download_progress.rs b/rust/src/ui/download_progress.rs index 192eefc..1bdc04a 100644 --- a/rust/src/ui/download_progress.rs +++ b/rust/src/ui/download_progress.rs @@ -1,7 +1,7 @@ pub mod download_progress { use eframe::egui::{Ui, ProgressBar, Color32}; use crate::app::SinfarInstallerApp; - use std::time::Duration; + use crate::ui::format_duration; fn format_bytes(bytes: f64) -> String { const KB: f64 = 1024.0; @@ -16,15 +16,6 @@ pub mod download_progress { } } - fn format_duration(duration: Duration) -> String { - let secs = duration.as_secs(); - if secs >= 3600 { - format!("{:02}:{:02}:{:02} remaining", secs / 3600, (secs % 3600) / 60, secs % 60) - } else { - format!("{:02}:{:02} remaining", secs / 60, secs % 60) - } - } - pub fn render(ui: &mut Ui, app: &mut SinfarInstallerApp) { ui.heading("Downloading Sinfar Custom Content"); ui.add_space(10.0); diff --git a/rust/src/ui/installation_progress.rs b/rust/src/ui/installation_progress.rs index 9d512ce..5d5188c 100644 --- a/rust/src/ui/installation_progress.rs +++ b/rust/src/ui/installation_progress.rs @@ -1,18 +1,31 @@ pub mod installation_progress { - use eframe::egui::{Ui, ProgressBar}; + use eframe::egui::{Ui, ProgressBar, Color32}; use crate::app::SinfarInstallerApp; + use crate::ui::format_duration; pub fn render(ui: &mut Ui, app: &mut SinfarInstallerApp) { ui.heading("Installing Sinfar Custom Content"); ui.add_space(10.0); - ui.horizontal(|ui| { - ui.label("Extracting files:"); - ui.add(ProgressBar::new(app.extraction_progress).show_percentage()); + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.label("Extracting files:"); + ui.add(ProgressBar::new(app.extraction_progress).show_percentage()); + }); + + if let Ok(state) = app.download_state.lock() { + if state.completed { + ui.colored_label(Color32::GREEN, "Installation Complete"); + } else if app.extraction_progress > 0.0 && app.extraction_progress < 1.0 { + ui.horizontal(|ui| { + ui.label(format_duration(state.estimated_remaining)); + }); + } + } }); if let Some(error) = &app.install_error { - ui.colored_label(eframe::egui::Color32::RED, error); + ui.colored_label(Color32::RED, error); if ui.button("Retry").clicked() { app.install_error = None; app.start_installation(); diff --git a/rust/src/ui/mod.rs b/rust/src/ui/mod.rs index 2c4beb7..589e609 100644 --- a/rust/src/ui/mod.rs +++ b/rust/src/ui/mod.rs @@ -1,4 +1,15 @@ -pub mod game_selection; -pub mod path_selection; pub mod download_progress; -pub mod installation_progress; \ No newline at end of file +pub mod game_selection; +pub mod installation_progress; +pub mod path_selection; + +use std::time::Duration; + +pub fn format_duration(duration: Duration) -> String { + let secs = duration.as_secs(); + if secs >= 3600 { + format!("{:02}h{:02}m{:02}s estimated remaining time", secs / 3600, (secs % 3600) / 60, secs % 60) + } else { + format!("{:02}m{:02}s estimated remaining time", secs / 60, secs % 60) + } +} \ No newline at end of file