using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; using VideoDownloader.Core.Detection; using VideoDownloader.Core.Social; namespace VideoDownloader.App.ViewModels; public class DownloadItemViewModel : INotifyPropertyChanged { private string _status = "대기"; private int _progress; private bool _isIndeterminate; private string _segments = ""; private string? _outputPath; private string? _error; private FormatOption? _selectedFormat; private bool _isFetchingFormats; private string _title; public string Title { get => _title; set => SetField(ref _title, value); } /// 항목 고유 ID(원격 API 노출용). public string Id { get; } = Guid.NewGuid().ToString("N"); /// 미디어 URL(원격 API 노출용). public string? Url => MediaUrl?.ToString(); public string FormatTag { get; init; } = "FILE"; // 다운로드 실행에 필요한 컨텍스트 (감지 시 고정) public MediaKind Kind { get; init; } public Uri MediaUrl { get; init; } = null!; public string OutputBase { get; set; } = ""; public string? PageUrl { get; init; } public string? ContentType { get; init; } // 소셜(YouTube/Instagram/X) 카드: 화질/형식 선택 public bool IsSocial => Kind == MediaKind.Social; public ObservableCollection Formats { get; } = new(); public FormatOption? SelectedFormat { get => _selectedFormat; set => SetField(ref _selectedFormat, value); } public bool IsFetchingFormats { get => _isFetchingFormats; set => SetField(ref _isFetchingFormats, value); } public CancellationTokenSource? Cts { get; set; } public bool Started { get; set; } /// §E 일시정지 상태(재개 대기) — 임시 파일 보존·"일시정지" 상태 표시 판단에 사용. public bool PausedForResume { get; set; } /// UC-D30 사이트별 묶음 — 출처 페이지 호스트(없으면 미디어 URL 호스트). public string SiteName { get { var source = PageUrl ?? Url ?? ""; return Uri.TryCreate(source, UriKind.Absolute, out var u) ? u.Host : "직접 링크"; } } public string Status { get => _status; set => SetField(ref _status, value); } public int Progress { get => _progress; set => SetField(ref _progress, value); } // #9 직링크 길이 미상 시 진행률 바를 부정확(애니메이션) 모드로 public bool IsIndeterminate { get => _isIndeterminate; set => SetField(ref _isIndeterminate, value); } public string Segments { get => _segments; set => SetField(ref _segments, value); } public string? OutputPath { get => _outputPath; set => SetField(ref _outputPath, value); } public string? Error { get => _error; set => SetField(ref _error, value); } // ---- §E 다운로드 관리자 확장 (docs/UI_USECASES.md) ---- /// UC-D05 현재 속도 표시("2.4 MB/s" 등). DownloadManager 진행 콜백에서 갱신. public string SpeedText { get => _speedText; set => SetField(ref _speedText, value); } private string _speedText = ""; /// UC-D04 남은 시간 추정("3분 남음" 등). 속도·진행률로 계산. public string RemainingText { get => _remainingText; set => SetField(ref _remainingText, value); } private string _remainingText = ""; /// UC-D19 파일 총 크기 표시("142.5 MB" 등). 길이 미상이면 "". public string TotalSizeText { get => _totalSizeText; set => SetField(ref _totalSizeText, value); } private string _totalSizeText = ""; /// UC-D09 재시도 횟수. public int RetryCount { get => _retryCount; set => SetField(ref _retryCount, value); } private int _retryCount; /// UC-D10 세그먼트별 진행 맵 — 각 세그먼트 진행률(0~1) 목록을 순서대로 유지. public System.Collections.ObjectModel.ObservableCollection SegmentMap { get; } = new(); /// UC-D24 대기열 우선순위(클수록 먼저). 기본 0. public int Priority { get => _priority; set => SetField(ref _priority, value); } private int _priority; /// UC-D26 충돌 회피 후 최종 저장 파일명(OutputPath 확정 시 갱신). public string? FinalFileName { get => _finalFileName; set => SetField(ref _finalFileName, value); } private string? _finalFileName; /// 수신 바이트 총량(속도·ETA 계산 원천). 진행 콜백에서 누적. public long ReceivedBytes { get => _receivedBytes; set => SetField(ref _receivedBytes, value); } private long _receivedBytes; /// 총 바이트(길이 미상 -1). TotalSizeText 와 ETA 계산에 사용. public long TotalBytes { get => _totalBytes; set => SetField(ref _totalBytes, value); } private long _totalBytes = -1; /// UC-D08 완료 시각(자동 치움 지연 판정). 미완료 null. public DateTime? CompletedAt { get => _completedAt; set => SetField(ref _completedAt, value); } private DateTime? _completedAt; /// UC-D05 부속: 진행률·수신량으로 속도/ETA 텍스트를 갱신한다. speedBps ≤ 0 이면 속도만 지운다. public void UpdateSpeedEta(long receivedBytes, long totalBytes, double speedBps) { ReceivedBytes = receivedBytes; if (totalBytes > 0) TotalBytes = totalBytes; SpeedText = speedBps <= 0 ? "" : speedBps < 1024 ? $"{speedBps:F0} B/s" : speedBps < 1024 * 1024 ? $"{speedBps / 1024:F1} KB/s" : $"{speedBps / 1024 / 1024:F1} MB/s"; if (TotalBytes > 0 && receivedBytes >= 0) TotalSizeText = $"{TotalBytes / 1048576.0:F1} MB"; RemainingText = speedBps > 0 && TotalBytes > receivedBytes && receivedBytes > 0 ? FormatEta(TimeSpan.FromSeconds((TotalBytes - receivedBytes) / speedBps)) : ""; } /// ETA 사람 표기(1시간 미만 분 단위, 이상 시 시간 단위). public static string FormatEta(TimeSpan eta) => eta.TotalHours >= 1 ? $"{(int)eta.TotalHours}시간 {eta.Minutes}분 남음" : eta.TotalMinutes >= 1 ? $"{(int)Math.Ceiling(eta.TotalMinutes)}분 남음" : $"{(int)Math.Ceiling(eta.TotalSeconds)}초 남음"; public DownloadItemViewModel(string title) { _title = title; } public event PropertyChangedEventHandler? PropertyChanged; private void SetField(ref T field, T value, [CallerMemberName] string? name = null) { if (Equals(field, value)) return; field = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } }