Phase B (T1-5/T1-6/T2-2/T2-8/T2-12): download manager flyout w/ speed+pause/resume/cancel, bookmark bar, tab sleeping, tab search (open+closed), context menu w/ 'download video from this link' (79 tests GREEN)

This commit is contained in:
Yun Chan 2026-08-15 15:09:59 +09:00
parent af19dba8f4
commit 0caa1111e7
9 changed files with 763 additions and 3 deletions

View file

@ -0,0 +1,108 @@
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();
public void ClearCompleted()
{
foreach (var done in Items.Where(i => i.IsFinished).ToList())
Items.Remove(done);
}
}