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:
l4kr
2026-07-04 02:35:26 +02:00
parent 9b1bd51bd3
commit 05c496c10b
12 changed files with 272 additions and 68 deletions
+52 -33
View File
@@ -15,13 +15,14 @@ namespace Futonizer;
public partial class MainWindow : FluentWindow
{
private const int MaxConcurrentScans = 4;
private const int MaxConcurrentScans = 10;
private const int MaxConcurrentStrips = 5;
private AppSettings _settings = new();
private readonly ObservableCollection<QueuedFileItem> _fileQueue = new();
private readonly SemaphoreSlim _scanSemaphore = new(MaxConcurrentScans);
private bool _isProcessing;
private bool _propagatingSubtitleSelection;
private bool _propagatingSelection;
public MainWindow()
{
@@ -76,6 +77,20 @@ public partial class MainWindow : FluentWindow
// ── Helpers ──────────────────────────────────────────────────────────────
private static void RestartApplication()
{
try
{
string? exePath = Environment.ProcessPath;
if (!string.IsNullOrEmpty(exePath))
Process.Start(exePath);
}
catch
{
}
System.Windows.Application.Current.Shutdown();
}
private void UpdateDropHint()
{
bool empty = _fileQueue.Count == 0;
@@ -111,11 +126,11 @@ public partial class MainWindow : FluentWindow
private void UpdateRunButtonEnabled()
{
bool anyLoading = _fileQueue.Any(f => f.IsLoading);
bool anySubtitleSelected = _fileQueue.Any(f => f.Tracks.Any(t => t.Type == "subtitles" && t.Copy));
RunButton.IsEnabled = !_isProcessing && !anyLoading && _fileQueue.Count > 0 && anySubtitleSelected;
RunButton.ToolTip = anySubtitleSelected
bool allCorrect = _fileQueue.Count > 0 && _fileQueue.All(f => f.IsCorrect);
RunButton.IsEnabled = !_isProcessing && !anyLoading && allCorrect;
RunButton.ToolTip = allCorrect
? null
: "Select a subtitle track in the track table to enable stripping.";
: "Every file in the queue needs exactly one audio and one subtitle track selected before stripping.";
}
private void QueuedFileItem_PropertyChanged(object? sender, PropertyChangedEventArgs e)
@@ -161,6 +176,7 @@ public partial class MainWindow : FluentWindow
_settings.MkvMergePath = win.Result.MkvMergePath;
_settings.OutputFolder = win.Result.OutputFolder;
SettingsService.Save(_settings);
RestartApplication();
}
}
@@ -240,9 +256,9 @@ public partial class MainWindow : FluentWindow
foreach (var t in tracks)
{
item.Tracks.Add(t);
if (t.Type == "subtitles")
if (t.Type is "audio" or "subtitles")
{
t.PropertyChanged += (s, e) => OnSubtitleTrackPropertyChanged(item, (TrackItem)s!, e);
t.PropertyChanged += (s, e) => OnTrackSelectionChanged(item, (TrackItem)s!, e);
}
}
@@ -261,36 +277,39 @@ public partial class MainWindow : FluentWindow
}
}
// ── Subtitle selection propagation ─────────────────────────────────────────
// ── Audio/subtitle selection propagation ────────────────────────────────────
/// <summary>
/// Propagates subtitle selection/deselection to every other file in the
/// queue:
/// - Checking a subtitle matches it to other files by its exact
/// Propagates audio/subtitle selection/deselection to every other file in
/// the queue:
/// - Checking a track matches it to other files by its exact
/// "Codec/Name" identity:
/// - If the source file had no other subtitle selected before this
/// check ("adding"), other files that don't have a matching track
/// are left untouched.
/// - If the source file had a different subtitle selected before
/// this check ("overriding"), other files that don't have a
/// matching track have their subtitle selection cleared.
/// - Unchecking a subtitle clears the subtitle selection in every other
/// file too, so a deselection always applies everywhere.
/// Within the source file, only one subtitle may ever be selected.
/// - If the source file had no other track of the same type
/// selected before this check ("adding"), other files that don't
/// have a matching track are left untouched.
/// - If the source file had a different track of the same type
/// selected before this check ("overriding"), other files that
/// don't have a matching track have their selection cleared.
/// - Unchecking a track clears the selection of that type in every
/// other file too, so a deselection always applies everywhere.
/// Within the source file, only one track of a given type may ever be
/// selected.
/// </summary>
private void OnSubtitleTrackPropertyChanged(QueuedFileItem sourceFile, TrackItem track, PropertyChangedEventArgs e)
private void OnTrackSelectionChanged(QueuedFileItem sourceFile, TrackItem track, PropertyChangedEventArgs e)
{
if (_propagatingSubtitleSelection) return;
if (_propagatingSelection) return;
if (e.PropertyName != nameof(TrackItem.Copy)) return;
_propagatingSubtitleSelection = true;
string type = track.Type;
_propagatingSelection = true;
try
{
if (track.Copy)
{
bool wasOverriding = sourceFile.Tracks.Any(t => t.Type == "subtitles" && t != track && t.Copy);
bool wasOverriding = sourceFile.Tracks.Any(t => t.Type == type && t != track && t.Copy);
foreach (var other in sourceFile.Tracks.Where(t => t.Type == "subtitles" && t != track))
foreach (var other in sourceFile.Tracks.Where(t => t.Type == type && t != track))
{
other.Copy = false;
other.DefaultTrack = false;
@@ -301,10 +320,10 @@ public partial class MainWindow : FluentWindow
foreach (var otherFile in _fileQueue.Where(f => f != sourceFile))
{
var match = otherFile.Tracks.FirstOrDefault(t => t.Type == "subtitles" && t.PropagationKey == key);
var match = otherFile.Tracks.FirstOrDefault(t => t.Type == type && t.PropagationKey == key);
if (match != null)
{
foreach (var sub in otherFile.Tracks.Where(t => t.Type == "subtitles"))
foreach (var sub in otherFile.Tracks.Where(t => t.Type == type))
{
sub.Copy = sub == match;
sub.DefaultTrack = sub == match;
@@ -312,7 +331,7 @@ public partial class MainWindow : FluentWindow
}
else if (wasOverriding)
{
foreach (var sub in otherFile.Tracks.Where(t => t.Type == "subtitles"))
foreach (var sub in otherFile.Tracks.Where(t => t.Type == type))
{
sub.Copy = false;
sub.DefaultTrack = false;
@@ -325,7 +344,7 @@ public partial class MainWindow : FluentWindow
track.DefaultTrack = false;
foreach (var otherFile in _fileQueue.Where(f => f != sourceFile))
{
foreach (var sub in otherFile.Tracks.Where(t => t.Type == "subtitles"))
foreach (var sub in otherFile.Tracks.Where(t => t.Type == type))
{
sub.Copy = false;
sub.DefaultTrack = false;
@@ -335,7 +354,7 @@ public partial class MainWindow : FluentWindow
}
finally
{
_propagatingSubtitleSelection = false;
_propagatingSelection = false;
}
}
@@ -475,14 +494,14 @@ public partial class MainWindow : FluentWindow
RunButton.IsEnabled = false;
StatusText.Text = string.Empty;
var progressWin = new ProgressWindow(readyFiles, _settings.MkvMergePath, _settings.OutputFolder)
var progressWin = new ProgressWindow(readyFiles, _settings.MkvMergePath, _settings.OutputFolder, MaxConcurrentStrips)
{
Owner = this
};
progressWin.Closed += (_, _) =>
{
_isProcessing = false;
if (progressWin.ProcessingCompleted)
if (progressWin.ShouldClearQueue)
{
foreach (var item in _fileQueue)
item.PropertyChanged -= QueuedFileItem_PropertyChanged;