namespace LlamaApp.Views
{
///
/// Pure formatting/speed helpers for the download-progress detail line —
/// kept separate from and
/// so the rules are unit-testable (no XAML objects involved), mirroring
/// .
///
public static class DownloadProgressPresentation
{
///
/// Formats a byte count as a human-readable size, e.g. 2526080992 →
/// "2.5 GB". Decimal units (1 GB = 1e9 B) with one fractional digit at
/// GB/TB — matching the catalog's pre-formatted size strings, so the
/// progress detail agrees with the size shown before the download.
///
public static string FormatBytes(double bytes)
{
if (bytes >= 1_000_000_000_000) return $"{bytes / 1_000_000_000_000.0:0.#} TB";
if (bytes >= 1_000_000_000) return $"{bytes / 1_000_000_000.0:0.#} GB";
if (bytes >= 1_000_000) return $"{bytes / 1_000_000.0:0} MB";
if (bytes >= 1_000) return $"{bytes / 1_000.0:0} KB";
return $"{(long)bytes} B";
}
///
/// Builds the download detail line shown in place of the row's
/// "params · size" subtitle while a download runs:
/// "3.2 GB of 12.1 GB", plus " · 45 MB/s" once a speed estimate
/// exists, plus " · ~4 min left" once the ETA is meaningful. Segments
/// appear only when their inputs are known — a stalled stream (speed
/// 0) never shows a bogus "0 MB/s · ∞ left".
///
public static string FormatDetail(long downloadedBytes, long totalBytes, double bytesPerSecond)
{
var text = $"{FormatBytes(downloadedBytes)} of {FormatBytes(totalBytes)}";
if (bytesPerSecond <= 0) return text;
text += $" · {FormatBytes(bytesPerSecond)}/s";
var remainingBytes = totalBytes - downloadedBytes;
if (remainingBytes > 0)
text += $" · {FormatEta(remainingBytes / bytesPerSecond)}";
return text;
}
///
/// Coarse ETA label: "~30 s left" under a minute, "~N min left" up to
/// 90 minutes, "~N h left" beyond. Deliberately imprecise — a
/// second-accurate countdown would lie about a fluctuating stream.
///
public static string FormatEta(double seconds)
{
if (seconds < 60) return $"~{Math.Max(1, (int)Math.Round(seconds))} s left";
var minutes = seconds / 60;
if (minutes < 90) return $"~{Math.Max(1, (int)Math.Round(minutes))} min left";
return $"~{(int)Math.Round(minutes / 60)} h left";
}
///
/// Exponentially-smoothed download speed (alpha 0.3): the SSE stream's
/// per-chunk events are bursty, so the instantaneous rate between two
/// throttled UI samples jitters wildly. The first sample seeds the
/// average outright.
///
public static double SmoothSpeed(double previousBytesPerSecond, double instantaneousBytesPerSecond)
=> previousBytesPerSecond <= 0
? instantaneousBytesPerSecond
: previousBytesPerSecond * 0.7 + instantaneousBytesPerSecond * 0.3;
}
}