using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Windows; using System.Windows.Shell; using System.Windows.Threading; using EmbyToolbox.Models; using EmbyToolbox.Services; using Microsoft.Win32; namespace EmbyToolbox.ViewModels; public sealed class MainWindowViewModel : INotifyPropertyChanged { private readonly AppSettingsService _settingsService; private readonly LoggingService _logging; private readonly RecentPathService _recentPathService; private readonly NotificationService _notificationService; private AppSettings _loadedSettings; private string _processingTempDirectory = string.Empty; private string _minimumFileLogLevel = LogLevel.Info.ToString(); private string _hardwareAcceleration = HardwareAccelerationMode.Auto; private bool _notifyCompletionSoundAfterQueue = true; private bool _notifyWindowsToastAfterQueue = true; private int _selectedTabIndex; private string _appStatusText = "Готово"; private double _taskbarProgressValue; private TaskbarItemProgressState _taskbarProgressState = TaskbarItemProgressState.None; private bool _wasConversionExecuting; private bool _wasMergeRunning; private bool _wasTrackExtractionBusy; private bool _queueEndedWithConversionErrors; private bool _completionFlashActive; private DispatcherTimer? _completionFlashTimer; private double _longOperationProgressPercent; private string _longOperationProgressText = string.Empty; private bool _isLongOperationRunning; private bool _showLongOperationIdlePlaceholder = true; public MainWindowViewModel() { _settingsService = new AppSettingsService(); _logging = new LoggingService(); _recentPathService = new RecentPathService(_settingsService); _loadedSettings = _settingsService.Load(); _recentPathService.HydrateFrom(_loadedSettings); _processingTempDirectory = _loadedSettings.ProcessingTempDirectory; _minimumFileLogLevel = _loadedSettings.MinimumFileLogLevel; _hardwareAcceleration = _loadedSettings.HardwareAcceleration; _notifyCompletionSoundAfterQueue = _loadedSettings.NotifyCompletionSoundAfterQueue; _notifyWindowsToastAfterQueue = _loadedSettings.NotifyWindowsToastAfterQueue; LoadConversionProfiles(_loadedSettings.ConversionProfiles); ApplyLogLevelToService(); SeriesRenamer = new SeriesRenamerViewModel(new SeriesRenamerService(), _logging, _recentPathService); IProfileSettingsProvider profileProvider = new ProfileSettingsProvider( name => ConversionProfiles.FirstOrDefault( p => string.Equals(p.Profile, name, StringComparison.OrdinalIgnoreCase))?.ToSettingsEntry()); var planService = new ConversionPlanService(); var sidecar = new SidecarDiscoveryService(_logging); var ff = new FfprobeService(); var trackSnapshotService = new TrackSettingsSnapshotService(_logging); var exec = new ConversionExecutionService( _logging, new FfmpegCommandBuilder(), new FfmpegService(), ff, new FfmpegEncoderDiscoveryService(), () => HardwareAcceleration, new SafeFileReplaceService(), new ExternalFileCleanupService()); _notificationService = new NotificationService( _logging, () => NotifyCompletionSoundAfterQueue, () => NotifyWindowsToastAfterQueue, Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher); Conversion = new ConversionViewModel( _logging, new FileDiscoveryService(), new QueueAnalysisService(ff, _logging, sidecar, planService, profileProvider), planService, profileProvider, trackSnapshotService, exec, () => ProcessingTempDirectory, _recentPathService, _notificationService, () => ConversionProfiles.Select(p => p.ToSettingsEntry()).ToList(), () => ConversionProfiles.ToList(), ApplyConversionProfilesFromQueueSetupDocument); Conversion.CopyPreviousTrackSettings = _loadedSettings.CopyPreviousTrackSettings; Conversion.DisableSubtitleDefault = _loadedSettings.DisableSubtitleDefault; Conversion.SyncDefaultProfileFromList(ConversionProfiles.ToList()); VideoInfo = new VideoInfoViewModel( ff, _logging, _recentPathService, sidecar, new VideoInfoSummaryService()); Merge = new MergeViewModel( _logging, new MergeService(_logging, ff, new ChapterBuilderService(), () => ProcessingTempDirectory), _recentPathService); TrackExtraction = new TrackExtractionViewModel(_logging, new TrackExtractionService(ff), _recentPathService); Logs = new LogsViewModel(_logging); ChooseTempDirectoryCommand = new RelayCommand(ExecuteChooseTempDirectory); SaveSettingsCommand = new RelayCommand(ExecuteSaveSettings); CancelSettingsCommand = new RelayCommand(ExecuteCancelSettings); CheckToolsCommand = new RelayCommand(ExecuteCheckTools); AddConversionProfileCommand = new RelayCommand(ExecuteAddConversionProfile); RemoveConversionProfileCommand = new RelayCommand(ExecuteRemoveConversionProfile, CanRemoveConversionProfile); TestWindowsNotificationCommand = new RelayCommand(ExecuteTestWindowsNotification); _logging.UiEntries.CollectionChanged += OnUiEntriesCollectionChanged; Conversion.PropertyChanged += OnConversionPropertyChanged; Merge.PropertyChanged += OnMergePropertyChanged; TrackExtraction.PropertyChanged += OnTrackExtractionPropertyChanged; RefreshStatusBar(); _logging.Info("приложение запущено", "app"); } public SeriesRenamerViewModel SeriesRenamer { get; } public VideoInfoViewModel VideoInfo { get; } public ConversionViewModel Conversion { get; } public MergeViewModel Merge { get; } public TrackExtractionViewModel TrackExtraction { get; } public LogsViewModel Logs { get; } public IReadOnlyList LogLevelOptions { get; } = new[] { LogLevel.Debug.ToString(), LogLevel.Info.ToString(), LogLevel.Warning.ToString(), LogLevel.Error.ToString() }; public ObservableCollection ConversionProfiles { get; } = new(); private ConversionProfilePresetRow? _selectedConversionProfile; public ConversionProfilePresetRow? SelectedConversionProfile { get => _selectedConversionProfile; set { if (_selectedConversionProfile == value) { return; } _selectedConversionProfile = value; OnPropertyChanged(); RemoveConversionProfileCommand.RaiseCanExecuteChanged(); } } public IReadOnlyList ConversionContainerOptions { get; } = ["MKV", "MP4", "MOV", "WEBM"]; public IReadOnlyList ConversionVideoCodecOptions { get; } = ["H.264", "H.265", "AV1", "Copy"]; public IReadOnlyList ConversionPixelFormatOptions { get; } = ["yuv420p", "yuv420p10le", "yuv422p", "yuv444p"]; public IReadOnlyList ConversionResolutionOptions { get; } = ["Без изменений", "Максимум 2160p", "Максимум 1440p", "Максимум 1080p", "Максимум 720p"]; public IReadOnlyList ConversionFpsOptions { get; } = ["Без изменений", "Максимум 60", "Максимум 30", "Максимум 25", "Максимум 24"]; public IReadOnlyList ConversionAudioCodecOptions { get; } = ["AAC", "AC3", "EAC3", "Opus", "MP3", "FLAC", "Copy"]; public IReadOnlyList ConversionBitrateOptions { get; } = ["96 kbps", "128 kbps", "160 kbps", "192 kbps", "256 kbps", "320 kbps"]; public IReadOnlyList ConversionVideoBitrateOptions => VideoBitratePolicy.UiOptions; public IReadOnlyList ConversionYesNoOptions { get; } = ["Да", "Нет"]; public IReadOnlyList HardwareAccelerationOptions { get; } = [ HardwareAccelerationMode.Auto, HardwareAccelerationMode.Nvenc, HardwareAccelerationMode.Qsv, HardwareAccelerationMode.Amf, HardwareAccelerationMode.Cpu ]; public RelayCommand ChooseTempDirectoryCommand { get; } public RelayCommand SaveSettingsCommand { get; } public RelayCommand CancelSettingsCommand { get; } public RelayCommand CheckToolsCommand { get; } public RelayCommand AddConversionProfileCommand { get; } public RelayCommand RemoveConversionProfileCommand { get; } /// Проверка звука и Windows toast без учёта флагов в настройках. public RelayCommand TestWindowsNotificationCommand { get; } public string ProcessingTempDirectory { get => _processingTempDirectory; set { if (_processingTempDirectory == value) { return; } _processingTempDirectory = value; OnPropertyChanged(); } } public string MinimumFileLogLevel { get => _minimumFileLogLevel; set { if (_minimumFileLogLevel == value) { return; } _minimumFileLogLevel = value; ApplyLogLevelToService(); OnPropertyChanged(); } } public string HardwareAcceleration { get => _hardwareAcceleration; set { if (_hardwareAcceleration == value) { return; } _hardwareAcceleration = value; OnPropertyChanged(); } } /// Звук Windows после успешной/неуспешной обработки всей очереди конвертации. public bool NotifyCompletionSoundAfterQueue { get => _notifyCompletionSoundAfterQueue; set { if (_notifyCompletionSoundAfterQueue == value) { return; } _notifyCompletionSoundAfterQueue = value; OnPropertyChanged(); } } /// Toast Windows после завершения очереди конвертации. public bool NotifyWindowsToastAfterQueue { get => _notifyWindowsToastAfterQueue; set { if (_notifyWindowsToastAfterQueue == value) { return; } _notifyWindowsToastAfterQueue = value; OnPropertyChanged(); } } public int SelectedTabIndex { get => _selectedTabIndex; set { if (_selectedTabIndex == value) { return; } _selectedTabIndex = value; OnPropertyChanged(); _logging.Debug($"открыта вкладка: {GetTabName(value)}", "ui.tabs"); } } /// 0–100 для : во время задачи не выше 99.99; 100 при коротком «флеше» после успеха. public double LongOperationProgressPercent => _longOperationProgressPercent; /// Описание текущей задачи для строки статуса (объединение, конвертация, завершение). public string LongOperationProgressText => _longOperationProgressText; /// Показывать «Нет задач», когда нет длительной операции и нет завершающего флеша. public bool ShowLongOperationIdlePlaceholder => _showLongOperationIdlePlaceholder; /// Показывать блок прогресса: конвертация, объединение или краткое завершение 100%. public bool IsLongOperationRunning => _isLongOperationRunning; public string AppStatusText { get => _appStatusText; private set { if (_appStatusText == value) { return; } _appStatusText = value; OnPropertyChanged(); } } public double TaskbarProgressValue { get => _taskbarProgressValue; private set { if (Math.Abs(_taskbarProgressValue - value) < 0.0001) { return; } _taskbarProgressValue = value; OnPropertyChanged(); } } public TaskbarItemProgressState TaskbarProgressState { get => _taskbarProgressState; private set { if (_taskbarProgressState == value) { return; } _taskbarProgressState = value; OnPropertyChanged(); } } public event PropertyChangedEventHandler? PropertyChanged; private void OnPropertyChanged([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); private void ExecuteChooseTempDirectory() { var dialog = new OpenFolderDialog { Title = "Выберите TEMP-каталог", InitialDirectory = _recentPathService.GetInitialDirectory( RecentPathScenario.SettingsTempFolder, extraFolderFallbackBeforeDefault: ProcessingTempDirectory), }; if (dialog.ShowDialog() != true || string.IsNullOrWhiteSpace(dialog.FolderName)) { return; } _recentPathService.RememberChosenFolder(RecentPathScenario.SettingsTempFolder, dialog.FolderName); ProcessingTempDirectory = dialog.FolderName; _logging.Info($"выбран TEMP-каталог: {dialog.FolderName}", "settings"); } private void ExecuteSaveSettings() { var updated = new AppSettings { ProcessingTempDirectory = ProcessingTempDirectory, MinimumFileLogLevel = MinimumFileLogLevel, HardwareAcceleration = HardwareAcceleration, IsLogCollapsed = true, CopyPreviousTrackSettings = Conversion.CopyPreviousTrackSettings, DisableSubtitleDefault = Conversion.DisableSubtitleDefault, NotifyCompletionSoundAfterQueue = NotifyCompletionSoundAfterQueue, NotifyWindowsToastAfterQueue = NotifyWindowsToastAfterQueue, ConversionProfiles = ConversionProfiles .Select(profile => new ConversionProfileSettingsEntry { Profile = profile.Profile, Container = profile.Container, Video = profile.Video, PixelFormat = profile.PixelFormat, Resolution = profile.Resolution, Fps = profile.Fps, Audio = profile.Audio, Bitrate = profile.Bitrate, VideoBitrateMode = profile.VideoBitrateMode, VideoBitrateMbps = profile.VideoBitrateMbps, Subtitles = profile.Subtitles, ExternalTracks = profile.ExternalTracks, ExternalSubtitles = profile.ExternalSubtitles, Fonts = profile.Fonts }) .ToList() }; _recentPathService.ApplyTo(updated); _settingsService.Save(updated); _loadedSettings = updated; _recentPathService.HydrateFrom(updated); Conversion.RecalculateAllAnalyzedForProfileUpdate(); _logging.Info("настройки сохранены", "settings"); } private void ExecuteCancelSettings() { _loadedSettings = _settingsService.Load(); _recentPathService.HydrateFrom(_loadedSettings); ProcessingTempDirectory = _loadedSettings.ProcessingTempDirectory; MinimumFileLogLevel = _loadedSettings.MinimumFileLogLevel; HardwareAcceleration = _loadedSettings.HardwareAcceleration; NotifyCompletionSoundAfterQueue = _loadedSettings.NotifyCompletionSoundAfterQueue; NotifyWindowsToastAfterQueue = _loadedSettings.NotifyWindowsToastAfterQueue; LoadConversionProfiles(_loadedSettings.ConversionProfiles); Conversion.CopyPreviousTrackSettings = _loadedSettings.CopyPreviousTrackSettings; Conversion.DisableSubtitleDefault = _loadedSettings.DisableSubtitleDefault; _logging.Info("изменения в настройках отменены", "settings"); } private void ExecuteTestWindowsNotification() { _notificationService.ShowSettingsTestNotification(); } private void ExecuteCheckTools() { var ffmpegPath = Path.Combine(AppContext.BaseDirectory, "Tools", "ffmpeg.exe"); var ffprobePath = Path.Combine(AppContext.BaseDirectory, "Tools", "ffprobe.exe"); var ffmpegOk = File.Exists(ffmpegPath); var ffprobeOk = File.Exists(ffprobePath); if (ffmpegOk && ffprobeOk) { _logging.Info("инструменты OK (ffmpeg/ffprobe)", "tools.check"); return; } if (!ffmpegOk) { _logging.Error($"не найден ffmpeg: {ffmpegPath}", "tools.check"); } if (!ffprobeOk) { _logging.Error($"не найден ffprobe: {ffprobePath}", "tools.check"); } } private void ApplyLogLevelToService() { if (Enum.TryParse(MinimumFileLogLevel, true, out var level)) { _logging.MinimumFileLogLevel = level; return; } _logging.MinimumFileLogLevel = LogLevel.Info; } private void LoadConversionProfiles(IEnumerable profiles) { ConversionProfiles.Clear(); foreach (var profile in profiles) { ConversionProfiles.Add(new ConversionProfilePresetRow { IsBuiltIn = ConversionProfileNames.IsBuiltIn(profile.Profile), Profile = profile.Profile, Container = profile.Container, Video = profile.Video, PixelFormat = profile.PixelFormat, Resolution = profile.Resolution, Fps = profile.Fps, Audio = profile.Audio, Bitrate = profile.Bitrate, VideoBitrateMode = profile.VideoBitrateMode, VideoBitrateMbps = profile.VideoBitrateMbps, Subtitles = profile.Subtitles, ExternalTracks = profile.ExternalTracks, ExternalSubtitles = profile.ExternalSubtitles, Fonts = profile.Fonts }); } SelectedConversionProfile = null; if (Conversion is not null) { Conversion.SyncDefaultProfileFromList(ConversionProfiles.ToList()); } } /// Применяет профили из загруженного .conv_setup (нормализация как при чтении settings.json). private void ApplyConversionProfilesFromQueueSetupDocument(IReadOnlyList raw) { if (raw is null || raw.Count == 0) { return; } var normalized = AppSettingsService.NormalizeStoredConversionProfiles(raw.ToList()); LoadConversionProfiles(normalized); } private void ExecuteAddConversionProfile() { var name = GenerateUniqueCustomProfileName(); var row = CreateCustomProfileRow(name); ConversionProfiles.Add(row); SelectedConversionProfile = row; _logging.Info($"добавлен пользовательский профиль: {name}", "settings.profiles"); } private void ExecuteRemoveConversionProfile() { if (SelectedConversionProfile is not { IsBuiltIn: false } row) { return; } var removedName = row.Profile; ConversionProfiles.Remove(row); if (SelectedConversionProfile == row) { SelectedConversionProfile = null; } _logging.Info($"удалён пользовательский профиль: {removedName}", "settings.profiles"); } private bool CanRemoveConversionProfile() => SelectedConversionProfile is { IsBuiltIn: false }; private string GenerateUniqueCustomProfileName() { for (var i = 1; i < 1000; i++) { var candidate = $"Мой профиль {i}"; if (ConversionProfiles.All(p => !p.Profile.Equals(candidate, StringComparison.OrdinalIgnoreCase))) { return candidate; } } return $"Мой профиль {Guid.NewGuid():N}"[..8]; } private static ConversionProfilePresetRow CreateCustomProfileRow(string name) { return new ConversionProfilePresetRow { IsBuiltIn = false, Profile = name, Container = "MP4", Video = "H.264", PixelFormat = "yuv420p", Resolution = "Без изменений", Fps = "Без изменений", Audio = "AAC", Bitrate = "256 kbps", VideoBitrateMode = VideoBitratePolicy.Auto, Subtitles = "Да", ExternalTracks = "Да", ExternalSubtitles = "Да", Fonts = "Нет" }; } private static string GetTabName(int index) { return index switch { 0 => "Переименование сериалов", 1 => "Конвертация", 2 => "Объединение", 3 => "Извлечение дорожек", 4 => "Video info", 5 => "Настройки", 6 => "Логи", _ => "Неизвестно" }; } private void OnUiEntriesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) => Logs.RaiseClearCommandState(); private void OnConversionPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(ConversionViewModel.CopyPreviousTrackSettings)) { PersistCopyPreviousTrackSettings(); } else if (e.PropertyName == nameof(ConversionViewModel.DisableSubtitleDefault)) { PersistDisableSubtitleDefault(); } if (e.PropertyName == nameof(ConversionViewModel.IsExecutionRunning) || e.PropertyName == nameof(ConversionViewModel.OverallProgressPercent) || e.PropertyName == nameof(ConversionViewModel.OverallQueueDoneCount) || e.PropertyName == nameof(ConversionViewModel.OverallQueueTotal) || e.PropertyName == nameof(ConversionViewModel.CurrentRunId) || e.PropertyName == nameof(ConversionViewModel.ExecutionPhaseCaption)) { RefreshStatusBar(); } } private void OnMergePropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName is nameof(MergeViewModel.IsRunning) or nameof(MergeViewModel.ProgressPercent) or nameof(MergeViewModel.LastMergeCompletion)) { RefreshStatusBar(); } } private void OnTrackExtractionPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName is nameof(TrackExtractionViewModel.IsBusy) or nameof(TrackExtractionViewModel.OverallProgressPercent) or nameof(TrackExtractionViewModel.ExecutionPhaseCaption) or nameof(TrackExtractionViewModel.LastRunOutcome)) { RefreshStatusBar(); } } private void PersistCopyPreviousTrackSettings() { try { var settings = _settingsService.Load(); settings.CopyPreviousTrackSettings = Conversion.CopyPreviousTrackSettings; _settingsService.Save(settings); _loadedSettings = settings; } catch { // ignore persistence errors for UI convenience flag } } private void PersistDisableSubtitleDefault() { try { var settings = _settingsService.Load(); settings.DisableSubtitleDefault = Conversion.DisableSubtitleDefault; _settingsService.Save(settings); _loadedSettings = settings; } catch { // ignore persistence errors for UI convenience flag } } private void RefreshStatusBar() { var dispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher; if (!dispatcher.CheckAccess()) { dispatcher.BeginInvoke(RefreshStatusBar, DispatcherPriority.Normal); return; } var convRunning = Conversion.IsExecutionRunning; if (convRunning && !_wasConversionExecuting) { _queueEndedWithConversionErrors = false; } if (_wasConversionExecuting && !convRunning) { var runId = Conversion.CurrentRunId; var runItems = string.IsNullOrWhiteSpace(runId) ? Conversion.QueueTasks.ToList() : Conversion.QueueTasks.Where(i => string.Equals(i.LastRunId, runId, StringComparison.Ordinal)).ToList(); var hasError = runItems.Any(i => i.Status == ConversionQueueStatus.Error); var hasCancelled = runItems.Any(i => i.Status == ConversionQueueStatus.Cancelled); _queueEndedWithConversionErrors = hasError; if (!hasError && !hasCancelled && runItems.Count > 0) { ScheduleCompletionFlash(); } } _wasConversionExecuting = convRunning; var mergeRunning = Merge.IsRunning; if (_wasMergeRunning && !mergeRunning) { if (Merge.LastMergeCompletion == MergeCompletionKind.Success) { ScheduleCompletionFlash(); } } _wasMergeRunning = mergeRunning; var trackBusy = TrackExtraction.IsBusy; if (_wasTrackExtractionBusy && !trackBusy) { if (TrackExtraction.LastRunOutcome == TrackExtractionRunOutcome.Success) { ScheduleCompletionFlash(); } } _wasTrackExtractionBusy = trackBusy; RefreshTaskbarProgress(); RefreshLongOperationUiProperties(); RefreshAppStatusText(); } private void ScheduleCompletionFlash() { var dispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher; if (!dispatcher.CheckAccess()) { dispatcher.BeginInvoke(ScheduleCompletionFlash, DispatcherPriority.Normal); return; } if (_completionFlashTimer is not null) { _completionFlashTimer.Stop(); _completionFlashTimer.Tick -= OnCompletionFlashTick; _completionFlashTimer = null; } _completionFlashActive = true; RefreshLongOperationUiProperties(); _completionFlashTimer = new DispatcherTimer(DispatcherPriority.Normal) { Interval = TimeSpan.FromMilliseconds(750), }; _completionFlashTimer.Tick += OnCompletionFlashTick; _completionFlashTimer.Start(); } private void OnCompletionFlashTick(object? sender, EventArgs e) { if (_completionFlashTimer is not null) { _completionFlashTimer.Stop(); _completionFlashTimer.Tick -= OnCompletionFlashTick; _completionFlashTimer = null; } _completionFlashActive = false; RefreshLongOperationUiProperties(); RefreshAppStatusText(); } private void RefreshLongOperationUiProperties() { var convRunning = Conversion.IsExecutionRunning; var mergeRunning = Merge.IsRunning; var trackExtractBusy = TrackExtraction.IsBusy; var showUi = convRunning || mergeRunning || trackExtractBusy || _completionFlashActive; double display; if (_completionFlashActive) { display = 100.0; } else if (convRunning) { display = Math.Min(Conversion.OverallProgressPercent, 99.99); } else if (mergeRunning) { display = Math.Min(Merge.ProgressPercent, 99.99); } else if (trackExtractBusy) { display = Math.Min(TrackExtraction.OverallProgressPercent, 99.99); } else { display = 0.0; } string taskText; if (_completionFlashActive) { taskText = "Готово"; } else if (convRunning) { taskText = string.IsNullOrEmpty(Conversion.ExecutionPhaseCaption) ? "Конвертация..." : Conversion.ExecutionPhaseCaption; } else if (mergeRunning) { taskText = "Объединение..."; } else if (trackExtractBusy) { taskText = string.IsNullOrEmpty(TrackExtraction.ExecutionPhaseCaption) ? "Извлечение дорожек..." : TrackExtraction.ExecutionPhaseCaption; } else { taskText = string.Empty; } var idle = !showUi; if (_showLongOperationIdlePlaceholder != idle) { _showLongOperationIdlePlaceholder = idle; OnPropertyChanged(nameof(ShowLongOperationIdlePlaceholder)); } if (Math.Abs(_longOperationProgressPercent - display) > 0.0001) { _longOperationProgressPercent = display; OnPropertyChanged(nameof(LongOperationProgressPercent)); } if (_longOperationProgressText != taskText) { _longOperationProgressText = taskText; OnPropertyChanged(nameof(LongOperationProgressText)); } if (_isLongOperationRunning != showUi) { _isLongOperationRunning = showUi; OnPropertyChanged(nameof(IsLongOperationRunning)); } } private void RefreshAppStatusText() { if (Conversion.IsExecutionRunning || Merge.IsRunning || TrackExtraction.IsBusy) { AppStatusText = "В работе"; return; } if (_queueEndedWithConversionErrors || Merge.LastMergeCompletion == MergeCompletionKind.Error || TrackExtraction.LastRunOutcome == TrackExtractionRunOutcome.Error) { AppStatusText = "Ошибка"; return; } AppStatusText = "Готово"; } private void RefreshTaskbarProgress() { if (!Conversion.IsExecutionRunning) { TaskbarProgressValue = 0; TaskbarProgressState = TaskbarItemProgressState.None; return; } var runId = Conversion.CurrentRunId; var runItems = string.IsNullOrWhiteSpace(runId) ? Conversion.QueueTasks.ToList() : Conversion.QueueTasks.Where(i => string.Equals(i.LastRunId, runId, StringComparison.Ordinal)).ToList(); var hasError = runItems.Any(i => i.Status == ConversionQueueStatus.Error); var hasCancelled = runItems.Any(i => i.Status == ConversionQueueStatus.Cancelled); TaskbarProgressState = hasError ? TaskbarItemProgressState.Error : hasCancelled ? TaskbarItemProgressState.Paused : TaskbarItemProgressState.Normal; TaskbarProgressValue = Math.Clamp(Conversion.OverallProgressPercent / 100.0, 0.0, 1.0); } }