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:
+52
-33
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user