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);
}
}