Compare commits

...

28 Commits

Author SHA1 Message Date
Fenopy
58cb75c38c Merge pull request #276 from jp64k/RSL-3.0-2
Several changes and fixes, see notes below
2025-12-23 07:07:17 -06:00
Fenopy
4383b9d398 store deviceId in variable instead of line-splitting
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-23 07:06:33 -06:00
jp64k
b3ce3ab214 Adjusted wireless ADB dialog widths to fix layout
Increased width of 'Wireless ADB Options' and 'Wireless ADB Connection' dialogs by 6 pixels each to improve layout centering
2025-12-22 03:47:53 +01:00
jp64k
ae72432aee Improved FlexibleMessageBox button and text placement
Reduced left padding for the title and text labels, reduced right padding of buttons for better alignment, added code to resize the title panel to fix the close button position - https://puu.sh/KFOEG/5c3d72aaee.png
2025-12-22 03:43:48 +01:00
jp64k
5a939d6234 Refactored ADBWirelessToggle and device detection
Replaced message box prompts with custom dialogs for wireless ADB options and connection methods, providing clearer choices for users. Enhanced wireless ADB state detection by verifying actual device connection, and readded old automatic USB setup option as third option for wireless ADB ("Automatic (USB)"). Also refactored CheckForDevice() to use Task.Run/await pattern and optimized battery check
2025-12-22 02:42:34 +01:00
jp64k
e6ce947700 Refactored filter/selection logic, now preserves filter and selection state on game list refresh
Refactored methods to use RefreshGameListAsync() instead of reinitializing the list view, ensuring filter and selection state are preserved after refreshes. Added logic to restore the last selected item by package name in both list and gallery views. Also refactored ModernListView to simplify marquee calculation logic.
2025-12-22 01:25:50 +01:00
jp64k
d24df061df Minor ModernListView.cs cleanup and fixed hovered text disappearing after scroll
Removed MarqueeEnabled and MarqueeOnlyWhenFocused fields and related conditional logic, as they were no longer used. Also fixed hover state updates after scrolling
2025-12-22 00:24:23 +01:00
Fenopy
1b06ab7981 Merge pull request #274 from jp64k/RSL-3.0-2
Implemented custom ModernListView class, added modern scrollbar to gallery view, reworked list view columns and sorting, added fix to skip 0 MB entries when MR-Fix version exists
2025-12-18 11:29:02 -06:00
jp64k
5f4cfc09fe Fixed "&" not being rendered in new list view
Included TextFormatFlags.NoPrefix in text rendering flags for both header and cell drawing to fix & rendering.
2025-12-18 16:11:37 +01:00
jp64k
1de339da75 Updated ModernListView.cs
Changed several public properties and enums in ModernListView to private, removed unused code, reduced text scroll speed (marquee)
2025-12-18 06:09:50 +01:00
jp64k
4f653f2131 Implemented Copilot suggestions 2025-12-18 05:53:52 +01:00
jp64k
3148ddcfa3 Implemented custom ModernListView class, added modern scrollbar to gallery view, reworked list view columns and sorting, added fix to skip 0 MB entries when MR-Fix version exists
Added custom ModernListView class with modern dark theme appearance and refined behavior with smooth text scrolling support. Required a lot of finicking to get the details right. Reworked ListView columns and sorting for size and popularity, including parsing with reformatted GB/MB size and popularity ranking. Updated GalleryView to support new formats and implemented modern scrollbars. Also added logic to skip 0 MB entries when an MR-Fix version of same game exists
2025-12-18 05:44:54 +01:00
fenopy
b793d2a140 bump rclone to 1.72.1 2025-12-17 15:36:55 -06:00
Fenopy
5f16ad13e2 Merge pull request #273 from jp64k/RSL-3.0-2
Fixed update sorting for gallery view to consider exact date+time (and not just date)
2025-12-17 15:20:01 -06:00
Fenopy
ddb503feec Update to handle UTC timestamps
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-17 15:19:44 -06:00
jp64k
1bcfbd132d Updated icon in forms I missed earlier; now reusing existing icon; dropping .exe size from 3,38 MB to 1,03 MB
Updated icon in forms I missed earlier; now reusing existing icon; dropping .exe size from 3,38 MB to 1,03 MB
2025-12-17 22:18:19 +01:00
jp64k
9862a6a8ca Fixed update sorting for gallery view to consider exact date+time (and not just date)
Updated ParseDate to use DateTime.TryParseExact with a specific format handling ' UTC' suffix
2025-12-17 19:43:39 +01:00
Fenopy
9ef3c9264e Merge pull request #272 from jp64k/RSL-3.0-2
Fixed UI lag during progress updates, unified DLS+ETA progress labels and implemented ETA tracking for extraction, installation, and copy operations
2025-12-17 06:11:27 -06:00
jp64k
12c371da84 Fixed gallery view placeholder text alignment for missing thumbnails
Adjusted the rectangle used for placeholder text when a thumbnail is missing to use a stable, non-scaled base thumbnail size to ensure consistent text layout regardless of thumbnail scaling
2025-12-17 05:13:10 +01:00
jp64k
782ead1c1e Remove InstalledVersion column, now showing it unified in single version tab
Removed the InstalledVersion column from the ListView and its designer references. Installed version information is now appended to the Version column text "Version (vs Installed)" when installed, improving clarity and reducing column clutter.
2025-12-17 05:07:08 +01:00
jp64k
e9f77449f0 Added 'Installed Version' column to games list
Introduces a 'Installed' column to the games ListView, displaying the installed version for each game. Updates sorting logic to handle the new column numerically and defines the corresponding index in RCLONE.cs.
2025-12-17 04:51:57 +01:00
jp64k
fd77c4db8b Reworked tile design in game gallery
For a cleaner UI and to allow larger thumbnails, game names are now shown on hover, except for tiles that don't have thumbnails. Adjusted thumbnail padding and corner radius to further increase thumbnail size and keep rounding consistent. Refined positioning of the delete button to align with the new tile layout
2025-12-17 04:18:36 +01:00
jp64k
d9a8d1c460 Fixed trailer player regression
For some reason this fixes trailers sometimes not playing correctly
2025-12-17 02:53:57 +01:00
jp64k
c5b151471a Refactored progress bar to show fractional percentages and improved ETA smoothing
Progress values and callbacks (labels, progressbar) now use float instead of int to reflect fractional percentages for a better / more responsive user experience. Improved ETA display for APK install, OBB copy and ZIP extraction operations by introducing a reusable EtaEstimator class for smoother and more accurate ETA calculations
2025-12-17 02:20:40 +01:00
jp64k
2f843bc458 Refactored game list refresh logic to use existing RefreshGameListAsync() method
Replaced manual list view initialization and checks with a call to the existing RefreshGameListAsync() method, simplifying the refresh logic and ensuring that the game list is properly refreshed when a filter is active and a game is updated (e.g., when installing an already installed game (updating) while in 'update available' view, the game status now correctly changes to reflect that it has been updated)
2025-12-17 00:31:25 +01:00
jp64k
f528520024 Simplified extraction and installation progress labels in inner progress bar
- Removed extraction filenames from progress status label in inner progress bar to keep it clean and simple, and to avoid confusion with OBB file copy operations
- Removed redundant ETA text for install progress in inner progress bar to match the style of other progress labels
- Changed the trailer player.html directory from 'webroot' to 'trailer', shortened YouTube player initialization script, added fs: 0 to hide unnecessary fullscreen toggle, iv_load_policy: 3 to hide video annotations
2025-12-17 00:05:03 +01:00
jp64k
a92d4c0267 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
2025-12-16 22:50:55 +01:00
jp64k
acaea1d243 Fixed UI lag during progress updates
Added throttling to progress and status callbacks during APK installation and OBB copy operations. UI updates are now limited to at most once every 100 ms or when the progress percentage changes, preventing excessive UI thread updates and associated lag.
2025-12-16 21:10:53 +01:00
32 changed files with 2445 additions and 41449 deletions

