diff --git a/LightlessSync/PlayerData/Pairs/PairManager.cs b/LightlessSync/PlayerData/Pairs/PairManager.cs index 512c7ad..51bead5 100644 --- a/LightlessSync/PlayerData/Pairs/PairManager.cs +++ b/LightlessSync/PlayerData/Pairs/PairManager.cs @@ -25,6 +25,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase private readonly LightlessConfigService _configurationService; private readonly IContextMenu _dalamudContextMenu; private readonly PairFactory _pairFactory; + private readonly LightlessNotificationService _lightlessNotificationService; private Lazy> _directPairsInternal; private Lazy>> _groupPairsInternal; private Lazy>> _pairsWithGroupsInternal; @@ -35,12 +36,14 @@ public sealed class PairManager : DisposableMediatorSubscriberBase public PairManager(ILogger logger, PairFactory pairFactory, LightlessConfigService configurationService, LightlessMediator mediator, - IContextMenu dalamudContextMenu, PairProcessingLimiter pairProcessingLimiter) : base(logger, mediator) + IContextMenu dalamudContextMenu, PairProcessingLimiter pairProcessingLimiter, + LightlessNotificationService lightlessNotificationService) : base(logger, mediator) { _pairFactory = pairFactory; _configurationService = configurationService; _dalamudContextMenu = dalamudContextMenu; _pairProcessingLimiter = pairProcessingLimiter; + _lightlessNotificationService = lightlessNotificationService; Mediator.Subscribe(this, (_) => ClearPairs()); Mediator.Subscribe(this, (_) => ReapplyPairData()); _directPairsInternal = DirectPairsLazy(); @@ -168,7 +171,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase var msg = !string.IsNullOrEmpty(note) ? $"{note} ({pair.UserData.AliasOrUID}) is now online" : $"{pair.UserData.AliasOrUID} is now online"; - Mediator.Publish(new NotificationMessage("User online", msg, NotificationType.Info, TimeSpan.FromSeconds(5))); + _lightlessNotificationService.ShowNotification("User Online", msg, NotificationType.Info, TimeSpan.FromSeconds(5)); } QueuePairCreation(pair, dto); diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 745afd3..082cb38 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -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(), contextMenu, s.GetRequiredService(), s.GetRequiredService())); collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(addonLifecycle); diff --git a/LightlessSync/Services/LightlessNotificationService.cs b/LightlessSync/Services/LightlessNotificationService.cs index 7dabe71..c926d34 100644 --- a/LightlessSync/Services/LightlessNotificationService.cs +++ b/LightlessSync/Services/LightlessNotificationService.cs @@ -6,7 +6,6 @@ using LightlessSync.UI; using LightlessSync.UI.Models; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using System.Numerics; namespace LightlessSync.Services; public class LightlessNotificationService : DisposableMediatorSubscriberBase, IHostedService { @@ -37,7 +36,7 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH _notificationUI = notificationUI; } public void ShowNotification(string title, string message, NotificationType type = NotificationType.Info, - TimeSpan? duration = null, List? actions = null) + TimeSpan? duration = null, List? actions = null, uint? soundEffectId = null) { var notification = new LightlessNotification { @@ -45,8 +44,16 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH Message = message, Type = type, Duration = duration ?? TimeSpan.FromSeconds(10), - Actions = actions ?? new List() + Actions = actions ?? new List(), + SoundEffectId = soundEffectId ?? NotificationSounds.GetDefaultSound(type) }; + + // Play sound effect if specified + if (notification.SoundEffectId.HasValue) + { + PlayNotificationSound(notification.SoundEffectId.Value); + } + Mediator.Publish(new LightlessNotificationMessage(notification)); } public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline) @@ -57,6 +64,7 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH Message = $"{senderName} wants to pair with you.", Type = NotificationType.Info, Duration = TimeSpan.FromSeconds(60), + SoundEffectId = NotificationSounds.PairRequest, Actions = new List { new() @@ -91,6 +99,13 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH } } }; + + // Play sound effect + if (notification.SoundEffectId.HasValue) + { + PlayNotificationSound(notification.SoundEffectId.Value); + } + Mediator.Publish(new LightlessNotificationMessage(notification)); } public void ShowDownloadCompleteNotification(string fileName, int fileCount, Action? onOpenFolder = null) @@ -121,8 +136,16 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH $"Downloaded {fileName} successfully.", Type = NotificationType.Info, Duration = TimeSpan.FromSeconds(8), - Actions = actions + Actions = actions, + SoundEffectId = NotificationSounds.DownloadComplete }; + + // Play sound effect + 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) @@ -161,21 +184,47 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH Message = exception != null ? $"{message}\n\nError: {exception.Message}" : message, Type = NotificationType.Error, Duration = TimeSpan.FromSeconds(15), - Actions = actions + Actions = actions, + SoundEffectId = NotificationSounds.Error }; + + // Play sound effect + if (notification.SoundEffectId.HasValue) + { + PlayNotificationSound(notification.SoundEffectId.Value); + } + Mediator.Publish(new LightlessNotificationMessage(notification)); } - public void ShowPairDownloadNotification(List<(string playerName, float progress, string status)> downloadStatus) + public void ShowPairDownloadNotification(List<(string playerName, float progress, string status)> downloadStatus, int queueWaiting = 0) { - var totalProgress = downloadStatus.Count > 0 ? downloadStatus.Average(x => x.progress) : 0f; - var completedCount = downloadStatus.Count(x => x.progress >= 1.0f); - var totalCount = downloadStatus.Count; + // Filter out queue status from user downloads + var userDownloads = downloadStatus.Where(x => x.playerName != "Pair Queue").ToList(); - var message = $"Progress: {completedCount}/{totalCount} completed"; - if (downloadStatus.Any(x => x.progress < 1.0f)) + 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 = ""; + + // Add queue status at the top if there are waiting items + if (queueWaiting > 0) { - var activeDownloads = downloadStatus.Where(x => x.progress < 1.0f).Take(3); - message += "\n" + string.Join("\n", activeDownloads.Select(x => + message = $"Queue: {queueWaiting} waiting"; + } + + // Add download progress if there are downloads + 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 { @@ -188,11 +237,12 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH return $"• {x.playerName}: {statusText}"; })); - if (downloadStatus.Count(x => x.progress < 1.0f) > 3) - { - message += $"\n• ... and {downloadStatus.Count(x => x.progress < 1.0f) - 3} more"; - } + message += string.IsNullOrEmpty(message) ? downloadLines : $"\n{downloadLines}"; } + + // Check if all downloads are completed + var allDownloadsCompleted = userDownloads.All(x => x.progress >= 1.0f) && userDownloads.Any(); + var notification = new LightlessNotification { Id = "pair_download_progress", @@ -204,9 +254,33 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH 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 + { + // TODO: Implement proper sound playback + // The ChatGui.PlaySoundEffect method doesn't exist in the current Dalamud API + // For now, just log what sound would be played + _logger.LogDebug("Would play notification sound effect {SoundId}", soundEffectId); + + // Future implementation options: + // 1. Use UIModule->PlaySound() with proper unsafe interop + // 2. Use game's sound system through SigScanner + // 3. Wait for official Dalamud sound API + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to play notification sound effect {SoundId}", soundEffectId); + } + } } \ No newline at end of file diff --git a/LightlessSync/UI/DownloadUi.cs b/LightlessSync/UI/DownloadUi.cs index eb38bc1..48432d7 100644 --- a/LightlessSync/UI/DownloadUi.cs +++ b/LightlessSync/UI/DownloadUi.cs @@ -1,5 +1,4 @@ -using System; -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface.Colors; using LightlessSync.LightlessConfiguration; using LightlessSync.PlayerData.Handlers; @@ -88,19 +87,7 @@ public class DownloadUi : WindowMediatorSubscriberBase if (_configService.Current.ShowTransferWindow) { var limiterSnapshot = _pairProcessingLimiter.GetSnapshot(); - if (limiterSnapshot.IsEnabled) - { - var queueColor = limiterSnapshot.Waiting > 0 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudGrey; - var queueText = $"Pair queue {limiterSnapshot.InFlight}/{limiterSnapshot.Limit}"; - queueText += limiterSnapshot.Waiting > 0 ? $" ({limiterSnapshot.Waiting} waiting, {limiterSnapshot.Remaining} free)" : $" ({limiterSnapshot.Remaining} free)"; - UiSharedService.DrawOutlinedFont(queueText, queueColor, new Vector4(0, 0, 0, 255), 1); - ImGui.NewLine(); - } - else - { - UiSharedService.DrawOutlinedFont("Pair apply limiter disabled", ImGuiColors.DalamudGrey, new Vector4(0, 0, 0, 255), 1); - ImGui.NewLine(); - } + try { if (_fileTransferManager.IsUploading) @@ -138,18 +125,30 @@ public class DownloadUi : WindowMediatorSubscriberBase // Use new notification stuff if (_currentDownloads.Any()) { - UpdateDownloadNotification(); + UpdateDownloadNotification(limiterSnapshot); _notificationDismissed = false; } else if (!_notificationDismissed) { _notificationService.DismissPairDownloadNotification(); _notificationDismissed = true; - } - } + } } else { - // text overlay + if (limiterSnapshot.IsEnabled) + { + var queueColor = limiterSnapshot.Waiting > 0 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudGrey; + var queueText = $"Pair queue {limiterSnapshot.InFlight}/{limiterSnapshot.Limit}"; + queueText += limiterSnapshot.Waiting > 0 ? $" ({limiterSnapshot.Waiting} waiting, {limiterSnapshot.Remaining} free)" : $" ({limiterSnapshot.Remaining} free)"; + UiSharedService.DrawOutlinedFont(queueText, queueColor, new Vector4(0, 0, 0, 255), 1); + ImGui.NewLine(); + } + else + { + UiSharedService.DrawOutlinedFont("Pair apply limiter disabled", ImGuiColors.DalamudGrey, new Vector4(0, 0, 0, 255), 1); + ImGui.NewLine(); + } + foreach (var item in _currentDownloads.ToList()) { var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot); @@ -292,7 +291,7 @@ public class DownloadUi : WindowMediatorSubscriberBase }; } - private void UpdateDownloadNotification() + private void UpdateDownloadNotification(PairProcessingLimiterSnapshot limiterSnapshot) { var downloadStatus = new List<(string playerName, float progress, string status)>(); @@ -319,9 +318,12 @@ public class DownloadUi : WindowMediatorSubscriberBase downloadStatus.Add((item.Key.Name, progress, status)); } - if (downloadStatus.Any()) + // Pass queue waiting count separately, show notification if there are downloads or queue items + var queueWaiting = limiterSnapshot.IsEnabled ? limiterSnapshot.Waiting : 0; + if (downloadStatus.Any() || queueWaiting > 0) { - _notificationService.ShowPairDownloadNotification(downloadStatus); + _notificationService.ShowPairDownloadNotification(downloadStatus, queueWaiting); } } + } \ No newline at end of file diff --git a/LightlessSync/UI/Models/LightlessNotification.cs b/LightlessSync/UI/Models/LightlessNotification.cs index c9000da..7013a97 100644 --- a/LightlessSync/UI/Models/LightlessNotification.cs +++ b/LightlessSync/UI/Models/LightlessNotification.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface; +using Dalamud.Interface; using LightlessSync.LightlessConfiguration.Models; using System.Numerics; namespace LightlessSync.UI.Models; @@ -21,6 +21,9 @@ public class LightlessNotification public float AnimationProgress { get; set; } = 0f; public bool IsAnimatingIn { get; set; } = true; public bool IsAnimatingOut { get; set; } = false; + + // Sound properties + public uint? SoundEffectId { get; set; } = null; } public class LightlessNotificationAction { diff --git a/LightlessSync/UI/Models/NotificationSounds.cs b/LightlessSync/UI/Models/NotificationSounds.cs new file mode 100644 index 0000000..962263a --- /dev/null +++ b/LightlessSync/UI/Models/NotificationSounds.cs @@ -0,0 +1,50 @@ +using LightlessSync.LightlessConfiguration.Models; + +namespace LightlessSync.UI.Models; + +/// +/// Common FFXIV sound effect IDs for notifications +/// +public static class NotificationSounds +{ + /// + /// General notification sound (quest complete) + /// + public const uint Info = 37; + + /// + /// Warning/alert sound (system error) + /// + public const uint Warning = 15; + + /// + /// Error sound (action failed) + /// + public const uint Error = 16; + + /// + /// Success sound (level up) + /// + public const uint Success = 25; + + /// + /// Pair request sound (tell received) + /// + public const uint PairRequest = 13; + + /// + /// Download complete sound (item obtained) + /// + public const uint DownloadComplete = 30; + + /// + /// Get default sound for notification type + /// + public static uint GetDefaultSound(NotificationType type) => type switch + { + NotificationType.Info => Info, + NotificationType.Warning => Warning, + NotificationType.Error => Error, + _ => Info + }; +} diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index 52b15a6..05ff04f 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -11,12 +11,8 @@ using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Utils; using LightlessSync.WebAPI; -using Serilog; -using System; -using System.Collections.Generic; using System.Numerics; -using System.Reflection.Emit; -using System.Threading.Tasks; + namespace LightlessSync.UI; @@ -269,7 +265,6 @@ public class TopTabMenu _lightlessNotificationService.ShowPairDownloadNotification(downloadStatus); } - ImGui.SameLine(); if (ImGui.Button("Dismiss Download")) {