using System; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Notifications; using Avalonia.Threading; using DesktopNotifications.FreeDesktop; using DesktopNotifications.Windows; using Microsoft.Extensions.Logging; using Nito.AsyncEx; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Services; using INotificationManager = DesktopNotifications.INotificationManager; namespace StabilityMatrix.Avalonia.Services; [Singleton(typeof(INotificationService))] public class NotificationService(ILogger logger, ISettingsManager settingsManager) : INotificationService, IDisposable { private WindowNotificationManager? notificationManager; private readonly AsyncLock nativeNotificationManagerLock = new(); private volatile INotificationManager? nativeNotificationManager; private volatile bool isNativeNotificationManagerInitialized; public void Initialize( Visual? visual, NotificationPosition position = NotificationPosition.BottomRight, int maxItems = 4 ) { if (notificationManager is not null) return; notificationManager = new WindowNotificationManager(TopLevel.GetTopLevel(visual)) { Position = position, MaxItems = maxItems }; } public void Show(INotification notification) { notificationManager?.Show(notification); } /// public Task ShowPersistentAsync(NotificationKey key, DesktopNotifications.Notification notification) { return ShowAsyncCore(key, notification, null, true); } /// public Task ShowAsync( NotificationKey key, DesktopNotifications.Notification notification, TimeSpan? expiration = null ) { // Use default expiration if not specified expiration ??= TimeSpan.FromSeconds(5); return ShowAsyncCore(key, notification, expiration, false); } private async Task ShowAsyncCore( NotificationKey key, DesktopNotifications.Notification notification, TimeSpan? expiration, bool isPersistent ) { // If settings has option preference, use that, otherwise default if (!settingsManager.Settings.NotificationOptions.TryGetValue(key, out var option)) { option = key.DefaultOption; } switch (option) { case NotificationOption.None: break; case NotificationOption.NativePush: { // If native option is not supported, fallback to toast if (await GetNativeNotificationManagerAsync() is not { } nativeManager) { // Show app toast if (isPersistent) { Dispatcher.UIThread.Invoke( () => ShowPersistent( notification.Title ?? "", notification.Body ?? "", key.Level.ToNotificationType() ) ); } else { Dispatcher.UIThread.Invoke( () => Show( notification.Title ?? "", notification.Body ?? "", key.Level.ToNotificationType(), expiration ) ); } return; } // Show native notification await nativeManager.ShowNotification( notification, expiration is null ? null : DateTimeOffset.Now.Add(expiration.Value) ); break; } case NotificationOption.AppToast: // Show app toast if (isPersistent) { Dispatcher.UIThread.Invoke( () => ShowPersistent( notification.Title ?? "", notification.Body ?? "", key.Level.ToNotificationType() ) ); } else { Dispatcher.UIThread.Invoke( () => Show( notification.Title ?? "", notification.Body ?? "", key.Level.ToNotificationType(), expiration ) ); } break; default: logger.LogError("Unknown notification option {Option}", option); break; } } public void Show( string title, string message, NotificationType appearance = NotificationType.Information, TimeSpan? expiration = null ) { Show(new Notification(title, message, appearance, expiration)); } public void ShowPersistent( string title, string message, NotificationType appearance = NotificationType.Information ) { Show(new Notification(title, message, appearance, TimeSpan.Zero)); } /// public void ShowPersistent( AppException exception, NotificationType appearance = NotificationType.Warning, LogLevel logLevel = LogLevel.Warning ) { // Log exception logger.Log(logLevel, exception, "{Message}", exception.Message); Show(new Notification(exception.Message, exception.Details, appearance, TimeSpan.Zero)); } /// public async Task> TryAsync( Task task, string title = "Error", string? message = null, NotificationType appearance = NotificationType.Error ) { try { return new TaskResult(await task); } catch (Exception e) { Show(new Notification(title, message ?? e.Message, appearance)); return TaskResult.FromException(e); } } /// public async Task> TryAsync( Task task, string title = "Error", string? message = null, NotificationType appearance = NotificationType.Error ) { try { await task; return new TaskResult(true); } catch (Exception e) { Show(new Notification(title, message ?? e.Message, appearance)); return new TaskResult(false, e); } } public void Dispose() { nativeNotificationManager?.Dispose(); GC.SuppressFinalize(this); } public async Task GetNativeNotificationManagerAsync() { if (isNativeNotificationManagerInitialized) return nativeNotificationManager; using var _ = await nativeNotificationManagerLock.LockAsync(); if (isNativeNotificationManagerInitialized) return nativeNotificationManager; try { if (Compat.IsWindows) { var context = WindowsApplicationContext.FromCurrentProcess("Stability Matrix"); nativeNotificationManager = new WindowsNotificationManager(context); await nativeNotificationManager.Initialize(); } else if (Compat.IsLinux) { var context = FreeDesktopApplicationContext.FromCurrentProcess(); nativeNotificationManager = new FreeDesktopNotificationManager(context); await nativeNotificationManager.Initialize(); } else { logger.LogInformation("Native notifications are not supported on this platform"); } } catch (Exception e) { logger.LogError(e, "Failed to initialize native notification manager"); } isNativeNotificationManagerInitialized = true; return nativeNotificationManager; } }