230
ADB.cs
View File

@@ -154,13 +154,13 @@ namespace AndroidSideloader
// Copies and installs an APK with real-time progress reporting using AdvancedSharpAdbClient
public static async Task<ProcessOutput> SideloadWithProgressAsync(
string path,
Action<int> progressCallback = null,
Action<float, TimeSpan?> progressCallback = null,
Action<string> statusCallback = null,
string packagename = "",
string gameName = "")
{
statusCallback?.Invoke("Installing APK...");
progressCallback?.Invoke(0);
progressCallback?.Invoke(0, null);
try
{
@@ -175,43 +175,87 @@ namespace AndroidSideloader
statusCallback?.Invoke("Installing APK...");
// Throttle UI updates to prevent lag
DateTime lastProgressUpdate = DateTime.MinValue;
float lastReportedPercent = -1;
const int ThrottleMs = 100; // Update UI every 100ms
// Shared ETA engine (percent-units)
var eta = new EtaEstimator(alpha: 0.05, reanchorThreshold: 0.20);
// Create install progress handler
Action<InstallProgressEventArgs> installProgress = (args) =>
{
// Map PackageInstallProgressState to percentage
int percent = 0;
float percent = 0;
string status = null;
TimeSpan? displayEta = null;
switch (args.State)
{
case PackageInstallProgressState.Preparing:
percent = 0;
statusCallback?.Invoke("Preparing...");
status = "Preparing...";
eta.Reset();
break;
case PackageInstallProgressState.Uploading:
percent = (int)Math.Round(args.UploadProgress);
statusCallback?.Invoke($"Installing · {args.UploadProgress:F0}%");
percent = (float)args.UploadProgress;
// Update ETA engine using percent as units (0..100)
if (percent > 0 && percent < 100)
{
eta.Update(totalUnits: 100, doneUnits: (long)Math.Round(percent));
displayEta = eta.GetDisplayEta();
}
else
{
displayEta = eta.GetDisplayEta();
}
status = $"Installing · {percent:0.0}%";
break;
case PackageInstallProgressState.Installing:
percent = 100;
statusCallback?.Invoke("Completing Installation...");
status = "Completing Installation...";
displayEta = null;
break;
case PackageInstallProgressState.Finished:
percent = 100;
statusCallback?.Invoke("");
status = "";
displayEta = null;
break;
default:
percent = 50;
percent = 100;
status = "";
displayEta = null;
break;
}
progressCallback?.Invoke(percent);
var updateNow = DateTime.UtcNow;
bool shouldUpdate = (updateNow - lastProgressUpdate).TotalMilliseconds >= ThrottleMs
|| Math.Abs(percent - lastReportedPercent) >= 0.1f
|| args.State != PackageInstallProgressState.Uploading;
if (shouldUpdate)
{
lastProgressUpdate = updateNow;
lastReportedPercent = percent;
// ETA goes back via progress callback (label); status remains percent-only string for inner bar
progressCallback?.Invoke(percent, displayEta);
if (status != null) statusCallback?.Invoke(status);
}
};
// Install the package with progress
await Task.Run(() =>
{
packageManager.InstallPackage(path, installProgress);
});
progressCallback?.Invoke(100);
progressCallback?.Invoke(100, null);
statusCallback?.Invoke("");
return new ProcessOutput($"{gameName}: Success\n");
@@ -220,7 +264,6 @@ namespace AndroidSideloader
{
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"))
{
@@ -241,7 +284,6 @@ namespace AndroidSideloader
if (cancelClicked)
return new ProcessOutput("", "Installation cancelled by user");
// Perform reinstall
statusCallback?.Invoke("Performing reinstall...");
try
@@ -250,26 +292,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<InstallProgressEventArgs> reinstallProgress = (args) =>
{
if (args.State == PackageInstallProgressState.Uploading)
{
progressCallback?.Invoke((int)Math.Round(args.UploadProgress));
progressCallback?.Invoke((float)args.UploadProgress, null);
}
};
packageManager.InstallPackage(path, reinstallProgress);
// Restore save data
statusCallback?.Invoke("Restoring save data...");
_ = RunAdbCommandToString($"push \"{Environment.CurrentDirectory}\\{MainForm.CurrPCKG}\" /sdcard/Android/data/");
@@ -279,7 +317,7 @@ namespace AndroidSideloader
Directory.Delete(directoryToDelete, true);
}
progressCallback?.Invoke(100);
progressCallback?.Invoke(100, null);
return new ProcessOutput($"{gameName}: Reinstall: Success\n", "");
}
catch (Exception reinstallEx)
@@ -295,7 +333,7 @@ namespace AndroidSideloader
// Copies OBB folder with real-time progress reporting using AdvancedSharpAdbClient
public static async Task<ProcessOutput> CopyOBBWithProgressAsync(
string localPath,
Action<int> progressCallback = null,
Action<float, TimeSpan?> progressCallback = null,
Action<string> statusCallback = null,
string gameName = "")
{
@@ -318,7 +356,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}\"");
@@ -329,6 +367,14 @@ namespace AndroidSideloader
long totalBytes = files.Sum(f => new FileInfo(f).Length);
long transferredBytes = 0;
// Throttle UI updates to prevent lag
DateTime lastProgressUpdate = DateTime.MinValue;
float lastReportedPercent = -1;
const int ThrottleMs = 100; // Update UI every 100ms
// Shared ETA engine (bytes-units)
var eta = new EtaEstimator(alpha: 0.10, reanchorThreshold: 0.20);
statusCallback?.Invoke($"Copying: {folderName}");
using (var syncService = new SyncService(client, device))
@@ -341,9 +387,6 @@ namespace AndroidSideloader
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}\"");
@@ -352,23 +395,37 @@ namespace AndroidSideloader
long fileSize = fileInfo.Length;
long capturedTransferredBytes = transferredBytes;
// Progress handler for this file
Action<SyncProgressChangedEventArgs> progressHandler = (args) =>
{
long totalProgressBytes = capturedTransferredBytes + args.ReceivedBytesSize;
double overallPercent = totalBytes > 0
? (totalProgressBytes * 100.0) / totalBytes
: 0.0;
float overallPercent = totalBytes > 0
? (float)(totalProgressBytes * 100.0 / totalBytes)
: 0f;
int overallPercentInt = (int)Math.Round(overallPercent);
overallPercentInt = Math.Max(0, Math.Min(100, overallPercentInt));
overallPercent = Math.Max(0, Math.Min(100, overallPercent));
// Single source of truth for UI (bar + label + text)
progressCallback?.Invoke(overallPercentInt);
// Update ETA engine in bytes
if (totalBytes > 0 && totalProgressBytes > 0 && overallPercent < 100)
{
eta.Update(totalUnits: totalBytes, doneUnits: totalProgressBytes);
}
TimeSpan? displayEta = eta.GetDisplayEta();
var now2 = DateTime.UtcNow;
bool shouldUpdate = (now2 - lastProgressUpdate).TotalMilliseconds >= ThrottleMs
|| Math.Abs(overallPercent - lastReportedPercent) >= 0.1f;
if (shouldUpdate)
{
lastProgressUpdate = now2;
lastReportedPercent = overallPercent;
progressCallback?.Invoke(overallPercent, displayEta);
statusCallback?.Invoke(fileName);
}
};
// Push the file with progress
using (var stream = File.OpenRead(file))
{
await Task.Run(() =>
@@ -383,13 +440,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", "");
@@ -397,7 +452,6 @@ namespace AndroidSideloader
catch (Exception ex)
{
Logger.Log($"CopyOBBWithProgressAsync error: {ex.Message}", LogLevel.ERROR);
return new ProcessOutput("", $"{gameName}: OBB transfer: Failed: {ex.Message}\n");
}
}
@@ -656,4 +710,102 @@ namespace AndroidSideloader
: new ProcessOutput("No OBB Folder found");
}
}
internal class EtaEstimator
{
private readonly double _alpha; // EWMA smoothing
private readonly double _reanchorThreshold; // % difference required to re-anchor
private readonly double _minSampleSeconds; // ignore too-short dt
private DateTime _lastSampleTimeUtc;
private long _lastSampleDoneUnits;
private double _smoothedUnitsPerSecond;
private TimeSpan? _etaAnchorValue;
private DateTime _etaAnchorTimeUtc;
public EtaEstimator(double alpha, double reanchorThreshold, double minSampleSeconds = 0.15)
{
_alpha = alpha;
_reanchorThreshold = reanchorThreshold;
_minSampleSeconds = minSampleSeconds;
Reset();
}
public void Reset()
{
_lastSampleTimeUtc = DateTime.UtcNow;
_lastSampleDoneUnits = 0;
_smoothedUnitsPerSecond = 0;
_etaAnchorValue = null;
_etaAnchorTimeUtc = DateTime.UtcNow;
}
// Updates internal rate estimate and re-anchors ETA
// totalUnits: total work units (e.g., 100 for percent, or totalBytes for bytes)
// doneUnits: completed work units so far (e.g., percent, or bytes transferred)
public void Update(long totalUnits, long doneUnits)
{
var now = DateTime.UtcNow;
if (totalUnits <= 0) return;
doneUnits = Math.Max(0, Math.Min(totalUnits, doneUnits));
long remainingUnits = Math.Max(0, totalUnits - doneUnits);
double dt = (now - _lastSampleTimeUtc).TotalSeconds;
long dUnits = doneUnits - _lastSampleDoneUnits;
if (dt >= _minSampleSeconds && dUnits > 0)
{
double instUnitsPerSecond = dUnits / dt;
if (_smoothedUnitsPerSecond <= 0)
_smoothedUnitsPerSecond = instUnitsPerSecond;
else
_smoothedUnitsPerSecond = _alpha * instUnitsPerSecond + (1 - _alpha) * _smoothedUnitsPerSecond;
_lastSampleTimeUtc = now;
_lastSampleDoneUnits = doneUnits;
}
if (_smoothedUnitsPerSecond > 1e-6 && remainingUnits > 0)
{
var newEta = TimeSpan.FromSeconds(remainingUnits / _smoothedUnitsPerSecond);
if (newEta < TimeSpan.Zero) newEta = TimeSpan.Zero;
if (!_etaAnchorValue.HasValue)
{
_etaAnchorValue = newEta;
_etaAnchorTimeUtc = now;
}
else
{
// What countdown would currently show
var predictedNow = _etaAnchorValue.Value - (now - _etaAnchorTimeUtc);
if (predictedNow < TimeSpan.Zero) predictedNow = TimeSpan.Zero;
double baseSeconds = Math.Max(1, predictedNow.TotalSeconds);
double diffRatio = Math.Abs(newEta.TotalSeconds - predictedNow.TotalSeconds) / baseSeconds;
if (diffRatio > _reanchorThreshold)
{
_etaAnchorValue = newEta;
_etaAnchorTimeUtc = now;
}
}
}
}
// Returns a countdown ETA for UI display
public TimeSpan? GetDisplayEta()
{
if (!_etaAnchorValue.HasValue) return null;
var remaining = _etaAnchorValue.Value - (DateTime.UtcNow - _etaAnchorTimeUtc);
if (remaining < TimeSpan.Zero) remaining = TimeSpan.Zero;
return TimeSpan.FromSeconds(Math.Ceiling(remaining.TotalSeconds));
}
}
}

