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:
+17
-11
@@ -74,8 +74,8 @@ public class MkvService
|
||||
// fill up and deadlock the child process indefinitely.
|
||||
var stdoutTask = process.StandardOutput.ReadToEndAsync(ct);
|
||||
var stderrTask = process.StandardError.ReadToEndAsync(ct);
|
||||
await Task.WhenAll(stdoutTask, stderrTask);
|
||||
await process.WaitForExitAsync(ct);
|
||||
await Task.WhenAll(stdoutTask, stderrTask).ConfigureAwait(false);
|
||||
await process.WaitForExitAsync(ct).ConfigureAwait(false);
|
||||
|
||||
string stdout = stdoutTask.Result;
|
||||
string stderr = stderrTask.Result;
|
||||
@@ -151,9 +151,15 @@ public class MkvService
|
||||
});
|
||||
}
|
||||
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
items[i].IsGroupStart = i > 0 && items[i].Type != items[i - 1].Type;
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Scans a single file and determines which audio/subtitle track IDs
|
||||
/// should be kept. Fails if only English audio is present, or if the
|
||||
@@ -164,7 +170,7 @@ public class MkvService
|
||||
var scan = new ScanResult { FilePath = filePath };
|
||||
try
|
||||
{
|
||||
var info = await IdentifyAsync(filePath, ct);
|
||||
var info = await IdentifyAsync(filePath, ct).ConfigureAwait(false);
|
||||
|
||||
var audioTracks = info.Tracks.Where(t => t.Type == "audio").ToList();
|
||||
var subtitleTracks = info.Tracks.Where(t => t.Type == "subtitles").ToList();
|
||||
@@ -325,9 +331,9 @@ public class MkvService
|
||||
{
|
||||
byte[] buffer = new byte[1024 * 1024];
|
||||
int read;
|
||||
while ((read = await source.ReadAsync(buffer, ct)) > 0)
|
||||
while ((read = await source.ReadAsync(buffer, ct).ConfigureAwait(false)) > 0)
|
||||
{
|
||||
await dest.WriteAsync(buffer.AsMemory(0, read), ct);
|
||||
await dest.WriteAsync(buffer.AsMemory(0, read), ct).ConfigureAwait(false);
|
||||
copied += read;
|
||||
|
||||
var elapsed = stopwatch.Elapsed;
|
||||
@@ -450,11 +456,11 @@ public class MkvService
|
||||
using var throughputCts = new CancellationTokenSource();
|
||||
var throughputTask = MonitorThroughputAsync(tempFile, fileName, percentState, progress, throughputCts.Token);
|
||||
|
||||
await process.WaitForExitAsync(CancellationToken.None);
|
||||
await process.WaitForExitAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
throughputCts.Cancel();
|
||||
string stdout = await stdoutTask;
|
||||
string stderr = await stderrTask;
|
||||
try { await throughputTask; } catch (OperationCanceledException) { }
|
||||
string stdout = await stdoutTask.ConfigureAwait(false);
|
||||
string stderr = await stderrTask.ConfigureAwait(false);
|
||||
try { await throughputTask.ConfigureAwait(false); } catch (OperationCanceledException) { }
|
||||
|
||||
if (wasKilled)
|
||||
{
|
||||
@@ -543,7 +549,7 @@ public class MkvService
|
||||
{
|
||||
var fullOutput = new System.Text.StringBuilder();
|
||||
string? line;
|
||||
while ((line = await process.StandardOutput.ReadLineAsync()) != null)
|
||||
while ((line = await process.StandardOutput.ReadLineAsync().ConfigureAwait(false)) != null)
|
||||
{
|
||||
fullOutput.AppendLine(line);
|
||||
var match = ProgressRegex.Match(line);
|
||||
@@ -565,7 +571,7 @@ public class MkvService
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(sampleInterval, stopToken);
|
||||
await Task.Delay(sampleInterval, stopToken).ConfigureAwait(false);
|
||||
|
||||
long currentBytes = 0;
|
||||
try { currentBytes = new FileInfo(tempFile).Length; } catch { }
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Futonizer.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Fires native Windows notifications (via a transient tray icon's balloon
|
||||
/// tip) to let the user know processing has finished, even if the app
|
||||
/// window isn't focused.
|
||||
/// </summary>
|
||||
public static class NotificationService
|
||||
{
|
||||
public static void ShowToast(string title, string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
Icon icon;
|
||||
try
|
||||
{
|
||||
icon = System.Drawing.Icon.ExtractAssociatedIcon(Environment.ProcessPath!) ?? SystemIcons.Application;
|
||||
}
|
||||
catch
|
||||
{
|
||||
icon = SystemIcons.Application;
|
||||
}
|
||||
|
||||
var notifyIcon = new NotifyIcon
|
||||
{
|
||||
Icon = icon,
|
||||
Visible = true,
|
||||
BalloonTipTitle = title,
|
||||
BalloonTipText = message,
|
||||
BalloonTipIcon = ToolTipIcon.Info,
|
||||
};
|
||||
|
||||
notifyIcon.BalloonTipClosed += (_, _) => notifyIcon.Dispose();
|
||||
notifyIcon.BalloonTipClicked += (_, _) => notifyIcon.Dispose();
|
||||
notifyIcon.ShowBalloonTip(5000);
|
||||
|
||||
// Fallback in case neither event fires (e.g. notifications disabled).
|
||||
_ = Task.Delay(10000).ContinueWith(_ => notifyIcon.Dispose());
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Notifications are best-effort; never let a failure here affect the app.
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user