using System; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Threading; using LANCommander.Launcher.Settings; using LANCommander.SDK.Providers; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Notify.NET.Abstractions; using Notify.NET.Builder; using LANCommander.Launcher.ViewModels; namespace LANCommander.Launcher.Services; /// /// Sends desktop notifications cross-platform: /// Windows — PowerShell + WinRT XML toast (cover art supported) /// Linux — notify-send (cover art supported via -i flag) /// macOS — osascript display notification (text only) /// public class NotificationService( INotificationService notificationService, ILogger logger, IServiceProvider serviceProvider) { private NotificationSettings GetNotificationSettings() { using var scope = serviceProvider.CreateScope(); var settingsProvider = scope.ServiceProvider.GetRequiredService>(); return settingsProvider.CurrentValue.Notifications; } private static NotificationBuilder ApplySoundTheme(NotificationBuilder builder, NotificationSoundTheme theme) { return theme switch { NotificationSoundTheme.Silent => builder.WithAudio(NotificationAudio.Silent), _ => builder }; } public void NotifyInstallComplete(string gameTitle, string? iconImagePath, string? gridImagePath, Guid gameId) { if (!notificationService.IsSupported) { logger.LogWarning("The notification service is not supported on this platform."); return; } var settings = GetNotificationSettings(); if (!settings.NotifyOnInstallComplete) return; try { var builder = NotificationBuilder.Create(Localize("InstallComplete")) .WithBody(Localize("GameReadyToPlay", gameTitle)); if (gridImagePath != null) builder = builder.WithHeroImage(gridImagePath); else if (iconImagePath != null) builder = builder.WithImage(iconImagePath); builder = ApplySoundTheme(builder, settings.SoundTheme); var request = builder .AddButton(Localize("Play"), _ => Dispatcher.UIThread.InvokeAsync(() => NavigateAndPlayAsync(gameId))) .AddButton(Localize("ViewInLibrary"), _ => Dispatcher.UIThread.InvokeAsync(() => NavigateToGameAsync(gameId))) .Build(); notificationService.ShowAsync(request); } catch (Exception ex) { logger.LogWarning(ex, "Failed to send install-complete notification for {Title}", gameTitle); } } public void NotifyInstallFailed(string gameTitle, Guid gameId) { var settings = GetNotificationSettings(); if (!settings.NotifyOnInstallFailed) return; try { var builder = NotificationBuilder.Create(Localize("InstallFailed")) .WithBody(Localize("GameInstallFailed", gameTitle)) .AddButton(Localize("ViewInLibrary"), _ => Dispatcher.UIThread.InvokeAsync(() => NavigateToGameAsync(gameId))); builder = ApplySoundTheme(builder, settings.SoundTheme); notificationService.ShowAsync(builder.Build()); } catch (Exception ex) { logger.LogWarning(ex, "Failed to send install-failed notification for {Title}", gameTitle); } } public void NotifyChatMessage(string threadTitle, string senderName, string messageContent, Action? onActivated = null) { if (!notificationService.IsSupported) return; var settings = GetNotificationSettings(); if (!settings.NotifyOnChatMessage) return; try { // Truncate long messages for the notification body var body = messageContent.Length > 120 ? messageContent[..117] + "…" : messageContent; var builder = NotificationBuilder.Create($"{senderName} in {threadTitle}") .WithBody(body); if (onActivated != null) builder = builder.AddButton(Localize("OpenChat"), _ => Dispatcher.UIThread.InvokeAsync(onActivated)); builder = ApplySoundTheme(builder, settings.SoundTheme); notificationService.ShowAsync(builder.Build()); } catch (Exception ex) { logger.LogWarning(ex, "Failed to send chat notification for thread {Thread}", threadTitle); } } // ── Navigation helpers ─────────────────────────────────────────────────── private async Task NavigateToGameAsync(Guid gameId) { var shell = serviceProvider.GetRequiredService().ShellViewModel; await shell.NavigateToGameByIdAsync(gameId); if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) desktop.MainWindow?.Activate(); } private async Task NavigateAndPlayAsync(Guid gameId) { var shell = serviceProvider.GetRequiredService().ShellViewModel; await shell.NavigateToGameByIdAsync(gameId); if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) desktop.MainWindow?.Activate(); await shell.GameDetailViewModel.ActionBar.PlayCommand.ExecuteAsync(null); } }