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
This commit is contained in:
266
ADB.cs
266
ADB.cs
@@ -154,7 +154,7 @@ namespace AndroidSideloader
|
||||
// Copies and installs an APK with real-time progress reporting using AdvancedSharpAdbClient
|
||||
public static async Task<ProcessOutput> SideloadWithProgressAsync(
|
||||
string path,
|
||||
Action<int, TimeSpan?> progressCallback = null, // Now includes ETA
|
||||
Action<float, TimeSpan?> progressCallback = null,
|
||||
Action<string> statusCallback = null,
|
||||
string packagename = "",
|
||||
string gameName = "")
|
||||
@@ -177,112 +177,79 @@ namespace AndroidSideloader
|
||||
|
||||
// Throttle UI updates to prevent lag
|
||||
DateTime lastProgressUpdate = DateTime.MinValue;
|
||||
int lastReportedPercent = -1;
|
||||
const int ThrottleMs = 100; // Update UI at most every 100ms
|
||||
float lastReportedPercent = -1;
|
||||
const int ThrottleMs = 100; // Update UI every 100ms
|
||||
|
||||
// ETA tracking with smoothing
|
||||
DateTime installStart = DateTime.UtcNow;
|
||||
int etaLastPercent = 0;
|
||||
DateTime etaLastPercentTime = DateTime.UtcNow;
|
||||
double smoothedSecondsPerPercent = 0;
|
||||
TimeSpan? lastReportedEta = null;
|
||||
const double SmoothingAlpha = 0.15; // Lower = smoother, less responsive
|
||||
const double EtaChangeThreshold = 0.10; // Only update if ETA changes by >10%
|
||||
// Shared ETA engine (percent-units)
|
||||
var eta = new EtaEstimator(alpha: 0.05, reanchorThreshold: 0.20);
|
||||
|
||||
// Create install progress handler
|
||||
Action<InstallProgressEventArgs> installProgress = (args) =>
|
||||
{
|
||||
int percent = 0;
|
||||
float percent = 0;
|
||||
string status = null;
|
||||
TimeSpan? eta = null;
|
||||
TimeSpan? displayEta = null;
|
||||
|
||||
switch (args.State)
|
||||
{
|
||||
case PackageInstallProgressState.Preparing:
|
||||
percent = 0;
|
||||
status = "Preparing...";
|
||||
installStart = DateTime.UtcNow;
|
||||
etaLastPercent = 0;
|
||||
etaLastPercentTime = installStart;
|
||||
smoothedSecondsPerPercent = 0;
|
||||
lastReportedEta = null;
|
||||
eta.Reset();
|
||||
break;
|
||||
|
||||
case PackageInstallProgressState.Uploading:
|
||||
percent = (int)Math.Round(args.UploadProgress);
|
||||
percent = (float)args.UploadProgress;
|
||||
|
||||
// Calculate ETA with smoothing
|
||||
if (percent > etaLastPercent && percent < 100)
|
||||
// Update ETA engine using percent as units (0..100)
|
||||
if (percent > 0 && percent < 100)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
double secondsForThisChunk = (now - etaLastPercentTime).TotalSeconds;
|
||||
int percentGained = percent - etaLastPercent;
|
||||
|
||||
if (percentGained > 0 && secondsForThisChunk > 0)
|
||||
{
|
||||
double secondsPerPercent = secondsForThisChunk / percentGained;
|
||||
|
||||
// Exponential smoothing
|
||||
if (smoothedSecondsPerPercent == 0)
|
||||
smoothedSecondsPerPercent = secondsPerPercent;
|
||||
else
|
||||
smoothedSecondsPerPercent = SmoothingAlpha * secondsPerPercent + (1 - SmoothingAlpha) * smoothedSecondsPerPercent;
|
||||
|
||||
int remainingPercent = 100 - percent;
|
||||
double etaSeconds = remainingPercent * smoothedSecondsPerPercent;
|
||||
var newEta = TimeSpan.FromSeconds(Math.Max(0, etaSeconds));
|
||||
|
||||
// Only update if significant change
|
||||
if (!lastReportedEta.HasValue ||
|
||||
Math.Abs(newEta.TotalSeconds - lastReportedEta.Value.TotalSeconds) / Math.Max(1, lastReportedEta.Value.TotalSeconds) > EtaChangeThreshold)
|
||||
{
|
||||
eta = newEta;
|
||||
lastReportedEta = eta;
|
||||
}
|
||||
else
|
||||
{
|
||||
eta = lastReportedEta; // Keep previous ETA
|
||||
}
|
||||
|
||||
etaLastPercent = percent;
|
||||
etaLastPercentTime = now;
|
||||
}
|
||||
eta.Update(totalUnits: 100, doneUnits: (long)Math.Round(percent));
|
||||
displayEta = eta.GetDisplayEta();
|
||||
}
|
||||
else
|
||||
{
|
||||
eta = lastReportedEta;
|
||||
displayEta = eta.GetDisplayEta();
|
||||
}
|
||||
|
||||
status = $"Installing · {percent}%";
|
||||
status = $"Installing · {percent:0.0}%";
|
||||
break;
|
||||
|
||||
case PackageInstallProgressState.Installing:
|
||||
percent = 100;
|
||||
status = "Completing Installation...";
|
||||
displayEta = null;
|
||||
break;
|
||||
|
||||
case PackageInstallProgressState.Finished:
|
||||
percent = 100;
|
||||
status = "";
|
||||
displayEta = null;
|
||||
break;
|
||||
|
||||
default:
|
||||
percent = 50;
|
||||
percent = 100;
|
||||
status = "";
|
||||
displayEta = null;
|
||||
break;
|
||||
}
|
||||
|
||||
// Throttle updates
|
||||
var updateNow = DateTime.UtcNow;
|
||||
bool shouldUpdate = (updateNow - lastProgressUpdate).TotalMilliseconds >= ThrottleMs
|
||||
|| percent != lastReportedPercent
|
||||
|| Math.Abs(percent - lastReportedPercent) >= 0.1f
|
||||
|| args.State != PackageInstallProgressState.Uploading;
|
||||
|
||||
if (shouldUpdate)
|
||||
{
|
||||
lastProgressUpdate = updateNow;
|
||||
lastReportedPercent = percent;
|
||||
progressCallback?.Invoke(percent, eta);
|
||||
|
||||
// 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);
|
||||
@@ -297,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"))
|
||||
{
|
||||
@@ -318,7 +284,6 @@ namespace AndroidSideloader
|
||||
if (cancelClicked)
|
||||
return new ProcessOutput("", "Installation cancelled by user");
|
||||
|
||||
// Perform reinstall
|
||||
statusCallback?.Invoke("Performing reinstall...");
|
||||
|
||||
try
|
||||
@@ -338,7 +303,7 @@ namespace AndroidSideloader
|
||||
{
|
||||
if (args.State == PackageInstallProgressState.Uploading)
|
||||
{
|
||||
progressCallback?.Invoke((int)Math.Round(args.UploadProgress), null);
|
||||
progressCallback?.Invoke((float)args.UploadProgress, null);
|
||||
}
|
||||
};
|
||||
packageManager.InstallPackage(path, reinstallProgress);
|
||||
@@ -368,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, TimeSpan?> progressCallback = null, // Now includes ETA
|
||||
Action<float, TimeSpan?> progressCallback = null,
|
||||
Action<string> statusCallback = null,
|
||||
string gameName = "")
|
||||
{
|
||||
@@ -404,18 +369,11 @@ namespace AndroidSideloader
|
||||
|
||||
// Throttle UI updates to prevent lag
|
||||
DateTime lastProgressUpdate = DateTime.MinValue;
|
||||
int lastReportedPercent = -1;
|
||||
const int ThrottleMs = 100; // Update UI at most every 100ms
|
||||
float lastReportedPercent = -1;
|
||||
const int ThrottleMs = 100; // Update UI every 100ms
|
||||
|
||||
// ETA tracking with smoothing
|
||||
DateTime copyStart = DateTime.UtcNow;
|
||||
int etaLastPercent = 0;
|
||||
DateTime etaLastPercentTime = DateTime.UtcNow;
|
||||
double smoothedSecondsPerPercent = 0;
|
||||
TimeSpan? lastReportedEta = null;
|
||||
TimeSpan? currentEta = null;
|
||||
const double SmoothingAlpha = 0.15; // Lower = smoother, less responsive
|
||||
const double EtaChangeThreshold = 0.10; // 10% change threshold
|
||||
// Shared ETA engine (bytes-units)
|
||||
var eta = new EtaEstimator(alpha: 0.10, reanchorThreshold: 0.20);
|
||||
|
||||
statusCallback?.Invoke($"Copying: {folderName}");
|
||||
|
||||
@@ -437,71 +395,37 @@ namespace AndroidSideloader
|
||||
long fileSize = fileInfo.Length;
|
||||
long capturedTransferredBytes = transferredBytes;
|
||||
|
||||
// Progress handler for this file with throttling
|
||||
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));
|
||||
|
||||
// Calculate ETA with smoothing
|
||||
if (overallPercentInt > etaLastPercent && overallPercentInt < 100)
|
||||
// Update ETA engine in bytes
|
||||
if (totalBytes > 0 && totalProgressBytes > 0 && overallPercent < 100)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
double secondsForThisChunk = (now - etaLastPercentTime).TotalSeconds;
|
||||
int percentGained = overallPercentInt - etaLastPercent;
|
||||
|
||||
if (percentGained > 0 && secondsForThisChunk > 0)
|
||||
{
|
||||
double secondsPerPercent = secondsForThisChunk / percentGained;
|
||||
|
||||
// Exponential smoothing
|
||||
if (smoothedSecondsPerPercent == 0)
|
||||
smoothedSecondsPerPercent = secondsPerPercent;
|
||||
else
|
||||
smoothedSecondsPerPercent = SmoothingAlpha * secondsPerPercent + (1 - SmoothingAlpha) * smoothedSecondsPerPercent;
|
||||
|
||||
int remainingPercent = 100 - overallPercentInt;
|
||||
double etaSeconds = remainingPercent * smoothedSecondsPerPercent;
|
||||
var newEta = TimeSpan.FromSeconds(Math.Max(0, etaSeconds));
|
||||
|
||||
// Only update if significant change
|
||||
if (!lastReportedEta.HasValue ||
|
||||
Math.Abs(newEta.TotalSeconds - lastReportedEta.Value.TotalSeconds) / Math.Max(1, lastReportedEta.Value.TotalSeconds) > EtaChangeThreshold)
|
||||
{
|
||||
currentEta = newEta;
|
||||
lastReportedEta = currentEta;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentEta = lastReportedEta;
|
||||
}
|
||||
|
||||
etaLastPercent = overallPercentInt;
|
||||
etaLastPercentTime = now;
|
||||
}
|
||||
eta.Update(totalUnits: totalBytes, doneUnits: totalProgressBytes);
|
||||
}
|
||||
|
||||
// Throttle updates
|
||||
TimeSpan? displayEta = eta.GetDisplayEta();
|
||||
|
||||
var now2 = DateTime.UtcNow;
|
||||
bool shouldUpdate = (now2 - lastProgressUpdate).TotalMilliseconds >= ThrottleMs
|
||||
|| overallPercentInt != lastReportedPercent;
|
||||
|| Math.Abs(overallPercent - lastReportedPercent) >= 0.1f;
|
||||
|
||||
if (shouldUpdate)
|
||||
{
|
||||
lastProgressUpdate = now2;
|
||||
lastReportedPercent = overallPercentInt;
|
||||
progressCallback?.Invoke(overallPercentInt, currentEta);
|
||||
lastReportedPercent = overallPercent;
|
||||
progressCallback?.Invoke(overallPercent, displayEta);
|
||||
statusCallback?.Invoke(fileName);
|
||||
}
|
||||
};
|
||||
|
||||
// Push the file with progress
|
||||
using (var stream = File.OpenRead(file))
|
||||
{
|
||||
await Task.Run(() =>
|
||||
@@ -786,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
33
MainForm.cs
33
MainForm.cs
@@ -3052,7 +3052,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
|
||||
gamesQueueList.RemoveAt(0);
|
||||
}
|
||||
|
||||
public void SetProgress(int progress)
|
||||
public void SetProgress(float progress)
|
||||
{
|
||||
if (progressBar.InvokeRequired)
|
||||
{
|
||||
@@ -3264,7 +3264,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
|
||||
speedLabel.Text = "Starting download...";
|
||||
|
||||
// Track the highest valid progress to prevent brief progress bar flashes during multi-file transfers
|
||||
int highestValidPercent = 0;
|
||||
float highestValidPercent = 0;
|
||||
|
||||
// Download
|
||||
while (t1.IsAlive)
|
||||
@@ -3311,10 +3311,10 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
|
||||
|
||||
progressBar.IsIndeterminate = false;
|
||||
|
||||
int percent = 0;
|
||||
float percent = 0;
|
||||
if (totalSize > 0)
|
||||
{
|
||||
percent = Convert.ToInt32((downloadedSize / totalSize) * 100);
|
||||
percent = (float)(downloadedSize / totalSize * 100);
|
||||
}
|
||||
|
||||
// Clamp to 0-99 while download is in progress to prevent brief 100% flashes
|
||||
@@ -3339,7 +3339,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
|
||||
"Downloading",
|
||||
(int)transfersComplete + 1,
|
||||
(int)fileCount,
|
||||
percent,
|
||||
(int)Math.Round(percent),
|
||||
time,
|
||||
downloadSpeed);
|
||||
}
|
||||
@@ -3441,9 +3441,9 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
|
||||
this.Invoke(() =>
|
||||
{
|
||||
progressBar.Value = percent;
|
||||
UpdateProgressStatus("Extracting", percent: percent, eta: eta);
|
||||
UpdateProgressStatus("Extracting", percent: (int)Math.Round(percent), eta: eta);
|
||||
|
||||
progressBar.StatusText = $"Extracting · {percent}%";
|
||||
progressBar.StatusText = $"Extracting · {percent:0.0}%";
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3570,14 +3570,17 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
|
||||
progressBar.IsIndeterminate = false;
|
||||
progressBar.Value = progress;
|
||||
}
|
||||
UpdateProgressStatus("Installing APK", percent: progress, eta: eta);
|
||||
progressBar.StatusText = $"Installing · {progress}%";
|
||||
UpdateProgressStatus("Installing", percent: (int)Math.Round(progress), eta: eta);
|
||||
progressBar.StatusText = $"Installing · {progress:0.0}%";
|
||||
}),
|
||||
status => this.Invoke(() => {
|
||||
if (!string.IsNullOrEmpty(status))
|
||||
{
|
||||
// "Completing Installation..."
|
||||
speedLabel.Text = status;
|
||||
if (status.Contains("Completing Installation"))
|
||||
{
|
||||
// "Completing Installation..."
|
||||
speedLabel.Text = status;
|
||||
}
|
||||
progressBar.StatusText = status;
|
||||
}
|
||||
}),
|
||||
@@ -3605,15 +3608,15 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
|
||||
(progress, eta) => this.Invoke(() =>
|
||||
{
|
||||
progressBar.Value = progress;
|
||||
UpdateProgressStatus("Copying OBB", percent: progress, eta: eta);
|
||||
UpdateProgressStatus("Copying OBB", percent: (int)Math.Round(progress), eta: eta);
|
||||
|
||||
if (!string.IsNullOrEmpty(currentObbStatusBase))
|
||||
{
|
||||
progressBar.StatusText = $"{currentObbStatusBase} · {progress}%";
|
||||
progressBar.StatusText = $"{currentObbStatusBase} · {progress:0.0}%";
|
||||
}
|
||||
else
|
||||
{
|
||||
progressBar.StatusText = $"{progress}%";
|
||||
progressBar.StatusText = $"{progress:0.0}%";
|
||||
}
|
||||
}),
|
||||
status => this.Invoke(() =>
|
||||
@@ -4743,6 +4746,8 @@ let player;
|
||||
let pendingId = null;
|
||||
function onYouTubeIframeAPIReady() {
|
||||
player = new YT.Player('player', {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
playerVars: {
|
||||
autoplay: 0,
|
||||
mute: 1,
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
131
Utilities/Zip.cs
131
Utilities/Zip.cs
@@ -18,7 +18,7 @@ namespace AndroidSideloader.Utilities
|
||||
private static readonly SettingsManager settings = SettingsManager.Instance;
|
||||
|
||||
// Progress callback: (percent, eta)
|
||||
public static Action<int, TimeSpan?> ExtractionProgressCallback { get; set; }
|
||||
public static Action<float, TimeSpan?> ExtractionProgressCallback { get; set; }
|
||||
public static Action<string> ExtractionStatusCallback { get; set; }
|
||||
|
||||
public static void ExtractFile(string sourceArchive, string destination)
|
||||
@@ -35,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")))
|
||||
@@ -70,15 +71,20 @@ namespace AndroidSideloader.Utilities
|
||||
|
||||
_ = Logger.Log($"Extract: 7z {string.Join(" ", args.Split(' ').Where(a => !a.StartsWith("-p")))}");
|
||||
|
||||
// ETA tracking
|
||||
DateTime extractStart = DateTime.UtcNow;
|
||||
int etaLastPercent = 0;
|
||||
DateTime etaLastPercentTime = DateTime.UtcNow;
|
||||
double smoothedSecondsPerPercent = 0;
|
||||
TimeSpan? lastReportedEta = null;
|
||||
int lastReportedPercent = -1;
|
||||
const double SmoothingAlpha = 0.15;
|
||||
const double EtaChangeThreshold = 0.10;
|
||||
// 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())
|
||||
{
|
||||
@@ -86,59 +92,67 @@ namespace AndroidSideloader.Utilities
|
||||
|
||||
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)
|
||||
{
|
||||
// Parse 7-Zip progress output (e.g., " 45% - filename")
|
||||
var match = Regex.Match(e.Data, @"^\s*(\d+)%");
|
||||
if (match.Success && int.TryParse(match.Groups[1].Value, out int percent))
|
||||
if (match.Success && float.TryParse(match.Groups[1].Value, out float percent))
|
||||
{
|
||||
TimeSpan? eta = 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));
|
||||
|
||||
// Calculate ETA
|
||||
if (percent > etaLastPercent && percent < 100)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
double secondsForThisChunk = (now - etaLastPercentTime).TotalSeconds;
|
||||
int percentGained = percent - etaLastPercent;
|
||||
// Reset smoothing baseline on each integer tick
|
||||
smoothLastTickPercent = percent;
|
||||
smoothLastTickTime = DateTime.UtcNow;
|
||||
smoothLastReported = percent;
|
||||
|
||||
if (percentGained > 0 && secondsForThisChunk > 0)
|
||||
{
|
||||
double secondsPerPercent = secondsForThisChunk / percentGained;
|
||||
|
||||
if (smoothedSecondsPerPercent == 0)
|
||||
smoothedSecondsPerPercent = secondsPerPercent;
|
||||
else
|
||||
smoothedSecondsPerPercent = SmoothingAlpha * secondsPerPercent + (1 - SmoothingAlpha) * smoothedSecondsPerPercent;
|
||||
|
||||
int remainingPercent = 100 - percent;
|
||||
double etaSeconds = remainingPercent * smoothedSecondsPerPercent;
|
||||
var newEta = TimeSpan.FromSeconds(Math.Max(0, etaSeconds));
|
||||
|
||||
// Only update if significant change
|
||||
if (!lastReportedEta.HasValue ||
|
||||
Math.Abs(newEta.TotalSeconds - lastReportedEta.Value.TotalSeconds) / Math.Max(1, lastReportedEta.Value.TotalSeconds) > EtaChangeThreshold)
|
||||
{
|
||||
eta = newEta;
|
||||
lastReportedEta = eta;
|
||||
}
|
||||
else
|
||||
{
|
||||
eta = lastReportedEta;
|
||||
}
|
||||
|
||||
etaLastPercent = percent;
|
||||
etaLastPercentTime = now;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
eta = lastReportedEta;
|
||||
}
|
||||
|
||||
// Only report if percent changed
|
||||
if (percent != lastReportedPercent)
|
||||
if (Math.Abs(percent - lastReportedPercent) >= 0.1f)
|
||||
{
|
||||
lastReportedPercent = percent;
|
||||
|
||||
@@ -148,7 +162,7 @@ namespace AndroidSideloader.Utilities
|
||||
mainForm.Invoke((Action)(() => mainForm.SetProgress(percent)));
|
||||
}
|
||||
|
||||
ExtractionProgressCallback?.Invoke(percent, eta);
|
||||
ExtractionProgressCallback?.Invoke(percent, etaEstimator.GetDisplayEta());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,6 +197,11 @@ namespace AndroidSideloader.Utilities
|
||||
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("");
|
||||
|
||||
Reference in New Issue
Block a user