View File

@@ -13,6 +13,10 @@ namespace AndroidSideloader
public AdbCommandForm()
{
InitializeComponent();
// Use same icon as the executable
this.Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
this.ShowIcon = true; // Enable icon
}
private void InitializeComponent()

View File

@@ -190,6 +190,7 @@
<Compile Include="GalleryView.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="ModernListView.cs" />
<Compile Include="ModernProgessBar.cs">
<SubType>Component</SubType>
</Compile>

View File

@@ -1,4 +1,5 @@
using System.Collections;
using System;
using System.Collections;
using System.Windows.Forms;
/// <summary>
@@ -41,23 +42,41 @@ public class ListViewColumnSorter : IComparer
ListViewItem listviewX = (ListViewItem)x;
ListViewItem listviewY = (ListViewItem)y;
// Determine if the column requires numeric comparison
if (SortColumn == 3 || SortColumn == 5) // Numeric columns: VersionCodeIndex, VersionNameIndex
// Special handling for column 6 (Popularity ranking)
if (SortColumn == 6)
{
try
{
// Parse and compare numeric values directly
int xNum = ParseNumber(listviewX.SubItems[SortColumn].Text);
int yNum = ParseNumber(listviewY.SubItems[SortColumn].Text);
string textX = listviewX.SubItems[SortColumn].Text;
string textY = listviewY.SubItems[SortColumn].Text;
// Compare numerically
compareResult = xNum.CompareTo(yNum);
}
catch
// Extract numeric values from "#1", "#10", etc.
// "-" represents unranked and should go to the end
int rankX = int.MaxValue; // Default for unranked (-)
int rankY = int.MaxValue;
if (textX.StartsWith("#") && int.TryParse(textX.Substring(1), out int parsedX))
{
// Fallback to string comparison if parsing fails
compareResult = ObjectCompare.Compare(listviewX.SubItems[SortColumn].Text, listviewY.SubItems[SortColumn].Text);
rankX = parsedX;
}
if (textY.StartsWith("#") && int.TryParse(textY.Substring(1), out int parsedY))
{
rankY = parsedY;
}
// Compare the numeric ranks
compareResult = rankX.CompareTo(rankY);
}
// Special handling for column 5 (Size)
else if (SortColumn == 5)
{
string textX = listviewX.SubItems[SortColumn].Text;
string textY = listviewY.SubItems[SortColumn].Text;
double sizeX = ParseSize(textX);
double sizeY = ParseSize(textY);
// Compare the numeric sizes
compareResult = sizeX.CompareTo(sizeY);
}
else
{
@@ -91,6 +110,49 @@ public class ListViewColumnSorter : IComparer
return int.TryParse(text, out int result) ? result : 0;
}
/// <summary>
/// Parses size strings with units (GB/MB) and converts them to MB for comparison.
/// </summary>
/// <param name="sizeStr">Size string (e.g., "1.23 GB", "123 MB")</param>
/// <returns>Size in MB as a double</returns>
private double ParseSize(string sizeStr)
{
if (string.IsNullOrEmpty(sizeStr))
return 0;
// Remove whitespace
sizeStr = sizeStr.Trim();
// Handle new format: "1.23 GB" or "123 MB"
if (sizeStr.EndsWith(" GB", StringComparison.OrdinalIgnoreCase))
{
string numPart = sizeStr.Substring(0, sizeStr.Length - 3).Trim();
if (double.TryParse(numPart, System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out double gb))
{
return gb * 1024.0; // Convert GB to MB for consistent sorting
}
}
else if (sizeStr.EndsWith(" MB", StringComparison.OrdinalIgnoreCase))
{
string numPart = sizeStr.Substring(0, sizeStr.Length - 3).Trim();
if (double.TryParse(numPart, System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out double mb))
{
return mb;
}
}
// Fallback: try parsing as raw number
if (double.TryParse(sizeStr, System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out double rawMb))
{
return rawMb;
}
return 0;
}
/// <summary>
/// Gets or sets the index of the column to be sorted (default is '0').
/// </summary>

