Add overall progress, notifications, and UI polish
- Unify audio/subtitle selection: single-select, auto-default, cross-file propagation by exact name - Show file size in queue, group divider and reordered columns in track table, drop accent highlight on row selection - Add cumulative progress bar (files finished, MB copied) to the stripping window - Add Windows toast notifications on stripping completion/cancel/error - Restart the app after settings are changed; Cancel no longer clears the queue, only closes the window - Add ConfigureAwait(false) throughout MkvService so concurrent strips don't starve the UI thread - Hardcode scan/strip concurrency (10/5) instead of exposing it in Settings
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Active jobs -->
|
||||
@@ -53,8 +54,25 @@
|
||||
BorderThickness="0" Background="Transparent"/>
|
||||
</Border>
|
||||
|
||||
<!-- Status + speed + button -->
|
||||
<!-- Total progress -->
|
||||
<Grid Grid.Row="2" Margin="0,10,0,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<ProgressBar x:Name="TotalProgressBar" Grid.Row="0" Height="8" Minimum="0" Maximum="100" Value="0"/>
|
||||
<Grid Grid.Row="1" Margin="0,4,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock x:Name="TotalFilesText" Grid.Column="0" FontSize="11" Opacity="0.75"/>
|
||||
<TextBlock x:Name="TotalBytesText" Grid.Column="1" FontSize="11" Opacity="0.75"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<!-- Status + speed + button -->
|
||||
<Grid Grid.Row="3" Margin="0,10,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Futonizer.Views;
|
||||
|
||||
public partial class ProgressWindow : FluentWindow
|
||||
{
|
||||
private const int MaxConcurrentStrips = 5;
|
||||
private readonly int _maxConcurrentStrips;
|
||||
|
||||
private readonly IReadOnlyList<QueuedFileItem> _files;
|
||||
private readonly string _mkvMergePath;
|
||||
@@ -19,6 +19,9 @@ public partial class ProgressWindow : FluentWindow
|
||||
|
||||
private CancellationTokenSource? _cts;
|
||||
private bool _isDone;
|
||||
private long _totalInputBytes;
|
||||
private long _completedBytes;
|
||||
private int _finishedCount;
|
||||
|
||||
/// <summary>
|
||||
/// True once processing has finished running (successfully, with failures,
|
||||
@@ -28,7 +31,15 @@ public partial class ProgressWindow : FluentWindow
|
||||
/// </summary>
|
||||
public bool ProcessingCompleted { get; private set; }
|
||||
|
||||
public ProgressWindow(IReadOnlyList<QueuedFileItem> files, string mkvMergePath, string outputFolder)
|
||||
/// <summary>
|
||||
/// True only if processing ran through to a normal finish (whether
|
||||
/// individual files succeeded or failed). False if the run was cancelled
|
||||
/// or crashed with an unexpected error — in either of those cases the
|
||||
/// caller should leave its queue untouched rather than clearing it.
|
||||
/// </summary>
|
||||
public bool ShouldClearQueue { get; private set; }
|
||||
|
||||
public ProgressWindow(IReadOnlyList<QueuedFileItem> files, string mkvMergePath, string outputFolder, int maxConcurrentStrips = 5)
|
||||
{
|
||||
Wpf.Ui.Appearance.SystemThemeWatcher.Watch(this);
|
||||
InitializeComponent();
|
||||
@@ -36,6 +47,7 @@ public partial class ProgressWindow : FluentWindow
|
||||
_files = files;
|
||||
_mkvMergePath = mkvMergePath;
|
||||
_outputFolder = outputFolder;
|
||||
_maxConcurrentStrips = maxConcurrentStrips > 0 ? maxConcurrentStrips : 5;
|
||||
|
||||
Title = $"Stripping {files.Count} file{(files.Count == 1 ? "" : "s")}";
|
||||
ActiveJobsList.ItemsSource = _activeJobs;
|
||||
@@ -82,6 +94,28 @@ public partial class ProgressWindow : FluentWindow
|
||||
: $"{total:F1} MB/s across {_activeJobs.Count} file(s)";
|
||||
}
|
||||
|
||||
private void UpdateTotalProgress()
|
||||
{
|
||||
long activeBytes = _activeJobs.Sum(j => (long)(j.FileSizeBytes * (j.Percent / 100.0)));
|
||||
long doneBytes = Math.Min(_totalInputBytes, _completedBytes + activeBytes);
|
||||
|
||||
TotalProgressBar.Value = _totalInputBytes > 0
|
||||
? doneBytes * 100.0 / _totalInputBytes
|
||||
: 0;
|
||||
|
||||
TotalFilesText.Text = $"{_finishedCount}/{_files.Count} file{(_files.Count == 1 ? "" : "s")} finished";
|
||||
|
||||
double doneMb = doneBytes / 1024.0 / 1024.0;
|
||||
double totalMb = _totalInputBytes / 1024.0 / 1024.0;
|
||||
TotalBytesText.Text = $"{doneMb:F0} / {totalMb:F0} MB copied";
|
||||
}
|
||||
|
||||
private static long SafeFileLength(string path)
|
||||
{
|
||||
try { return new FileInfo(path).Length; }
|
||||
catch { return 0; }
|
||||
}
|
||||
|
||||
private async Task StartProcessingAsync()
|
||||
{
|
||||
StatusText.Text = "Stripping tracks...";
|
||||
@@ -91,26 +125,32 @@ public partial class ProgressWindow : FluentWindow
|
||||
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($"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();
|
||||
_totalInputBytes = _files.Sum(f => SafeFileLength(f.FilePath));
|
||||
_completedBytes = 0;
|
||||
_finishedCount = 0;
|
||||
UpdateTotalProgress();
|
||||
|
||||
try
|
||||
{
|
||||
using var semaphore = new SemaphoreSlim(MaxConcurrentStrips);
|
||||
using var semaphore = new SemaphoreSlim(_maxConcurrentStrips);
|
||||
|
||||
var stripTasks = _files.Select(async file =>
|
||||
{
|
||||
await semaphore.WaitAsync(ct);
|
||||
|
||||
var job = new JobProgressItem { FileName = file.FileName };
|
||||
var job = new JobProgressItem { FileName = file.FileName, FileSizeBytes = SafeFileLength(file.FilePath) };
|
||||
job.PropertyChanged += (_, args) =>
|
||||
{
|
||||
if (args.PropertyName == nameof(JobProgressItem.MegabytesPerSecond))
|
||||
UpdateTotalSpeed();
|
||||
if (args.PropertyName == nameof(JobProgressItem.Percent))
|
||||
UpdateTotalProgress();
|
||||
};
|
||||
_activeJobs.Add(job);
|
||||
UpdateTotalSpeed();
|
||||
@@ -172,7 +212,10 @@ public partial class ProgressWindow : FluentWindow
|
||||
finally
|
||||
{
|
||||
_activeJobs.Remove(job);
|
||||
Interlocked.Add(ref _completedBytes, job.FileSizeBytes);
|
||||
Interlocked.Increment(ref _finishedCount);
|
||||
UpdateTotalSpeed();
|
||||
UpdateTotalProgress();
|
||||
semaphore.Release();
|
||||
}
|
||||
}).ToList();
|
||||
@@ -189,17 +232,21 @@ public partial class ProgressWindow : FluentWindow
|
||||
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.";
|
||||
NotificationService.ShowToast("Futonizer", $"Done: {successCount}/{_files.Count} file{(_files.Count == 1 ? "" : "s")} succeeded.");
|
||||
ShouldClearQueue = true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
AppendLog(new string('-', 80));
|
||||
AppendLog("Cancelled by user. Nothing further was modified.");
|
||||
StatusText.Text = "Cancelled.";
|
||||
NotificationService.ShowToast("Futonizer", "Cancelled — nothing further was modified.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppendLog($"Unexpected error: {ex.Message}");
|
||||
StatusText.Text = "Error.";
|
||||
NotificationService.ShowToast("Futonizer", $"Error: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
@@ -47,6 +47,7 @@ public partial class SettingsWindow : FluentWindow
|
||||
DialogResult = true;
|
||||
}
|
||||
|
||||
|
||||
private void CancelButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DialogResult = false;
|
||||
|
||||
Reference in New Issue
Block a user