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));
}
}