releases/VideoDownloader.App/Browser/Features/PageDownloadTracker.cs

122 lines
4.1 KiB
C#

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace VideoDownloader.App.Browser.Features;
/// <summary>CoreWebView2DownloadOperation 의 최소 추상화(단위 테스트 fake 교체 가능).</summary>
public interface IDownloadOperation
{
string Uri { get; }
string FileName { get; }
long BytesReceived { get; }
long TotalBytesToReceive { get; }
/// <summary>InProgress / Interrupted / Completed / Cancelled</summary>
string State { get; }
void Pause();
void Resume();
void Cancel();
/// <summary>BytesReceived/State 변경 통지(구현은 호스트 스레드에서 발화).</summary>
event Action? Changed;
}
/// <summary>다운로드 매니저 UI 항목(T1-5) — 진행률/속도/상태를 INPC 로 노출.</summary>
public sealed class PageDownloadItem : INotifyPropertyChanged
{
public IDownloadOperation Op { get; }
public string FileName { get; }
public string Uri { get; }
public long TotalBytes { get; }
private long _received;
private long _bps;
private string _state;
public PageDownloadItem(IDownloadOperation op, string fileName)
{
Op = op;
FileName = fileName;
Uri = op.Uri;
TotalBytes = op.TotalBytesToReceive;
_received = op.BytesReceived;
_state = op.State;
op.Changed += () =>
{
_received = op.BytesReceived;
_state = op.State;
OnPropertyChangedAll();
};
}
public double Percent => TotalBytes > 0 ? Math.Clamp(_received * 100.0 / TotalBytes, 0, 100) : 0;
public long BytesPerSecond => _bps;
public string State => _state;
public bool IsFinished => _state is "Completed" or "Interrupted" or "Cancelled";
public bool CanControl => _state == "InProgress";
public string SpeedText => _bps switch
{
<= 0 => "—",
< 1024 => $"{(double)_bps:F1} B/s",
< 1024 * 1024 => $"{_bps / 1024.0:F1} KB/s",
_ => $"{_bps / 1024.0 / 1024.0:F1} MB/s",
};
private long? _lastSpeedReceived;
/// <summary>속도 갱신: (현재까지 수신 바이트, 직전 호출로부터 경과).</summary>
public void UpdateSpeed(long receivedNow, TimeSpan elapsed)
{
_received = receivedNow;
if (elapsed.TotalSeconds > 0)
{
var delta = Math.Max(0, receivedNow - (_lastSpeedReceived ?? 0));
_bps = (long)(delta / elapsed.TotalSeconds);
}
_lastSpeedReceived = receivedNow;
OnPropertyChangedAll();
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChangedAll()
{
foreach (var n in new[] { nameof(Percent), nameof(BytesPerSecond), nameof(SpeedText), nameof(State), nameof(IsFinished), nameof(CanControl) })
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
}
}
/// <summary>페이지 다운로드 목록(T1-5) — 최신순, 일시정지/재개/취소, 완료 정리.</summary>
public sealed class PageDownloadTracker
{
public ObservableCollection<PageDownloadItem> Items { get; } = new();
public PageDownloadItem Add(IDownloadOperation op, string fileName)
{
var item = new PageDownloadItem(op, fileName);
Items.Insert(0, item);
return item;
}
public void Pause(PageDownloadItem item) => item.Op.Pause();
public void Resume(PageDownloadItem item) => item.Op.Resume();
public void Cancel(PageDownloadItem item) => item.Op.Cancel();
/// <summary>UC-D28 페이지 파일 다운로드 전체 일시정지.</summary>
public void PauseAll()
{
foreach (var item in Items.Where(i => i.CanControl).ToList())
try { item.Op.Pause(); } catch { }
}
/// <summary>UC-D28 짝 맞춤: 일시정지 항목 전체 재개(Interrupted 포함).</summary>
public void ResumeAll()
{
foreach (var item in Items.Where(i => !i.IsFinished).ToList())
try { item.Op.Resume(); } catch { }
}
public void ClearCompleted()
{
foreach (var done in Items.Where(i => i.IsFinished).ToList())
Items.Remove(done);
}
}