using Dalamud.Interface; using Dalamud.Plugin.Services; using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration.Models; using LightlessSync.Services.Mediator; using LightlessSync.UI; using LightlessSync.UI.Models; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using FFXIVClientStructs.FFXIV.Client.UI; namespace LightlessSync.Services; public class LightlessNotificationService : DisposableMediatorSubscriberBase, IHostedService { private readonly ILogger _logger; private readonly LightlessConfigService _configService; private readonly DalamudUtilService _dalamudUtilService; private LightlessNotificationUI? _notificationUI; public LightlessNotificationService( ILogger logger, LightlessConfigService configService, DalamudUtilService dalamudUtilService, LightlessMediator mediator) : base(logger, mediator) { _logger = logger; _configService = configService; _dalamudUtilService = dalamudUtilService; } public Task StartAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } public void SetNotificationUI(LightlessNotificationUI notificationUI) { _notificationUI = notificationUI; } public void ShowNotification(string title, string message, NotificationType type = NotificationType.Info, TimeSpan? duration = null, List? actions = null, uint? soundEffectId = null) { var notification = new LightlessNotification { Title = title, Message = message, Type = type, Duration = duration ?? TimeSpan.FromSeconds(10), Actions = actions ?? new List(), SoundEffectId = soundEffectId ?? NotificationSounds.GetDefaultSound(type) }; if (notification.SoundEffectId.HasValue) { PlayNotificationSound(notification.SoundEffectId.Value); } Mediator.Publish(new LightlessNotificationMessage(notification)); } public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline) { var notification = new LightlessNotification { Title = "Pair Request Received", Message = $"{senderName} wants to pair with you.", Type = NotificationType.Info, Duration = TimeSpan.FromSeconds(60), SoundEffectId = NotificationSounds.PairRequest, Actions = new List { new() { Id = "accept", Label = "Accept", Icon = FontAwesomeIcon.Check, Color = UIColors.Get("LightlessGreen"), IsPrimary = true, OnClick = (n) => { _logger.LogInformation("Pair request accepted"); onAccept(); n.IsDismissed = true; n.IsAnimatingOut = true; } }, new() { Id = "decline", Label = "Decline", Icon = FontAwesomeIcon.Times, Color = UIColors.Get("DimRed"), IsDestructive = true, OnClick = (n) => { _logger.LogInformation("Pair request declined"); onDecline(); n.IsDismissed = true; n.IsAnimatingOut = true; } } } }; if (notification.SoundEffectId.HasValue) { PlayNotificationSound(notification.SoundEffectId.Value); } Mediator.Publish(new LightlessNotificationMessage(notification)); } public void ShowDownloadCompleteNotification(string fileName, int fileCount, Action? onOpenFolder = null) { var actions = new List(); if (onOpenFolder != null) { actions.Add(new LightlessNotificationAction { Id = "open_folder", Label = "Open Folder", Icon = FontAwesomeIcon.FolderOpen, Color = UIColors.Get("LightlessBlue"), OnClick = (n) => { onOpenFolder(); n.IsDismissed = true; n.IsAnimatingOut = true; } }); } var notification = new LightlessNotification { Title = "Download Complete", Message = fileCount > 1 ? $"Downloaded {fileCount} files successfully." : $"Downloaded {fileName} successfully.", Type = NotificationType.Info, Duration = TimeSpan.FromSeconds(8), Actions = actions, SoundEffectId = NotificationSounds.DownloadComplete }; if (notification.SoundEffectId.HasValue) { PlayNotificationSound(notification.SoundEffectId.Value); } Mediator.Publish(new LightlessNotificationMessage(notification)); } public void ShowErrorNotification(string title, string message, Exception? exception = null, Action? onRetry = null, Action? onViewLog = null) { var actions = new List(); if (onRetry != null) { actions.Add(new LightlessNotificationAction { Id = "retry", Label = "Retry", Icon = FontAwesomeIcon.Redo, Color = UIColors.Get("LightlessBlue"), OnClick = (n) => { onRetry(); n.IsDismissed = true; n.IsAnimatingOut = true; } }); } if (onViewLog != null) { actions.Add(new LightlessNotificationAction { Id = "view_log", Label = "View Log", Icon = FontAwesomeIcon.FileAlt, Color = UIColors.Get("LightlessYellow"), OnClick = (n) => onViewLog() }); } var notification = new LightlessNotification { Title = title, Message = exception != null ? $"{message}\n\nError: {exception.Message}" : message, Type = NotificationType.Error, Duration = TimeSpan.FromSeconds(15), Actions = actions, SoundEffectId = NotificationSounds.Error }; if (notification.SoundEffectId.HasValue) { PlayNotificationSound(notification.SoundEffectId.Value); } Mediator.Publish(new LightlessNotificationMessage(notification)); } public void ShowPairDownloadNotification(List<(string playerName, float progress, string status)> downloadStatus, int queueWaiting = 0) { var userDownloads = downloadStatus.Where(x => x.playerName != "Pair Queue").ToList(); var totalProgress = userDownloads.Count > 0 ? userDownloads.Average(x => x.progress) : 0f; var completedCount = userDownloads.Count(x => x.progress >= 1.0f); var totalCount = userDownloads.Count; var message = ""; if (queueWaiting > 0) { message = $"Queue: {queueWaiting} waiting"; } if (totalCount > 0) { var progressMessage = $"Progress: {completedCount}/{totalCount} completed"; message = string.IsNullOrEmpty(message) ? progressMessage : $"{message}\n{progressMessage}"; } if (userDownloads.Any(x => x.progress < 1.0f)) { var maxNamesToShow = _configService.Current.MaxConcurrentPairApplications; var activeDownloads = userDownloads.Where(x => x.progress < 1.0f).Take(maxNamesToShow); var downloadLines = string.Join("\n", activeDownloads.Select(x => { var statusText = x.status switch { "downloading" => $"{x.progress:P0}", "decompressing" => "decompressing", "queued" => "queued", "waiting" => "waiting for slot", _ => x.status }; return $"• {x.playerName}: {statusText}"; })); message += string.IsNullOrEmpty(message) ? downloadLines : $"\n{downloadLines}"; } var allDownloadsCompleted = userDownloads.All(x => x.progress >= 1.0f) && userDownloads.Any(); var notification = new LightlessNotification { Id = "pair_download_progress", Title = "Downloading Pair Data", Message = message, Type = NotificationType.Info, Duration = TimeSpan.FromMinutes(5), ShowProgress = true, Progress = totalProgress }; Mediator.Publish(new LightlessNotificationMessage(notification)); if (allDownloadsCompleted) { DismissPairDownloadNotification(); } } public void DismissPairDownloadNotification() { Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress")); } private void PlayNotificationSound(uint soundEffectId) { try { try { UIGlobals.PlayChatSoundEffect(soundEffectId); _logger.LogDebug("Played notification sound effect {SoundId} via ChatGui", soundEffectId); } catch (Exception chatEx) { _logger.LogWarning(chatEx, "Failed to play sound via ChatGui for ID {SoundId}", soundEffectId); } } catch (Exception ex) { _logger.LogWarning(ex, "Failed to play notification sound effect {SoundId}", soundEffectId); } } }