Drag in subtitle files alongside .mkv files and they're matched to the right video by episode code (S04E06, zero-padding and separator insensitive, plus 4x06 as a fallback) and muxed in as extra subtitle tracks. Any subtitle format mkvmerge accepts is recognized: SRT, (S)SA, VobSub, WebVTT, PGS/SUP, USF, SAMI, TTML/DFXP, STL, Kate. Matching works regardless of drop order — subtitles dropped before their video are held pending until a matching video shows up, and vice versa. Language, forced, and SDH flags are guessed from common filename tokens (e.g. "eng", "forced", "sdh") to set the muxed track's language/name/flags; unmatched subtitles are reported in the status bar.
670 lines
24 KiB
C#
670 lines
24 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Threading;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Input;
|
|
using Futonizer.Models;
|
|
using Futonizer.Services;
|
|
using Futonizer.Views;
|
|
using Wpf.Ui.Controls;
|
|
|
|
namespace Futonizer;
|
|
|
|
public partial class MainWindow : FluentWindow
|
|
{
|
|
private const int MaxConcurrentScans = 10;
|
|
private const int MaxConcurrentStrips = 3;
|
|
|
|
private AppSettings _settings = new();
|
|
private readonly ObservableCollection<QueuedFileItem> _fileQueue = new();
|
|
private readonly List<string> _pendingSubtitles = new();
|
|
private readonly SemaphoreSlim _scanSemaphore = new(MaxConcurrentScans);
|
|
private bool _isProcessing;
|
|
private bool _propagatingSelection;
|
|
|
|
public MainWindow()
|
|
{
|
|
Wpf.Ui.Appearance.SystemThemeWatcher.Watch(this);
|
|
InitializeComponent();
|
|
|
|
FileQueueList.ItemsSource = _fileQueue;
|
|
|
|
_fileQueue.CollectionChanged += (_, _) => UpdateDropHint();
|
|
|
|
LoadSettingsIntoUi();
|
|
Closing += (_, _) => SaveSettingsFromUi();
|
|
Loaded += MainWindow_Loaded;
|
|
}
|
|
|
|
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
|
|
{
|
|
EnsureRequiredSettingsConfigured();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Warns the user and opens Settings if mkvmerge.exe isn't configured (or
|
|
/// no longer exists), or if a previously chosen output folder has since
|
|
/// disappeared. Runs once on startup; does nothing once everything is
|
|
/// already valid.
|
|
/// </summary>
|
|
private void EnsureRequiredSettingsConfigured()
|
|
{
|
|
bool mkvMissing = string.IsNullOrWhiteSpace(_settings.MkvMergePath) || !File.Exists(_settings.MkvMergePath);
|
|
bool outputInvalid = !string.IsNullOrWhiteSpace(_settings.OutputFolder) && !Directory.Exists(_settings.OutputFolder);
|
|
|
|
if (!mkvMissing && !outputInvalid) return;
|
|
|
|
string message = mkvMissing
|
|
? "mkvmerge.exe could not be found. Please locate it to enable stripping tracks."
|
|
: "The previously configured output folder no longer exists. Please choose a new one, or leave it blank to overwrite files in place.";
|
|
|
|
System.Windows.MessageBox.Show(this, message, "Futonizer — Setup required",
|
|
System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Warning);
|
|
|
|
if (outputInvalid)
|
|
_settings.OutputFolder = string.Empty;
|
|
|
|
var win = new SettingsWindow(_settings) { Owner = this };
|
|
if (win.ShowDialog() == true && win.Result != null)
|
|
{
|
|
_settings.MkvMergePath = win.Result.MkvMergePath;
|
|
_settings.OutputFolder = win.Result.OutputFolder;
|
|
SettingsService.Save(_settings);
|
|
}
|
|
}
|
|
|
|
// ── 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;
|
|
DropHint.Visibility = empty ? Visibility.Visible : Visibility.Collapsed;
|
|
ClearQueueButton.IsEnabled = !empty;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows a blocking overlay with a simple "loaded / total" progress bar
|
|
/// while any queued file is still being scanned, and keeps the Run button
|
|
/// disabled until every dropped file has finished loading.
|
|
/// </summary>
|
|
private void UpdateLoadingOverlay()
|
|
{
|
|
int total = _fileQueue.Count;
|
|
int loaded = _fileQueue.Count(f => !f.IsLoading);
|
|
|
|
if (total > 0 && loaded < total)
|
|
{
|
|
LoadingOverlay.Visibility = Visibility.Visible;
|
|
LoadingProgressBar.Maximum = total;
|
|
LoadingProgressBar.Value = loaded;
|
|
LoadingOverlayText.Text = $"Loading files... ({loaded}/{total})";
|
|
}
|
|
else
|
|
{
|
|
LoadingOverlay.Visibility = Visibility.Collapsed;
|
|
}
|
|
|
|
UpdateRunButtonEnabled();
|
|
}
|
|
|
|
private void UpdateRunButtonEnabled()
|
|
{
|
|
bool anyLoading = _fileQueue.Any(f => f.IsLoading);
|
|
bool allCorrect = _fileQueue.Count > 0 && _fileQueue.All(f => f.IsCorrect);
|
|
RunButton.IsEnabled = !_isProcessing && !anyLoading && allCorrect;
|
|
RunButton.ToolTip = allCorrect
|
|
? null
|
|
: "Every file in the queue needs exactly one audio and one subtitle track selected before stripping.";
|
|
|
|
UpdateRunButtonLabel();
|
|
}
|
|
|
|
private void UpdateRunButtonLabel()
|
|
{
|
|
bool anyRename = _fileQueue.Any(f =>
|
|
!string.Equals(f.DisplayFileName, f.FileName, StringComparison.Ordinal));
|
|
RunButton.Content = anyRename ? "Rename and Strip Tracks" : "Strip Tracks";
|
|
}
|
|
|
|
private void QueuedFileItem_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
{
|
|
if (e.PropertyName == nameof(QueuedFileItem.IsLoading))
|
|
UpdateLoadingOverlay();
|
|
if (e.PropertyName == nameof(QueuedFileItem.LoadState))
|
|
UpdateRunButtonEnabled();
|
|
}
|
|
|
|
private static string GuessDefaultMkvMergePath()
|
|
{
|
|
string[] candidates =
|
|
{
|
|
@"C:\Program Files\MKVToolNix\mkvmerge.exe",
|
|
@"C:\Program Files (x86)\MKVToolNix\mkvmerge.exe",
|
|
};
|
|
return candidates.FirstOrDefault(File.Exists) ?? string.Empty;
|
|
}
|
|
|
|
// ── Settings persistence ──────────────────────────────────────────────────
|
|
|
|
private void LoadSettingsIntoUi()
|
|
{
|
|
_settings = SettingsService.Load();
|
|
|
|
if (string.IsNullOrWhiteSpace(_settings.MkvMergePath))
|
|
_settings.MkvMergePath = GuessDefaultMkvMergePath();
|
|
}
|
|
|
|
private void SaveSettingsFromUi()
|
|
{
|
|
SettingsService.Save(_settings);
|
|
}
|
|
|
|
// ── Settings button ───────────────────────────────────────────────────────
|
|
|
|
private void SettingsButton_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var win = new SettingsWindow(_settings) { Owner = this };
|
|
if (win.ShowDialog() == true && win.Result != null)
|
|
{
|
|
_settings.MkvMergePath = win.Result.MkvMergePath;
|
|
_settings.OutputFolder = win.Result.OutputFolder;
|
|
SettingsService.Save(_settings);
|
|
RestartApplication();
|
|
}
|
|
}
|
|
|
|
// ── Drag & Drop ───────────────────────────────────────────────────────────
|
|
|
|
private void Window_DragOver(object sender, DragEventArgs e)
|
|
{
|
|
if (e.Data.GetDataPresent(DataFormats.FileDrop))
|
|
{
|
|
var paths = (string[])e.Data.GetData(DataFormats.FileDrop);
|
|
bool hasAcceptedFile = paths.Any(p =>
|
|
File.Exists(p) && (IsMkvFile(p) || SubtitleMatcher.IsSubtitleFile(p)));
|
|
e.Effects = hasAcceptedFile ? DragDropEffects.Copy : DragDropEffects.None;
|
|
}
|
|
else
|
|
{
|
|
e.Effects = DragDropEffects.None;
|
|
}
|
|
e.Handled = true;
|
|
}
|
|
|
|
private static bool IsMkvFile(string path)
|
|
=> string.Equals(Path.GetExtension(path), ".mkv", StringComparison.OrdinalIgnoreCase);
|
|
|
|
private void Window_Drop(object sender, DragEventArgs e)
|
|
{
|
|
if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
|
|
|
|
Activate();
|
|
|
|
var paths = (string[])e.Data.GetData(DataFormats.FileDrop);
|
|
|
|
var newVideoPaths = paths
|
|
.Where(p => IsMkvFile(p)
|
|
&& File.Exists(p)
|
|
&& !_fileQueue.Any(q => string.Equals(q.FilePath, p, StringComparison.OrdinalIgnoreCase)))
|
|
.ToList();
|
|
|
|
var subtitlePaths = paths
|
|
.Where(p => SubtitleMatcher.IsSubtitleFile(p) && File.Exists(p))
|
|
.ToList();
|
|
|
|
var newItems = new List<QueuedFileItem>();
|
|
foreach (var path in newVideoPaths)
|
|
{
|
|
var item = new QueuedFileItem(path);
|
|
item.PropertyChanged += QueuedFileItem_PropertyChanged;
|
|
_fileQueue.Add(item);
|
|
newItems.Add(item);
|
|
_ = ScanFileItemAsync(item);
|
|
}
|
|
|
|
// Newly-added videos might satisfy subtitles dropped in earlier (or
|
|
// in this same batch, if the subtitle sorts before its video).
|
|
foreach (var item in newItems)
|
|
MatchPendingSubtitlesToFile(item);
|
|
|
|
int matchedSubtitles = 0;
|
|
foreach (var subPath in subtitlePaths)
|
|
{
|
|
if (MatchSubtitleToQueue(subPath))
|
|
matchedSubtitles++;
|
|
else if (!_pendingSubtitles.Contains(subPath, StringComparer.OrdinalIgnoreCase))
|
|
_pendingSubtitles.Add(subPath);
|
|
}
|
|
|
|
UpdateLoadingOverlay();
|
|
|
|
if (newItems.Count > 0)
|
|
{
|
|
var first = _fileQueue.FirstOrDefault(q =>
|
|
string.Equals(q.FilePath, newVideoPaths[0], StringComparison.OrdinalIgnoreCase));
|
|
if (first != null) FileQueueList.SelectedItem = first;
|
|
}
|
|
|
|
int unmatchedSubtitles = subtitlePaths.Count - matchedSubtitles;
|
|
if (matchedSubtitles > 0 || unmatchedSubtitles > 0)
|
|
{
|
|
StatusText.Text = unmatchedSubtitles > 0
|
|
? $"Matched {matchedSubtitles} subtitle(s); {unmatchedSubtitles} couldn't be matched to an episode code (S04E06 style)."
|
|
: $"Matched {matchedSubtitles} subtitle(s) to their episode.";
|
|
}
|
|
}
|
|
|
|
// ── Subtitle matching ───────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Attempts to attach a dropped subtitle file to whichever queued video(s)
|
|
/// share the same episode code (e.g. S04E06). Falls back to the single
|
|
/// queued file when there's exactly one and no episode code could be
|
|
/// extracted from the subtitle's filename. Returns false if no target
|
|
/// could be determined, in which case the file is kept pending until a
|
|
/// matching video is dropped later.
|
|
/// </summary>
|
|
private bool MatchSubtitleToQueue(string subtitlePath)
|
|
{
|
|
string subFileName = Path.GetFileName(subtitlePath);
|
|
string? code = SubtitleMatcher.ExtractEpisodeCode(subFileName);
|
|
|
|
List<QueuedFileItem> targets;
|
|
if (code != null)
|
|
{
|
|
targets = _fileQueue
|
|
.Where(q => string.Equals(SubtitleMatcher.ExtractEpisodeCode(q.FileName), code, StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
}
|
|
else if (_fileQueue.Count == 1)
|
|
{
|
|
targets = new List<QueuedFileItem> { _fileQueue[0] };
|
|
}
|
|
else
|
|
{
|
|
targets = new List<QueuedFileItem>();
|
|
}
|
|
|
|
if (targets.Count == 0) return false;
|
|
|
|
foreach (var target in targets)
|
|
AttachExternalSubtitle(target, subtitlePath);
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called whenever a new video is added to the queue: checks the pending
|
|
/// (previously unmatched) subtitle files for one sharing this video's
|
|
/// episode code, and attaches + removes any matches from the pending list.
|
|
/// </summary>
|
|
private void MatchPendingSubtitlesToFile(QueuedFileItem item)
|
|
{
|
|
if (_pendingSubtitles.Count == 0) return;
|
|
|
|
string? code = SubtitleMatcher.ExtractEpisodeCode(item.FileName);
|
|
if (code == null) return;
|
|
|
|
var matches = _pendingSubtitles
|
|
.Where(p => string.Equals(SubtitleMatcher.ExtractEpisodeCode(Path.GetFileName(p)), code, StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
|
|
foreach (var match in matches)
|
|
{
|
|
AttachExternalSubtitle(item, match);
|
|
_pendingSubtitles.Remove(match);
|
|
}
|
|
}
|
|
|
|
private static void AttachExternalSubtitle(QueuedFileItem target, string subtitlePath)
|
|
{
|
|
if (target.ExternalSubtitles.Any(s => string.Equals(s.FilePath, subtitlePath, StringComparison.OrdinalIgnoreCase)))
|
|
return;
|
|
|
|
var info = SubtitleLanguageHelper.Detect(Path.GetFileName(subtitlePath));
|
|
var sub = new ExternalSubtitleTrack(subtitlePath)
|
|
{
|
|
Language = info.Language,
|
|
TrackName = info.TrackName,
|
|
Forced = info.Forced,
|
|
HearingImpaired = info.HearingImpaired,
|
|
IsDefault = target.ExternalSubtitles.Count == 0,
|
|
};
|
|
target.ExternalSubtitles.Add(sub);
|
|
}
|
|
|
|
private void RemoveExternalSubtitle_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (sender is not System.Windows.Controls.Button btn || btn.Tag is not ExternalSubtitleTrack sub) return;
|
|
|
|
var owner = _fileQueue.FirstOrDefault(f => f.ExternalSubtitles.Contains(sub));
|
|
if (owner == null) return;
|
|
|
|
bool wasDefault = sub.IsDefault;
|
|
owner.ExternalSubtitles.Remove(sub);
|
|
|
|
if (wasDefault)
|
|
{
|
|
var next = owner.ExternalSubtitles.FirstOrDefault();
|
|
if (next != null) next.IsDefault = true;
|
|
}
|
|
|
|
if (ReferenceEquals(FileQueueList.SelectedItem, owner))
|
|
ExternalSubsPanel.Visibility = owner.HasExternalSubtitles ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
e.Handled = true;
|
|
}
|
|
|
|
// ── File scanning ─────────────────────────────────────────────────────────
|
|
|
|
private async Task ScanFileItemAsync(QueuedFileItem item)
|
|
{
|
|
item.IsScanning = true;
|
|
item.Status = "Scanning...";
|
|
item.Tracks.Clear();
|
|
|
|
await _scanSemaphore.WaitAsync();
|
|
try
|
|
{
|
|
if (string.IsNullOrWhiteSpace(_settings.MkvMergePath) || !File.Exists(_settings.MkvMergePath))
|
|
{
|
|
item.IsScanning = false;
|
|
item.HasError = true;
|
|
item.Status = "mkvmerge.exe not found — open Settings";
|
|
return;
|
|
}
|
|
|
|
var service = new MkvService(_settings.MkvMergePath);
|
|
var info = await service.IdentifyAsync(item.FilePath);
|
|
var tracks = service.BuildTrackItems(info);
|
|
|
|
foreach (var t in tracks)
|
|
{
|
|
item.Tracks.Add(t);
|
|
if (t.Type is "audio" or "subtitles")
|
|
{
|
|
t.PropertyChanged += (s, e) => OnTrackSelectionChanged(item, (TrackItem)s!, e);
|
|
}
|
|
}
|
|
|
|
item.IsScanning = false;
|
|
item.Status = $"{tracks.Count} track{(tracks.Count == 1 ? "" : "s")}";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
item.IsScanning = false;
|
|
item.HasError = true;
|
|
item.Status = ex.Message;
|
|
}
|
|
finally
|
|
{
|
|
_scanSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
// ── Audio/subtitle selection propagation ────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// 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 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 OnTrackSelectionChanged(QueuedFileItem sourceFile, TrackItem track, PropertyChangedEventArgs e)
|
|
{
|
|
if (_propagatingSelection) return;
|
|
if (e.PropertyName != nameof(TrackItem.Copy)) return;
|
|
|
|
string type = track.Type;
|
|
|
|
_propagatingSelection = true;
|
|
try
|
|
{
|
|
if (track.Copy)
|
|
{
|
|
bool wasOverriding = sourceFile.Tracks.Any(t => t.Type == type && t != track && t.Copy);
|
|
|
|
foreach (var other in sourceFile.Tracks.Where(t => t.Type == type && t != track))
|
|
{
|
|
other.Copy = false;
|
|
other.DefaultTrack = false;
|
|
}
|
|
track.DefaultTrack = true;
|
|
|
|
string key = track.PropagationKey;
|
|
|
|
foreach (var otherFile in _fileQueue.Where(f => f != sourceFile))
|
|
{
|
|
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 == type))
|
|
{
|
|
sub.Copy = sub == match;
|
|
sub.DefaultTrack = sub == match;
|
|
}
|
|
}
|
|
else if (wasOverriding)
|
|
{
|
|
foreach (var sub in otherFile.Tracks.Where(t => t.Type == type))
|
|
{
|
|
sub.Copy = false;
|
|
sub.DefaultTrack = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
track.DefaultTrack = false;
|
|
foreach (var otherFile in _fileQueue.Where(f => f != sourceFile))
|
|
{
|
|
foreach (var sub in otherFile.Tracks.Where(t => t.Type == type))
|
|
{
|
|
sub.Copy = false;
|
|
sub.DefaultTrack = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_propagatingSelection = false;
|
|
}
|
|
}
|
|
|
|
// ── File queue UI ─────────────────────────────────────────────────────────
|
|
|
|
private void RemoveFile_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (sender is System.Windows.Controls.Button btn && btn.Tag is QueuedFileItem item)
|
|
{
|
|
item.PropertyChanged -= QueuedFileItem_PropertyChanged;
|
|
_fileQueue.Remove(item);
|
|
UpdateLoadingOverlay();
|
|
}
|
|
e.Handled = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes every currently-selected item from the queue (multi-select
|
|
/// via Ctrl/Shift-click), for batch removal without clearing everything.
|
|
/// </summary>
|
|
private void FileQueueList_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
|
|
{
|
|
if (e.Key == System.Windows.Input.Key.Space)
|
|
{
|
|
OpenSelectedFileInDefaultPlayer();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
if (e.Key != System.Windows.Input.Key.Delete) return;
|
|
|
|
var selected = FileQueueList.SelectedItems.Cast<QueuedFileItem>().ToList();
|
|
if (selected.Count == 0) return;
|
|
|
|
foreach (var item in selected)
|
|
{
|
|
item.PropertyChanged -= QueuedFileItem_PropertyChanged;
|
|
_fileQueue.Remove(item);
|
|
}
|
|
UpdateLoadingOverlay();
|
|
e.Handled = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Opens the currently-selected queue item with whatever application
|
|
/// Windows has associated with .mkv files (typically the default video
|
|
/// player), so the user can quickly preview a file without leaving the app.
|
|
/// </summary>
|
|
private void TrackGrid_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
|
|
{
|
|
if (e.Key != System.Windows.Input.Key.Space) return;
|
|
OpenSelectedFileInDefaultPlayer();
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void OpenSelectedFileInDefaultPlayer()
|
|
{
|
|
if (FileQueueList.SelectedItem is not QueuedFileItem item) return;
|
|
|
|
try
|
|
{
|
|
Process.Start(new ProcessStartInfo(item.FilePath) { UseShellExecute = true });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StatusText.Text = $"Couldn't open {item.FileName}: {ex.Message}";
|
|
}
|
|
}
|
|
|
|
private void ClearQueueButton_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
foreach (var item in _fileQueue)
|
|
item.PropertyChanged -= QueuedFileItem_PropertyChanged;
|
|
_fileQueue.Clear();
|
|
UpdateLoadingOverlay();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scrolls the track grid or queue list by an amount proportional to the
|
|
/// wheel delta instead of relying on WPF's default per-event line
|
|
/// scrolling. Shared by both the file queue and track table so both
|
|
/// panes scroll consistently.
|
|
/// </summary>
|
|
private void SmoothList_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
|
|
=> SmoothScrollHelper.HandlePreviewMouseWheel(sender, e);
|
|
|
|
private void FileQueueList_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (FileQueueList.SelectedItem is QueuedFileItem file)
|
|
{
|
|
TrackGrid.ItemsSource = file.Tracks;
|
|
TrackGrid.Visibility = Visibility.Visible;
|
|
NoSelectionHint.Visibility = Visibility.Collapsed;
|
|
ExternalSubsList.ItemsSource = file.ExternalSubtitles;
|
|
ExternalSubsPanel.Visibility = file.HasExternalSubtitles ? Visibility.Visible : Visibility.Collapsed;
|
|
}
|
|
else
|
|
{
|
|
TrackGrid.ItemsSource = null;
|
|
TrackGrid.Visibility = Visibility.Collapsed;
|
|
NoSelectionHint.Visibility = Visibility.Visible;
|
|
ExternalSubsList.ItemsSource = null;
|
|
ExternalSubsPanel.Visibility = Visibility.Collapsed;
|
|
}
|
|
}
|
|
|
|
// ── Keyboard shortcuts ───────────────────────────────────────────
|
|
|
|
private void Window_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
|
|
{
|
|
if (e.Key == System.Windows.Input.Key.R
|
|
&& Keyboard.Modifiers == ModifierKeys.Control
|
|
&& RunButton.IsEnabled)
|
|
{
|
|
RunButton_Click(RunButton, new RoutedEventArgs());
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
|
|
// ── Run ─────────────────────────────────────────────────────────────────────
|
|
|
|
private void RunButton_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isProcessing) return;
|
|
if (_fileQueue.Any(f => f.IsLoading)) return;
|
|
|
|
if (string.IsNullOrWhiteSpace(_settings.MkvMergePath) || !File.Exists(_settings.MkvMergePath))
|
|
{
|
|
StatusText.Text = "mkvmerge.exe not set — open Settings.";
|
|
return;
|
|
}
|
|
|
|
var readyFiles = _fileQueue
|
|
.Where(f => f.IsCorrect)
|
|
.ToList();
|
|
|
|
if (readyFiles.Count == 0)
|
|
{
|
|
StatusText.Text = _fileQueue.Count == 0
|
|
? "No files in queue."
|
|
: "No files ready (need exactly one audio + one subtitle selected).";
|
|
return;
|
|
}
|
|
|
|
SaveSettingsFromUi();
|
|
|
|
_isProcessing = true;
|
|
RunButton.IsEnabled = false;
|
|
StatusText.Text = string.Empty;
|
|
ProcessingOverlay.Visibility = Visibility.Visible;
|
|
|
|
var progressWin = new ProgressWindow(readyFiles, _settings.MkvMergePath, _settings.OutputFolder, MaxConcurrentStrips)
|
|
{
|
|
Owner = this
|
|
};
|
|
progressWin.Closed += (_, _) =>
|
|
{
|
|
_isProcessing = false;
|
|
ProcessingOverlay.Visibility = Visibility.Collapsed;
|
|
if (progressWin.ShouldClearQueue)
|
|
{
|
|
foreach (var item in _fileQueue)
|
|
item.PropertyChanged -= QueuedFileItem_PropertyChanged;
|
|
_fileQueue.Clear();
|
|
}
|
|
UpdateLoadingOverlay();
|
|
};
|
|
progressWin.Show();
|
|
}
|
|
}
|