259 lines
9.3 KiB
C#
259 lines
9.3 KiB
C#
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.IsNullOrWhiteSpace(ToastShortcutRegistration.LastDiagnostics))
|
||
{
|
||
_logging.Warning(
|
||
$"Windows toast: ярлык Start Menu не подготовлен ({ToastShortcutRegistration.LastDiagnostics})",
|
||
"notify");
|
||
}
|
||
|
||
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("&", "&", StringComparison.Ordinal)
|
||
.Replace("<", "<", StringComparison.Ordinal)
|
||
.Replace(">", ">", StringComparison.Ordinal)
|
||
.Replace("\"", """, StringComparison.Ordinal)
|
||
.Replace("'", "'", StringComparison.Ordinal);
|
||
}
|
||
}
|