emby-toolbox/EmbyToolbox/ViewModels/MainWindowViewModel.cs
2026-05-16 15:09:56 +05:00

915 lines
33 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.RemoveForeignTracksByDefault = _loadedSettings.RemoveForeignTracksByDefault;
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<string> LogLevelOptions { get; } = new[]
{
LogLevel.Debug.ToString(),
LogLevel.Info.ToString(),
LogLevel.Warning.ToString(),
LogLevel.Error.ToString()
};
public ObservableCollection<ConversionProfilePresetRow> ConversionProfiles { get; } = new();
private ConversionProfilePresetRow? _selectedConversionProfile;
public ConversionProfilePresetRow? SelectedConversionProfile
{
get => _selectedConversionProfile;
set
{
if (_selectedConversionProfile == value)
{
return;
}
_selectedConversionProfile = value;
OnPropertyChanged();
RemoveConversionProfileCommand.RaiseCanExecuteChanged();
}
}
public IReadOnlyList<string> ConversionContainerOptions { get; } = ["MKV", "MP4", "MOV", "WEBM"];
public IReadOnlyList<string> ConversionVideoCodecOptions { get; } = ["H.264", "H.265", "AV1", "Copy"];
public IReadOnlyList<string> ConversionPixelFormatOptions { get; } = ["yuv420p", "yuv420p10le", "yuv422p", "yuv444p"];
public IReadOnlyList<string> ConversionResolutionOptions { get; } = ["Без изменений", "Максимум 2160p", "Максимум 1440p", "Максимум 1080p", "Максимум 720p"];
public IReadOnlyList<string> ConversionFpsOptions { get; } = ["Без изменений", "Максимум 60", "Максимум 30", "Максимум 25", "Максимум 24"];
public IReadOnlyList<string> ConversionAudioCodecOptions { get; } = ["AAC", "AC3", "EAC3", "Opus", "MP3", "FLAC", "Copy"];
public IReadOnlyList<string> ConversionBitrateOptions { get; } = ["96 kbps", "128 kbps", "160 kbps", "192 kbps", "256 kbps", "320 kbps"];
public IReadOnlyList<string> ConversionVideoBitrateOptions => VideoBitratePolicy.UiOptions;
public IReadOnlyList<string> ConversionYesNoOptions { get; } = ["Да", "Нет"];
public IReadOnlyList<string> 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; }
/// <summary>Проверка звука и Windows toast без учёта флагов в настройках.</summary>
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();
}
}
/// <summary>Звук Windows после успешной/неуспешной обработки всей очереди конвертации.</summary>
public bool NotifyCompletionSoundAfterQueue
{
get => _notifyCompletionSoundAfterQueue;
set
{
if (_notifyCompletionSoundAfterQueue == value)
{
return;
}
_notifyCompletionSoundAfterQueue = value;
OnPropertyChanged();
}
}
/// <summary>Toast Windows после завершения очереди конвертации.</summary>
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");
}
}
/// <summary>0100 для <see cref="System.Windows.Controls.ProgressBar"/>: во время задачи не выше 99.99; 100 при коротком «флеше» после успеха.</summary>
public double LongOperationProgressPercent => _longOperationProgressPercent;
/// <summary>Описание текущей задачи для строки статуса (объединение, конвертация, завершение).</summary>
public string LongOperationProgressText => _longOperationProgressText;
/// <summary>Показывать «Нет задач», когда нет длительной операции и нет завершающего флеша.</summary>
public bool ShowLongOperationIdlePlaceholder => _showLongOperationIdlePlaceholder;
/// <summary>Показывать блок прогресса: конвертация, объединение или краткое завершение 100%.</summary>
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,
RemoveForeignTracksByDefault = Conversion.RemoveForeignTracksByDefault,
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;
Conversion.RemoveForeignTracksByDefault = _loadedSettings.RemoveForeignTracksByDefault;
_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<LogLevel>(MinimumFileLogLevel, true, out var level))
{
_logging.MinimumFileLogLevel = level;
return;
}
_logging.MinimumFileLogLevel = LogLevel.Info;
}
private void LoadConversionProfiles(IEnumerable<ConversionProfileSettingsEntry> 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());
}
}
/// <summary>Применяет профили из загруженного .conv_setup (нормализация как при чтении settings.json).</summary>
private void ApplyConversionProfilesFromQueueSetupDocument(IReadOnlyList<ConversionProfileSettingsEntry> 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();
}
else if (e.PropertyName == nameof(ConversionViewModel.RemoveForeignTracksByDefault))
{
PersistRemoveForeignTracksByDefault();
}
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 PersistRemoveForeignTracksByDefault()
{
try
{
var settings = _settingsService.Load();
settings.RemoveForeignTracksByDefault = Conversion.RemoveForeignTracksByDefault;
_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);
}
}