View File

@@ -30,7 +30,6 @@ namespace AndroidSideloader
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(DonorsListViewForm));
this.DonationTimer = new System.Windows.Forms.Timer(this.components);
this.panel1 = new System.Windows.Forms.Panel();
this.skip_forever = new AndroidSideloader.RoundButton();
@@ -310,7 +309,6 @@ namespace AndroidSideloader
this.Controls.Add(this.panel1);
this.ForeColor = System.Drawing.Color.White;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.Name = "DonorsListViewForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Load += new System.EventHandler(this.DonorsListViewForm_Load);

View File

@@ -42,6 +42,10 @@ namespace AndroidSideloader
public DonorsListViewForm()
{
InitializeComponent();
// Use same icon as the executable
this.Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
ApplyModernTheme();
CenterToScreen();

File diff suppressed because it is too large Load Diff

View File

@@ -170,7 +170,7 @@ namespace JR.Utils.GUI.Forms
titleLabel.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold);
titleLabel.Location = new System.Drawing.Point(0, 0);
titleLabel.Name = "titleLabel";
titleLabel.Padding = new System.Windows.Forms.Padding(18, 0, 0, 0);
titleLabel.Padding = new System.Windows.Forms.Padding(12, 0, 0, 0);
titleLabel.Size = new System.Drawing.Size(218, 28);
titleLabel.TabIndex = 0;
titleLabel.Text = "<Caption>";
@@ -198,7 +198,7 @@ namespace JR.Utils.GUI.Forms
//
button1.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
button1.DialogResult = System.Windows.Forms.DialogResult.OK;
button1.Location = new System.Drawing.Point(16, 80);
button1.Location = new System.Drawing.Point(26, 80);
button1.Name = "button1";
button1.Size = new System.Drawing.Size(75, 28);
button1.TabIndex = 2;
@@ -222,7 +222,7 @@ namespace JR.Utils.GUI.Forms
richTextBoxMessage.BorderStyle = System.Windows.Forms.BorderStyle.None;
richTextBoxMessage.DataBindings.Add(new System.Windows.Forms.Binding("Text", FlexibleMessageBoxFormBindingSource, "MessageText", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged));
richTextBoxMessage.Font = new System.Drawing.Font("Microsoft Sans Serif", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0);
richTextBoxMessage.Location = new System.Drawing.Point(52, 6);
richTextBoxMessage.Location = new System.Drawing.Point(46, 6);
richTextBoxMessage.Margin = new System.Windows.Forms.Padding(0);
richTextBoxMessage.Name = "richTextBoxMessage";
richTextBoxMessage.ReadOnly = true;
@@ -259,7 +259,7 @@ namespace JR.Utils.GUI.Forms
//
button2.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
button2.DialogResult = System.Windows.Forms.DialogResult.OK;
button2.Location = new System.Drawing.Point(97, 80);
button2.Location = new System.Drawing.Point(107, 80);
button2.Name = "button2";
button2.Size = new System.Drawing.Size(75, 28);
button2.TabIndex = 3;
@@ -277,7 +277,7 @@ namespace JR.Utils.GUI.Forms
//
button3.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
button3.DialogResult = System.Windows.Forms.DialogResult.OK;
button3.Location = new System.Drawing.Point(178, 80);
button3.Location = new System.Drawing.Point(188, 80);
button3.Name = "button3";
button3.Size = new System.Drawing.Size(75, 28);
button3.TabIndex = 0;
@@ -843,6 +843,11 @@ namespace JR.Utils.GUI.Forms
flexibleMessageBoxForm.richTextBoxMessage.Font = FONT;
SetDialogSizes(flexibleMessageBoxForm, text, caption);
// Force panel resize to fix closebutton position
int contentWidth = flexibleMessageBoxForm.ClientSize.Width - 16; // 8px padding
flexibleMessageBoxForm.titlePanel.Width = contentWidth;
SetDialogStartPosition(flexibleMessageBoxForm, owner);
return flexibleMessageBoxForm.ShowDialog(owner);

View File

