using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.IO; using System.Runtime.CompilerServices; using Futonizer.Services; namespace Futonizer.Models; /// /// The four states shown as an icon in the file queue list. /// public enum FileLoadState { /// Currently being scanned with mkvmerge -J. Loading, /// Scan finished (or failed) but track selection isn't evaluated as correct/incorrect yet. Loaded, /// Exactly one audio track and one subtitle track are selected to be kept. Correct, /// Anything other than exactly one audio + one subtitle selected. Incorrect, } /// /// Represents one MKV file in the processing queue. /// public class QueuedFileItem : INotifyPropertyChanged { private string _status = string.Empty; private bool _isScanning; private bool _hasError; public string FilePath { get; } public string FileName => Path.GetFileName(FilePath); /// /// The filename as it will appear after rename adjustments (release-tag prefix /// and/or show-name injection). Equals when no /// changes would be applied. /// public string DisplayFileName => FileNameHelper.ComputeOutputFileName(FilePath); public string FileSizeDisplay { get; } public ObservableCollection Tracks { get; } = new(); /// /// External subtitle files (dropped separately and matched to this file by /// episode — e.g. "S04E06", or just a bare episode number like "06") that /// will be muxed in as additional subtitle tracks when this file is /// processed. /// public ObservableCollection ExternalSubtitles { get; } = new(); public bool HasExternalSubtitles => ExternalSubtitles.Count > 0; public string ExternalSubtitlesTooltip => ExternalSubtitles.Count == 0 ? string.Empty : "External subtitles to mux in:\n" + string.Join("\n", ExternalSubtitles.Select(s => $"- {s.FileName} ({s.LanguageDisplay})")); public string Status { get => _status; set { _status = value; OnPropertyChanged(); } } public bool IsScanning { get => _isScanning; set { _isScanning = value; OnPropertyChanged(); NotifyLoadState(); } } public bool HasError { get => _hasError; set { _hasError = value; OnPropertyChanged(); NotifyLoadState(); } } public FileLoadState LoadState { get { if (_isScanning) return FileLoadState.Loading; if (_hasError || Tracks.Count == 0) return FileLoadState.Loaded; int audioSelected = Tracks.Count(t => t.Type == "audio" && t.Copy); int subtitleSelected = Tracks.Count(t => t.Type == "subtitles" && t.Copy); // A subtitle requirement is met either by picking exactly one // internal track, or by having at least one matched external // subtitle file (which can also be combined with one internal track). bool subtitleOk = subtitleSelected == 1 || (subtitleSelected == 0 && ExternalSubtitles.Count > 0); return audioSelected == 1 && subtitleOk ? FileLoadState.Correct : FileLoadState.Incorrect; } } public bool IsLoading => LoadState == FileLoadState.Loading; public bool IsLoadedNeutral => LoadState == FileLoadState.Loaded; public bool IsCorrect => LoadState == FileLoadState.Correct; public bool IsIncorrect => LoadState == FileLoadState.Incorrect; public QueuedFileItem(string filePath) { FilePath = filePath; FileSizeDisplay = FormatFileSize(filePath); Tracks.CollectionChanged += OnTracksChanged; ExternalSubtitles.CollectionChanged += OnExternalSubtitlesChanged; } private static string FormatFileSize(string filePath) { try { long bytes = new FileInfo(filePath).Length; string[] units = { "B", "KB", "MB", "GB", "TB" }; double size = bytes; int unitIndex = 0; while (size >= 1024 && unitIndex < units.Length - 1) { size /= 1024; unitIndex++; } return unitIndex == 0 ? $"{size:0} {units[unitIndex]}" : $"{size:0.#} {units[unitIndex]}"; } catch { return string.Empty; } } private void OnExternalSubtitlesChanged(object? sender, NotifyCollectionChangedEventArgs e) { OnPropertyChanged(nameof(HasExternalSubtitles)); OnPropertyChanged(nameof(ExternalSubtitlesTooltip)); NotifyLoadState(); } private void OnTracksChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (e.NewItems != null) { foreach (TrackItem t in e.NewItems) t.PropertyChanged += TrackPropertyChanged; } if (e.OldItems != null) { foreach (TrackItem t in e.OldItems) t.PropertyChanged -= TrackPropertyChanged; } NotifyLoadState(); } private void TrackPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(TrackItem.Copy)) NotifyLoadState(); } private void NotifyLoadState() { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(LoadState))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsLoading))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsLoadedNeutral))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsCorrect))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsIncorrect))); } public event PropertyChangedEventHandler? PropertyChanged; private void OnPropertyChanged([CallerMemberName] string? name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); }