using System.Globalization; using System.Media; using System.Runtime.Versioning; using System.Windows.Threading; using Windows.Data.Xml.Dom; using Windows.UI.Notifications; using EmbyToolbox.Interop; namespace EmbyToolbox.Services; /// Звуковые и toast-уведомления после обработки очереди конвертации. public sealed class NotificationService { /// Должен совпадать с SetCurrentProcessExplicitAppUserModelID при старте приложения. public const string ToastAppUserModelId = "EmbyToolbox.Desktop"; private readonly LoggingService _logging; private readonly Func _soundPref; private readonly Func _toastPref; private readonly Dispatcher? _dispatcher; public NotificationService( LoggingService logging, Func notifyCompletionSoundAfterQueueEnabled, Func notifyWindowsToastAfterQueueEnabled, Dispatcher? uiDispatcher) { _logging = logging; _soundPref = notifyCompletionSoundAfterQueueEnabled; _toastPref = notifyWindowsToastAfterQueueEnabled; _dispatcher = uiDispatcher; LogAppUserModelRegistrationState(); } private void LogAppUserModelRegistrationState() { if (string.Equals(AppUserModelIdRegistration.LastRegisteredId, ToastAppUserModelId, StringComparison.Ordinal)) { _logging.Info($"App User Model ID: {ToastAppUserModelId}", "notify"); return; } if (!string.IsNullOrWhiteSpace(AppUserModelIdRegistration.LastDiagnostics)) { _logging.Warning( $"Windows toast недоступен: не удалось зарегистрировать AppUserModelID ({AppUserModelIdRegistration.LastDiagnostics})", "notify"); return; } _logging.Warning("Windows toast недоступен: статус регистрации AppUserModelID неизвестен", "notify"); } public void NotifyQueueCompleted(int successCount, int errorCount) { successCount = Math.Max(0, successCount); errorCount = Math.Max(0, errorCount); QueueOnUiIdle( () => { PlayQueueCompletionSound(successCount, errorCount); string body; if (errorCount > 0) { body = string.Format( CultureInfo.CurrentCulture, "Обработка завершена с ошибками. Успешно: {0}, ошибок: {1}.", successCount, errorCount); } else { body = string.Format( CultureInfo.CurrentCulture, "Обработка завершена. Файлов обработано: {0}.", successCount); } TryShowWindowsToastInternal("Emby Toolbox", body, respectToastPreference: true, contextHint: null); }); } public void NotifyQueueCancelled() { QueueOnUiIdle( () => TryShowWindowsToastInternal( "Emby Toolbox", "Обработка остановлена пользователем.", respectToastPreference: true, contextHint: null)); } public void PlayCompletionSound(bool hasErrors) { QueueOnUiIdle( () => { if (!_soundPref()) { _logging.Info("уведомления отключены в настройках (звук)", "notify"); return; } try { ResolveCompletionSound(hasErrors ? 2 : 0).Play(); _logging.Info("звук уведомления о завершении очереди воспроизведён", "notify"); } catch (Exception ex) { _logging.Warning($"ошибка воспроизведения звука: {ex.Message}", "notify"); } }); } /// Кнопка «Проверить уведомление»: звук и toast вне зависимости от флагов уведомлений. public void ShowSettingsTestNotification() { QueueOnUiIdle( () => { try { SystemSounds.Asterisk.Play(); _logging.Info("тест: воспроизведён звук Asterisk", "notify"); } catch (Exception ex) { _logging.Warning($"ошибка тестового звука: {ex.Message}", "notify"); } TryShowWindowsToastInternal( "Emby Toolbox", "Тестовое уведомление", respectToastPreference: false, contextHint: "тест из настроек"); }); } private static SystemSound ResolveCompletionSound(int kind) => kind switch { 1 => SystemSounds.Exclamation, >= 2 => SystemSounds.Hand, _ => SystemSounds.Asterisk }; private void QueueOnUiIdle(Action work) { if (_dispatcher is { HasShutdownStarted: false }) { _dispatcher.BeginInvoke(work, DispatcherPriority.ApplicationIdle); } else { try { work(); } catch (Exception ex) { _logging.Warning($"уведомление: ошибка без UI dispatcher — {ex.Message}", "notify"); } } } private void PlayQueueCompletionSound(int successCount, int errorCount) { if (!_soundPref()) { _logging.Info("уведомления отключены в настройках (звук)", "notify"); return; } try { var kind = errorCount <= 0 ? 0 : successCount > 0 ? 1 : 2; ResolveCompletionSound(kind).Play(); _logging.Info("звук уведомления о завершении очереди воспроизведён", "notify"); } catch (Exception ex) { _logging.Warning($"ошибка воспроизведения звука: {ex.Message}", "notify"); } } private void TryShowWindowsToastInternal(string title, string body, bool respectToastPreference, string? contextHint) { if (respectToastPreference && !_toastPref()) { _logging.Info("уведомления отключены в настройках", "notify"); return; } var ctx = string.IsNullOrWhiteSpace(contextHint) ? string.Empty : $" ({contextHint})"; _logging.Info($"попытка показать toast: «{EscapeForLog(title)}»{ctx}", "notify"); if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 10240)) { _logging.Warning("Windows toast недоступен: требуется Windows 10 (10240) или новее", "notify"); return; } try { ShowWindowsToastCore(title, body); _logging.Info("toast успешно отправлен", "notify"); } catch (Exception ex) { _logging.Warning($"ошибка toast: {ex.Message}", "notify", ex); _logging.Warning($"Windows toast недоступен: {ex.Message}", "notify"); } } private static string EscapeForLog(string s) => s.Replace("\r\n", " ", StringComparison.Ordinal).Trim(); [SupportedOSPlatform("windows10.0.10240.0")] private static void ShowWindowsToastCore(string title, string body) { var xmlPayload = "" + "" + "" + "" + EscapeXml(title) + "" + "" + EscapeXml(body) + "" + "" + ""; var doc = new XmlDocument(); doc.LoadXml(xmlPayload); var toast = new ToastNotification(doc); toast.ExpirationTime = DateTimeOffset.UtcNow.AddMinutes(30); var notifier = ToastNotificationManager.CreateToastNotifier(ToastAppUserModelId); notifier.Show(toast); } private static string EscapeXml(string s) { return s.Replace("&", "&", StringComparison.Ordinal) .Replace("<", "<", StringComparison.Ordinal) .Replace(">", ">", StringComparison.Ordinal) .Replace("\"", """, StringComparison.Ordinal) .Replace("'", "'", StringComparison.Ordinal); } }