@@ -6,6 +6,7 @@ using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public enum SortField { Name, LastUpdated, Size, Popularity }
@@ -51,7 +52,7 @@ public class FastGalleryPanel : Control
// Interaction
private int _hoveredIndex = -1;
private int _selectedIndex = -1;
public int _selectedIndex = -1;
private bool _isHoveringDeleteButton = false;
// Context Menu & Favorites
@@ -64,7 +65,7 @@ public class FastGalleryPanel : Control
// Visual constants
private const int CORNER_RADIUS = 10;
private const int THUMB_CORNER_RADIUS = 6;
private const int THUMB_CORNER_RADIUS = 8;
private const float HOVER_SCALE = 1.07f;
private const float ANIMATION_SPEED = 0.25f;
private const float SCROLL_SMOOTHING = 0.3f;
@@ -90,6 +91,24 @@ public class FastGalleryPanel : Control
public event EventHandler<int> TileRightClicked;
public event EventHandler<SortField> SortChanged;
[DllImport("dwmapi.dll")]
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
[DllImport("uxtheme.dll", CharSet = CharSet.Unicode)]
private static extern int SetWindowTheme(IntPtr hwnd, string pszSubAppName, string pszSubIdList);
private void ApplyModernScrollbars()
{
if (_scrollBar == null || !_scrollBar.IsHandleCreated) return;
int dark = 1;
int hr = DwmSetWindowAttribute(_scrollBar.Handle, 20, ref dark, sizeof(int));
if (hr != 0)
DwmSetWindowAttribute(_scrollBar.Handle, 19, ref dark, sizeof(int));
if (SetWindowTheme(_scrollBar.Handle, "DarkMode_Explorer", null) != 0)
SetWindowTheme(_scrollBar.Handle, "Explorer", null);
}
private class TileAnimationState
{
public float Scale = 1.0f;
@@ -155,6 +174,8 @@ public class FastGalleryPanel : Control
_isScrolling = false;
Invalidate();
};
_scrollBar.HandleCreated += (s, e) => ApplyModernScrollbars();
Controls.Add(_scrollBar);
// Animation timer (~120fps)
@@ -353,10 +374,10 @@ public class FastGalleryPanel : Control
case SortField.Popularity:
if (_currentSortDirection == SortDirection.Ascending)
_items = _items.OrderBy(i => ParsePopularity(i.SubItems.Count > 6 ? i.SubItems[6].Text : "0"))
_items = _items.OrderByDescending(i => ParsePopularity(i.SubItems.Count > 6 ? i.SubItems[6].Text : "-"))
.ThenBy(i => i.Text, new GameNameComparer()).ToList();
else
_items = _items.OrderByDescending(i => ParsePopularity(i.SubItems.Count > 6 ? i.SubItems[6].Text : "0"))
_items = _items.OrderBy(i => ParsePopularity(i.SubItems.Count > 6 ? i.SubItems[6].Text : "-"))
.ThenBy(i => i.Text, new GameNameComparer()).ToList();
break;
}
@@ -378,12 +399,35 @@ public class FastGalleryPanel : Control
Invalidate();
}
private double ParsePopularity(string popStr)
private int ParsePopularity(string popStr)
{
if (double.TryParse(popStr?.Trim(), System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out double pop))
return pop;
return 0;
if (string.IsNullOrEmpty(popStr))
return int.MaxValue; // Unranked goes to end
popStr = popStr.Trim();
// Handle new format: "#123" or "-"
if (popStr == "-")
{
return int.MaxValue; // Unranked items sort to the end
}
if (popStr.StartsWith("#"))
{
string numPart = popStr.Substring(1);
if (int.TryParse(numPart, out int rank))
{
return rank;
}
}
// Fallback: try parsing as raw number
if (int.TryParse(popStr, out int rawNum))
{
return rawNum;
}
return int.MaxValue; // Unparseable goes to end
}
// Custom sort to match list sort behaviour: '_' before digits, digits before letters (case-insensitive)
@@ -430,15 +474,54 @@ public class FastGalleryPanel : Control
private DateTime ParseDate(string dateStr)
{
if (string.IsNullOrEmpty(dateStr)) return DateTime.MinValue;
string datePart = dateStr.Split(' ')[0];
return DateTime.TryParse(datePart, out DateTime date) ? date : DateTime.MinValue;
string[] formats = { "yyyy-MM-dd HH:mm 'UTC'", "yyyy-MM-dd HH:mm" };
return DateTime.TryParseExact(
dateStr,
formats,
System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.AssumeUniversal | System.Globalization.DateTimeStyles.AdjustToUniversal,
out DateTime date)
? date
: DateTime.MinValue;
}
private double ParseSize(string sizeStr)
{
if (double.TryParse(sizeStr?.Trim(), System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out double mb))
return mb;
if (string.IsNullOrEmpty(sizeStr))
return 0;
// Remove whitespace
sizeStr = sizeStr.Trim();
// Handle new format: "1.23 GB" or "123 MB"
if (sizeStr.EndsWith(" GB", StringComparison.OrdinalIgnoreCase))
{
string numPart = sizeStr.Substring(0, sizeStr.Length - 3).Trim();
if (double.TryParse(numPart, System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out double gb))
{
return gb * 1024.0; // Convert GB to MB for consistent sorting
}
}
else if (sizeStr.EndsWith(" MB", StringComparison.OrdinalIgnoreCase))
{
string numPart = sizeStr.Substring(0, sizeStr.Length - 3).Trim();
if (double.TryParse(numPart, System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out double mb))
{
return mb;
}
}
// Fallback: try parsing as raw number
if (double.TryParse(sizeStr, System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out double rawMb))
{
return rawMb;
}
return 0;
}
@@ -500,12 +583,12 @@ public class FastGalleryPanel : Control
int y = baseY - (scaledH - _tileHeight) / 2;
// Calculate thumbnail area
int thumbPadding = 4;
int thumbHeight = scaledH - 26; // Same as in DrawTile
int thumbPadding = 2;
int thumbHeight = scaledH - (thumbPadding * 2);
// Position delete button in bottom-right corner of thumbnail
int btnX = x + scaledW - DELETE_BUTTON_SIZE - thumbPadding - DELETE_BUTTON_MARGIN;
int btnY = y + thumbPadding + thumbHeight - DELETE_BUTTON_SIZE - DELETE_BUTTON_MARGIN;
int btnY = y + thumbPadding + thumbHeight - DELETE_BUTTON_SIZE - DELETE_BUTTON_MARGIN - 20;
return new Rectangle(btnX, btnY, DELETE_BUTTON_SIZE, DELETE_BUTTON_SIZE);
}
@@ -792,9 +875,26 @@ public class FastGalleryPanel : Control
}
// Thumbnail
int thumbPadding = 4;
int thumbHeight = scaledH - 26;
var thumbRect = new Rectangle(x + thumbPadding, y + thumbPadding, scaledW - thumbPadding * 2, thumbHeight);
int thumbPadding = 2;
int thumbHeight = scaledH - (thumbPadding * 2);
var thumbRect = new Rectangle(
x + thumbPadding,
y + thumbPadding,
scaledW - (thumbPadding * 2),
thumbHeight
);
// Base (non-scaled) thumbnail size for stable placeholder text layout
int baseThumbW = _tileWidth - (thumbPadding * 2);
int baseThumbH = _tileHeight - (thumbPadding * 2);
var baseThumbRect = new Rectangle(
thumbRect.X + (thumbRect.Width - baseThumbW) / 2,
thumbRect.Y + (thumbRect.Height - baseThumbH) / 2,
baseThumbW,
baseThumbH
);
string packageName = item.SubItems.Count > 2 ? item.SubItems[2].Text : "";
var thumbnail = GetCachedImage(packageName);
@@ -817,12 +917,24 @@ public class FastGalleryPanel : Control
{
using (var brush = new SolidBrush(Color.FromArgb(35, 35, 40)))
g.FillPath(brush, thumbPath);
using (var textBrush = new SolidBrush(Color.FromArgb(70, 70, 80)))
// Show game name when thumbnail is missing, centered
var nameRect = new Rectangle(baseThumbRect.X + 10, baseThumbRect.Y, baseThumbRect.Width - 20, baseThumbRect.Height);
using (var font = new Font("Segoe UI", 10f, FontStyle.Bold))
{
var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
g.DrawString("🎮", new Font("Segoe UI Emoji", 18f), textBrush, thumbRect, sf);
var sfName = new StringFormat
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Center,
Trimming = StringTrimming.EllipsisCharacter
};
using (var text = new SolidBrush(Color.FromArgb(110, 110, 120)))
g.DrawString(item.Text, font, text, nameRect, sfName);
}
}
g.Clip = oldClip;
}
@@ -861,7 +973,7 @@ public class FastGalleryPanel : Control
// Size badge (top right) - always visible
if (item.SubItems.Count > 5)
{
string sizeText = FormatSize(item.SubItems[5].Text);
string sizeText = item.SubItems[5].Text;
if (!string.IsNullOrEmpty(sizeText))
{
DrawRightAlignedBadge(g, sizeText, x + scaledW - thumbPadding - 4, rightBadgeY, 1.0f);
@@ -887,12 +999,40 @@ public class FastGalleryPanel : Control
}
// Game name
var nameRect = new Rectangle(x + 6, y + thumbHeight + thumbPadding, scaledW - 12, 20);
using (var font = new Font("Segoe UI Semibold", 8f))
using (var brush = new SolidBrush(TextColor))
if (state.TooltipOpacity > 0.01f)
{
var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center, Trimming = StringTrimming.EllipsisCharacter, FormatFlags = StringFormatFlags.NoWrap };
g.DrawString(item.Text, font, brush, nameRect, sf);
int overlayH = 20;
var overlayRect = new Rectangle(thumbRect.X, thumbRect.Bottom - overlayH, thumbRect.Width, overlayH);
// Clip to the exact rounded thumbnail so the overlay corners match perfectly
Region oldClip = g.Clip;
using (var clipPath = CreateRoundedRectangle(thumbRect, THUMB_CORNER_RADIUS))
{
g.SetClip(clipPath, CombineMode.Intersect);
// Slightly overdraw to avoid 1px seams from AA / integer rounding
var fillRect = new Rectangle(overlayRect.X - 1, overlayRect.Y, overlayRect.Width + 2, overlayRect.Height + 1);
using (var overlayBrush = new SolidBrush(Color.FromArgb((int)(180 * state.TooltipOpacity), 0, 0, 0)))
g.FillRectangle(overlayBrush, fillRect);
g.Clip = oldClip;
}
using (var font = new Font("Segoe UI", 8f, FontStyle.Bold))
using (var brush = new SolidBrush(Color.FromArgb((int)(TextColor.A * state.TooltipOpacity), TextColor.R, TextColor.G, TextColor.B)))
{
var sf = new StringFormat
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Center,
Trimming = StringTrimming.EllipsisCharacter,
FormatFlags = StringFormatFlags.NoWrap
};
var textRect = new Rectangle(overlayRect.X, overlayRect.Y + 1, overlayRect.Width, overlayRect.Height);
g.DrawString(item.Text, font, brush, textRect, sf);
}
}
}
@@ -900,7 +1040,7 @@ public class FastGalleryPanel : Control
{
// Position in bottom-right corner of thumbnail
int btnX = tileX + tileWidth - DELETE_BUTTON_SIZE - thumbPadding - DELETE_BUTTON_MARGIN;
int btnY = tileY + thumbPadding + thumbHeight - DELETE_BUTTON_SIZE - DELETE_BUTTON_MARGIN;
int btnY = tileY + thumbPadding + thumbHeight - DELETE_BUTTON_SIZE - DELETE_BUTTON_MARGIN - 20;
var btnRect = new Rectangle(btnX, btnY, DELETE_BUTTON_SIZE, DELETE_BUTTON_SIZE);
int bgAlpha = (int)(opacity * 255);

104
MainForm.Designer.cs generated
View File

@@ -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();
@@ -93,6 +91,7 @@ namespace AndroidSideloader
this.speedLabel_Tooltip = new System.Windows.Forms.ToolTip(this.components);
this.etaLabel_Tooltip = new System.Windows.Forms.ToolTip(this.components);
this.progressDLbtnContainer = new System.Windows.Forms.Panel();
this.progressBar = new AndroidSideloader.ModernProgressBar();
this.diskLabel = new System.Windows.Forms.Label();
this.questStorageProgressBar = new System.Windows.Forms.Panel();
this.batteryLevImg = new System.Windows.Forms.PictureBox();
@@ -172,60 +171,19 @@ namespace AndroidSideloader
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
//
@@ -310,10 +268,12 @@ namespace AndroidSideloader
this.DownloadsIndex});
this.gamesListView.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.gamesListView.ForeColor = System.Drawing.Color.White;
this.gamesListView.FullRowSelect = true;
this.gamesListView.HideSelection = false;
this.gamesListView.ImeMode = System.Windows.Forms.ImeMode.Off;
this.gamesListView.Location = new System.Drawing.Point(258, 44);
this.gamesListView.Name = "gamesListView";
this.gamesListView.OwnerDraw = true;
this.gamesListView.ShowGroups = false;
this.gamesListView.Size = new System.Drawing.Size(984, 409);
this.gamesListView.TabIndex = 6;
@@ -329,39 +289,41 @@ namespace AndroidSideloader
// GameNameIndex
//
this.GameNameIndex.Text = "Game Name";
this.GameNameIndex.Width = 158;
this.GameNameIndex.Width = 160;
//
// ReleaseNameIndex
//
this.ReleaseNameIndex.Text = "Release Name";
this.ReleaseNameIndex.Width = 244;
this.ReleaseNameIndex.Width = 220;
//
// PackageNameIndex
//
this.PackageNameIndex.Text = "Package Name";
this.PackageNameIndex.Width = 87;
this.PackageNameIndex.Width = 120;
//
// VersionCodeIndex
//
this.VersionCodeIndex.Text = "Version";
this.VersionCodeIndex.Width = 75;
this.VersionCodeIndex.Text = "Version (Rookie/Local)";
this.VersionCodeIndex.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.VersionCodeIndex.Width = 164;
//
// ReleaseAPKPathIndex
//
this.ReleaseAPKPathIndex.Text = "Last Updated";
this.ReleaseAPKPathIndex.Width = 145;
this.ReleaseAPKPathIndex.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.ReleaseAPKPathIndex.Width = 135;
//
// VersionNameIndex
//
this.VersionNameIndex.Text = "Size (MB)";
this.VersionNameIndex.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
this.VersionNameIndex.Width = 66;
this.VersionNameIndex.Text = "Size";
this.VersionNameIndex.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.VersionNameIndex.Width = 85;
//
// DownloadsIndex
//
this.DownloadsIndex.Text = "Popularity";
this.DownloadsIndex.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
this.DownloadsIndex.Width = 80;
this.DownloadsIndex.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.DownloadsIndex.Width = 100;
//
// gamesQueueLabel
//
@@ -808,14 +770,38 @@ 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;
//
// 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, 23);
this.progressBar.Maximum = 100F;
this.progressBar.Minimum = 0F;
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 = 0F;
//
// diskLabel
//
this.diskLabel.BackColor = System.Drawing.Color.Transparent;
@@ -1646,7 +1632,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;
@@ -1751,5 +1736,6 @@ namespace AndroidSideloader
private Label activeMirrorLabel;
private Label sideloadingStatusLabel;
private Label rookieStatusLabel;
private ModernListView _listViewRenderer;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -120,9 +120,6 @@
<metadata name="speedLabel_Tooltip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>1165, 17</value>
</metadata>
<metadata name="etaLabel_Tooltip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>428, 54</value>
</metadata>
<metadata name="startsideloadbutton_Tooltip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>966, 17</value>
</metadata>
@@ -177,6 +174,9 @@
<metadata name="listApkButton_Tooltip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>1320, 17</value>
</metadata>
<metadata name="etaLabel_Tooltip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>428, 54</value>
</metadata>
<metadata name="favoriteGame.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>1021, 91</value>
</metadata>

