emby-toolbox/EmbyToolbox/ViewModels/BulkFileConversionSettingsViewModel.cs
Emby Toolbox 6264b487fe Initial commit: Emby Toolbox (conversion scroll fix, bulk Del for tracks).
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 21:33:47 +05:00

200 lines
6.3 KiB
C#

using System.Collections;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using EmbyToolbox.Models;
using EmbyToolbox.Services;
namespace EmbyToolbox.ViewModels;
public sealed class BulkFileConversionSettingsViewModel : INotifyPropertyChanged, ITrackPlanPreviewHost
{
private readonly ConversionQueueItem _representative;
private readonly ConversionTaskOverride _draft;
private readonly Action<IReadOnlyList<TrackOverrideEntry>> _onApply;
private readonly Action _onClose;
public BulkFileConversionSettingsViewModel(
ConversionQueueItem representative,
IReadOnlyList<ConversionQueueItem> targets,
ConversionFormOptions formOptions,
Action<IReadOnlyList<TrackOverrideEntry>> onApply,
Action onClose)
{
_representative = representative;
_draft = representative.TaskOverride.Clone();
_onApply = onApply;
_onClose = onClose;
FormOptions = formOptions;
TargetRowNumbersText = string.Join(", ", targets.OrderBy(i => i.OrderNumber).Select(i => i.OrderNumber));
FilesCount = targets.Count;
BuildRows();
ValidateDefaultConflicts();
SaveCommand = new RelayCommand(ExecuteSave);
CancelCommand = new RelayCommand(() => _onClose());
OnSelectionChangedCommand = new RelayCommand(OnSelectionChanged);
SetSelectedTracksRemoveCommand = new RelayCommand(SetSelectedTracksRemove, CanSetSelectedTracksRemove);
}
public ConversionFormOptions FormOptions { get; }
public ObservableCollection<TrackSettingsRowViewModel> TrackRows { get; } = new();
public ObservableCollection<TrackSettingsRowViewModel> SelectedTracks { get; } = new();
public RelayCommand SaveCommand { get; }
public RelayCommand CancelCommand { get; }
public RelayCommand OnSelectionChangedCommand { get; }
public RelayCommand SetSelectedTracksRemoveCommand { get; }
public int FilesCount { get; }
public string TargetRowNumbersText { get; }
public event PropertyChangedEventHandler? PropertyChanged;
public void RecalculatePlanPreview()
{
// No per-file plan preview in bulk dialog.
}
public void OnTrackDefaultEnabled(TrackSettingsRowViewModel row)
{
if (row.DataModel.StreamKind is not (MediaStreamKind.Audio or MediaStreamKind.Subtitle))
{
return;
}
foreach (var r in TrackRows)
{
if (ReferenceEquals(r, row))
{
continue;
}
if (r.DataModel.StreamKind == row.DataModel.StreamKind)
{
r.Default = false;
}
}
}
public void ValidateDefaultConflicts()
{
var audioDefaults = TrackRows
.Where(r => r.DataModel.StreamKind == MediaStreamKind.Audio && r.Action != TrackActionKind.Remove && r.Default is true)
.ToList();
var subDefaults = TrackRows
.Where(r => r.DataModel.StreamKind == MediaStreamKind.Subtitle && r.Action != TrackActionKind.Remove && r.Default is true)
.ToList();
foreach (var r in TrackRows)
{
r.HasDefaultConflict = false;
}
if (audioDefaults.Count > 1)
{
foreach (var r in audioDefaults)
{
r.HasDefaultConflict = true;
}
}
if (subDefaults.Count > 1)
{
foreach (var r in subDefaults)
{
r.HasDefaultConflict = true;
}
}
}
private void OnSelectionChanged(object? parameter)
{
SelectedTracks.Clear();
if (parameter is IList list)
{
foreach (var r in list.OfType<TrackSettingsRowViewModel>())
{
SelectedTracks.Add(r);
}
}
SetSelectedTracksRemoveCommand.RaiseCanExecuteChanged();
}
private bool CanSetSelectedTracksRemove(object? parameter)
{
if (parameter is not IList list || list.Count == 0)
{
return false;
}
return list.OfType<TrackSettingsRowViewModel>().Any(r => r.ValidActions.Contains(TrackActionKind.Remove));
}
private void SetSelectedTracksRemove(object? parameter)
{
if (parameter is not IList list || list.Count == 0)
{
return;
}
var rows = list.OfType<TrackSettingsRowViewModel>().ToList();
var changed = false;
foreach (var row in rows)
{
if (!row.ValidActions.Contains(TrackActionKind.Remove))
{
continue;
}
row.Action = TrackActionKind.Remove;
changed = true;
}
if (changed)
{
ValidateDefaultConflicts();
SetSelectedTracksRemoveCommand.RaiseCanExecuteChanged();
}
}
private void ExecuteSave()
{
ValidateDefaultConflicts();
var hasAudioConflict = TrackRows.Any(r => r.HasDefaultConflict && r.DataModel.StreamKind == MediaStreamKind.Audio);
var hasSubConflict = TrackRows.Any(r => r.HasDefaultConflict && r.DataModel.StreamKind == MediaStreamKind.Subtitle);
if (hasAudioConflict || hasSubConflict)
{
MessageBox.Show(
"Ошибка: для Audio и Subtitle может быть только одна дорожка Default (исключая Remove).",
"Валидация",
MessageBoxButton.OK,
MessageBoxImage.Warning);
return;
}
_onApply(_draft.TrackOverrides.Select(t => t.Clone()).ToList());
_onClose();
}
private void BuildRows()
{
TrackRows.Clear();
var media = _representative.MediaAnalysis;
var idx = 1;
foreach (var t in _draft.TrackOverrides)
{
MediaStreamInfo? em = t.Source == SourceKind.Embedded && media is not null
? media.AllStreams.FirstOrDefault(s => s.Index == t.StreamIndex)
: null;
TrackRows.Add(new TrackSettingsRowViewModel(this, t, idx, em, _draft.TargetContainer));
idx++;
}
}
private void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}