diff --git a/LightlessSync/PlayerData/Pairs/PairManager.cs b/LightlessSync/PlayerData/Pairs/PairManager.cs index 51bead5..5a92649 100644 --- a/LightlessSync/PlayerData/Pairs/PairManager.cs +++ b/LightlessSync/PlayerData/Pairs/PairManager.cs @@ -25,7 +25,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase private readonly LightlessConfigService _configurationService; private readonly IContextMenu _dalamudContextMenu; private readonly PairFactory _pairFactory; - private readonly LightlessNotificationService _lightlessNotificationService; + private readonly NotificationService _lightlessNotificationService; private Lazy> _directPairsInternal; private Lazy>> _groupPairsInternal; private Lazy>> _pairsWithGroupsInternal; @@ -37,7 +37,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase public PairManager(ILogger logger, PairFactory pairFactory, LightlessConfigService configurationService, LightlessMediator mediator, IContextMenu dalamudContextMenu, PairProcessingLimiter pairProcessingLimiter, - LightlessNotificationService lightlessNotificationService) : base(logger, mediator) + NotificationService lightlessNotificationService) : base(logger, mediator) { _pairFactory = pairFactory; _configurationService = configurationService; diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 054bfbc..d370dd3 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -1,4 +1,4 @@ -using Dalamud.Game; +using Dalamud.Game; using Dalamud.Game.ClientState.Objects; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Windowing; @@ -145,7 +145,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton((s) => new DtrEntry(s.GetRequiredService>(), dtrBar, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddSingleton(s => new PairManager(s.GetRequiredService>(), s.GetRequiredService(), - s.GetRequiredService(), s.GetRequiredService(), contextMenu, s.GetRequiredService(), s.GetRequiredService())); + s.GetRequiredService(), s.GetRequiredService(), contextMenu, s.GetRequiredService(), s.GetRequiredService())); collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(addonLifecycle); @@ -172,16 +172,14 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); - collection.AddSingleton((s) => new NotificationService(s.GetRequiredService>(), - s.GetRequiredService(), s.GetRequiredService(), - notificationManager, chatGui, s.GetRequiredService())); - collection.AddSingleton((s) => new LightlessNotificationService( - s.GetRequiredService>(), + collection.AddSingleton((s) => new NotificationService( + s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), notificationManager, chatGui, - s.GetRequiredService())); + s.GetRequiredService(), + s.GetServices())); collection.AddSingleton((s) => { var httpClient = new HttpClient(); @@ -257,7 +255,7 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), - s.GetRequiredService())); + s.GetRequiredService())); collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); @@ -276,7 +274,6 @@ public sealed class Plugin : IDalamudPlugin collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); - collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); diff --git a/LightlessSync/Services/LightlessNotificationService.cs b/LightlessSync/Services/LightlessNotificationService.cs deleted file mode 100644 index 9e16bde..0000000 --- a/LightlessSync/Services/LightlessNotificationService.cs +++ /dev/null @@ -1,501 +0,0 @@ -using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Interface; -using Dalamud.Interface.ImGuiNotification; -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; -using NotificationType = LightlessSync.LightlessConfiguration.Models.NotificationType; - -namespace LightlessSync.Services; -public class LightlessNotificationService : DisposableMediatorSubscriberBase, IHostedService -{ - private readonly ILogger _logger; - private readonly LightlessConfigService _configService; - private readonly DalamudUtilService _dalamudUtilService; - private readonly INotificationManager _notificationManager; - private readonly IChatGui _chatGui; - private LightlessNotificationUI? _notificationUI; - public LightlessNotificationService( - ILogger logger, - LightlessConfigService configService, - DalamudUtilService dalamudUtilService, - INotificationManager notificationManager, - IChatGui chatGui, - LightlessMediator mediator) : base(logger, mediator) - { - _logger = logger; - _configService = configService; - _dalamudUtilService = dalamudUtilService; - _notificationManager = notificationManager; - _chatGui = chatGui; - } - public Task StartAsync(CancellationToken cancellationToken) - { - Mediator.Subscribe(this, HandleNotificationMessage); - 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(_configService.Current.DefaultNotificationDurationSeconds), - Actions = actions ?? new List(), - SoundEffectId = GetSoundEffectId(type, soundEffectId), - ShowProgress = _configService.Current.ShowNotificationProgress, - CreatedAt = DateTime.UtcNow - }; - - if (_configService.Current.AutoDismissOnAction && notification.Actions.Any()) - { - foreach (var action in notification.Actions) - { - var originalOnClick = action.OnClick; - action.OnClick = (n) => - { - originalOnClick(n); - if (_configService.Current.AutoDismissOnAction) - { - n.IsDismissed = true; - n.IsAnimatingOut = true; - } - }; - } - } - - if (notification.SoundEffectId.HasValue && _configService.Current.EnableNotificationSounds) - { - 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 uint? GetSoundEffectId(NotificationType type, uint? overrideSoundId) - { - if (!_configService.Current.EnableNotificationSounds) - return null; - - if (overrideSoundId.HasValue) - return overrideSoundId; - - if (_configService.Current.UseCustomSounds) - { - return type switch - { - NotificationType.Info => _configService.Current.CustomInfoSoundId, - NotificationType.Warning => _configService.Current.CustomWarningSoundId, - NotificationType.Error => _configService.Current.CustomErrorSoundId, - _ => NotificationSounds.GetDefaultSound(type) - }; - } - - return NotificationSounds.GetDefaultSound(type); - } - - 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); - } - } - - private void HandleNotificationMessage(NotificationMessage msg) - { - _logger.LogInformation("{msg}", msg.ToString()); - - if (!_dalamudUtilService.IsLoggedIn) return; - - // Get both old and new notification locations - var oldLocation = msg.Type switch - { - NotificationType.Info => _configService.Current.InfoNotification, - NotificationType.Warning => _configService.Current.WarningNotification, - NotificationType.Error => _configService.Current.ErrorNotification, - _ => NotificationLocation.Nowhere - }; - - var newLocation = msg.Type switch - { - NotificationType.Info => _configService.Current.LightlessInfoNotification, - NotificationType.Warning => _configService.Current.LightlessWarningNotification, - NotificationType.Error => _configService.Current.LightlessErrorNotification, - _ => NotificationLocation.LightlessUI - }; - - // Show notifications based on system selection with backwards compatibility - if (!_configService.Current.UseLightlessNotifications) - { - // Only use old system when new system is disabled - ShowNotificationLocationBased(msg, oldLocation); - } - else - { - // Use new enhanced system as primary - ShowNotificationLocationBased(msg, newLocation); - - // Also use old system as fallback for backwards compatibility - // Only if it's different from the new location and not "Nowhere" - if (oldLocation != NotificationLocation.Nowhere && - oldLocation != newLocation && - !IsLightlessLocation(oldLocation)) - { - ShowNotificationLocationBased(msg, oldLocation); - } - } - } - - private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location) - { - switch (location) - { - case NotificationLocation.Toast: - ShowToast(msg); - break; - - case NotificationLocation.Chat: - ShowChat(msg); - break; - - case NotificationLocation.Both: - ShowToast(msg); - ShowChat(msg); - break; - - case NotificationLocation.LightlessUI: - ShowLightlessNotification(msg); - break; - - case NotificationLocation.ChatAndLightlessUI: - ShowChat(msg); - ShowLightlessNotification(msg); - break; - - case NotificationLocation.Nowhere: - break; - } - } - - private void ShowLightlessNotification(NotificationMessage msg) - { - var duration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(_configService.Current.DefaultNotificationDurationSeconds); - uint? soundId = null; - - if (_configService.Current.EnableNotificationSounds) - { - if (_configService.Current.UseCustomSounds) - { - soundId = msg.Type switch - { - NotificationType.Info => _configService.Current.CustomInfoSoundId, - NotificationType.Warning => _configService.Current.CustomWarningSoundId, - NotificationType.Error => _configService.Current.CustomErrorSoundId, - _ => NotificationSounds.GetDefaultSound(msg.Type) - }; - } - else - { - soundId = NotificationSounds.GetDefaultSound(msg.Type); - } - } - - ShowNotification(msg.Title ?? "Lightless Sync", msg.Message ?? string.Empty, msg.Type, duration, null, soundId); - } - - private void ShowToast(NotificationMessage msg) - { - Dalamud.Interface.ImGuiNotification.NotificationType dalamudType = msg.Type switch - { - NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error, - NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning, - NotificationType.Info => Dalamud.Interface.ImGuiNotification.NotificationType.Info, - _ => Dalamud.Interface.ImGuiNotification.NotificationType.Info - }; - - _notificationManager.AddNotification(new Notification() - { - Content = msg.Message ?? string.Empty, - Title = msg.Title, - Type = dalamudType, - Minimized = false, - InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3) - }); - } - - private void ShowChat(NotificationMessage msg) - { - switch (msg.Type) - { - case NotificationType.Info: - PrintInfoChat(msg.Message); - break; - - case NotificationType.Warning: - PrintWarnChat(msg.Message); - break; - - case NotificationType.Error: - PrintErrorChat(msg.Message); - break; - } - } - - private void PrintErrorChat(string? message) - { - SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message); - _chatGui.PrintError(se.BuiltString); - } - - private void PrintInfoChat(string? message) - { - SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ").AddItalics(message ?? string.Empty); - _chatGui.Print(se.BuiltString); - } - - private void PrintWarnChat(string? message) - { - SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ").AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff(); - _chatGui.Print(se.BuiltString); - } - - private bool IsLightlessLocation(NotificationLocation location) - { - return location switch - { - NotificationLocation.LightlessUI => true, - NotificationLocation.ChatAndLightlessUI => true, - _ => false - }; - } -} \ No newline at end of file diff --git a/LightlessSync/Services/NotificationService.cs b/LightlessSync/Services/NotificationService.cs index 4ee361b..990e2b6 100644 --- a/LightlessSync/Services/NotificationService.cs +++ b/LightlessSync/Services/NotificationService.cs @@ -1,99 +1,377 @@ -using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; 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; using NotificationType = LightlessSync.LightlessConfiguration.Models.NotificationType; namespace LightlessSync.Services; - public class NotificationService : DisposableMediatorSubscriberBase, IHostedService { + private readonly ILogger _logger; + private readonly LightlessConfigService _configService; private readonly DalamudUtilService _dalamudUtilService; private readonly INotificationManager _notificationManager; private readonly IChatGui _chatGui; - private readonly LightlessConfigService _configurationService; - - public NotificationService(ILogger logger, LightlessMediator mediator, + private readonly LightlessNotificationUI? _notificationUI; + + public NotificationService( + ILogger logger, + LightlessConfigService configService, DalamudUtilService dalamudUtilService, INotificationManager notificationManager, - IChatGui chatGui, LightlessConfigService configurationService) : base(logger, mediator) + IChatGui chatGui, + LightlessMediator mediator, + IEnumerable windows) : base(logger, mediator) { + _logger = logger; + _configService = configService; _dalamudUtilService = dalamudUtilService; _notificationManager = notificationManager; _chatGui = chatGui; - _configurationService = configurationService; + _notificationUI = windows.OfType().FirstOrDefault(); } - public Task StartAsync(CancellationToken cancellationToken) { - Mediator.Subscribe(this, ShowNotification); + Mediator.Subscribe(this, HandleNotificationMessage); return Task.CompletedTask; } - public Task StopAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } - - private void PrintErrorChat(string? message) + public void ShowNotification(string title, string message, NotificationType type = NotificationType.Info, + TimeSpan? duration = null, List? actions = null, uint? soundEffectId = null) { - SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message); - _chatGui.PrintError(se.BuiltString); - } - - private void PrintInfoChat(string? message) - { - SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ").AddItalics(message ?? string.Empty); - _chatGui.Print(se.BuiltString); - } - - private void PrintWarnChat(string? message) - { - SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ").AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff(); - _chatGui.Print(se.BuiltString); - } - - private void ShowChat(NotificationMessage msg) - { - switch (msg.Type) + var notification = new LightlessNotification { - case NotificationType.Info: - PrintInfoChat(msg.Message); - break; + Title = title, + Message = message, + Type = type, + Duration = duration ?? TimeSpan.FromSeconds(_configService.Current.DefaultNotificationDurationSeconds), + Actions = actions ?? new List(), + SoundEffectId = GetSoundEffectId(type, soundEffectId), + ShowProgress = _configService.Current.ShowNotificationProgress, + CreatedAt = DateTime.UtcNow + }; + + if (_configService.Current.AutoDismissOnAction && notification.Actions.Any()) + { + foreach (var action in notification.Actions) + { + var originalOnClick = action.OnClick; + action.OnClick = (n) => + { + originalOnClick(n); + if (_configService.Current.AutoDismissOnAction) + { + n.IsDismissed = true; + n.IsAnimatingOut = true; + } + }; + } + } + + if (notification.SoundEffectId.HasValue && _configService.Current.EnableNotificationSounds) + { + 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; + } + } + } + }; - case NotificationType.Warning: - PrintWarnChat(msg.Message); - break; + if (notification.SoundEffectId.HasValue) + { + PlayNotificationSound(notification.SoundEffectId.Value); + } - case NotificationType.Error: - PrintErrorChat(msg.Message); - break; + 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(); } } - private void ShowNotification(NotificationMessage msg) + public void DismissPairDownloadNotification() { - Logger.LogInformation("{msg}", msg.ToString()); + Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress")); + } + + private uint? GetSoundEffectId(NotificationType type, uint? overrideSoundId) + { + if (!_configService.Current.EnableNotificationSounds) + return null; + + if (overrideSoundId.HasValue) + return overrideSoundId; + + if (_configService.Current.UseCustomSounds) + { + return type switch + { + NotificationType.Info => _configService.Current.CustomInfoSoundId, + NotificationType.Warning => _configService.Current.CustomWarningSoundId, + NotificationType.Error => _configService.Current.CustomErrorSoundId, + _ => NotificationSounds.GetDefaultSound(type) + }; + } + + return NotificationSounds.GetDefaultSound(type); + } + + 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); + } + } + + private void HandleNotificationMessage(NotificationMessage msg) + { + _logger.LogInformation("{msg}", msg.ToString()); if (!_dalamudUtilService.IsLoggedIn) return; - switch (msg.Type) + // Get both old and new notification locations + var oldLocation = msg.Type switch { - case NotificationType.Info: - ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification); - break; + NotificationType.Info => _configService.Current.InfoNotification, + NotificationType.Warning => _configService.Current.WarningNotification, + NotificationType.Error => _configService.Current.ErrorNotification, + _ => NotificationLocation.Nowhere + }; - case NotificationType.Warning: - ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification); - break; + var newLocation = msg.Type switch + { + NotificationType.Info => _configService.Current.LightlessInfoNotification, + NotificationType.Warning => _configService.Current.LightlessWarningNotification, + NotificationType.Error => _configService.Current.LightlessErrorNotification, + _ => NotificationLocation.LightlessUI + }; - case NotificationType.Error: - ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification); - break; + // Show notifications based on system selection with backwards compatibility + if (!_configService.Current.UseLightlessNotifications) + { + // Only use old system when new system is disabled + ShowNotificationLocationBased(msg, oldLocation); + } + else + { + // Use new system as primary + ShowNotificationLocationBased(msg, newLocation); + + // Also use old system as fallback for backwards compatibility + // Only if it's different from the new location and not "Nowhere" + if (oldLocation != NotificationLocation.Nowhere && + oldLocation != newLocation && + !IsLightlessLocation(oldLocation)) + { + ShowNotificationLocationBased(msg, oldLocation); + } } } @@ -114,11 +392,46 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ ShowChat(msg); break; + case NotificationLocation.LightlessUI: + ShowLightlessNotification(msg); + break; + + case NotificationLocation.ChatAndLightlessUI: + ShowChat(msg); + ShowLightlessNotification(msg); + break; + case NotificationLocation.Nowhere: break; } } + private void ShowLightlessNotification(NotificationMessage msg) + { + var duration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(_configService.Current.DefaultNotificationDurationSeconds); + uint? soundId = null; + + if (_configService.Current.EnableNotificationSounds) + { + if (_configService.Current.UseCustomSounds) + { + soundId = msg.Type switch + { + NotificationType.Info => _configService.Current.CustomInfoSoundId, + NotificationType.Warning => _configService.Current.CustomWarningSoundId, + NotificationType.Error => _configService.Current.CustomErrorSoundId, + _ => NotificationSounds.GetDefaultSound(msg.Type) + }; + } + else + { + soundId = NotificationSounds.GetDefaultSound(msg.Type); + } + } + + ShowNotification(msg.Title ?? "Lightless Sync", msg.Message ?? string.Empty, msg.Type, duration, null, soundId); + } + private void ShowToast(NotificationMessage msg) { Dalamud.Interface.ImGuiNotification.NotificationType dalamudType = msg.Type switch @@ -138,4 +451,50 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3) }); } + + private void ShowChat(NotificationMessage msg) + { + switch (msg.Type) + { + case NotificationType.Info: + PrintInfoChat(msg.Message); + break; + + case NotificationType.Warning: + PrintWarnChat(msg.Message); + break; + + case NotificationType.Error: + PrintErrorChat(msg.Message); + break; + } + } + + private void PrintErrorChat(string? message) + { + SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message); + _chatGui.PrintError(se.BuiltString); + } + + private void PrintInfoChat(string? message) + { + SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ").AddItalics(message ?? string.Empty); + _chatGui.Print(se.BuiltString); + } + + private void PrintWarnChat(string? message) + { + SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ").AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff(); + _chatGui.Print(se.BuiltString); + } + + private bool IsLightlessLocation(NotificationLocation location) + { + return location switch + { + NotificationLocation.LightlessUI => true, + NotificationLocation.ChatAndLightlessUI => true, + _ => false + }; + } } \ No newline at end of file diff --git a/LightlessSync/Services/UiService.cs b/LightlessSync/Services/UiService.cs index 73fc289..bb69301 100644 --- a/LightlessSync/Services/UiService.cs +++ b/LightlessSync/Services/UiService.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface; +using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Windowing; using LightlessSync.LightlessConfiguration; @@ -23,7 +23,7 @@ public sealed class UiService : DisposableMediatorSubscriberBase IEnumerable windows, UiFactory uiFactory, FileDialogManager fileDialogManager, LightlessMediator lightlessMediator, - LightlessNotificationService lightlessNotificationService) : base(logger, lightlessMediator) + NotificationService notificationService) : base(logger, lightlessMediator) { _logger = logger; _logger.LogTrace("Creating {type}", GetType().Name); @@ -41,12 +41,6 @@ public sealed class UiService : DisposableMediatorSubscriberBase foreach (var window in windows) { _windowSystem.AddWindow(window); - - // Connect the notification service to the notification UI - if (window is LightlessNotificationUI notificationUI) - { - lightlessNotificationService.SetNotificationUI(notificationUI); - } } Mediator.Subscribe(this, (msg) => diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index c724124..c264681 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -1,4 +1,4 @@ -using System; +using System; using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility; @@ -87,7 +87,7 @@ public class CompactUi : WindowMediatorSubscriberBase IpcManager ipcManager, BroadcastService broadcastService, CharacterAnalyzer characterAnalyzer, - PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, LightlessNotificationService lightlessNotificationService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService) + PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService) { _uiSharedService = uiShared; _configService = configService; diff --git a/LightlessSync/UI/DownloadUi.cs b/LightlessSync/UI/DownloadUi.cs index 48432d7..45fce1a 100644 --- a/LightlessSync/UI/DownloadUi.cs +++ b/LightlessSync/UI/DownloadUi.cs @@ -1,4 +1,4 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface.Colors; using LightlessSync.LightlessConfiguration; using LightlessSync.PlayerData.Handlers; @@ -21,12 +21,12 @@ public class DownloadUi : WindowMediatorSubscriberBase private readonly UiSharedService _uiShared; private readonly PairProcessingLimiter _pairProcessingLimiter; private readonly ConcurrentDictionary _uploadingPlayers = new(); - private readonly LightlessNotificationService _notificationService; + private readonly NotificationService _notificationService; private bool _notificationDismissed = true; public DownloadUi(ILogger logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService, PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared, - PerformanceCollectorService performanceCollectorService, LightlessNotificationService notificationService) + PerformanceCollectorService performanceCollectorService, NotificationService notificationService) : base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService) { _dalamudUtilService = dalamudUtilService; diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 2573a18..ce7e63a 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -61,7 +61,7 @@ public class SettingsUi : WindowMediatorSubscriberBase private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress; private readonly NameplateService _nameplateService; private readonly NameplateHandler _nameplateHandler; - private readonly LightlessNotificationService _lightlessNotificationService; + private readonly NotificationService _lightlessNotificationService; private (int, int, FileCacheEntity) _currentProgress; private bool _deleteAccountPopupModalShown = false; private bool _deleteFilesPopupModalShown = false; @@ -107,7 +107,7 @@ public class SettingsUi : WindowMediatorSubscriberBase DalamudUtilService dalamudUtilService, HttpClient httpClient, NameplateService nameplateService, NameplateHandler nameplateHandler, - LightlessNotificationService lightlessNotificationService) : base(logger, mediator, "Lightless Sync Settings", performanceCollector) + NotificationService lightlessNotificationService) : base(logger, mediator, "Lightless Sync Settings", performanceCollector) { _configService = configService; _pairManager = pairManager; diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index 782e211..f9f0667 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -29,14 +29,14 @@ public class TopTabMenu private bool _pairRequestsExpanded; // useless for now private int _lastRequestCount; private readonly UiSharedService _uiSharedService; - private readonly LightlessNotificationService _lightlessNotificationService; + private readonly NotificationService _lightlessNotificationService; private string _filter = string.Empty; private int _globalControlCountdown = 0; private float _pairRequestsHeight = 150f; private string _pairToAdd = string.Empty; private SelectedTab _selectedTab = SelectedTab.None; - public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, LightlessNotificationService lightlessNotificationService) + public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService) { _lightlessMediator = lightlessMediator; _apiController = apiController; diff --git a/LightlessSync/WebAPI/SignalR/ApiController.cs b/LightlessSync/WebAPI/SignalR/ApiController.cs index fb21d2a..b1170be 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.cs @@ -1,4 +1,4 @@ -using Dalamud.Utility; +using Dalamud.Utility; using LightlessSync.API.Data; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto; @@ -32,7 +32,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL private readonly ServerConfigurationManager _serverManager; private readonly TokenProvider _tokenProvider; private readonly LightlessConfigService _lightlessConfigService; - private readonly LightlessNotificationService _lightlessNotificationService; + private readonly NotificationService _lightlessNotificationService; private CancellationTokenSource _connectionCancellationTokenSource; private ConnectionDto? _connectionDto; private bool _doNotNotifyOnNextInfo = false; @@ -45,7 +45,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL public ApiController(ILogger logger, HubFactory hubFactory, DalamudUtilService dalamudUtil, PairManager pairManager, PairRequestService pairRequestService, ServerConfigurationManager serverManager, LightlessMediator mediator, - TokenProvider tokenProvider, LightlessConfigService lightlessConfigService, LightlessNotificationService lightlessNotificationService) : base(logger, mediator) + TokenProvider tokenProvider, LightlessConfigService lightlessConfigService, NotificationService lightlessNotificationService) : base(logger, mediator) { _hubFactory = hubFactory; _dalamudUtil = dalamudUtil;