1205
ModernListView.cs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -13,9 +13,9 @@ namespace AndroidSideloader
{
#region Fields
private int _value;
private int _minimum;
private int _maximum = 100;
private float _value;
private float _minimum;
private float _maximum = 100f;
private int _radius = 8;
private bool _isIndeterminate;
private string _statusText = string.Empty;
@@ -66,7 +66,7 @@ namespace AndroidSideloader
[Category("Progress")]
[Description("The current value of the progress bar.")]
public int Value
public float Value
{
get => _value;
set
@@ -78,7 +78,7 @@ namespace AndroidSideloader
[Category("Progress")]
[Description("The minimum value of the progress bar.")]
public int Minimum
public float Minimum
{
get => _minimum;
set
@@ -91,7 +91,7 @@ namespace AndroidSideloader
[Category("Progress")]
[Description("The maximum value of the progress bar.")]
public int Maximum
public float Maximum
{
get => _maximum;
set
@@ -122,7 +122,7 @@ namespace AndroidSideloader
set
{
// If there is no change, do nothing
if (_isIndeterminate == value)
if (_isIndeterminate == value)
return;
_isIndeterminate = value;
@@ -205,7 +205,7 @@ namespace AndroidSideloader
// Gets the progress as a percentage (0-100)
public float ProgressPercent =>
_maximum > _minimum ? (float)(_value - _minimum) / (_maximum - _minimum) * 100f : 0f;
_maximum > _minimum ? (_value - _minimum) / (_maximum - _minimum) * 100f : 0f;
#endregion
@@ -250,7 +250,7 @@ namespace AndroidSideloader
private void DrawProgress(Graphics g, Rectangle outerRect)
{
float percent = (_maximum > _minimum)
? (float)(_value - _minimum) / (_maximum - _minimum)
? (_value - _minimum) / (_maximum - _minimum)
: 0f;
if (percent <= 0f) return;
@@ -363,10 +363,11 @@ namespace AndroidSideloader
if (!_isIndeterminate && _value > _minimum)
{
string percentText = $"{(int)ProgressPercent}%";
// Show one decimal place for sub-percent precision
string percentText = $"{ProgressPercent:0.0}%";
if (!string.IsNullOrEmpty(_operationType))
{
// E.g. "Downloading · 73%"
// E.g. "Downloading · 73.5%"
return $"{_operationType} · {percentText}";
}
return percentText;
@@ -435,4 +436,4 @@ namespace AndroidSideloader
#endregion
}
}
}

2
NewApps.Designer.cs generated
View File

@@ -29,7 +29,6 @@ namespace AndroidSideloader
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(NewApps));
this.NewAppsListView = new System.Windows.Forms.ListView();
this.GameNameIndex = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.PackageNameIndex = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
@@ -173,7 +172,6 @@ namespace AndroidSideloader
this.Controls.Add(this.panel1);
this.ForeColor = System.Drawing.Color.White;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.Name = "NewApps";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Load += new System.EventHandler(this.NewApps_Load);

View File

@@ -39,6 +39,10 @@ namespace AndroidSideloader
public NewApps()
{
InitializeComponent();
// Use same icon as the executable
this.Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
ApplyModernTheme();
CenterToScreen();
}

File diff suppressed because it is too large Load Diff

3
QuestForm.Designer.cs generated
View File

@@ -30,7 +30,6 @@ namespace AndroidSideloader
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(QuestForm));
this.lblUsernameSection = new System.Windows.Forms.Label();
this.lblMediaSection = new System.Windows.Forms.Label();
this.lblPerformanceSection = new System.Windows.Forms.Label();
@@ -444,11 +443,9 @@ namespace AndroidSideloader
this.Controls.Add(this.btnApplyTempSettings);
this.Controls.Add(this.btnClose);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "QuestForm";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Quest Settings";
this.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.QuestForm_FormClosed);

View File

@@ -16,6 +16,9 @@ namespace AndroidSideloader
public QuestForm()
{
InitializeComponent();
// Use same icon as the executable
this.Icon = System.Drawing.Icon.ExtractAssociatedIcon(Application.ExecutablePath);
}
private void btnApplyTempSettings_Click(object sender, EventArgs e)

File diff suppressed because it is too large Load Diff

View File

@@ -28,7 +28,6 @@
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SettingsForm));
this.downloadDirectorySetter = new System.Windows.Forms.FolderBrowserDialog();
this.backupDirectorySetter = new System.Windows.Forms.FolderBrowserDialog();
this.crashlogID = new System.Windows.Forms.Label();
@@ -866,11 +865,9 @@
this.Controls.Add(this.resetSettingsButton);
this.ForeColor = System.Drawing.Color.White;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "SettingsForm";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Settings";
this.Load += new System.EventHandler(this.SettingsForm_Load);

