using System.Collections.ObjectModel; using System.ComponentModel; using System.IO; using System.Windows; using Futonizer.Models; using Futonizer.Services; using Wpf.Ui.Controls; namespace Futonizer.Views; public partial class ProgressWindow : FluentWindow { private const int MaxConcurrentStrips = 5; private readonly IReadOnlyList _files; private readonly string _mkvMergePath; private readonly string _outputFolder; private readonly ObservableCollection _activeJobs = new(); private CancellationTokenSource? _cts; private bool _isDone; /// /// True once processing has finished running (successfully, with failures, /// or cancelled) — i.e. whenever the Cancel button has turned into Close. /// Used by the caller to decide whether to clear its queue after this /// window is closed. /// public bool ProcessingCompleted { get; private set; } public ProgressWindow(IReadOnlyList files, string mkvMergePath, string outputFolder) { Wpf.Ui.Appearance.SystemThemeWatcher.Watch(this); InitializeComponent(); _files = files; _mkvMergePath = mkvMergePath; _outputFolder = outputFolder; Title = $"Stripping {files.Count} file{(files.Count == 1 ? "" : "s")}"; ActiveJobsList.ItemsSource = _activeJobs; } private void Window_Loaded(object sender, RoutedEventArgs e) { _ = StartProcessingAsync(); } private void Window_Closing(object sender, CancelEventArgs e) { _cts?.Cancel(); } private void CancelCloseButton_Click(object sender, RoutedEventArgs e) { if (_isDone) { Close(); return; } _cts?.Cancel(); CancelCloseButton.IsEnabled = false; StatusText.Text = "Cancelling..."; } private void AppendLog(string line) { LogBox.AppendText(line + Environment.NewLine); LogBox.ScrollToEnd(); } private void UpdateTotalSpeed() { if (_activeJobs.Count == 0) { TotalSpeedText.Text = string.Empty; return; } double total = _activeJobs.Sum(j => j.MegabytesPerSecond); TotalSpeedText.Text = _activeJobs.Count == 1 ? $"{total:F1} MB/s" : $"{total:F1} MB/s across {_activeJobs.Count} file(s)"; } private async Task StartProcessingAsync() { StatusText.Text = "Stripping tracks..."; _cts = new CancellationTokenSource(); var ct = _cts.Token; bool overwritingInPlace = string.IsNullOrWhiteSpace(_outputFolder); var service = new MkvService(_mkvMergePath); AppendLog($"Starting track removal for {_files.Count} file(s) (up to {MaxConcurrentStrips} at a time)..."); AppendLog(new string('-', 80)); int successCount = 0; long totalBytesWritten = 0; var stopwatch = System.Diagnostics.Stopwatch.StartNew(); try { using var semaphore = new SemaphoreSlim(MaxConcurrentStrips); var stripTasks = _files.Select(async file => { await semaphore.WaitAsync(ct); var job = new JobProgressItem { FileName = file.FileName }; job.PropertyChanged += (_, args) => { if (args.PropertyName == nameof(JobProgressItem.MegabytesPerSecond)) UpdateTotalSpeed(); }; _activeJobs.Add(job); UpdateTotalSpeed(); try { string outputFilePath = overwritingInPlace ? file.FilePath : Path.Combine(_outputFolder, file.FileName); var videoIds = file.Tracks .Where(t => t.Type == "video" && t.Copy) .Select(t => t.Id).ToList(); var audioIds = file.Tracks .Where(t => t.Type == "audio" && t.Copy) .Select(t => t.Id).ToList(); var subIds = file.Tracks .Where(t => t.Type == "subtitles" && t.Copy) .Select(t => t.Id).ToList(); bool nothingToStrip = file.Tracks.All(t => t.Copy); var progress = new Progress(p => { job.Percent = p.PercentComplete; job.MegabytesPerSecond = p.MegabytesPerSecond; }); var result = nothingToStrip ? await service.CopyFileAsync(file.FilePath, outputFilePath, progress, ct) : await service.StripTracksAsync( file.FilePath, outputFilePath, videoIds, audioIds, subIds, progress, ct); string tag = result.Success ? "[DONE]" : "[FAIL]"; AppendLog($"{tag} {result.FileName} — {result.Message}"); HistoryService.Add(new HistoryEntry { Timestamp = DateTime.Now, FileName = result.FileName, FilePath = file.FilePath, Success = result.Success, Message = result.Message, }); if (result.Success) { successCount++; try { long bytes = new FileInfo(outputFilePath).Length; Interlocked.Add(ref totalBytesWritten, bytes); } catch { /* best effort */ } } } finally { _activeJobs.Remove(job); UpdateTotalSpeed(); semaphore.Release(); } }).ToList(); await Task.WhenAll(stripTasks); AppendLog(new string('-', 80)); AppendLog($"Processing complete: {successCount}/{_files.Count} file(s) succeeded."); stopwatch.Stop(); double totalSec = stopwatch.Elapsed.TotalSeconds; double totalMb = totalBytesWritten / 1024.0 / 1024.0; double avgMbps = totalSec > 0 ? totalMb / totalSec : 0; AppendLog($"Total data written: {totalMb / 1024.0:F2} GB in {totalSec:F1}s (avg {avgMbps:F1} MB/s combined)."); StatusText.Text = $"Done: {successCount}/{_files.Count} succeeded."; } catch (OperationCanceledException) { AppendLog(new string('-', 80)); AppendLog("Cancelled by user. Nothing further was modified."); StatusText.Text = "Cancelled."; } catch (Exception ex) { AppendLog($"Unexpected error: {ex.Message}"); StatusText.Text = "Error."; } finally { _activeJobs.Clear(); TotalSpeedText.Text = string.Empty; _cts?.Dispose(); _cts = null; _isDone = true; ProcessingCompleted = true; CancelCloseButton.IsEnabled = true; CancelCloseButton.Content = "Close"; } } }