using System.Collections.ObjectModel; using System.ComponentModel; using System.Globalization; using System.Runtime.CompilerServices; using EmbyToolbox.Models; using EmbyToolbox.Services; namespace EmbyToolbox.ViewModels; public sealed class AddFilesSettingsViewModel : INotifyPropertyChanged { private readonly IReadOnlyList _profiles; private readonly Action _onAdd; private readonly Action _onCancel; private readonly ProfileOverrideBuilder _builder = new(); private ConversionProfileSettingsEntry _sourceProfile = new(); private ConversionProfilePresetRow? _selectedProfile; private string _container = string.Empty; private string _video = string.Empty; private string _pixelFormat = string.Empty; private string _resolution = string.Empty; private string _fps = string.Empty; private string _videoBitrateMode = VideoBitratePolicy.Auto; private string _videoBitrateCustomMbps = string.Empty; private string _audio = string.Empty; private string _audioBitrate = string.Empty; private bool _externalTracks; private bool _externalSubtitles; private bool _fonts; private bool _removeForeignTracks; private bool _disableSubtitleDefault; private string _validationMessage = string.Empty; private bool _isValid; public AddFilesSettingsViewModel( IReadOnlyList profiles, ConversionFormOptions formOptions, AddFilesSettingsState? previousState, string fallbackProfileName, bool fallbackDisableSubtitleDefault, bool fallbackRemoveForeignTracks, Action onAdd, Action onCancel) { _profiles = profiles; _onAdd = onAdd; _onCancel = onCancel; Profiles = new ObservableCollection(profiles); ContainerOptions = formOptions.ContainerOptions; VideoCodecOptions = formOptions.VideoCodecOptions; PixelFormatOptions = formOptions.PixelFormatOptions; ResolutionOptions = formOptions.ResolutionOptions; FpsOptions = formOptions.FpsOptions; AudioCodecOptions = formOptions.AudioCodecOptions; AudioBitrateOptions = formOptions.AudioBitrateKbps; VideoBitrateOptions = VideoBitratePolicy.UiOptions; YesNoOptions = ["Да", "Нет"]; AddCommand = new RelayCommand(ExecuteAdd, CanAdd); CancelCommand = new RelayCommand(() => _onCancel()); var profileName = string.IsNullOrWhiteSpace(previousState?.ProfileName) ? fallbackProfileName : previousState!.ProfileName; _selectedProfile = Profiles.FirstOrDefault(p => p.Profile.Equals(profileName, StringComparison.OrdinalIgnoreCase)) ?? Profiles.FirstOrDefault(p => p.Profile.Equals("Emby", StringComparison.OrdinalIgnoreCase)) ?? Profiles.FirstOrDefault(); if (_selectedProfile is not null) { LoadProfile(_selectedProfile.ToSettingsEntry(), resetUserValues: true); } if (previousState is not null) { ApplyState(previousState); } else { _disableSubtitleDefault = fallbackDisableSubtitleDefault; _removeForeignTracks = fallbackRemoveForeignTracks; } Validate(); } public ObservableCollection Profiles { get; } public IReadOnlyList ContainerOptions { get; } public IReadOnlyList VideoCodecOptions { get; } public IReadOnlyList PixelFormatOptions { get; } public IReadOnlyList ResolutionOptions { get; } public IReadOnlyList FpsOptions { get; } public IReadOnlyList AudioCodecOptions { get; } public IReadOnlyList AudioBitrateOptions { get; } public IReadOnlyList VideoBitrateOptions { get; } public IReadOnlyList YesNoOptions { get; } public RelayCommand AddCommand { get; } public RelayCommand CancelCommand { get; } public ConversionProfilePresetRow? SelectedProfile { get => _selectedProfile; set { if (ReferenceEquals(_selectedProfile, value)) { return; } _selectedProfile = value; OnPropertyChanged(); if (value is not null) { LoadProfile(value.ToSettingsEntry(), resetUserValues: true); } } } public string Container { get => _container; set => SetField(ref _container, value); } public string Video { get => _video; set => SetField(ref _video, value); } public string PixelFormat { get => _pixelFormat; set => SetField(ref _pixelFormat, value); } public string Resolution { get => _resolution; set => SetField(ref _resolution, value); } public string Fps { get => _fps; set => SetField(ref _fps, value); } public string VideoBitrateMode { get => _videoBitrateMode; set { if (SetField(ref _videoBitrateMode, string.IsNullOrWhiteSpace(value) ? VideoBitratePolicy.Auto : value)) { OnPropertyChanged(nameof(IsVideoBitrateCustomVisible)); } } } public string VideoBitrateCustomMbps { get => _videoBitrateCustomMbps; set => SetField(ref _videoBitrateCustomMbps, value); } public string Audio { get => _audio; set => SetField(ref _audio, value); } public string AudioBitrate { get => _audioBitrate; set => SetField(ref _audioBitrate, value); } public bool ExternalTracks { get => _externalTracks; set => SetField(ref _externalTracks, value); } public bool ExternalSubtitles { get => _externalSubtitles; set => SetField(ref _externalSubtitles, value); } public bool Fonts { get => _fonts; set => SetField(ref _fonts, value); } public bool RemoveForeignTracks { get => _removeForeignTracks; set => SetField(ref _removeForeignTracks, value); } public bool DisableSubtitleDefault { get => _disableSubtitleDefault; set => SetField(ref _disableSubtitleDefault, value); } public bool IsVideoBitrateCustomVisible => string.Equals(VideoBitrateMode, VideoBitratePolicy.Custom, StringComparison.Ordinal); public bool HasValidationMessage => !string.IsNullOrWhiteSpace(ValidationMessage); public string ValidationMessage { get => _validationMessage; private set { if (_validationMessage == value) { return; } _validationMessage = value; OnPropertyChanged(); OnPropertyChanged(nameof(HasValidationMessage)); } } public AddFilesSettingsState CaptureState() => new AddFilesSettingsState { ProfileName = SelectedProfile?.Profile ?? "Emby", ChangedFields = BuildEffectiveSettings().ChangedFields.ToArray(), Container = Container, Video = Video, PixelFormat = PixelFormat, Resolution = Resolution, Fps = Fps, VideoBitrateMode = VideoBitrateMode, VideoBitrateCustomMbps = VideoBitrateCustomMbps, Audio = Audio, AudioBitrate = AudioBitrate, ExternalTracks = ExternalTracks, ExternalSubtitles = ExternalSubtitles, Fonts = Fonts, RemoveForeignTracks = RemoveForeignTracks, DisableSubtitleDefault = DisableSubtitleDefault }; private void LoadProfile(ConversionProfileSettingsEntry profile, bool resetUserValues) { _sourceProfile = ProfileOverrideBuilder.Clone(profile); if (!resetUserValues) { return; } Container = profile.Container; Video = profile.Video; PixelFormat = profile.PixelFormat; Resolution = profile.Resolution; Fps = profile.Fps; VideoBitrateMode = VideoBitratePolicy.NormalizeMode(profile.VideoBitrateMode); VideoBitrateCustomMbps = profile.VideoBitrateMbps?.ToString("0.###", CultureInfo.InvariantCulture) ?? string.Empty; Audio = profile.Audio; AudioBitrate = profile.Bitrate; ExternalTracks = IsYes(profile.ExternalTracks); ExternalSubtitles = IsYes(profile.ExternalSubtitles); Fonts = IsYes(profile.Fonts); } private void ApplyState(AddFilesSettingsState state) { var changed = state.ChangedFields.ToHashSet(StringComparer.Ordinal); if (changed.Contains("Container")) Container = state.Container; if (changed.Contains("Video")) Video = state.Video; if (changed.Contains("PixelFormat")) PixelFormat = state.PixelFormat; if (changed.Contains("Resolution")) Resolution = state.Resolution; if (changed.Contains("Fps")) Fps = state.Fps; if (changed.Contains("VideoBitrateMode")) VideoBitrateMode = state.VideoBitrateMode; if (changed.Contains("VideoBitrateMbps")) VideoBitrateCustomMbps = state.VideoBitrateCustomMbps; if (changed.Contains("Audio")) Audio = state.Audio; if (changed.Contains("Bitrate")) AudioBitrate = state.AudioBitrate; if (changed.Contains("ExternalTracks")) ExternalTracks = state.ExternalTracks; if (changed.Contains("ExternalSubtitles")) ExternalSubtitles = state.ExternalSubtitles; if (changed.Contains("Fonts")) Fonts = state.Fonts; RemoveForeignTracks = state.RemoveForeignTracks; DisableSubtitleDefault = state.DisableSubtitleDefault; } private bool CanAdd() => _isValid; private void ExecuteAdd() { if (!Validate()) { return; } var effective = BuildEffectiveSettings(); _onAdd( new AddFilesOptions { Profile = effective.SourceProfileName, DisableSubtitleDefault = DisableSubtitleDefault, RemoveForeignAudioAndSubtitles = RemoveForeignTracks, EffectiveSettings = effective }); } private EffectiveProfileSettings BuildEffectiveSettings() { var form = new ConversionProfileSettingsEntry { Profile = SelectedProfile?.Profile ?? "Emby", Container = Container, Video = Video, PixelFormat = PixelFormat, Resolution = Resolution, Fps = Fps, Audio = Audio, Bitrate = AudioBitrate, VideoBitrateMode = VideoBitrateMode, VideoBitrateMbps = IsVideoBitrateCustomVisible && TryParseCustomVideoBitrate(VideoBitrateCustomMbps, out var mbps) ? mbps : null, Subtitles = _sourceProfile.Subtitles, ExternalTracks = ToYesNo(ExternalTracks), ExternalSubtitles = ToYesNo(ExternalSubtitles), Fonts = ToYesNo(Fonts) }; return _builder.Build(_sourceProfile, form, SelectedProfile?.Profile ?? "Emby"); } private bool Validate() { if (SelectedProfile is null) { ValidationMessage = "Выберите профиль."; _isValid = false; AddCommand.RaiseCanExecuteChanged(); return false; } if (IsVideoBitrateCustomVisible && !TryParseCustomVideoBitrate(VideoBitrateCustomMbps, out _)) { ValidationMessage = "Видеобитрейт должен быть числом больше 0."; _isValid = false; AddCommand.RaiseCanExecuteChanged(); return false; } ValidationMessage = string.Empty; _isValid = true; AddCommand.RaiseCanExecuteChanged(); return true; } private bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) { if (EqualityComparer.Default.Equals(field, value)) { return false; } field = value; OnPropertyChanged(propertyName); Validate(); return true; } private static bool TryParseCustomVideoBitrate(string? raw, out double mbps) { mbps = 0; if (string.IsNullOrWhiteSpace(raw)) { return false; } return double.TryParse(raw.Trim().Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out mbps) && mbps > 0; } private static bool IsYes(string? value) => value is not null && (value.Equals("Да", StringComparison.OrdinalIgnoreCase) || value.Equals("Yes", StringComparison.OrdinalIgnoreCase) || value.Equals("true", StringComparison.OrdinalIgnoreCase)); private static string ToYesNo(bool value) => value ? "Да" : "Нет"; public event PropertyChangedEventHandler? PropertyChanged; private void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } public sealed class AddFilesSettingsState { public string ProfileName { get; init; } = "Emby"; public IReadOnlyList ChangedFields { get; init; } = []; public string Container { get; init; } = string.Empty; public string Video { get; init; } = string.Empty; public string PixelFormat { get; init; } = string.Empty; public string Resolution { get; init; } = string.Empty; public string Fps { get; init; } = string.Empty; public string VideoBitrateMode { get; init; } = VideoBitratePolicy.Auto; public string VideoBitrateCustomMbps { get; init; } = string.Empty; public string Audio { get; init; } = string.Empty; public string AudioBitrate { get; init; } = string.Empty; public bool ExternalTracks { get; init; } public bool ExternalSubtitles { get; init; } public bool Fonts { get; init; } public bool RemoveForeignTracks { get; init; } public bool DisableSubtitleDefault { get; init; } }