From a92d4c0267732e05fbfee96c2b73e4e518f5d80b Mon Sep 17 00:00:00 2001 From: jp64k <122999544+jp64k@users.noreply.github.com> Date: Tue, 16 Dec 2025 22:50:55 +0100 Subject: [PATCH] Unified DLS+ETA progress labels and implemented ETA tracking for extraction, installation, and copy operations - Removed separate DLS+ETA labels, unified into a clearer single label - Repositioned and resized that label slightly to avoid top of label getting cut off - Added guards to prevent brief progress bar flashes during multi-file downloads - Added ETA for file extraction, APK installation, and OBB copy operations by tracking elapsed time and calculating a smoothed ETA based on the rate of progress --- ADB.cs | 153 +++++++++++++++++++++----- MainForm.Designer.cs | 257 ++++++++++++++++++++----------------------- MainForm.cs | 194 ++++++++++++++++++++++++-------- MainForm.resx | 6 +- Utilities/Zip.cs | 96 ++++++++++++++-- 5 files changed, 484 insertions(+), 222 deletions(-) diff --git a/ADB.cs b/ADB.cs index 2acb0d8..7798c4f 100644 --- a/ADB.cs +++ b/ADB.cs @@ -154,13 +154,13 @@ namespace AndroidSideloader // Copies and installs an APK with real-time progress reporting using AdvancedSharpAdbClient public static async Task SideloadWithProgressAsync( string path, - Action progressCallback = null, + Action progressCallback = null, // Now includes ETA Action statusCallback = null, string packagename = "", string gameName = "") { statusCallback?.Invoke("Installing APK..."); - progressCallback?.Invoke(0); + progressCallback?.Invoke(0, null); try { @@ -180,21 +180,81 @@ namespace AndroidSideloader int lastReportedPercent = -1; const int ThrottleMs = 100; // Update UI at most every 100ms + // ETA tracking with smoothing + DateTime installStart = DateTime.UtcNow; + int etaLastPercent = 0; + DateTime etaLastPercentTime = DateTime.UtcNow; + double smoothedSecondsPerPercent = 0; + TimeSpan? lastReportedEta = null; + const double SmoothingAlpha = 0.15; // Lower = smoother, less responsive + const double EtaChangeThreshold = 0.10; // Only update if ETA changes by >10% + // Create install progress handler Action installProgress = (args) => { int percent = 0; string status = null; + TimeSpan? eta = null; switch (args.State) { case PackageInstallProgressState.Preparing: percent = 0; status = "Preparing..."; + installStart = DateTime.UtcNow; + etaLastPercent = 0; + etaLastPercentTime = installStart; + smoothedSecondsPerPercent = 0; + lastReportedEta = null; break; case PackageInstallProgressState.Uploading: percent = (int)Math.Round(args.UploadProgress); - status = $"Installing · {args.UploadProgress:F0}%"; + + // Calculate ETA with smoothing + if (percent > etaLastPercent && percent < 100) + { + var now = DateTime.UtcNow; + double secondsForThisChunk = (now - etaLastPercentTime).TotalSeconds; + int percentGained = percent - etaLastPercent; + + if (percentGained > 0 && secondsForThisChunk > 0) + { + double secondsPerPercent = secondsForThisChunk / percentGained; + + // Exponential smoothing + if (smoothedSecondsPerPercent == 0) + smoothedSecondsPerPercent = secondsPerPercent; + else + smoothedSecondsPerPercent = SmoothingAlpha * secondsPerPercent + (1 - SmoothingAlpha) * smoothedSecondsPerPercent; + + int remainingPercent = 100 - percent; + double etaSeconds = remainingPercent * smoothedSecondsPerPercent; + var newEta = TimeSpan.FromSeconds(Math.Max(0, etaSeconds)); + + // Only update if significant change + if (!lastReportedEta.HasValue || + Math.Abs(newEta.TotalSeconds - lastReportedEta.Value.TotalSeconds) / Math.Max(1, lastReportedEta.Value.TotalSeconds) > EtaChangeThreshold) + { + eta = newEta; + lastReportedEta = eta; + } + else + { + eta = lastReportedEta; // Keep previous ETA + } + + etaLastPercent = percent; + etaLastPercentTime = now; + } + } + else + { + eta = lastReportedEta; + } + + status = eta.HasValue && eta.Value.TotalSeconds > 0 + ? $"Installing · {percent}% · ETA: {eta.Value:mm\\:ss}" + : $"Installing · {percent}%"; break; case PackageInstallProgressState.Installing: percent = 100; @@ -209,17 +269,17 @@ namespace AndroidSideloader break; } - // Throttle updates: only update if enough time passed or percent changed - var now = DateTime.UtcNow; - bool shouldUpdate = (now - lastProgressUpdate).TotalMilliseconds >= ThrottleMs + // Throttle updates + var updateNow = DateTime.UtcNow; + bool shouldUpdate = (updateNow - lastProgressUpdate).TotalMilliseconds >= ThrottleMs || percent != lastReportedPercent || args.State != PackageInstallProgressState.Uploading; if (shouldUpdate) { - lastProgressUpdate = now; + lastProgressUpdate = updateNow; lastReportedPercent = percent; - progressCallback?.Invoke(percent); + progressCallback?.Invoke(percent, eta); if (status != null) statusCallback?.Invoke(status); } }; @@ -230,7 +290,7 @@ namespace AndroidSideloader packageManager.InstallPackage(path, installProgress); }); - progressCallback?.Invoke(100); + progressCallback?.Invoke(100, null); statusCallback?.Invoke(""); return new ProcessOutput($"{gameName}: Success\n"); @@ -269,26 +329,22 @@ namespace AndroidSideloader var client = GetAdbClient(); var packageManager = new PackageManager(client, device); - // Backup save data statusCallback?.Invoke("Backing up save data..."); _ = RunAdbCommandToString($"pull \"/sdcard/Android/data/{MainForm.CurrPCKG}\" \"{Environment.CurrentDirectory}\""); - // Uninstall statusCallback?.Invoke("Uninstalling old version..."); packageManager.UninstallPackage(packagename); - // Reinstall with progress statusCallback?.Invoke("Reinstalling game..."); Action reinstallProgress = (args) => { if (args.State == PackageInstallProgressState.Uploading) { - progressCallback?.Invoke((int)Math.Round(args.UploadProgress)); + progressCallback?.Invoke((int)Math.Round(args.UploadProgress), null); } }; packageManager.InstallPackage(path, reinstallProgress); - // Restore save data statusCallback?.Invoke("Restoring save data..."); _ = RunAdbCommandToString($"push \"{Environment.CurrentDirectory}\\{MainForm.CurrPCKG}\" /sdcard/Android/data/"); @@ -298,7 +354,7 @@ namespace AndroidSideloader Directory.Delete(directoryToDelete, true); } - progressCallback?.Invoke(100); + progressCallback?.Invoke(100, null); return new ProcessOutput($"{gameName}: Reinstall: Success\n", ""); } catch (Exception reinstallEx) @@ -314,7 +370,7 @@ namespace AndroidSideloader // Copies OBB folder with real-time progress reporting using AdvancedSharpAdbClient public static async Task CopyOBBWithProgressAsync( string localPath, - Action progressCallback = null, + Action progressCallback = null, // Now includes ETA Action statusCallback = null, string gameName = "") { @@ -337,7 +393,7 @@ namespace AndroidSideloader string remotePath = $"/sdcard/Android/obb/{folderName}"; statusCallback?.Invoke($"Preparing: {folderName}"); - progressCallback?.Invoke(0); + progressCallback?.Invoke(0, null); // Delete existing OBB folder and create new one ExecuteShellCommand(client, device, $"rm -rf \"{remotePath}\""); @@ -353,6 +409,16 @@ namespace AndroidSideloader int lastReportedPercent = -1; const int ThrottleMs = 100; // Update UI at most every 100ms + // ETA tracking with smoothing + DateTime copyStart = DateTime.UtcNow; + int etaLastPercent = 0; + DateTime etaLastPercentTime = DateTime.UtcNow; + double smoothedSecondsPerPercent = 0; + TimeSpan? lastReportedEta = null; + TimeSpan? currentEta = null; + const double SmoothingAlpha = 0.15; // Lower = smoother, less responsive + const double EtaChangeThreshold = 0.10; // 10% change threshold + statusCallback?.Invoke($"Copying: {folderName}"); using (var syncService = new SyncService(client, device)) @@ -385,16 +451,54 @@ namespace AndroidSideloader int overallPercentInt = (int)Math.Round(overallPercent); overallPercentInt = Math.Max(0, Math.Min(100, overallPercentInt)); - // Throttle updates: only update if enough time passed or percent changed - var now = DateTime.UtcNow; - bool shouldUpdate = (now - lastProgressUpdate).TotalMilliseconds >= ThrottleMs + // Calculate ETA with smoothing + if (overallPercentInt > etaLastPercent && overallPercentInt < 100) + { + var now = DateTime.UtcNow; + double secondsForThisChunk = (now - etaLastPercentTime).TotalSeconds; + int percentGained = overallPercentInt - etaLastPercent; + + if (percentGained > 0 && secondsForThisChunk > 0) + { + double secondsPerPercent = secondsForThisChunk / percentGained; + + // Exponential smoothing + if (smoothedSecondsPerPercent == 0) + smoothedSecondsPerPercent = secondsPerPercent; + else + smoothedSecondsPerPercent = SmoothingAlpha * secondsPerPercent + (1 - SmoothingAlpha) * smoothedSecondsPerPercent; + + int remainingPercent = 100 - overallPercentInt; + double etaSeconds = remainingPercent * smoothedSecondsPerPercent; + var newEta = TimeSpan.FromSeconds(Math.Max(0, etaSeconds)); + + // Only update if significant change + if (!lastReportedEta.HasValue || + Math.Abs(newEta.TotalSeconds - lastReportedEta.Value.TotalSeconds) / Math.Max(1, lastReportedEta.Value.TotalSeconds) > EtaChangeThreshold) + { + currentEta = newEta; + lastReportedEta = currentEta; + } + else + { + currentEta = lastReportedEta; + } + + etaLastPercent = overallPercentInt; + etaLastPercentTime = now; + } + } + + // Throttle updates + var now2 = DateTime.UtcNow; + bool shouldUpdate = (now2 - lastProgressUpdate).TotalMilliseconds >= ThrottleMs || overallPercentInt != lastReportedPercent; if (shouldUpdate) { - lastProgressUpdate = now; + lastProgressUpdate = now2; lastReportedPercent = overallPercentInt; - progressCallback?.Invoke(overallPercentInt); + progressCallback?.Invoke(overallPercentInt, currentEta); statusCallback?.Invoke(fileName); } }; @@ -414,13 +518,11 @@ namespace AndroidSideloader }); } - // Mark this file as fully transferred transferredBytes += fileSize; } } - // Ensure final 100% and clear status - progressCallback?.Invoke(100); + progressCallback?.Invoke(100, null); statusCallback?.Invoke(""); return new ProcessOutput($"{gameName}: OBB transfer: Success\n", ""); @@ -428,7 +530,6 @@ namespace AndroidSideloader catch (Exception ex) { Logger.Log($"CopyOBBWithProgressAsync error: {ex.Message}", LogLevel.ERROR); - return new ProcessOutput("", $"{gameName}: OBB transfer: Failed: {ex.Message}\n"); } } diff --git a/MainForm.Designer.cs b/MainForm.Designer.cs index 0349e6e..0d0cf8e 100644 --- a/MainForm.Designer.cs +++ b/MainForm.Designer.cs @@ -34,9 +34,7 @@ namespace AndroidSideloader { this.components = new System.ComponentModel.Container(); this.m_combo = new SergeUtils.EasyCompletionComboBox(); - this.progressBar = new AndroidSideloader.ModernProgressBar(); this.speedLabel = new System.Windows.Forms.Label(); - this.etaLabel = new System.Windows.Forms.Label(); this.freeDisclaimer = new System.Windows.Forms.Label(); this.gamesQueListBox = new System.Windows.Forms.ListBox(); this.devicesComboBox = new System.Windows.Forms.ComboBox(); @@ -121,9 +119,14 @@ namespace AndroidSideloader this.deviceIdLabel = new System.Windows.Forms.Label(); this.rookieStatusLabel = new System.Windows.Forms.Label(); this.sidebarMediaPanel = new System.Windows.Forms.Panel(); - this.downloadInstallGameButton = new AndroidSideloader.RoundButton(); this.selectedGameLabel = new System.Windows.Forms.Label(); this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + this.webView21 = new Microsoft.Web.WebView2.WinForms.WebView2(); + this.favoriteGame = new System.Windows.Forms.ContextMenuStrip(this.components); + this.favoriteButton = new System.Windows.Forms.ToolStripMenuItem(); + this.gamesGalleryView = new System.Windows.Forms.FlowLayoutPanel(); + this.btnViewToggle_Tooltip = new System.Windows.Forms.ToolTip(this.components); + this.webViewPlaceholderPanel = new System.Windows.Forms.Panel(); this.searchPanel = new AndroidSideloader.RoundButton(); this.searchIconPictureBox = new System.Windows.Forms.PictureBox(); this.searchTextBox = new System.Windows.Forms.TextBox(); @@ -132,12 +135,8 @@ namespace AndroidSideloader this.btnInstalled = new AndroidSideloader.RoundButton(); this.btnUpdateAvailable = new AndroidSideloader.RoundButton(); this.btnNewerThanList = new AndroidSideloader.RoundButton(); - this.webView21 = new Microsoft.Web.WebView2.WinForms.WebView2(); - this.favoriteGame = new System.Windows.Forms.ContextMenuStrip(this.components); - this.favoriteButton = new System.Windows.Forms.ToolStripMenuItem(); - this.gamesGalleryView = new System.Windows.Forms.FlowLayoutPanel(); - this.btnViewToggle_Tooltip = new System.Windows.Forms.ToolTip(this.components); - this.webViewPlaceholderPanel = new System.Windows.Forms.Panel(); + this.progressBar = new AndroidSideloader.ModernProgressBar(); + this.downloadInstallGameButton = new AndroidSideloader.RoundButton(); ((System.ComponentModel.ISupportInitialize)(this.gamesPictureBox)).BeginInit(); this.gamesPictureBox.SuspendLayout(); this.progressDLbtnContainer.SuspendLayout(); @@ -153,10 +152,10 @@ namespace AndroidSideloader this.statusInfoPanel.SuspendLayout(); this.sidebarMediaPanel.SuspendLayout(); this.tableLayoutPanel1.SuspendLayout(); - this.searchPanel.SuspendLayout(); - ((System.ComponentModel.ISupportInitialize)(this.searchIconPictureBox)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.webView21)).BeginInit(); this.favoriteGame.SuspendLayout(); + this.searchPanel.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.searchIconPictureBox)).BeginInit(); this.SuspendLayout(); // // m_combo @@ -167,65 +166,24 @@ namespace AndroidSideloader this.m_combo.ForeColor = global::AndroidSideloader.Properties.Settings.Default.FontColor; this.m_combo.Location = new System.Drawing.Point(253, 9); this.m_combo.Name = "m_combo"; - this.m_combo.Size = new System.Drawing.Size(374, 25); + this.m_combo.Size = new System.Drawing.Size(374, 24); this.m_combo.TabIndex = 0; this.m_combo.Text = "Select an Installed App..."; this.m_combo.Visible = false; // - // progressBar - // - this.progressBar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.progressBar.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(32)))), ((int)(((byte)(35)))), ((int)(((byte)(45))))); - this.progressBar.BackgroundColor = System.Drawing.Color.FromArgb(((int)(((byte)(28)))), ((int)(((byte)(32)))), ((int)(((byte)(38))))); - this.progressBar.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold); - this.progressBar.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); - this.progressBar.IndeterminateColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); - this.progressBar.IsIndeterminate = false; - this.progressBar.Location = new System.Drawing.Point(1, 18); - this.progressBar.Maximum = 100; - this.progressBar.Minimum = 0; - this.progressBar.MinimumSize = new System.Drawing.Size(200, 13); - this.progressBar.Name = "progressBar"; - this.progressBar.OperationType = ""; - this.progressBar.ProgressEndColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(160)))), ((int)(((byte)(130))))); - this.progressBar.ProgressStartColor = System.Drawing.Color.FromArgb(((int)(((byte)(120)))), ((int)(((byte)(220)))), ((int)(((byte)(190))))); - this.progressBar.Radius = 6; - this.progressBar.Size = new System.Drawing.Size(983, 13); - this.progressBar.StatusText = ""; - this.progressBar.TabIndex = 7; - this.progressBar.TextColor = System.Drawing.Color.FromArgb(((int)(((byte)(230)))), ((int)(((byte)(230)))), ((int)(((byte)(230))))); - this.progressBar.Value = 0; - // // speedLabel // this.speedLabel.AutoSize = true; this.speedLabel.BackColor = System.Drawing.Color.Transparent; this.speedLabel.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Bold); this.speedLabel.ForeColor = System.Drawing.Color.White; - this.speedLabel.Location = new System.Drawing.Point(-2, -3); + this.speedLabel.Location = new System.Drawing.Point(-1, 3); this.speedLabel.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.speedLabel.Name = "speedLabel"; this.speedLabel.Size = new System.Drawing.Size(152, 16); this.speedLabel.TabIndex = 76; this.speedLabel.Text = "DLS: Speed in MBPS"; this.speedLabel_Tooltip.SetToolTip(this.speedLabel, "Current download speed, updates every second, in mbps"); - // - // etaLabel - // - this.etaLabel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); - this.etaLabel.BackColor = System.Drawing.Color.Transparent; - this.etaLabel.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.etaLabel.ForeColor = System.Drawing.Color.White; - this.etaLabel.Location = new System.Drawing.Point(790, -3); - this.etaLabel.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); - this.etaLabel.Name = "etaLabel"; - this.etaLabel.Size = new System.Drawing.Size(196, 18); - this.etaLabel.TabIndex = 75; - this.etaLabel.Text = "ETA: HH:MM:SS Left"; - this.etaLabel.TextAlign = System.Drawing.ContentAlignment.TopRight; - this.etaLabel_Tooltip.SetToolTip(this.etaLabel, "Estimated time when game will finish download, updates every 5 seconds, format is" + - " HH:MM:SS"); // // freeDisclaimer // @@ -272,7 +230,7 @@ namespace AndroidSideloader this.devicesComboBox.Location = new System.Drawing.Point(253, 39); this.devicesComboBox.Margin = new System.Windows.Forms.Padding(2); this.devicesComboBox.Name = "devicesComboBox"; - this.devicesComboBox.Size = new System.Drawing.Size(164, 25); + this.devicesComboBox.Size = new System.Drawing.Size(164, 24); this.devicesComboBox.TabIndex = 1; this.devicesComboBox.Text = "Select your device"; this.devicesComboBox.Visible = false; @@ -288,7 +246,7 @@ namespace AndroidSideloader this.remotesList.Location = new System.Drawing.Point(567, 40); this.remotesList.Margin = new System.Windows.Forms.Padding(2); this.remotesList.Name = "remotesList"; - this.remotesList.Size = new System.Drawing.Size(67, 25); + this.remotesList.Size = new System.Drawing.Size(67, 24); this.remotesList.TabIndex = 3; this.remotesList.Visible = false; this.remotesList.SelectedIndexChanged += new System.EventHandler(this.remotesList_SelectedIndexChanged); @@ -808,12 +766,11 @@ namespace AndroidSideloader this.progressDLbtnContainer.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; this.progressDLbtnContainer.BackColor = System.Drawing.Color.Transparent; this.progressDLbtnContainer.Controls.Add(this.progressBar); - this.progressDLbtnContainer.Controls.Add(this.etaLabel); this.progressDLbtnContainer.Controls.Add(this.speedLabel); - this.progressDLbtnContainer.Location = new System.Drawing.Point(258, 459); + this.progressDLbtnContainer.Location = new System.Drawing.Point(258, 453); this.progressDLbtnContainer.MinimumSize = new System.Drawing.Size(600, 34); this.progressDLbtnContainer.Name = "progressDLbtnContainer"; - this.progressDLbtnContainer.Size = new System.Drawing.Size(984, 34); + this.progressDLbtnContainer.Size = new System.Drawing.Size(984, 40); this.progressDLbtnContainer.TabIndex = 96; // // diskLabel @@ -1281,35 +1238,6 @@ namespace AndroidSideloader this.sidebarMediaPanel.Size = new System.Drawing.Size(233, 214); this.sidebarMediaPanel.TabIndex = 101; // - // downloadInstallGameButton - // - this.downloadInstallGameButton.Active1 = System.Drawing.Color.FromArgb(((int)(((byte)(110)))), ((int)(((byte)(215)))), ((int)(((byte)(190))))); - this.downloadInstallGameButton.Active2 = System.Drawing.Color.FromArgb(((int)(((byte)(110)))), ((int)(((byte)(215)))), ((int)(((byte)(190))))); - this.downloadInstallGameButton.BackColor = System.Drawing.Color.Transparent; - this.downloadInstallGameButton.Cursor = System.Windows.Forms.Cursors.Hand; - this.downloadInstallGameButton.DialogResult = System.Windows.Forms.DialogResult.OK; - this.downloadInstallGameButton.Disabled1 = System.Drawing.Color.FromArgb(((int)(((byte)(16)))), ((int)(((byte)(18)))), ((int)(((byte)(22))))); - this.downloadInstallGameButton.Disabled2 = System.Drawing.Color.FromArgb(((int)(((byte)(16)))), ((int)(((byte)(18)))), ((int)(((byte)(22))))); - this.downloadInstallGameButton.DisabledStrokeColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(55)))), ((int)(((byte)(65))))); - this.downloadInstallGameButton.Enabled = false; - this.downloadInstallGameButton.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold); - this.downloadInstallGameButton.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(59)))), ((int)(((byte)(67)))), ((int)(((byte)(82))))); - this.downloadInstallGameButton.Inactive1 = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); - this.downloadInstallGameButton.Inactive2 = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); - this.downloadInstallGameButton.Location = new System.Drawing.Point(6, 177); - this.downloadInstallGameButton.Margin = new System.Windows.Forms.Padding(0); - this.downloadInstallGameButton.Name = "downloadInstallGameButton"; - this.downloadInstallGameButton.Radius = 4; - this.downloadInstallGameButton.Size = new System.Drawing.Size(238, 30); - this.downloadInstallGameButton.Stroke = true; - this.downloadInstallGameButton.StrokeColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); - this.downloadInstallGameButton.TabIndex = 94; - this.downloadInstallGameButton.Text = "DOWNLOAD AND INSTALL"; - this.downloadInstallGameButton.Transparency = false; - this.downloadInstallGameButton.Click += new System.EventHandler(this.downloadInstallGameButton_Click); - this.downloadInstallGameButton.DragDrop += new System.Windows.Forms.DragEventHandler(this.Form1_DragDrop); - this.downloadInstallGameButton.DragEnter += new System.Windows.Forms.DragEventHandler(this.Form1_DragEnter); - // // selectedGameLabel // this.selectedGameLabel.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(20)))), ((int)(((byte)(24)))), ((int)(((byte)(29))))); @@ -1348,6 +1276,58 @@ namespace AndroidSideloader this.tableLayoutPanel1.Size = new System.Drawing.Size(984, 34); this.tableLayoutPanel1.TabIndex = 97; // + // webView21 + // + this.webView21.AllowExternalDrop = true; + this.webView21.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.webView21.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30))))); + this.webView21.CreationProperties = null; + this.webView21.DefaultBackgroundColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30))))); + this.webView21.Location = new System.Drawing.Point(259, 496); + this.webView21.Name = "webView21"; + this.webView21.Size = new System.Drawing.Size(384, 216); + this.webView21.TabIndex = 98; + this.webView21.ZoomFactor = 1D; + // + // favoriteGame + // + this.favoriteGame.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(42)))), ((int)(((byte)(48))))); + this.favoriteGame.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.favoriteButton}); + this.favoriteGame.Name = "favoriteGame"; + this.favoriteGame.ShowImageMargin = false; + this.favoriteGame.Size = new System.Drawing.Size(149, 26); + // + // favoriteButton + // + this.favoriteButton.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(42)))), ((int)(((byte)(48))))); + this.favoriteButton.ForeColor = System.Drawing.Color.White; + this.favoriteButton.Name = "favoriteButton"; + this.favoriteButton.Size = new System.Drawing.Size(148, 22); + this.favoriteButton.Text = "★ Add to Favorites"; + this.favoriteButton.Click += new System.EventHandler(this.favoriteButton_Click); + // + // gamesGalleryView + // + this.gamesGalleryView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.gamesGalleryView.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15))))); + this.gamesGalleryView.Location = new System.Drawing.Point(258, 44); + this.gamesGalleryView.Name = "gamesGalleryView"; + this.gamesGalleryView.Size = new System.Drawing.Size(984, 409); + this.gamesGalleryView.TabIndex = 102; + // + // webViewPlaceholderPanel + // + this.webViewPlaceholderPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.webViewPlaceholderPanel.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30))))); + this.webViewPlaceholderPanel.Location = new System.Drawing.Point(259, 496); + this.webViewPlaceholderPanel.Name = "webViewPlaceholderPanel"; + this.webViewPlaceholderPanel.Size = new System.Drawing.Size(384, 217); + this.webViewPlaceholderPanel.TabIndex = 103; + this.webViewPlaceholderPanel.Paint += new System.Windows.Forms.PaintEventHandler(this.webViewPlaceholderPanel_Paint); + // // searchPanel // this.searchPanel.Active1 = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(56)))), ((int)(((byte)(70))))); @@ -1531,57 +1511,59 @@ namespace AndroidSideloader this.btnNewerThanList.Transparency = false; this.btnNewerThanList.Click += new System.EventHandler(this.btnNewerThanList_Click); // - // webView21 + // progressBar // - this.webView21.AllowExternalDrop = true; - this.webView21.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.webView21.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30))))); - this.webView21.CreationProperties = null; - this.webView21.DefaultBackgroundColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30))))); - this.webView21.Location = new System.Drawing.Point(259, 496); - this.webView21.Name = "webView21"; - this.webView21.Size = new System.Drawing.Size(384, 216); - this.webView21.TabIndex = 98; - this.webView21.ZoomFactor = 1D; - // - // favoriteGame - // - this.favoriteGame.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(42)))), ((int)(((byte)(48))))); - this.favoriteGame.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.favoriteButton}); - this.favoriteGame.Name = "favoriteGame"; - this.favoriteGame.ShowImageMargin = false; - this.favoriteGame.Size = new System.Drawing.Size(149, 26); - // - // favoriteButton - // - this.favoriteButton.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(42)))), ((int)(((byte)(48))))); - this.favoriteButton.ForeColor = System.Drawing.Color.White; - this.favoriteButton.Name = "favoriteButton"; - this.favoriteButton.Size = new System.Drawing.Size(148, 22); - this.favoriteButton.Text = "★ Add to Favorites"; - this.favoriteButton.Click += new System.EventHandler(this.favoriteButton_Click); - // - // gamesGalleryView - // - this.gamesGalleryView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) + this.progressBar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); - this.gamesGalleryView.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15))))); - this.gamesGalleryView.Location = new System.Drawing.Point(258, 44); - this.gamesGalleryView.Name = "gamesGalleryView"; - this.gamesGalleryView.Size = new System.Drawing.Size(984, 409); - this.gamesGalleryView.TabIndex = 102; + this.progressBar.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(32)))), ((int)(((byte)(35)))), ((int)(((byte)(45))))); + this.progressBar.BackgroundColor = System.Drawing.Color.FromArgb(((int)(((byte)(28)))), ((int)(((byte)(32)))), ((int)(((byte)(38))))); + this.progressBar.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold); + this.progressBar.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); + this.progressBar.IndeterminateColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); + this.progressBar.IsIndeterminate = false; + this.progressBar.Location = new System.Drawing.Point(1, 23); + this.progressBar.Maximum = 100; + this.progressBar.Minimum = 0; + this.progressBar.MinimumSize = new System.Drawing.Size(200, 13); + this.progressBar.Name = "progressBar"; + this.progressBar.OperationType = ""; + this.progressBar.ProgressEndColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(160)))), ((int)(((byte)(130))))); + this.progressBar.ProgressStartColor = System.Drawing.Color.FromArgb(((int)(((byte)(120)))), ((int)(((byte)(220)))), ((int)(((byte)(190))))); + this.progressBar.Radius = 6; + this.progressBar.Size = new System.Drawing.Size(983, 13); + this.progressBar.StatusText = ""; + this.progressBar.TabIndex = 7; + this.progressBar.TextColor = System.Drawing.Color.FromArgb(((int)(((byte)(230)))), ((int)(((byte)(230)))), ((int)(((byte)(230))))); + this.progressBar.Value = 0; // - // webViewPlaceholderPanel + // downloadInstallGameButton // - this.webViewPlaceholderPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.webViewPlaceholderPanel.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30))))); - this.webViewPlaceholderPanel.Location = new System.Drawing.Point(259, 496); - this.webViewPlaceholderPanel.Name = "webViewPlaceholderPanel"; - this.webViewPlaceholderPanel.Size = new System.Drawing.Size(384, 217); - this.webViewPlaceholderPanel.TabIndex = 103; - this.webViewPlaceholderPanel.Paint += new System.Windows.Forms.PaintEventHandler(this.webViewPlaceholderPanel_Paint); + this.downloadInstallGameButton.Active1 = System.Drawing.Color.FromArgb(((int)(((byte)(110)))), ((int)(((byte)(215)))), ((int)(((byte)(190))))); + this.downloadInstallGameButton.Active2 = System.Drawing.Color.FromArgb(((int)(((byte)(110)))), ((int)(((byte)(215)))), ((int)(((byte)(190))))); + this.downloadInstallGameButton.BackColor = System.Drawing.Color.Transparent; + this.downloadInstallGameButton.Cursor = System.Windows.Forms.Cursors.Hand; + this.downloadInstallGameButton.DialogResult = System.Windows.Forms.DialogResult.OK; + this.downloadInstallGameButton.Disabled1 = System.Drawing.Color.FromArgb(((int)(((byte)(16)))), ((int)(((byte)(18)))), ((int)(((byte)(22))))); + this.downloadInstallGameButton.Disabled2 = System.Drawing.Color.FromArgb(((int)(((byte)(16)))), ((int)(((byte)(18)))), ((int)(((byte)(22))))); + this.downloadInstallGameButton.DisabledStrokeColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(55)))), ((int)(((byte)(65))))); + this.downloadInstallGameButton.Enabled = false; + this.downloadInstallGameButton.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold); + this.downloadInstallGameButton.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(59)))), ((int)(((byte)(67)))), ((int)(((byte)(82))))); + this.downloadInstallGameButton.Inactive1 = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); + this.downloadInstallGameButton.Inactive2 = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); + this.downloadInstallGameButton.Location = new System.Drawing.Point(6, 177); + this.downloadInstallGameButton.Margin = new System.Windows.Forms.Padding(0); + this.downloadInstallGameButton.Name = "downloadInstallGameButton"; + this.downloadInstallGameButton.Radius = 4; + this.downloadInstallGameButton.Size = new System.Drawing.Size(238, 30); + this.downloadInstallGameButton.Stroke = true; + this.downloadInstallGameButton.StrokeColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); + this.downloadInstallGameButton.TabIndex = 94; + this.downloadInstallGameButton.Text = "DOWNLOAD AND INSTALL"; + this.downloadInstallGameButton.Transparency = false; + this.downloadInstallGameButton.Click += new System.EventHandler(this.downloadInstallGameButton_Click); + this.downloadInstallGameButton.DragDrop += new System.Windows.Forms.DragEventHandler(this.Form1_DragDrop); + this.downloadInstallGameButton.DragEnter += new System.Windows.Forms.DragEventHandler(this.Form1_DragEnter); // // MainForm // @@ -1633,11 +1615,11 @@ namespace AndroidSideloader this.statusInfoPanel.ResumeLayout(false); this.sidebarMediaPanel.ResumeLayout(false); this.tableLayoutPanel1.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.webView21)).EndInit(); + this.favoriteGame.ResumeLayout(false); this.searchPanel.ResumeLayout(false); this.searchPanel.PerformLayout(); ((System.ComponentModel.ISupportInitialize)(this.searchIconPictureBox)).EndInit(); - ((System.ComponentModel.ISupportInitialize)(this.webView21)).EndInit(); - this.favoriteGame.ResumeLayout(false); this.ResumeLayout(false); this.PerformLayout(); @@ -1646,7 +1628,6 @@ namespace AndroidSideloader #endregion private SergeUtils.EasyCompletionComboBox m_combo; private ModernProgressBar progressBar; - private System.Windows.Forms.Label etaLabel; private System.Windows.Forms.Label speedLabel; private System.Windows.Forms.Label freeDisclaimer; private System.Windows.Forms.ComboBox devicesComboBox; diff --git a/MainForm.cs b/MainForm.cs index 4fbb364..d034381 100755 --- a/MainForm.cs +++ b/MainForm.cs @@ -401,7 +401,6 @@ namespace AndroidSideloader gamesListView.View = View.Details; gamesListView.FullRowSelect = true; gamesListView.GridLines = false; - etaLabel.Text = String.Empty; speedLabel.Text = String.Empty; diskLabel.Text = String.Empty; @@ -965,24 +964,26 @@ namespace AndroidSideloader output = await ADB.CopyOBBWithProgressAsync( path, - progress => this.Invoke(() => { + (progress, eta) => this.Invoke(() => { progressBar.Value = progress; - speedLabel.Text = $"Progress: {progress}%"; + string etaStr = eta.HasValue && eta.Value.TotalSeconds > 0 + ? $" · ETA: {eta.Value:mm\\:ss}" + : ""; + speedLabel.Text = $"Progress: {progress}%{etaStr}"; }), status => this.Invoke(() => { progressBar.StatusText = status; - etaLabel.Text = status; }), folderName); progressBar.Value = 100; + progressBar.StatusText = ""; changeTitle("Done."); showAvailableSpace(); ShowPrcOutput(output); changeTitle(""); speedLabel.Text = ""; - etaLabel.Text = ""; } } @@ -3047,7 +3048,6 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord public async void cleanupActiveDownloadStatus() { speedLabel.Text = String.Empty; - etaLabel.Text = String.Empty; progressBar.Value = 0; gamesQueueList.RemoveAt(0); } @@ -3262,16 +3262,17 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord changeTitle("Downloading game " + gameName); speedLabel.Text = "Starting download..."; - etaLabel.Text = "Please wait..."; - //Download + // Track the highest valid progress to prevent brief progress bar flashes during multi-file transfers + int highestValidPercent = 0; + + // Download while (t1.IsAlive) { try { HttpResponseMessage response = await client.PostAsync("http://127.0.0.1:5572/core/stats", null); string foo = await response.Content.ReadAsStringAsync(); - //Debug.WriteLine("RESP CONTENT " + foo); dynamic results = JsonConvert.DeserializeObject(foo); if (results["transferring"] != null) @@ -3308,24 +3309,45 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord totalSize /= 1000000; downloadedSize /= 1000000; - // Logger.Log("Files: " + transfersComplete.ToString() + "/" + fileCount.ToString() + " (" + Convert.ToInt32((downloadedSize / totalSize) * 100).ToString() + "% Complete)"); - // Logger.Log("Downloaded: " + downloadedSize.ToString() + " of " + totalSize.ToString()); - progressBar.IsIndeterminate = false; - progressBar.Value = Convert.ToInt32((downloadedSize / totalSize) * 100); + + int percent = 0; + if (totalSize > 0) + { + percent = Convert.ToInt32((downloadedSize / totalSize) * 100); + } + + // Clamp to 0-99 while download is in progress to prevent brief 100% flashes + percent = Math.Max(0, Math.Min(99, percent)); + + // Only allow progress to increase + if (percent >= highestValidPercent) + { + highestValidPercent = percent; + } + else + { + // Progress went backwards? Keep showing the highest valid percent we've seen + percent = highestValidPercent; + } + + progressBar.Value = percent; TimeSpan time = TimeSpan.FromSeconds(globalEta); - etaLabel.Text = etaLabel.Text = "ETA: " + time.ToString(@"hh\:mm\:ss") + " left"; - speedLabel.Text = "DLS: " + transfersComplete.ToString() + "/" + fileCount.ToString() + " files - " + string.Format("{0:0.00}", downloadSpeed) + " MB/s"; + UpdateProgressStatus( + "Downloading", + (int)transfersComplete + 1, + (int)fileCount, + percent, + time, + downloadSpeed); } } catch { } - await Task.Delay(100); - } if (removedownloading) @@ -3399,18 +3421,42 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord if (UsingPublicConfig && otherError == false && gameDownloadOutput.Output != "Download skipped.") { + // ETA tracking for extraction + DateTime extractStart = DateTime.UtcNow; + string currentExtractFile = ""; + Thread extractionThread = new Thread(() => { Invoke(new Action(() => { - speedLabel.Text = "Extracting..."; etaLabel.Text = "Please wait..."; + speedLabel.Text = "Extracting..."; progressBar.IsIndeterminate = false; progressBar.Value = 0; + progressBar.OperationType = "Extracting"; isInDownloadExtract = true; })); + + // Set up extraction callbacks + Zip.ExtractionProgressCallback = (percent, eta) => + { + this.Invoke(() => + { + progressBar.Value = percent; + UpdateProgressStatus("Extracting", percent: percent, eta: eta); + + progressBar.StatusText = !string.IsNullOrEmpty(currentExtractFile) + ? $"{currentExtractFile} · {percent}%" + : $"{percent}%"; + }); + }; + + Zip.ExtractionStatusCallback = (fileName) => + { + currentExtractFile = fileName ?? ""; + }; + try { - progressBar.OperationType = "Extracting"; changeTitle("Extracting " + gameName); Zip.ExtractFile($"{settings.DownloadDir}\\{gameNameHash}\\{gameNameHash}.7z.001", $"{settings.DownloadDir}", PublicConfigFile.Password); changeTitle(""); @@ -3425,6 +3471,12 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord this.Invoke(() => _ = FlexibleMessageBox.Show(Program.form, $"7zip error: {ex.Message}")); output += new ProcessOutput("", "Extract Failed"); } + finally + { + // Clear callbacks + Zip.ExtractionProgressCallback = null; + Zip.ExtractionStatusCallback = null; + } }) { IsBackground = true @@ -3436,6 +3488,8 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord await Task.Delay(100); } + progressBar.StatusText = ""; // Clear status after extraction + if (Directory.Exists($"{settings.DownloadDir}\\{gameNameHash}")) { Directory.Delete($"{settings.DownloadDir}\\{gameNameHash}", true); @@ -3449,8 +3503,6 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord progressBar.Value = 0; progressBar.IsIndeterminate = false; changeTitle("Installing game APK " + gameName); - etaLabel.Text = "ETA: Wait for install..."; - speedLabel.Text = "DLS: Finished"; if (File.Exists(Path.Combine(settings.DownloadDir, gameName, "install.txt"))) { isinstalltxt = true; @@ -3508,7 +3560,6 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord t.Start(); changeTitle($"Sideloading APK..."); - etaLabel.Text = "Installing APK..."; progressBar.IsIndeterminate = false; progressBar.OperationType = "Installing"; progressBar.Value = 0; @@ -3516,7 +3567,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord // Use async method with progress output += await ADB.SideloadWithProgressAsync( apkFile, - progress => this.Invoke(() => { + (progress, eta) => this.Invoke(() => { if (progress == 0) { progressBar.IsIndeterminate = true; @@ -3527,16 +3578,23 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord progressBar.IsIndeterminate = false; progressBar.Value = progress; } + UpdateProgressStatus("Installing APK", percent: progress, eta: eta); + progressBar.StatusText = $"Installing · {progress}%"; }), status => this.Invoke(() => { - progressBar.StatusText = status; - etaLabel.Text = status; + if (!string.IsNullOrEmpty(status)) + { + // "Completing Installation..." + speedLabel.Text = status; + progressBar.StatusText = status; + } }), packagename, Sideloader.gameNameToSimpleName(gameName)); t.Stop(); progressBar.IsIndeterminate = false; + progressBar.StatusText = ""; // Clear status after APK install Debug.WriteLine(wrDelimiter); if (Directory.Exists($"{settings.DownloadDir}\\{gameName}\\{packagename}")) @@ -3544,7 +3602,6 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord deleteOBB(packagename); changeTitle($"Copying {packagename} OBB to device..."); - etaLabel.Text = "Copying OBB..."; progressBar.Value = 0; progressBar.OperationType = "Copying OBB"; @@ -3553,41 +3610,27 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord output += await ADB.CopyOBBWithProgressAsync( $"{settings.DownloadDir}\\{gameName}\\{packagename}", - progress => this.Invoke(() => + (progress, eta) => this.Invoke(() => { progressBar.Value = progress; - speedLabel.Text = $"OBB: {progress}%"; + UpdateProgressStatus("Copying OBB", percent: progress, eta: eta); if (!string.IsNullOrEmpty(currentObbStatusBase)) { - if (currentObbStatusBase.StartsWith("Preparing:", StringComparison.OrdinalIgnoreCase) || - currentObbStatusBase.StartsWith("Copying:", StringComparison.OrdinalIgnoreCase)) - { - progressBar.StatusText = currentObbStatusBase; - } - else - { - // "filename · 73%" - progressBar.StatusText = $"{currentObbStatusBase} · {progress}%"; - } + progressBar.StatusText = $"{currentObbStatusBase} · {progress}%"; } else { - // Fallback: just show the numeric percent in the bar progressBar.StatusText = $"{progress}%"; } }), status => this.Invoke(() => { currentObbStatusBase = status ?? string.Empty; - if (currentObbStatusBase.StartsWith("Preparing:", StringComparison.OrdinalIgnoreCase) || - currentObbStatusBase.StartsWith("Copying:", StringComparison.OrdinalIgnoreCase)) - { - progressBar.StatusText = currentObbStatusBase; - } }), Sideloader.gameNameToSimpleName(gameName)); + progressBar.StatusText = ""; // Clear status after OBB copy changeTitle(""); if (!nodeviceonstart | DeviceConnected) @@ -3646,8 +3689,6 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord ShowPrcOutput(output); } progressBar.IsIndeterminate = false; - etaLabel.Text = "ETA: Finished Queue"; - speedLabel.Text = "DLS: Finished Queue"; gamesAreDownloading = false; isinstalling = false; @@ -3768,8 +3809,6 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord ShowPrcOutput(output); } progressBar.IsIndeterminate = false; - etaLabel.Text = "ETA: Finished Queue"; - speedLabel.Text = "DLS: Finished Queue"; gamesAreDownloading = false; isinstalling = false; @@ -7149,6 +7188,65 @@ function onYouTubeIframeAPIReady() { sideloadingStatusLabel.ForeColor = Color.FromArgb(93, 203, 173); // Accent green for enabled } } + + public void UpdateProgressStatus(string operation, + int current = 0, + int total = 0, + int percent = 0, + TimeSpan? eta = null, + double? speedMBps = null) + { + if (this.InvokeRequired) + { + this.Invoke(() => UpdateProgressStatus(operation, current, total, percent, eta, speedMBps)); + return; + } + + // Sync the progress bar's operation type to the current operation + progressBar.OperationType = operation; + + var sb = new StringBuilder(); + + // Operation name + sb.Append(operation); + + // Percentage + if (percent > 0) + { + sb.Append($" ({percent}%)"); + } + + // Speed + if (speedMBps.HasValue && speedMBps.Value > 0) + { + sb.Append($" @ {speedMBps.Value:F1} MB/s"); + } + + // File count if applicable + if (total > 1) + { + sb.Append($" ({current}/{total})"); + } + + // ETA + if (eta.HasValue && eta.Value.TotalSeconds > 0) + { + sb.Append($" • ETA: {eta.Value:hh\\:mm\\:ss}"); + } + + speedLabel.Text = sb.ToString(); + } + + public void ClearProgressStatus() + { + if (this.InvokeRequired) + { + this.Invoke(ClearProgressStatus); + return; + } + + speedLabel.Text = ""; + } } public static class ControlExtensions diff --git a/MainForm.resx b/MainForm.resx index 5f0ed55..768df39 100644 --- a/MainForm.resx +++ b/MainForm.resx @@ -120,9 +120,6 @@ 1165, 17 - - 428, 54 - 966, 17 @@ -177,6 +174,9 @@ 1320, 17 + + 428, 54 + 1021, 91 diff --git a/Utilities/Zip.cs b/Utilities/Zip.cs index 762c357..dd1f6be 100644 --- a/Utilities/Zip.cs +++ b/Utilities/Zip.cs @@ -19,6 +19,11 @@ namespace AndroidSideloader.Utilities internal class Zip { private static readonly SettingsManager settings = SettingsManager.Instance; + + // Progress callback: (percent, eta) + public static Action ExtractionProgressCallback { get; set; } + public static Action ExtractionStatusCallback { get; set; } + public static void ExtractFile(string sourceArchive, string destination) { string args = $"x \"{sourceArchive}\" -y -o\"{destination}\" -bsp1"; @@ -68,6 +73,16 @@ namespace AndroidSideloader.Utilities _ = Logger.Log($"Extract: 7z {string.Join(" ", args.Split(' ').Where(a => !a.StartsWith("-p")))}"); + // ETA tracking + DateTime extractStart = DateTime.UtcNow; + int etaLastPercent = 0; + DateTime etaLastPercentTime = DateTime.UtcNow; + double smoothedSecondsPerPercent = 0; + TimeSpan? lastReportedEta = null; + int lastReportedPercent = -1; + const double SmoothingAlpha = 0.15; + const double EtaChangeThreshold = 0.10; + using (Process x = new Process()) { x.StartInfo = pro; @@ -78,14 +93,76 @@ namespace AndroidSideloader.Utilities { if (e.Data != null) { - var match = Regex.Match(e.Data, @"(\d+)%"); - if (match.Success) + // Parse 7-Zip progress output (e.g., " 45% - filename") + var match = Regex.Match(e.Data, @"^\s*(\d+)%"); + if (match.Success && int.TryParse(match.Groups[1].Value, out int percent)) { - int progress = int.Parse(match.Groups[1].Value); - MainForm mainForm = (MainForm)Application.OpenForms[0]; - if (mainForm != null) + TimeSpan? eta = null; + + // Calculate ETA + if (percent > etaLastPercent && percent < 100) { - mainForm.Invoke((Action)(() => mainForm.SetProgress(progress))); + var now = DateTime.UtcNow; + double secondsForThisChunk = (now - etaLastPercentTime).TotalSeconds; + int percentGained = percent - etaLastPercent; + + if (percentGained > 0 && secondsForThisChunk > 0) + { + double secondsPerPercent = secondsForThisChunk / percentGained; + + if (smoothedSecondsPerPercent == 0) + smoothedSecondsPerPercent = secondsPerPercent; + else + smoothedSecondsPerPercent = SmoothingAlpha * secondsPerPercent + (1 - SmoothingAlpha) * smoothedSecondsPerPercent; + + int remainingPercent = 100 - percent; + double etaSeconds = remainingPercent * smoothedSecondsPerPercent; + var newEta = TimeSpan.FromSeconds(Math.Max(0, etaSeconds)); + + // Only update if significant change + if (!lastReportedEta.HasValue || + Math.Abs(newEta.TotalSeconds - lastReportedEta.Value.TotalSeconds) / Math.Max(1, lastReportedEta.Value.TotalSeconds) > EtaChangeThreshold) + { + eta = newEta; + lastReportedEta = eta; + } + else + { + eta = lastReportedEta; + } + + etaLastPercent = percent; + etaLastPercentTime = now; + } + } + else + { + eta = lastReportedEta; + } + + // Only report if percent changed + if (percent != lastReportedPercent) + { + lastReportedPercent = percent; + + MainForm mainForm = (MainForm)Application.OpenForms[0]; + if (mainForm != null) + { + mainForm.Invoke((Action)(() => mainForm.SetProgress(percent))); + } + + ExtractionProgressCallback?.Invoke(percent, eta); + } + } + + // Extract filename from output + var fileMatch = Regex.Match(e.Data, @"- (.+)$"); + if (fileMatch.Success) + { + string fileName = Path.GetFileName(fileMatch.Groups[1].Value.Trim()); + if (!string.IsNullOrEmpty(fileName)) + { + ExtractionStatusCallback?.Invoke(fileName); } } } @@ -119,6 +196,11 @@ namespace AndroidSideloader.Utilities x.BeginOutputReadLine(); x.BeginErrorReadLine(); x.WaitForExit(); + + // Clear callbacks + ExtractionProgressCallback?.Invoke(100, null); + ExtractionStatusCallback?.Invoke(""); + errorMessageShown = false; if (!string.IsNullOrEmpty(extractionError)) @@ -130,4 +212,4 @@ namespace AndroidSideloader.Utilities } } } -} \ No newline at end of file +}