From f8dea1e1352c74e99b786453f2be2792c4672137 Mon Sep 17 00:00:00 2001 From: jp64k <122999544+jp64k@users.noreply.github.com> Date: Thu, 11 Dec 2025 23:06:49 +0100 Subject: [PATCH] Added modern progress bar, real-time install/OBB progress updates using AdvancedSharpAdbClient, updated sideload confirmation messages, and a new icon based on the VRP server icon. Introduce ModernProgressBar, a custom control with gradient fill, rounded corners, indeterminate animation, and optional status text. Integrated AdvancedSharpAdbClient to provide real-time progress updates for APK installs and OBB copies, updating the UI to reflect the current operation and its progress. Refactored ADB methods to support async progress reporting, updated MainForm to use the new progress bar features, improved user feedback with clearer confirmation messages during and after sideloading, added a new icon based on the VRP server icon and enabled it in the window title bar. --- ADB.cs | 509 +++++++++++++++++++++++++++++++-------- AndroidSideloader.csproj | 6 + DonorsListView.cs | 4 +- MainForm.Designer.cs | 103 ++++---- MainForm.cs | 235 ++++++++++++------ ModernProgessBar.cs | 438 +++++++++++++++++++++++++++++++++ icon.ico | Bin 32038 -> 32038 bytes packages.config | 1 + 8 files changed, 1075 insertions(+), 221 deletions(-) create mode 100644 ModernProgessBar.cs diff --git a/ADB.cs b/ADB.cs index cba9e6e..4387fd4 100644 --- a/ADB.cs +++ b/ADB.cs @@ -1,8 +1,14 @@ -using AndroidSideloader.Utilities; +using AdvancedSharpAdbClient; +using AdvancedSharpAdbClient.DeviceCommands; +using AdvancedSharpAdbClient.Models; +using AdvancedSharpAdbClient.Receivers; +using AndroidSideloader.Utilities; using JR.Utils.GUI.Forms; using System; using System.Diagnostics; using System.IO; +using System.Linq; +using System.Threading.Tasks; using System.Windows.Forms; namespace AndroidSideloader @@ -10,32 +16,82 @@ namespace AndroidSideloader internal class ADB { private static readonly SettingsManager settings = SettingsManager.Instance; - private static readonly Process adb = new Process(); public static string adbFolderPath = Path.Combine(Environment.CurrentDirectory, "platform-tools"); public static string adbFilePath = Path.Combine(adbFolderPath, "adb.exe"); public static string DeviceID = ""; public static string package = ""; + public static bool wirelessadbON; + + // AdbClient for direct protocol communication + private static AdbClient _adbClient; + private static DeviceData _currentDevice; + + // Gets or initializes the AdbClient instance + private static AdbClient GetAdbClient() + { + if (_adbClient == null) + { + // Ensure ADB server is started + if (!AdbServer.Instance.GetStatus().IsRunning) + { + var server = new AdbServer(); + var result = server.StartServer(adbFilePath, false); + Logger.Log($"ADB server start result: {result}"); + } + + _adbClient = new AdbClient(); + } + return _adbClient; + } + + // Gets the current device for AdbClient operations + private static DeviceData GetCurrentDevice() + { + var client = GetAdbClient(); + var devices = client.GetDevices(); + + if (devices == null || !devices.Any()) + { + Logger.Log("No devices found via AdbClient", LogLevel.WARNING); + return default; + } + + // If DeviceID is set, find that specific device + if (!string.IsNullOrEmpty(DeviceID) && DeviceID.Length > 1) + { + var device = devices.FirstOrDefault(d => d.Serial == DeviceID || d.Serial.StartsWith(DeviceID)); + if (device.Serial != null) + { + _currentDevice = device; + return device; + } + } + + // Otherwise return the first available device + _currentDevice = devices.First(); + return _currentDevice; + } + public static ProcessOutput RunAdbCommandToString(string command) { - // Replacing "adb" from command if the user added it command = command.Replace("adb", ""); settings.ADBFolder = adbFolderPath; settings.ADBPath = adbFilePath; settings.Save(); + if (DeviceID.Length > 1) { command = $" -s {DeviceID} {command}"; } + if (!command.Contains("dumpsys") && !command.Contains("shell pm list packages") && !command.Contains("KEYCODE_WAKEUP")) { string logcmd = command; - if (logcmd.Contains(Environment.CurrentDirectory)) { logcmd = logcmd.Replace($"{Environment.CurrentDirectory}", $"CurrentDirectory"); } - _ = Logger.Log($"Running command: {logcmd}"); } @@ -88,6 +144,264 @@ namespace AndroidSideloader } } + // Executes a shell command on the device. + private static void ExecuteShellCommand(AdbClient client, DeviceData device, string command) + { + var receiver = new ConsoleOutputReceiver(); + client.ExecuteRemoteCommand(command, device, receiver); + } + + // Copies and installs an APK with real-time progress reporting using AdvancedSharpAdbClient + public static async Task SideloadWithProgressAsync( + string path, + Action progressCallback = null, + Action statusCallback = null, + string packagename = "", + string gameName = "") + { + statusCallback?.Invoke("Installing APK..."); + progressCallback?.Invoke(0); + + try + { + var device = GetCurrentDevice(); + if (device.Serial == null) + { + return new ProcessOutput("", "No device connected"); + } + + var client = GetAdbClient(); + var packageManager = new PackageManager(client, device); + + statusCallback?.Invoke("Installing APK..."); + + // Create install progress handler + Action installProgress = (args) => + { + // Map PackageInstallProgressState to percentage + int percent = 0; + switch (args.State) + { + case PackageInstallProgressState.Preparing: + percent = 0; + statusCallback?.Invoke("Preparing..."); + break; + case PackageInstallProgressState.Uploading: + percent = (int)Math.Round(args.UploadProgress); + statusCallback?.Invoke($"Installing · {args.UploadProgress:F0}%"); + break; + case PackageInstallProgressState.Installing: + percent = 100; + statusCallback?.Invoke("Completing Installation..."); + break; + case PackageInstallProgressState.Finished: + percent = 100; + statusCallback?.Invoke(""); + break; + default: + percent = 50; + break; + } + progressCallback?.Invoke(percent); + }; + + // Install the package with progress + await Task.Run(() => + { + packageManager.InstallPackage(path, installProgress); + }); + + progressCallback?.Invoke(100); + statusCallback?.Invoke(""); + + return new ProcessOutput($"{gameName}: Success\n"); + } + catch (Exception ex) + { + Logger.Log($"SideloadWithProgressAsync error: {ex.Message}", LogLevel.ERROR); + + // Check for signature mismatch errors + if (ex.Message.Contains("INSTALL_FAILED") || + ex.Message.Contains("signatures do not match")) + { + bool cancelClicked = false; + + if (!settings.AutoReinstall) + { + Program.form.Invoke(() => + { + DialogResult dialogResult1 = FlexibleMessageBox.Show(Program.form, + "In place upgrade has failed. Rookie can attempt to backup your save data and reinstall the game automatically, however some games do not store their saves in an accessible location (less than 5%). Continue with reinstall?", + "In place upgrade failed.", MessageBoxButtons.OKCancel); + if (dialogResult1 == DialogResult.Cancel) + cancelClicked = true; + }); + } + + if (cancelClicked) + return new ProcessOutput("", "Installation cancelled by user"); + + // Perform reinstall + statusCallback?.Invoke("Performing reinstall..."); + + try + { + var device = GetCurrentDevice(); + 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)); + } + }; + packageManager.InstallPackage(path, reinstallProgress); + + // Restore save data + statusCallback?.Invoke("Restoring save data..."); + _ = RunAdbCommandToString($"push \"{Environment.CurrentDirectory}\\{MainForm.CurrPCKG}\" /sdcard/Android/data/"); + + string directoryToDelete = Path.Combine(Environment.CurrentDirectory, MainForm.CurrPCKG); + if (Directory.Exists(directoryToDelete) && directoryToDelete != Environment.CurrentDirectory) + { + Directory.Delete(directoryToDelete, true); + } + + progressCallback?.Invoke(100); + return new ProcessOutput($"{gameName}: Reinstall: Success\n", ""); + } + catch (Exception reinstallEx) + { + return new ProcessOutput($"{gameName}: Reinstall: Failed: {reinstallEx.Message}\n"); + } + } + + return new ProcessOutput("", ex.Message); + } + } + + // Copies OBB folder with real-time progress reporting using AdvancedSharpAdbClient + public static async Task CopyOBBWithProgressAsync( + string localPath, + Action progressCallback = null, + Action statusCallback = null, + string gameName = "") + { + string folderName = Path.GetFileName(localPath); + + if (!folderName.Contains(".")) + { + return new ProcessOutput("No OBB Folder found"); + } + + try + { + var device = GetCurrentDevice(); + if (device.Serial == null) + { + return new ProcessOutput("", "No device connected"); + } + + var client = GetAdbClient(); + string remotePath = $"/sdcard/Android/obb/{folderName}"; + + statusCallback?.Invoke($"Preparing: {folderName}"); + progressCallback?.Invoke(0); + + // Delete existing OBB folder and create new one + ExecuteShellCommand(client, device, $"rm -rf \"{remotePath}\""); + ExecuteShellCommand(client, device, $"mkdir -p \"{remotePath}\""); + + // Get all files to push and calculate total size + var files = Directory.GetFiles(localPath, "*", SearchOption.AllDirectories); + long totalBytes = files.Sum(f => new FileInfo(f).Length); + long transferredBytes = 0; + + statusCallback?.Invoke($"Copying: {folderName}"); + + using (var syncService = new SyncService(client, device)) + { + foreach (var file in files) + { + string relativePath = file.Substring(localPath.Length) + .TrimStart('\\', '/') + .Replace('\\', '/'); + string remoteFilePath = $"{remotePath}/{relativePath}"; + string fileName = Path.GetFileName(file); + + // Let UI know which file we're currently on + statusCallback?.Invoke(fileName); + + // Ensure remote directory exists + string remoteDir = remoteFilePath.Substring(0, remoteFilePath.LastIndexOf('/')); + ExecuteShellCommand(client, device, $"mkdir -p \"{remoteDir}\""); + + var fileInfo = new FileInfo(file); + long fileSize = fileInfo.Length; + long capturedTransferredBytes = transferredBytes; + + // Progress handler for this file + Action progressHandler = (args) => + { + long totalProgressBytes = capturedTransferredBytes + args.ReceivedBytesSize; + + double overallPercent = totalBytes > 0 + ? (totalProgressBytes * 100.0) / totalBytes + : 0.0; + + int overallPercentInt = (int)Math.Round(overallPercent); + overallPercentInt = Math.Max(0, Math.Min(100, overallPercentInt)); + + // Single source of truth for UI (bar + label + text) + progressCallback?.Invoke(overallPercentInt); + }; + + // Push the file with progress + using (var stream = File.OpenRead(file)) + { + await Task.Run(() => + { + syncService.Push( + stream, + remoteFilePath, + UnixFileStatus.DefaultFileMode, + DateTime.Now, + progressHandler, + false); + }); + } + + // Mark this file as fully transferred + transferredBytes += fileSize; + } + } + + // Ensure final 100% and clear status + progressCallback?.Invoke(100); + statusCallback?.Invoke(""); + + return new ProcessOutput($"{gameName}: OBB transfer: Success\n", ""); + } + catch (Exception ex) + { + Logger.Log($"CopyOBBWithProgressAsync error: {ex.Message}", LogLevel.ERROR); + + return new ProcessOutput("", $"{gameName}: OBB transfer: Failed: {ex.Message}\n"); + } + } + public static ProcessOutput RunAdbCommandToStringWOADB(string result, string path) { string command = result; @@ -99,54 +413,51 @@ namespace AndroidSideloader _ = Logger.Log($"Running command: {logcmd}"); - adb.StartInfo.FileName = "cmd.exe"; - adb.StartInfo.RedirectStandardError = true; - adb.StartInfo.RedirectStandardInput = true; - adb.StartInfo.RedirectStandardOutput = true; - adb.StartInfo.CreateNoWindow = true; - adb.StartInfo.UseShellExecute = false; - adb.StartInfo.WorkingDirectory = Path.GetDirectoryName(path); - _ = adb.Start(); - adb.StandardInput.WriteLine(command); - adb.StandardInput.Flush(); - adb.StandardInput.Close(); - - - string output = ""; - string error = ""; - - try + using (var adb = new Process()) { - output += adb.StandardOutput.ReadToEnd(); - error += adb.StandardError.ReadToEnd(); - } - catch { } - if (command.Contains("connect")) - { - bool graceful = adb.WaitForExit(3000); - if (!graceful) + adb.StartInfo.FileName = "cmd.exe"; + adb.StartInfo.RedirectStandardError = true; + adb.StartInfo.RedirectStandardInput = true; + adb.StartInfo.RedirectStandardOutput = true; + adb.StartInfo.CreateNoWindow = true; + adb.StartInfo.UseShellExecute = false; + adb.StartInfo.WorkingDirectory = Path.GetDirectoryName(path); + _ = adb.Start(); + adb.StandardInput.WriteLine(command); + adb.StandardInput.Flush(); + adb.StandardInput.Close(); + + string output = ""; + string error = ""; + + try { - adb.Kill(); - adb.WaitForExit(); + output += adb.StandardOutput.ReadToEnd(); + error += adb.StandardError.ReadToEnd(); } - } - else if (command.Contains("connect")) - { - bool graceful = adb.WaitForExit(3000); - if (!graceful) + catch { } + + if (command.Contains("connect")) { - adb.Kill(); - adb.WaitForExit(); + bool graceful = adb.WaitForExit(3000); + if (!graceful) + { + adb.Kill(); + adb.WaitForExit(); + } } + + if (error.Contains("ADB_VENDOR_KEYS") && settings.AdbDebugWarned) + { + ADBDebugWarning(); + } + + _ = Logger.Log(output); + _ = Logger.Log(error, LogLevel.ERROR); + return new ProcessOutput(output, error); } - if (error.Contains("ADB_VENDOR_KEYS") && settings.AdbDebugWarned) - { - ADBDebugWarning(); - } - _ = Logger.Log(output); - _ = Logger.Log(error, LogLevel.ERROR); - return new ProcessOutput(output, error); } + public static ProcessOutput RunCommandToString(string result, string path = "") { string command = result; @@ -160,37 +471,37 @@ namespace AndroidSideloader try { - using (var adb = new Process()) + using (var proc = new Process()) { - adb.StartInfo.FileName = $@"{Path.GetPathRoot(Environment.SystemDirectory)}\Windows\System32\cmd.exe"; - adb.StartInfo.Arguments = command; - adb.StartInfo.RedirectStandardError = true; - adb.StartInfo.RedirectStandardInput = true; - adb.StartInfo.RedirectStandardOutput = true; - adb.StartInfo.CreateNoWindow = true; - adb.StartInfo.UseShellExecute = false; - adb.StartInfo.WorkingDirectory = Path.GetDirectoryName(path); + proc.StartInfo.FileName = $@"{Path.GetPathRoot(Environment.SystemDirectory)}\Windows\System32\cmd.exe"; + proc.StartInfo.Arguments = command; + proc.StartInfo.RedirectStandardError = true; + proc.StartInfo.RedirectStandardInput = true; + proc.StartInfo.RedirectStandardOutput = true; + proc.StartInfo.CreateNoWindow = true; + proc.StartInfo.UseShellExecute = false; + proc.StartInfo.WorkingDirectory = Path.GetDirectoryName(path); - adb.Start(); - adb.StandardInput.WriteLine(command); - adb.StandardInput.Flush(); - adb.StandardInput.Close(); + proc.Start(); + proc.StandardInput.WriteLine(command); + proc.StandardInput.Flush(); + proc.StandardInput.Close(); - string output = adb.StandardOutput.ReadToEnd(); - string error = adb.StandardError.ReadToEnd(); + string output = proc.StandardOutput.ReadToEnd(); + string error = proc.StandardError.ReadToEnd(); if (command.Contains("connect")) { - bool graceful = adb.WaitForExit(3000); + bool graceful = proc.WaitForExit(3000); if (!graceful) { - adb.Kill(); - adb.WaitForExit(); + proc.Kill(); + proc.WaitForExit(); } } else { - adb.WaitForExit(); + proc.WaitForExit(); } if (error.Contains("ADB_VENDOR_KEYS") && settings.AdbDebugWarned) @@ -198,12 +509,6 @@ namespace AndroidSideloader ADBDebugWarning(); } - if (error.Contains("Asset path") && error.Contains("is neither a directory nor file")) - { - Logger.Log("Asset path error detected. The specified path might not exist or be accessible.", LogLevel.WARNING); - // You might want to handle this specific error differently - } - Logger.Log(output); Logger.Log(error, LogLevel.ERROR); @@ -221,10 +526,11 @@ namespace AndroidSideloader { Program.form.Invoke(() => { - DialogResult dialogResult = FlexibleMessageBox.Show(Program.form, "On your headset, click on the Notifications Bell, and then select the USB Detected notification to enable Connections.", "ADB Debugging not enabled.", MessageBoxButtons.OKCancel); + DialogResult dialogResult = FlexibleMessageBox.Show(Program.form, + "On your headset, click on the Notifications Bell, and then select the USB Detected notification to enable Connections.", + "ADB Debugging not enabled.", MessageBoxButtons.OKCancel); if (dialogResult == DialogResult.Cancel) { - // settings.adbdebugwarned = true; settings.Save(); } }); @@ -234,6 +540,20 @@ namespace AndroidSideloader { ProcessOutput output = new ProcessOutput("", ""); output += RunAdbCommandToString($"shell pm uninstall {package}"); + + // Prefix the output with the simple game name + string label = Sideloader.gameNameToSimpleName(Sideloader.PackageNametoGameName(package)); + + if (!string.IsNullOrEmpty(output.Output)) + { + output.Output = $"{label}: {output.Output}"; + } + + if (!string.IsNullOrEmpty(output.Error)) + { + output.Error = $"{label}: {output.Error}"; + } + return output; } @@ -255,7 +575,7 @@ namespace AndroidSideloader totalSize = long.Parse(foo[1]) / 1000; usedSize = long.Parse(foo[2]) / 1000; freeSize = long.Parse(foo[3]) / 1000; - break; // Assuming we only need the first matching line + break; } } } @@ -263,51 +583,52 @@ namespace AndroidSideloader return $"Total space: {string.Format("{0:0.00}", (double)totalSize / 1000)}GB\nUsed space: {string.Format("{0:0.00}", (double)usedSize / 1000)}GB\nFree space: {string.Format("{0:0.00}", (double)freeSize / 1000)}GB"; } - public static bool wirelessadbON; public static ProcessOutput Sideload(string path, string packagename = "") { ProcessOutput ret = new ProcessOutput(); ret += RunAdbCommandToString($"install -g \"{path}\""); string out2 = ret.Output + ret.Error; + if (out2.Contains("failed")) { _ = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups"); _ = Logger.Log(out2); + if (out2.Contains("offline") && !settings.NodeviceMode) { DialogResult dialogResult2 = FlexibleMessageBox.Show(Program.form, "Device is offline. Press Yes to reconnect, or if you don't wish to connect and just want to download the game (requires unchecking \"Delete games after install\" from settings menu) then press No.", "Device offline.", MessageBoxButtons.YesNoCancel); } + if (out2.Contains($"signatures do not match previously") || out2.Contains("INSTALL_FAILED_VERSION_DOWNGRADE") || out2.Contains("signatures do not match") || out2.Contains("failed to install")) { ret.Error = string.Empty; ret.Output = string.Empty; + + bool cancelClicked = false; + if (!settings.AutoReinstall) { - bool cancelClicked = false; - - if (!settings.AutoReinstall) + Program.form.Invoke((MethodInvoker)(() => { - Program.form.Invoke((MethodInvoker)(() => - { - DialogResult dialogResult1 = FlexibleMessageBox.Show(Program.form, "In place upgrade has failed. Rookie can attempt to backup your save data and reinstall the game automatically, however some games do not store their saves in an accessible location (less than 5%). Continue with reinstall?", "In place upgrade failed.", MessageBoxButtons.OKCancel); - if (dialogResult1 == DialogResult.Cancel) - cancelClicked = true; - })); - } - - if (cancelClicked) - return ret; + DialogResult dialogResult1 = FlexibleMessageBox.Show(Program.form, "In place upgrade has failed. Rookie can attempt to backup your save data and reinstall the game automatically, however some games do not store their saves in an accessible location (less than 5%). Continue with reinstall?", "In place upgrade failed.", MessageBoxButtons.OKCancel); + if (dialogResult1 == DialogResult.Cancel) + cancelClicked = true; + })); } + if (cancelClicked) + return ret; + Program.form.changeTitle("Performing reinstall, please wait..."); - _ = ADB.RunAdbCommandToString("kill-server"); - _ = ADB.RunAdbCommandToString("devices"); - _ = ADB.RunAdbCommandToString($"pull \"/sdcard/Android/data/{MainForm.CurrPCKG}\" \"{Environment.CurrentDirectory}\""); + _ = RunAdbCommandToString("kill-server"); + _ = RunAdbCommandToString("devices"); + _ = RunAdbCommandToString($"pull \"/sdcard/Android/data/{MainForm.CurrPCKG}\" \"{Environment.CurrentDirectory}\""); Program.form.changeTitle("Uninstalling game..."); _ = Sideloader.UninstallGame(MainForm.CurrPCKG); Program.form.changeTitle("Reinstalling game..."); - ret += ADB.RunAdbCommandToString($"install -g \"{path}\""); - _ = ADB.RunAdbCommandToString($"push \"{Environment.CurrentDirectory}\\{MainForm.CurrPCKG}\" /sdcard/Android/data/"); + ret += RunAdbCommandToString($"install -g \"{path}\""); + _ = RunAdbCommandToString($"push \"{Environment.CurrentDirectory}\\{MainForm.CurrPCKG}\" /sdcard/Android/data/"); + string directoryToDelete = Path.Combine(Environment.CurrentDirectory, MainForm.CurrPCKG); if (Directory.Exists(directoryToDelete)) { @@ -335,4 +656,4 @@ namespace AndroidSideloader : new ProcessOutput("No OBB Folder found"); } } -} +} \ No newline at end of file diff --git a/AndroidSideloader.csproj b/AndroidSideloader.csproj index f135480..2086f73 100644 --- a/AndroidSideloader.csproj +++ b/AndroidSideloader.csproj @@ -136,6 +136,9 @@ AndroidSideloader_TemporaryKey.pfx + + packages\AdvancedSharpAdbClient.3.5.15\lib\net45\AdvancedSharpAdbClient.dll + packages\Costura.Fody.5.7.0\lib\netstandard1.0\Costura.dll @@ -187,6 +190,9 @@ Component + + Component + True True diff --git a/DonorsListView.cs b/DonorsListView.cs index 56843f8..55d2d6f 100644 --- a/DonorsListView.cs +++ b/DonorsListView.cs @@ -305,7 +305,9 @@ namespace AndroidSideloader File.WriteAllText(blacklistPath, Newtonsoft.Json.JsonConvert.SerializeObject(existingBlacklist.ToArray(), Newtonsoft.Json.Formatting.Indented)); Logger.Log($"Added {appsToBlacklist.Count} apps to local blacklist"); - MessageBox.Show($"{appsToBlacklist.Count} app(s) added to blacklist.", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); + MessageBox.Show($"{appsToBlacklist.Count} {(appsToBlacklist.Count == 1 ? "app" : "apps")} added to blacklist.", + "Success", + MessageBoxButtons.OK, MessageBoxIcon.Information); Close(); } catch (Exception ex) diff --git a/MainForm.Designer.cs b/MainForm.Designer.cs index 2e7735f..d39582e 100644 --- a/MainForm.Designer.cs +++ b/MainForm.Designer.cs @@ -34,7 +34,7 @@ namespace AndroidSideloader { this.components = new System.ComponentModel.Container(); this.m_combo = new SergeUtils.EasyCompletionComboBox(); - this.progressBar = new System.Windows.Forms.ProgressBar(); + 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(); @@ -117,9 +117,9 @@ namespace AndroidSideloader this.leftNavContainer = new System.Windows.Forms.Panel(); this.statusInfoPanel = new System.Windows.Forms.Panel(); this.sideloadingStatusLabel = new System.Windows.Forms.Label(); - this.rookieStatusLabel = new System.Windows.Forms.Label(); this.activeMirrorLabel = new System.Windows.Forms.Label(); 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(); @@ -176,14 +176,26 @@ namespace AndroidSideloader // 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)(64)))), ((int)(((byte)(64)))), ((int)(((byte)(64))))); + 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.Style = System.Windows.Forms.ProgressBarStyle.Continuous; + 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 // @@ -1200,7 +1212,6 @@ namespace AndroidSideloader this.statusInfoPanel.Controls.Add(this.activeMirrorLabel); this.statusInfoPanel.Controls.Add(this.deviceIdLabel); this.statusInfoPanel.Controls.Add(this.rookieStatusLabel); - this.statusInfoPanel.AutoSize = false; this.statusInfoPanel.Dock = System.Windows.Forms.DockStyle.Bottom; this.statusInfoPanel.Location = new System.Drawing.Point(0, 1019); this.statusInfoPanel.Name = "statusInfoPanel"; @@ -1208,58 +1219,54 @@ namespace AndroidSideloader this.statusInfoPanel.Size = new System.Drawing.Size(233, 81); this.statusInfoPanel.TabIndex = 102; // - // rookieStatusLabel - // - this.rookieStatusLabel.AutoSize = false; - this.rookieStatusLabel.Font = new System.Drawing.Font("Segoe UI", 8F, System.Drawing.FontStyle.Bold); - this.rookieStatusLabel.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); - this.rookieStatusLabel.Location = new System.Drawing.Point(8, 4); - this.rookieStatusLabel.Name = "rookieStatusLabel"; - this.rookieStatusLabel.Padding = new System.Windows.Forms.Padding(0, 2, 0, 2); - this.rookieStatusLabel.Size = new System.Drawing.Size(225, 17); - this.rookieStatusLabel.AutoEllipsis = true; - this.rookieStatusLabel.TabIndex = 0; - this.rookieStatusLabel.Text = "Status"; - // - // deviceIdLabel - // - this.deviceIdLabel.AutoSize = false; - this.deviceIdLabel.Font = new System.Drawing.Font("Segoe UI", 8F); - this.deviceIdLabel.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(140)))), ((int)(((byte)(145)))), ((int)(((byte)(150))))); - this.deviceIdLabel.Location = new System.Drawing.Point(8, 21); - this.deviceIdLabel.Name = "deviceIdLabel"; - this.deviceIdLabel.Padding = new System.Windows.Forms.Padding(0, 2, 0, 2); - this.deviceIdLabel.Size = new System.Drawing.Size(225, 17); - this.deviceIdLabel.AutoEllipsis = true; - this.deviceIdLabel.TabIndex = 1; - this.deviceIdLabel.Text = "Device: Not connected"; - // - // activeMirrorLabel - // - this.activeMirrorLabel.AutoSize = false; - this.activeMirrorLabel.Font = new System.Drawing.Font("Segoe UI", 8F); - this.activeMirrorLabel.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(140)))), ((int)(((byte)(145)))), ((int)(((byte)(150))))); - this.activeMirrorLabel.Location = new System.Drawing.Point(8, 38); - this.activeMirrorLabel.Name = "activeMirrorLabel"; - this.activeMirrorLabel.Padding = new System.Windows.Forms.Padding(0, 2, 0, 2); - this.activeMirrorLabel.Size = new System.Drawing.Size(225, 17); - this.activeMirrorLabel.AutoEllipsis = true; - this.activeMirrorLabel.TabIndex = 2; - this.activeMirrorLabel.Text = "Mirror: None"; - // // sideloadingStatusLabel // - this.sideloadingStatusLabel.AutoSize = false; + this.sideloadingStatusLabel.AutoEllipsis = true; this.sideloadingStatusLabel.Font = new System.Drawing.Font("Segoe UI", 8F, System.Drawing.FontStyle.Bold); this.sideloadingStatusLabel.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); this.sideloadingStatusLabel.Location = new System.Drawing.Point(8, 55); this.sideloadingStatusLabel.Name = "sideloadingStatusLabel"; this.sideloadingStatusLabel.Padding = new System.Windows.Forms.Padding(0, 2, 0, 2); this.sideloadingStatusLabel.Size = new System.Drawing.Size(225, 17); - this.sideloadingStatusLabel.AutoEllipsis = true; this.sideloadingStatusLabel.TabIndex = 3; this.sideloadingStatusLabel.Text = "Sideloading: Enabled"; // + // activeMirrorLabel + // + this.activeMirrorLabel.AutoEllipsis = true; + this.activeMirrorLabel.Font = new System.Drawing.Font("Segoe UI", 8F); + this.activeMirrorLabel.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(140)))), ((int)(((byte)(145)))), ((int)(((byte)(150))))); + this.activeMirrorLabel.Location = new System.Drawing.Point(8, 38); + this.activeMirrorLabel.Name = "activeMirrorLabel"; + this.activeMirrorLabel.Padding = new System.Windows.Forms.Padding(0, 2, 0, 2); + this.activeMirrorLabel.Size = new System.Drawing.Size(225, 17); + this.activeMirrorLabel.TabIndex = 2; + this.activeMirrorLabel.Text = "Mirror: None"; + // + // deviceIdLabel + // + this.deviceIdLabel.AutoEllipsis = true; + this.deviceIdLabel.Font = new System.Drawing.Font("Segoe UI", 8F); + this.deviceIdLabel.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(140)))), ((int)(((byte)(145)))), ((int)(((byte)(150))))); + this.deviceIdLabel.Location = new System.Drawing.Point(8, 21); + this.deviceIdLabel.Name = "deviceIdLabel"; + this.deviceIdLabel.Padding = new System.Windows.Forms.Padding(0, 2, 0, 2); + this.deviceIdLabel.Size = new System.Drawing.Size(225, 17); + this.deviceIdLabel.TabIndex = 1; + this.deviceIdLabel.Text = "Device: Not connected"; + // + // rookieStatusLabel + // + this.rookieStatusLabel.AutoEllipsis = true; + this.rookieStatusLabel.Font = new System.Drawing.Font("Segoe UI", 8F, System.Drawing.FontStyle.Bold); + this.rookieStatusLabel.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); + this.rookieStatusLabel.Location = new System.Drawing.Point(8, 4); + this.rookieStatusLabel.Name = "rookieStatusLabel"; + this.rookieStatusLabel.Padding = new System.Windows.Forms.Padding(0, 2, 0, 2); + this.rookieStatusLabel.Size = new System.Drawing.Size(225, 17); + this.rookieStatusLabel.TabIndex = 0; + this.rookieStatusLabel.Text = "Status"; + // // sidebarMediaPanel // this.sidebarMediaPanel.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(20)))), ((int)(((byte)(24)))), ((int)(((byte)(29))))); @@ -1600,7 +1607,6 @@ namespace AndroidSideloader this.HelpButton = true; this.MinimumSize = new System.Drawing.Size(1048, 760); this.Name = "MainForm"; - this.ShowIcon = false; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; this.Text = "Rookie Sideloader"; this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing); @@ -1623,7 +1629,6 @@ namespace AndroidSideloader this.leftNavContainer.ResumeLayout(false); this.leftNavContainer.PerformLayout(); this.statusInfoPanel.ResumeLayout(false); - this.statusInfoPanel.PerformLayout(); this.sidebarMediaPanel.ResumeLayout(false); this.tableLayoutPanel1.ResumeLayout(false); this.searchPanel.ResumeLayout(false); @@ -1638,7 +1643,7 @@ namespace AndroidSideloader #endregion private SergeUtils.EasyCompletionComboBox m_combo; - private System.Windows.Forms.ProgressBar progressBar; + private ModernProgressBar progressBar; private System.Windows.Forms.Label etaLabel; private System.Windows.Forms.Label speedLabel; private System.Windows.Forms.Label freeDisclaimer; diff --git a/MainForm.cs b/MainForm.cs index f34f296..570a5e5 100755 --- a/MainForm.cs +++ b/MainForm.cs @@ -105,6 +105,9 @@ namespace AndroidSideloader InitializeTimeReferences(); CheckCommandLineArguments(); + // Use same icon as the executable + this.Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); + // Load user's preferred view from settings isGalleryView = settings.UseGalleryView; @@ -597,7 +600,8 @@ namespace AndroidSideloader btnNoDevice.Text = "ENABLE SIDELOADING"; } - progressBar.Style = ProgressBarStyle.Marquee; + progressBar.IsIndeterminate = true; + progressBar.OperationType = "Loading"; // Update check if (!debugMode && settings.CheckForUpdates && !isOffline) @@ -729,7 +733,8 @@ namespace AndroidSideloader await Task.WhenAll(tasksToWait); } - progressBar.Style = ProgressBarStyle.Marquee; + progressBar.IsIndeterminate = true; + progressBar.OperationType = "Loading"; changeTitle("Populating Game List..."); _ = await CheckForDevice(); @@ -950,27 +955,34 @@ namespace AndroidSideloader if (dialog.Show(Handle)) { - progressBar.Style = ProgressBarStyle.Marquee; string path = dialog.FileName; - changeTitle($"Copying {path} OBB to device..."); - Thread t1 = new Thread(() => - { - output += output += ADB.CopyOBB(path); - }) - { - IsBackground = true - }; - t1.Start(); + string folderName = Path.GetFileName(path); - while (t1.IsAlive) - { - await Task.Delay(100); - } + changeTitle($"Copying {folderName} OBB to device..."); + progressBar.IsIndeterminate = false; + progressBar.Value = 0; + progressBar.OperationType = "Copying OBB"; + + output = await ADB.CopyOBBWithProgressAsync( + path, + progress => this.Invoke(() => { + progressBar.Value = progress; + speedLabel.Text = $"Progress: {progress}%"; + }), + status => this.Invoke(() => { + progressBar.StatusText = status; + etaLabel.Text = status; + }), + folderName); + + progressBar.Value = 100; changeTitle("Done."); showAvailableSpace(); ShowPrcOutput(output); changeTitle(""); + speedLabel.Text = ""; + etaLabel.Text = ""; } } @@ -1356,7 +1368,8 @@ namespace AndroidSideloader if (!isworking) { isworking = true; - progressBar.Style = ProgressBarStyle.Marquee; + progressBar.IsIndeterminate = true; + progressBar.OperationType = "Loading"; string HWID = SideloaderUtilities.UUID(); string GameName = selectedApp; string packageName = Sideloader.gameNameToPackageName(GameName); @@ -1418,7 +1431,7 @@ namespace AndroidSideloader changeTitle("Zipping extracted application..."); string cmd = $"7z a -mx1 \"{gameZipName}\" .\\{packageName}\\*"; string path = $"{settings.MainDir}\\7z.exe"; - progressBar.Style = ProgressBarStyle.Continuous; + progressBar.IsIndeterminate = false; Thread t4 = new Thread(() => { _ = ADB.RunCommandToString(cmd, path); @@ -1513,7 +1526,8 @@ namespace AndroidSideloader Sideloader.BackupGame(packagename); } ProcessOutput output = new ProcessOutput("", ""); - progressBar.Style = ProgressBarStyle.Marquee; + progressBar.IsIndeterminate = true; + progressBar.OperationType = "Loading"; Thread t1 = new Thread(() => { output += Sideloader.UninstallGame(packagename); @@ -1527,7 +1541,7 @@ namespace AndroidSideloader ShowPrcOutput(output); showAvailableSpace(); - progressBar.Style = ProgressBarStyle.Continuous; + progressBar.IsIndeterminate = false; } private async void copyBulkObbButton_Click(object sender, EventArgs e) @@ -1576,7 +1590,8 @@ namespace AndroidSideloader DragDropLbl.Visible = false; ProcessOutput output = new ProcessOutput(String.Empty, String.Empty); ADB.DeviceID = GetDeviceID(); - progressBar.Style = ProgressBarStyle.Marquee; + progressBar.IsIndeterminate = true; + progressBar.OperationType = "Loading"; CurrPCKG = String.Empty; string[] datas = (string[])e.Data.GetData(DataFormats.FileDrop); foreach (string data in datas) @@ -1928,7 +1943,7 @@ namespace AndroidSideloader } } - progressBar.Style = ProgressBarStyle.Continuous; + progressBar.IsIndeterminate = false; showAvailableSpace(); @@ -2050,7 +2065,8 @@ namespace AndroidSideloader if (SideloaderRCLONE.games.Count > 5) { - progressBar.Style = ProgressBarStyle.Marquee; + progressBar.IsIndeterminate = true; + progressBar.OperationType = ""; // Use full dumpsys to get all version codes at once Dictionary installedVersions = new Dictionary(packageList.Length, StringComparer.OrdinalIgnoreCase); @@ -2257,7 +2273,7 @@ namespace AndroidSideloader await ProcessNewApps(newGamesList, blacklistSet.ToList()); } - progressBar.Style = ProgressBarStyle.Continuous; + progressBar.IsIndeterminate = false; if (either && !updatesNotified && !noAppCheck) { @@ -2572,7 +2588,8 @@ namespace AndroidSideloader public async Task extractAndPrepareGameToUploadAsync(string GameName, string packagename, ulong installedVersionInt, bool isupdate) { - progressBar.Style = ProgressBarStyle.Marquee; + progressBar.IsIndeterminate = true; + progressBar.OperationType = ""; Thread t1 = new Thread(() => { @@ -2604,7 +2621,7 @@ namespace AndroidSideloader string HWID = SideloaderUtilities.UUID(); File.WriteAllText($"{settings.MainDir}\\{packagename}\\HWID.txt", HWID); - progressBar.Style = ProgressBarStyle.Continuous; + progressBar.IsIndeterminate = false; UploadGame game = new UploadGame { isUpdate = isupdate, @@ -2905,7 +2922,8 @@ Additional Thanks & Resources if (isLoading) { return; } isLoading = true; - progressBar.Style = ProgressBarStyle.Marquee; + progressBar.IsIndeterminate = true; + progressBar.OperationType = "Refreshing"; devicesbutton_Click(sender, e); await initMirrors(); @@ -2919,7 +2937,8 @@ Additional Thanks & Resources changeTitle(titleMessage); if (isLoading) { return; } isLoading = true; - progressBar.Style = ProgressBarStyle.Marquee; + progressBar.IsIndeterminate = true; + progressBar.OperationType = "Refreshing"; Thread t1 = new Thread(() => { @@ -3030,7 +3049,14 @@ Please visit our Telegram (https://t.me/VRPirates) or Discord (https://discord.g public void SetProgress(int progress) { - progressBar.Value = progress; + if (progressBar.InvokeRequired) + { + progressBar.Invoke(new Action(() => progressBar.Value = progress)); + } + else + { + progressBar.Value = progress; + } } public bool isinstalling = false; @@ -3051,10 +3077,11 @@ Please visit our Telegram (https://t.me/VRPirates) or Discord (https://discord.g showAvailableSpace(); listAppsBtn(); } - progressBar.Style = ProgressBarStyle.Marquee; + progressBar.IsIndeterminate = true; + progressBar.OperationType = "Downloading"; if (gamesListView.SelectedItems.Count == 0) { - progressBar.Style = ProgressBarStyle.Continuous; + progressBar.IsIndeterminate = false; changeTitle("You must select a game from the game list!"); return; } @@ -3077,7 +3104,7 @@ Please visit our Telegram (https://t.me/VRPirates) or Discord (https://discord.g } progressBar.Value = 0; - progressBar.Style = ProgressBarStyle.Continuous; + progressBar.IsIndeterminate = false; string game = gamesToDownload.Length == 1 ? $"\"{gamesToDownload[0]}\"" : "the selected games"; isinstalling = true; //Add games to the queue @@ -3279,7 +3306,7 @@ Please visit our Telegram (https://t.me/VRPirates) or Discord (https://discord.g // Logger.Log("Files: " + transfersComplete.ToString() + "/" + fileCount.ToString() + " (" + Convert.ToInt32((downloadedSize / totalSize) * 100).ToString() + "% Complete)"); // Logger.Log("Downloaded: " + downloadedSize.ToString() + " of " + totalSize.ToString()); - progressBar.Style = ProgressBarStyle.Continuous; + progressBar.IsIndeterminate = false; progressBar.Value = Convert.ToInt32((downloadedSize / totalSize) * 100); TimeSpan time = TimeSpan.FromSeconds(globalEta); @@ -3367,18 +3394,18 @@ Please visit our Telegram (https://t.me/VRPirates) or Discord (https://discord.g if (UsingPublicConfig && otherError == false && gameDownloadOutput.Output != "Download skipped.") { - Thread extractionThread = new Thread(() => { Invoke(new Action(() => { speedLabel.Text = "Extracting..."; etaLabel.Text = "Please wait..."; - progressBar.Style = ProgressBarStyle.Continuous; + progressBar.IsIndeterminate = false; progressBar.Value = 0; isInDownloadExtract = true; })); try { + progressBar.OperationType = "Extracting"; changeTitle("Extracting " + gameName); Zip.ExtractFile($"{settings.DownloadDir}\\{gameNameHash}\\{gameNameHash}.7z.001", $"{settings.DownloadDir}", PublicConfigFile.Password); changeTitle(""); @@ -3415,7 +3442,7 @@ Please visit our Telegram (https://t.me/VRPirates) or Discord (https://discord.g ADB.DeviceID = GetDeviceID(); quotaTries = 0; progressBar.Value = 0; - progressBar.Style = ProgressBarStyle.Continuous; + progressBar.IsIndeterminate = false; changeTitle("Installing game APK " + gameName); etaLabel.Text = "ETA: Wait for install..."; speedLabel.Text = "DLS: Finished"; @@ -3474,41 +3501,90 @@ Please visit our Telegram (https://t.me/VRPirates) or Discord (https://discord.g }; t.Tick += new EventHandler(timer_Tick4); t.Start(); - Thread apkThread = new Thread(() => - { - changeTitle($"Sideloading APK..."); - etaLabel.Text = "Sideloading APK..."; - output += ADB.Sideload(apkFile, packagename); - }) - { - IsBackground = true - }; - apkThread.Start(); - while (apkThread.IsAlive) - { - await Task.Delay(100); - } + + changeTitle($"Sideloading APK..."); + etaLabel.Text = "Installing APK..."; + progressBar.IsIndeterminate = false; + progressBar.OperationType = "Installing"; + progressBar.Value = 0; + + // Use async method with progress + output += await ADB.SideloadWithProgressAsync( + apkFile, + progress => this.Invoke(() => { + if (progress == 0) + { + progressBar.IsIndeterminate = true; + progressBar.OperationType = "Installing"; + } + else + { + progressBar.IsIndeterminate = false; + progressBar.Value = progress; + } + }), + status => this.Invoke(() => { + progressBar.StatusText = status; + etaLabel.Text = status; + }), + packagename, + Sideloader.gameNameToSimpleName(gameName)); + t.Stop(); + progressBar.IsIndeterminate = false; Debug.WriteLine(wrDelimiter); if (Directory.Exists($"{settings.DownloadDir}\\{gameName}\\{packagename}")) { deleteOBB(packagename); - Thread obbThread = new Thread(() => - { - changeTitle($"Copying {packagename} OBB to device..."); - ADB.RunAdbCommandToString($"shell mkdir \"/sdcard/Android/obb/{packagename}\""); - output += ADB.RunAdbCommandToString($"push \"{settings.DownloadDir}\\{gameName}\\{packagename}\" \"/sdcard/Android/obb\""); - changeTitle(""); - }) - { - IsBackground = true - }; - obbThread.Start(); - while (obbThread.IsAlive) - { - await Task.Delay(100); - } + + changeTitle($"Copying {packagename} OBB to device..."); + etaLabel.Text = "Copying OBB..."; + progressBar.Value = 0; + progressBar.OperationType = "Copying OBB"; + + // Use async method with progress for OBB + string currentObbStatusBase = string.Empty; // phase or filename + + output += await ADB.CopyOBBWithProgressAsync( + $"{settings.DownloadDir}\\{gameName}\\{packagename}", + progress => this.Invoke(() => + { + progressBar.Value = progress; + speedLabel.Text = $"OBB: {progress}%"; + + 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}%"; + } + } + 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)); + + changeTitle(""); + if (!nodeviceonstart | DeviceConnected) { if (!output.Output.Contains("offline")) @@ -3564,7 +3640,7 @@ Please visit our Telegram (https://t.me/VRPirates) or Discord (https://discord.g { ShowPrcOutput(output); } - progressBar.Style = ProgressBarStyle.Continuous; + progressBar.IsIndeterminate = false; etaLabel.Text = "ETA: Finished Queue"; speedLabel.Text = "DLS: Finished Queue"; gamesAreDownloading = false; @@ -3686,7 +3762,7 @@ Please visit our Telegram (https://t.me/VRPirates) or Discord (https://discord.g { ShowPrcOutput(output); } - progressBar.Style = ProgressBarStyle.Continuous; + progressBar.IsIndeterminate = false; etaLabel.Text = "ETA: Finished Queue"; speedLabel.Text = "DLS: Finished Queue"; gamesAreDownloading = false; @@ -3849,7 +3925,8 @@ Please visit our Telegram (https://t.me/VRPirates) or Discord (https://discord.g { ADB.wirelessadbON = false; changeTitle("Disabling wireless ADB..."); - progressBar.Style = ProgressBarStyle.Marquee; + progressBar.IsIndeterminate = true; + progressBar.OperationType = ""; await Task.Run(() => { @@ -3866,7 +3943,7 @@ Please visit our Telegram (https://t.me/VRPirates) or Discord (https://discord.g try { File.Delete(storedIpPath); } catch { } } - progressBar.Style = ProgressBarStyle.Continuous; + progressBar.IsIndeterminate = false; _ = await CheckForDevice(); changeTitlebarToDevice(); changeTitle("Wireless ADB disabled.", true); @@ -3917,12 +3994,13 @@ Please visit our Telegram (https://t.me/VRPirates) or Discord (https://discord.g // Connect to the device changeTitle($"Connecting to {ipAddress}..."); - progressBar.Style = ProgressBarStyle.Marquee; + progressBar.IsIndeterminate = true; + progressBar.OperationType = ""; string ipCommand = $"connect {ipAddress}:5555"; string connectResult = await Task.Run(() => ADB.RunAdbCommandToString(ipCommand).Output); - progressBar.Style = ProgressBarStyle.Continuous; + progressBar.IsIndeterminate = false; if (connectResult.Contains("cannot resolve host") || connectResult.Contains("cannot connect to") || @@ -4090,7 +4168,8 @@ Please visit our Telegram (https://t.me/VRPirates) or Discord (https://discord.g } changeTitle("Scanning network for ADB devices..."); - progressBar.Style = ProgressBarStyle.Marquee; + progressBar.IsIndeterminate = true; + progressBar.OperationType = ""; // Scan common IP range (1-254) on port 5555 var tasks = new List>(); @@ -4104,7 +4183,7 @@ Please visit our Telegram (https://t.me/VRPirates) or Discord (https://discord.g var results = await Task.WhenAll(tasks); foundDevices.AddRange(results.Where(r => !string.IsNullOrEmpty(r))); - progressBar.Style = ProgressBarStyle.Continuous; + progressBar.IsIndeterminate = false; changeTitle(""); return foundDevices; @@ -5100,7 +5179,8 @@ function onYouTubeIframeAPIReady() { if (!isworking) { isworking = true; - progressBar.Style = ProgressBarStyle.Marquee; + progressBar.IsIndeterminate = true; + progressBar.OperationType = "Loading"; string HWID = SideloaderUtilities.UUID(); string GameName = selectedApp; string packageName = Sideloader.gameNameToPackageName(GameName); @@ -5179,7 +5259,7 @@ function onYouTubeIframeAPIReady() { Directory.Delete($"{settings.MainDir}\\{packageName}", true); isworking = false; changeTitle(""); - progressBar.Style = ProgressBarStyle.Continuous; + progressBar.IsIndeterminate = false; _ = FlexibleMessageBox.Show(Program.form, $"{GameName} pulled to:\n\n{GameName} v{VersionInt} {packageName}.zip\n\nOn your desktop!"); } } @@ -6894,7 +6974,8 @@ function onYouTubeIframeAPIReady() { // Perform uninstall ProcessOutput output = new ProcessOutput("", ""); - progressBar.Style = ProgressBarStyle.Marquee; + progressBar.IsIndeterminate = true; + progressBar.OperationType = ""; await Task.Run(() => { output += Sideloader.UninstallGame(packageName); @@ -6902,7 +6983,7 @@ function onYouTubeIframeAPIReady() { ShowPrcOutput(output); showAvailableSpace(); - progressBar.Style = ProgressBarStyle.Continuous; + progressBar.IsIndeterminate = false; // Remove from combo box for (int i = 0; i < m_combo.Items.Count; i++) diff --git a/ModernProgessBar.cs b/ModernProgessBar.cs new file mode 100644 index 0000000..1eb4b4a --- /dev/null +++ b/ModernProgessBar.cs @@ -0,0 +1,438 @@ +using System; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; + +namespace AndroidSideloader +{ + // A modern progress bar with rounded corners, left-to-right gradient fill, + // animated indeterminate mode, and optional status text overlay + [Description("Modern Themed Progress Bar")] + public class ModernProgressBar : Control + { + #region Fields + + private int _value; + private int _minimum; + private int _maximum = 100; + private int _radius = 8; + private bool _isIndeterminate; + private string _statusText = string.Empty; + private string _operationType = string.Empty; + + // Indeterminate animation + private readonly Timer _animationTimer; + private float _animationOffset; + private const float AnimationSpeed = 4f; + private const int IndeterminateBlockWidth = 80; + + // Colors + private Color _backgroundColor = Color.FromArgb(28, 32, 38); + private Color _progressStartColor = Color.FromArgb(120, 220, 190); // lighter accent + private Color _progressEndColor = Color.FromArgb(50, 160, 130); // darker accent + private Color _indeterminateColor = Color.FromArgb(93, 203, 173); // accent + private Color _textColor = Color.FromArgb(230, 230, 230); + private Color _textShadowColor = Color.FromArgb(90, 0, 0, 0); + + #endregion + + #region Constructor + + public ModernProgressBar() + { + SetStyle( + ControlStyles.AllPaintingInWmPaint | + ControlStyles.OptimizedDoubleBuffer | + ControlStyles.ResizeRedraw | + ControlStyles.UserPaint | + ControlStyles.SupportsTransparentBackColor, + true); + + BackColor = Color.Transparent; + + // Size + Font + Height = 28; + Width = 220; + Font = new Font("Segoe UI", 9f, FontStyle.Bold); + + _animationTimer = new Timer { Interval = 16 }; // ~60fps + _animationTimer.Tick += AnimationTimer_Tick; + } + + #endregion + + #region Properties + + [Category("Progress")] + [Description("The current value of the progress bar.")] + public int Value + { + get => _value; + set + { + _value = Math.Max(_minimum, Math.Min(_maximum, value)); + Invalidate(); + } + } + + [Category("Progress")] + [Description("The minimum value of the progress bar.")] + public int Minimum + { + get => _minimum; + set + { + _minimum = value; + if (_value < _minimum) _value = _minimum; + Invalidate(); + } + } + + [Category("Progress")] + [Description("The maximum value of the progress bar.")] + public int Maximum + { + get => _maximum; + set + { + _maximum = value; + if (_value > _maximum) _value = _maximum; + Invalidate(); + } + } + + [Category("Appearance")] + [Description("The corner radius of the progress bar.")] + public int Radius + { + get => _radius; + set + { + _radius = Math.Max(0, value); + Invalidate(); + } + } + + [Category("Progress")] + [Description("Whether the progress bar shows indeterminate (marquee) progress.")] + public bool IsIndeterminate + { + get => _isIndeterminate; + set + { + // If there is no change, do nothing + if (_isIndeterminate == value) + return; + + _isIndeterminate = value; + if (_isIndeterminate) + { + _animationOffset = -IndeterminateBlockWidth; + _animationTimer.Start(); + } + else + { + _animationTimer.Stop(); + } + Invalidate(); + } + } + + [Category("Appearance")] + [Description("Optional status text to display on the progress bar.")] + public string StatusText + { + get => _statusText; + set + { + _statusText = value ?? string.Empty; + Invalidate(); + } + } + + [Category("Appearance")] + [Description("Operation type label (e.g., 'Downloading', 'Installing').")] + public string OperationType + { + get => _operationType; + set + { + _operationType = value ?? string.Empty; + Invalidate(); + } + } + + [Category("Appearance")] + [Description("Background color of the progress bar track.")] + public Color BackgroundColor + { + get => _backgroundColor; + set { _backgroundColor = value; Invalidate(); } + } + + [Category("Appearance")] + [Description("Start color of the progress gradient (left side).")] + public Color ProgressStartColor + { + get => _progressStartColor; + set { _progressStartColor = value; Invalidate(); } + } + + [Category("Appearance")] + [Description("End color of the progress gradient (right side).")] + public Color ProgressEndColor + { + get => _progressEndColor; + set { _progressEndColor = value; Invalidate(); } + } + + [Category("Appearance")] + [Description("Color used for indeterminate animation.")] + public Color IndeterminateColor + { + get => _indeterminateColor; + set { _indeterminateColor = value; Invalidate(); } + } + + [Category("Appearance")] + [Description("Text color for status overlay.")] + public Color TextColor + { + get => _textColor; + set { _textColor = value; Invalidate(); } + } + + // Gets the progress as a percentage (0-100) + public float ProgressPercent => + _maximum > _minimum ? (float)(_value - _minimum) / (_maximum - _minimum) * 100f : 0f; + + #endregion + + #region Painting + + protected override void OnPaint(PaintEventArgs e) + { + var g = e.Graphics; + g.SmoothingMode = SmoothingMode.AntiAlias; + g.PixelOffsetMode = PixelOffsetMode.HighQuality; + g.Clear(BackColor); + + int w = ClientSize.Width; + int h = ClientSize.Height; + if (w <= 0 || h <= 0) return; + + var outerRect = new Rectangle(0, 0, w - 1, h - 1); + + // Draw background track + using (var path = CreateRoundedPath(outerRect, _radius)) + using (var bgBrush = new SolidBrush(_backgroundColor)) + { + g.FillPath(bgBrush, path); + } + + // Draw progress or indeterminate animation + if (_isIndeterminate) + { + DrawIndeterminate(g, outerRect); + } + else if (_value > _minimum) + { + DrawProgress(g, outerRect); + } + + // Draw text overlay + DrawTextOverlay(g, outerRect); + + base.OnPaint(e); + } + + private void DrawProgress(Graphics g, Rectangle outerRect) + { + float percent = (_maximum > _minimum) + ? (float)(_value - _minimum) / (_maximum - _minimum) + : 0f; + + if (percent <= 0f) return; + if (percent > 1f) percent = 1f; + + int progressWidth = (int)Math.Round(outerRect.Width * percent); + if (progressWidth <= 0) return; + if (progressWidth > outerRect.Width) progressWidth = outerRect.Width; + + using (var outerPath = CreateRoundedPath(outerRect, _radius)) + { + // Clip to progress area inside rounded track + Rectangle progressRect = new Rectangle(outerRect.X, outerRect.Y, progressWidth, outerRect.Height); + using (var progressClip = new Region(progressRect)) + using (var trackRegion = new Region(outerPath)) + { + trackRegion.Intersect(progressClip); + + Region prevClip = g.Clip; + try + { + g.SetClip(trackRegion, CombineMode.Replace); + + // Left-to-right gradient, based on accent color + using (var gradientBrush = new LinearGradientBrush( + progressRect, + _progressStartColor, + _progressEndColor, + LinearGradientMode.Horizontal)) + { + g.FillPath(gradientBrush, outerPath); + } + } + finally + { + g.Clip = prevClip; + } + } + } + } + + private void DrawIndeterminate(Graphics g, Rectangle outerRect) + { + using (var outerPath = CreateRoundedPath(outerRect, _radius)) + { + Region prevClip = g.Clip; + try + { + g.SetClip(outerPath, CombineMode.Replace); + + int blockWidth = Math.Min(IndeterminateBlockWidth, outerRect.Width); + int blockX = (int)_animationOffset; + var blockRect = new Rectangle(blockX, outerRect.Y, blockWidth, outerRect.Height); + + // Solid bar with slight left-to-right gradient + using (var brush = new LinearGradientBrush( + blockRect, + ControlPaint.Light(_indeterminateColor, 0.1f), + ControlPaint.Dark(_indeterminateColor, 0.1f), + LinearGradientMode.Horizontal)) + { + g.FillRectangle(brush, blockRect); + } + } + finally + { + g.Clip = prevClip; + } + } + } + + private void DrawTextOverlay(Graphics g, Rectangle outerRect) + { + string displayText = BuildDisplayText(); + if (string.IsNullOrEmpty(displayText)) return; + + using (var sf = new StringFormat + { + Alignment = StringAlignment.Center, + LineAlignment = StringAlignment.Center, + Trimming = StringTrimming.EllipsisCharacter + }) + { + // Slight shadow for legibility on accent background + var shadowRect = new Rectangle(outerRect.X + 1, outerRect.Y + 1, outerRect.Width, outerRect.Height); + using (var shadowBrush = new SolidBrush(_textShadowColor)) + { + g.DrawString(displayText, Font, shadowBrush, shadowRect, sf); + } + + using (var textBrush = new SolidBrush(_textColor)) + { + g.DrawString(displayText, Font, textBrush, outerRect, sf); + } + } + } + + private string BuildDisplayText() + { + if (!string.IsNullOrEmpty(_statusText)) + { + return _statusText; + } + + if (_isIndeterminate && !string.IsNullOrEmpty(_operationType)) + { + // E.g. "Downloading..." + return _operationType + "..."; + } + + if (!_isIndeterminate && _value > _minimum) + { + string percentText = $"{(int)ProgressPercent}%"; + if (!string.IsNullOrEmpty(_operationType)) + { + // E.g. "Downloading · 73%" + return $"{_operationType} · {percentText}"; + } + return percentText; + } + + return string.Empty; + } + + private GraphicsPath CreateRoundedPath(Rectangle rect, int radius) + { + var path = new GraphicsPath(); + + if (radius <= 0) + { + path.AddRectangle(rect); + return path; + } + + int diameter = radius * 2; + diameter = Math.Min(diameter, Math.Min(rect.Width, rect.Height)); + radius = diameter / 2; + + var arcRect = new Rectangle(rect.Location, new Size(diameter, diameter)); + + path.AddArc(arcRect, 180, 90); + arcRect.X = rect.Right - diameter; + path.AddArc(arcRect, 270, 90); + arcRect.Y = rect.Bottom - diameter; + path.AddArc(arcRect, 0, 90); + arcRect.X = rect.Left; + path.AddArc(arcRect, 90, 90); + + path.CloseFigure(); + return path; + } + + #endregion + + #region Animation + + private void AnimationTimer_Tick(object sender, EventArgs e) + { + _animationOffset += AnimationSpeed; + + if (_animationOffset > ClientSize.Width + IndeterminateBlockWidth) + { + _animationOffset = -IndeterminateBlockWidth; + } + + Invalidate(); + } + + #endregion + + #region Cleanup + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _animationTimer?.Stop(); + _animationTimer?.Dispose(); + } + base.Dispose(disposing); + } + + #endregion + } +} diff --git a/icon.ico b/icon.ico index cf2c1730000b39d7321ac70bf9b5d771099aacb3..b2ca138a688dbb96f567465d845e0314b8dca464 100644 GIT binary patch literal 32038 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�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%0DMio6d476RyO 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 Rde!)EwO%#p-xBHJ|9@#^rosRK diff --git a/packages.config b/packages.config index f655c08..f998aa6 100644 --- a/packages.config +++ b/packages.config @@ -1,5 +1,6 @@  +