915 lines
33 KiB
C#
915 lines
33 KiB
C#
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>0–100 для <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);
|
||
}
|
||
|
||
}
|