emby-toolbox/EmbyToolbox/Services/NotificationService.cs
Emby Toolbox 6264b487fe Initial commit: Emby Toolbox (conversion scroll fix, bulk Del for tracks).
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 21:33:47 +05:00

252 lines
9.0 KiB
C#
Raw 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.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;
/// <summary>Звуковые и toast-уведомления после обработки очереди конвертации.</summary>
public sealed class NotificationService
{
/// <summary>Должен совпадать с SetCurrentProcessExplicitAppUserModelID при старте приложения.</summary>
public const string ToastAppUserModelId = "EmbyToolbox.Desktop";
private readonly LoggingService _logging;
private readonly Func<bool> _soundPref;
private readonly Func<bool> _toastPref;
private readonly Dispatcher? _dispatcher;
public NotificationService(
LoggingService logging,
Func<bool> notifyCompletionSoundAfterQueueEnabled,
Func<bool> 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");
}
});
}
/// <summary>Кнопка «Проверить уведомление»: звук и toast вне зависимости от флагов уведомлений.</summary>
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 =
"<?xml version=\"1.0\" encoding=\"UTF-16\"?>" +
"<toast>" +
"<visual><binding template=\"ToastGeneric\">" +
"<text id=\"1\">" + EscapeXml(title) + "</text>" +
"<text id=\"2\">" + EscapeXml(body) + "</text>" +
"</binding></visual>" +
"</toast>";
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("&", "&amp;", StringComparison.Ordinal)
.Replace("<", "&lt;", StringComparison.Ordinal)
.Replace(">", "&gt;", StringComparison.Ordinal)
.Replace("\"", "&quot;", StringComparison.Ordinal)
.Replace("'", "&apos;", StringComparison.Ordinal);
}
}