View File

@@ -17,6 +17,9 @@ namespace AndroidSideloader
public SettingsForm()
{
InitializeComponent();
// Use same icon as the executable
this.Icon = System.Drawing.Icon.ExtractAssociatedIcon(Application.ExecutablePath);
}
private void SettingsForm_Load(object sender, EventArgs e)

File diff suppressed because it is too large Load Diff

View File

@@ -137,7 +137,7 @@ namespace AndroidSideloader
Application.Exit();
}
string wantedRcloneVersion = "1.68.2";
string wantedRcloneVersion = "1.72.1";
bool rcloneSuccess = false;
rcloneSuccess = downloadRclone(wantedRcloneVersion, false);

View File

@@ -14,8 +14,6 @@ namespace AndroidSideloader
public static string RcloneGamesFolder = "Quest Games";
//This shit sucks but i'll switch to programatically adding indexes from the gamelist txt sometimes maybe
public static int GameNameIndex = 0;
public static int ReleaseNameIndex = 1;
public static int PackageNameIndex = 2;
@@ -23,6 +21,7 @@ namespace AndroidSideloader
public static int ReleaseAPKPathIndex = 4;
public static int VersionNameIndex = 5;
public static int DownloadsIndex = 6;
public static int InstalledVersion = 7;
public static List<string> gameProperties = new List<string>();
/* Game Name

View File

@@ -28,7 +28,6 @@
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(UpdateForm));
this.panel1 = new System.Windows.Forms.Panel();
this.YesUpdate = new AndroidSideloader.RoundButton();
this.panel3 = new System.Windows.Forms.Panel();
@@ -166,7 +165,6 @@
this.ControlBox = false;
this.Controls.Add(this.panel1);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.Name = "UpdateForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.UpdateForm_MouseDown);

View File

@@ -20,6 +20,10 @@ namespace AndroidSideloader
public UpdateForm()
{
InitializeComponent();
// Use same icon as the executable
this.Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
ApplyModernTheme();
CenterToScreen();
CurVerLabel.Text = $"Current Version: {Updater.LocalVersion}";

File diff suppressed because it is too large Load Diff

View File

@@ -28,28 +28,30 @@
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(UsernameForm));
this.textBox1 = new System.Windows.Forms.TextBox();
this.button1 = new AndroidSideloader.RoundButton();
this.SuspendLayout();
//
//
// textBox1
//
//
this.textBox1.BackColor = global::AndroidSideloader.Properties.Settings.Default.TextBoxColor;
this.textBox1.Font = global::AndroidSideloader.Properties.Settings.Default.FontStyle;
this.textBox1.ForeColor = System.Drawing.Color.White;
this.textBox1.Location = new System.Drawing.Point(13, 13);
this.textBox1.Name = "textBox1";
this.textBox1.Size = new System.Drawing.Size(418, 24);
this.textBox1.Size = new System.Drawing.Size(418, 23);
this.textBox1.TabIndex = 0;
this.textBox1.Text = "Enter your username here";
//
//
// button1
//
//
this.button1.Active1 = System.Drawing.Color.FromArgb(((int)(((byte)(45)))), ((int)(((byte)(45)))), ((int)(((byte)(45)))));
this.button1.Active2 = System.Drawing.Color.FromArgb(((int)(((byte)(45)))), ((int)(((byte)(45)))), ((int)(((byte)(45)))));
this.button1.BackColor = System.Drawing.Color.Transparent;
this.button1.DialogResult = System.Windows.Forms.DialogResult.OK;
this.button1.Disabled1 = System.Drawing.Color.FromArgb(((int)(((byte)(32)))), ((int)(((byte)(35)))), ((int)(((byte)(45)))));
this.button1.Disabled2 = System.Drawing.Color.FromArgb(((int)(((byte)(25)))), ((int)(((byte)(28)))), ((int)(((byte)(35)))));
this.button1.DisabledStrokeColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(55)))), ((int)(((byte)(65)))));
this.button1.Font = new System.Drawing.Font("Microsoft Sans Serif", 11.25F);
this.button1.ForeColor = System.Drawing.Color.White;
this.button1.Inactive1 = System.Drawing.Color.FromArgb(((int)(((byte)(25)))), ((int)(((byte)(25)))), ((int)(((byte)(25)))));
@@ -64,9 +66,9 @@
this.button1.Text = "Create User.Json";
this.button1.Transparency = false;
this.button1.Click += new System.EventHandler(this.button1_Click);
//
//
// UsernameForm
//
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = global::AndroidSideloader.Properties.Settings.Default.BackColor;
@@ -74,11 +76,9 @@
this.Controls.Add(this.button1);
this.Controls.Add(this.textBox1);
this.ForeColor = System.Drawing.Color.White;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MaximumSize = new System.Drawing.Size(459, 139);
this.MinimumSize = new System.Drawing.Size(459, 139);
this.Name = "UsernameForm";
this.ShowIcon = false;
this.Text = "USER.JSON";
this.Load += new System.EventHandler(this.usernameForm_Load);
this.ResumeLayout(false);

View File

@@ -12,6 +12,9 @@ namespace AndroidSideloader
public UsernameForm()
{
InitializeComponent();
// Use same icon as the executable
this.Icon = System.Drawing.Icon.ExtractAssociatedIcon(Application.ExecutablePath);
}
private string defaultText;

File diff suppressed because it is too large Load Diff

View File

@@ -3,10 +3,7 @@ using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Forms;
namespace AndroidSideloader.Utilities
@@ -19,6 +16,11 @@ namespace AndroidSideloader.Utilities
internal class Zip
{
private static readonly SettingsManager settings = SettingsManager.Instance;
// Progress callback: (percent, eta)
public static Action<float, TimeSpan?> ExtractionProgressCallback { get; set; }
public static Action<string> ExtractionStatusCallback { get; set; }
public static void ExtractFile(string sourceArchive, string destination)
{
string args = $"x \"{sourceArchive}\" -y -o\"{destination}\" -bsp1";
@@ -33,6 +35,7 @@ namespace AndroidSideloader.Utilities
private static string extractionError = null;
private static bool errorMessageShown = false;
private static void DoExtract(string args)
{
if (!File.Exists(Path.Combine(Environment.CurrentDirectory, "7z.exe")) || !File.Exists(Path.Combine(Environment.CurrentDirectory, "7z.dll")))
@@ -68,24 +71,98 @@ namespace AndroidSideloader.Utilities
_ = Logger.Log($"Extract: 7z {string.Join(" ", args.Split(' ').Where(a => !a.StartsWith("-p")))}");
// Throttle percent reports
float lastReportedPercent = -1;
// ETA engine (percent units)
var etaEstimator = new EtaEstimator(alpha: 0.10, reanchorThreshold: 0.20, minSampleSeconds: 0.10);
// Smooth progress (sub-percent) interpolation (because 7z -bsp1 is integer-only)
System.Threading.Timer smoothTimer = null;
int extractingFlag = 1; // 1 = extracting, 0 = stop
float smoothLastTickPercent = 0f;
DateTime smoothLastTickTime = DateTime.UtcNow;
float smoothLastReported = -1f;
const int SmoothIntervalMs = 80; // ~12.5 updates/sec
const float SmoothReportDelta = 0.10f; // report only if change >= 0.10%
using (Process x = new Process())
{
x.StartInfo = pro;
if (MainForm.isInDownloadExtract && x != null)
{
// Smooth sub-percent UI, while keeping ETA ticking
smoothTimer = new System.Threading.Timer(_ =>
{
if (System.Threading.Volatile.Read(ref extractingFlag) == 0) return;
if (smoothLastTickPercent <= 0) return; // need at least one 7z tick
// Use current ETA to approximate seconds-per-percent
TimeSpan? displayEta = etaEstimator.GetDisplayEta();
if (!displayEta.HasValue) return; // Skip until ETA exists
var now = DateTime.UtcNow;
var elapsed = (now - smoothLastTickTime).TotalSeconds;
// Approx seconds-per-percent from remaining ETA / remaining percent
double remainingPercent = Math.Max(1.0, 100.0 - smoothLastTickPercent);
double spp = Math.Max(0.05, displayEta.Value.TotalSeconds / remainingPercent);
float candidate = smoothLastTickPercent + (float)(elapsed / spp);
// Clamp
float floorTick = (float)Math.Floor(smoothLastTickPercent);
float ceiling = Math.Min(99.99f, floorTick + 0.999f);
if (candidate > ceiling) candidate = ceiling;
if (candidate < smoothLastTickPercent) candidate = smoothLastTickPercent;
if (smoothLastReported >= 0 && Math.Abs(candidate - smoothLastReported) < SmoothReportDelta) return;
smoothLastReported = candidate;
try
{
MainForm mainForm = (MainForm)Application.OpenForms[0];
if (mainForm != null && !mainForm.IsDisposed)
{
mainForm.BeginInvoke((Action)(() => mainForm.SetProgress(candidate)));
}
}
catch { }
// ETA countdown ticks even if 7z percent is unchanged
ExtractionProgressCallback?.Invoke(candidate, etaEstimator.GetDisplayEta());
}, null, SmoothIntervalMs, SmoothIntervalMs);
x.OutputDataReceived += (sender, e) =>
{
if (e.Data != null)
{
var match = Regex.Match(e.Data, @"(\d+)%");
if (match.Success)
var match = Regex.Match(e.Data, @"^\s*(\d+)%");
if (match.Success && float.TryParse(match.Groups[1].Value, out float percent))
{
int progress = int.Parse(match.Groups[1].Value);
MainForm mainForm = (MainForm)Application.OpenForms[0];
if (mainForm != null)
// Update ETA from integer percent
if (percent <= 0.0f) etaEstimator.Reset();
else if (percent < 100.0f) etaEstimator.Update(totalUnits: 100, doneUnits: (long)Math.Round(percent));
// Reset smoothing baseline on each integer tick
smoothLastTickPercent = percent;
smoothLastTickTime = DateTime.UtcNow;
smoothLastReported = percent;
if (Math.Abs(percent - lastReportedPercent) >= 0.1f)
{
mainForm.Invoke((Action)(() => mainForm.SetProgress(progress)));
lastReportedPercent = percent;
MainForm mainForm = (MainForm)Application.OpenForms[0];
if (mainForm != null)
{
mainForm.Invoke((Action)(() => mainForm.SetProgress(percent)));
}
ExtractionProgressCallback?.Invoke(percent, etaEstimator.GetDisplayEta());
}
}
}
@@ -119,6 +196,16 @@ namespace AndroidSideloader.Utilities
x.BeginOutputReadLine();
x.BeginErrorReadLine();
x.WaitForExit();
// Stop smoother
System.Threading.Interlocked.Exchange(ref extractingFlag, 0);
smoothTimer?.Dispose();
smoothTimer = null;
// Clear callbacks
ExtractionProgressCallback?.Invoke(100, null);
ExtractionStatusCallback?.Invoke("");
errorMessageShown = false;
if (!string.IsNullOrEmpty(extractionError))