From 27e7fb7ed91bfb89f5cf4d6ffa43701d307dc864 Mon Sep 17 00:00:00 2001 From: choco Date: Wed, 8 Oct 2025 23:20:58 +0200 Subject: [PATCH] more to notification system with new settings tab --- .../Configurations/LightlessConfig.cs | 25 + .../Models/NotificationLocation.cs | 6 +- LightlessSync/Plugin.cs | 5 +- .../Services/LightlessNotificationService.cs | 267 ++++++- LightlessSync/UI/LightlessNotificationUI.cs | 90 ++- LightlessSync/UI/SettingsUi.cs | 658 +++++++++++++++--- LightlessSync/UI/TopTabMenu.cs | 22 +- .../ApiController.Functions.Callbacks.cs | 40 +- 8 files changed, 963 insertions(+), 150 deletions(-) diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs index 1a4f6ae..b102cf0 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs @@ -70,6 +70,31 @@ public class LightlessConfig : ILightlessConfiguration public bool AutoPopulateEmptyNotesFromCharaName { get; set; } = false; public int Version { get; set; } = 1; public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both; + + // Lightless Notification Configuration + // TODO: clean these + public bool UseLightlessNotifications { get; set; } = true; + public bool EnableNotificationSounds { get; set; } = true; + public int DefaultNotificationDurationSeconds { get; set; } = 10; + public bool ShowNotificationProgress { get; set; } = true; + public NotificationLocation LightlessInfoNotification { get; set; } = NotificationLocation.LightlessUI; + public NotificationLocation LightlessWarningNotification { get; set; } = NotificationLocation.LightlessUI; + public NotificationLocation LightlessErrorNotification { get; set; } = NotificationLocation.ChatAndLightlessUI; + + public float NotificationOpacity { get; set; } = 0.95f; + public bool EnableNotificationAnimations { get; set; } = true; + public int MaxSimultaneousNotifications { get; set; } = 5; + public bool AutoDismissOnAction { get; set; } = true; + public bool ShowNotificationTimestamp { get; set; } = false; + public bool EnableNotificationHistory { get; set; } = true; + public int NotificationHistorySize { get; set; } = 50; + + public uint CustomInfoSoundId { get; set; } = 2; // Se2 + public uint CustomWarningSoundId { get; set; } = 15; // Se15 + public uint CustomErrorSoundId { get; set; } = 16; // Se16 + public bool UseCustomSounds { get; set; } = false; + public float NotificationSoundVolume { get; set; } = 1.0f; + // till here c: public bool UseFocusTarget { get; set; } = false; public bool overrideFriendColor { get; set; } = false; public bool overridePartyColor { get; set; } = false; diff --git a/LightlessSync/LightlessConfiguration/Models/NotificationLocation.cs b/LightlessSync/LightlessConfiguration/Models/NotificationLocation.cs index a33e727..657715f 100644 --- a/LightlessSync/LightlessConfiguration/Models/NotificationLocation.cs +++ b/LightlessSync/LightlessConfiguration/Models/NotificationLocation.cs @@ -1,11 +1,13 @@ -namespace LightlessSync.LightlessConfiguration.Models; +namespace LightlessSync.LightlessConfiguration.Models; public enum NotificationLocation { Nowhere, Chat, Toast, - Both + Both, + LightlessUI, + ChatAndLightlessUI, } public enum NotificationType diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 082cb38..054bfbc 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -179,6 +179,8 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), + notificationManager, + chatGui, s.GetRequiredService())); collection.AddSingleton((s) => { @@ -244,7 +246,8 @@ public sealed class Plugin : IDalamudPlugin new LightlessNotificationUI( s.GetRequiredService>(), s.GetRequiredService(), - s.GetRequiredService())); + s.GetRequiredService(), + s.GetRequiredService())); collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); diff --git a/LightlessSync/Services/LightlessNotificationService.cs b/LightlessSync/Services/LightlessNotificationService.cs index 1afd9a5..9e16bde 100644 --- a/LightlessSync/Services/LightlessNotificationService.cs +++ b/LightlessSync/Services/LightlessNotificationService.cs @@ -1,4 +1,6 @@ +using Dalamud.Game.Text.SeStringHandling; using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration.Models; @@ -8,6 +10,7 @@ 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 @@ -15,19 +18,26 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH 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) @@ -46,12 +56,31 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH Title = title, Message = message, Type = type, - Duration = duration ?? TimeSpan.FromSeconds(10), + Duration = duration ?? TimeSpan.FromSeconds(_configService.Current.DefaultNotificationDurationSeconds), Actions = actions ?? new List(), - SoundEffectId = soundEffectId ?? NotificationSounds.GetDefaultSound(type) + SoundEffectId = GetSoundEffectId(type, soundEffectId), + ShowProgress = _configService.Current.ShowNotificationProgress, + CreatedAt = DateTime.UtcNow }; - if (notification.SoundEffectId.HasValue) + 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); } @@ -101,18 +130,18 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH } } }; - + 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 @@ -132,20 +161,20 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH var notification = new LightlessNotification { Title = "Download Complete", - Message = fileCount > 1 ? - $"Downloaded {fileCount} files successfully." : + 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) @@ -187,40 +216,40 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH 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 downloadLines = string.Join("\n", activeDownloads.Select(x => { var statusText = x.status switch { @@ -232,12 +261,12 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH }; 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", @@ -251,15 +280,37 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH Mediator.Publish(new LightlessNotificationMessage(notification)); if (allDownloadsCompleted) { - DismissPairDownloadNotification(); + 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 @@ -279,4 +330,172 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH _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/UI/LightlessNotificationUI.cs b/LightlessSync/UI/LightlessNotificationUI.cs index f1c8455..c74c5b1 100644 --- a/LightlessSync/UI/LightlessNotificationUI.cs +++ b/LightlessSync/UI/LightlessNotificationUI.cs @@ -3,11 +3,13 @@ using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; +using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration.Models; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.UI.Models; using Microsoft.Extensions.Logging; + using System.Numerics; using Dalamud.Bindings.ImGui; @@ -17,18 +19,22 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase { private readonly List _notifications = new(); private readonly object _notificationLock = new(); + private readonly LightlessConfigService _configService; private const float NotificationWidth = 350f; private const float NotificationMinHeight = 60f; + private const float NotificationMaxHeight = 200f; private const float NotificationSpacing = 8f; private const float AnimationSpeed = 10f; + private const float EdgeXMargin = 0; private const float EdgeYMargin = 30f; private const float SlideDistance = 100f; - public LightlessNotificationUI(ILogger logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector) + public LightlessNotificationUI(ILogger logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService) : base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector) { + _configService = configService; Flags = ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize | @@ -141,6 +147,19 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase { var deltaTime = ImGui.GetIO().DeltaTime; + var maxNotifications = _configService.Current.MaxSimultaneousNotifications; + while (_notifications.Count(n => !n.IsAnimatingOut) > maxNotifications) + { + var oldestNotification = _notifications + .Where(n => !n.IsAnimatingOut) + .OrderBy(n => n.CreatedAt) + .FirstOrDefault(); + if (oldestNotification != null) + { + oldestNotification.IsAnimatingOut = true; + oldestNotification.IsAnimatingIn = false; + } + } for (int i = _notifications.Count - 1; i >= 0; i--) { var notification = _notifications[i]; @@ -174,9 +193,16 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase private void DrawNotification(LightlessNotification notification, int index) { var alpha = notification.AnimationProgress; - if (alpha <= 0f) return; + + if (_configService.Current.EnableNotificationAnimations && alpha <= 0f) + return; + + var slideOffset = 0f; + if (_configService.Current.EnableNotificationAnimations) + { + slideOffset = (1f - alpha) * SlideDistance; + } - var slideOffset = (1f - alpha) * SlideDistance; var originalCursorPos = ImGui.GetCursorPos(); ImGui.SetCursorPosX(originalCursorPos.X + slideOffset); @@ -198,13 +224,18 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); - var bgColor = new Vector4(30f/255f, 30f/255f, 30f/255f, 0.95f * alpha); + var baseOpacity = _configService.Current.NotificationOpacity; + var finalOpacity = _configService.Current.EnableNotificationAnimations ? baseOpacity * alpha : baseOpacity; + var bgColor = new Vector4(30f/255f, 30f/255f, 30f/255f, finalOpacity); var accentColor = GetNotificationAccentColor(notification.Type); var progressBarColor = UIColors.Get("LightlessBlue"); - accentColor.W *= alpha; + + var finalAccentAlpha = _configService.Current.EnableNotificationAnimations ? alpha : 1f; + accentColor.W *= finalAccentAlpha; var shadowOffset = new Vector2(1f, 1f); - var shadowColor = new Vector4(0f, 0f, 0f, 0.4f * alpha); + var shadowAlpha = _configService.Current.EnableNotificationAnimations ? 0.4f * alpha : 0.4f; + var shadowColor = new Vector4(0f, 0f, 0f, shadowAlpha); drawList.AddRectFilled( windowPos + shadowOffset, windowPos + windowSize + shadowOffset, @@ -278,14 +309,31 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase ImGui.SetCursorPos(contentPos); + float titleHeight = 0f; using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1f, 1f, 1f, alpha))) { - ImGui.Text(notification.Title); + // Set text wrap position to prevent title overflow + ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + contentSize.X); + + var titleStartY = ImGui.GetCursorPosY(); + + if (_configService.Current.ShowNotificationTimestamp) + { + var timestamp = notification.CreatedAt.ToLocalTime().ToString("HH:mm:ss"); + ImGui.TextWrapped($"[{timestamp}] {notification.Title}"); + } + else + { + ImGui.TextWrapped(notification.Title); + } + + titleHeight = ImGui.GetCursorPosY() - titleStartY; + ImGui.PopTextWrapPos(); } if (!string.IsNullOrEmpty(notification.Message)) { - ImGui.SetCursorPos(contentPos + new Vector2(0f, 18f)); + ImGui.SetCursorPos(contentPos + new Vector2(0f, titleHeight + 4f)); ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + contentSize.X); using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(0.9f, 0.9f, 0.9f, alpha))) { @@ -436,24 +484,40 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase private float CalculateNotificationHeight(LightlessNotification notification) { - var height = 40f; - if (!string.IsNullOrEmpty(notification.Message)) + var contentWidth = NotificationWidth - 35f; // Account for padding and accent bar + var height = 20f; // Base height for padding + + var titleText = notification.Title; + if (_configService.Current.ShowNotificationTimestamp) { - var textSize = ImGui.CalcTextSize(notification.Message, true, NotificationWidth - 35f); - height += textSize.Y + 4f; + var timestamp = notification.CreatedAt.ToLocalTime().ToString("HH:mm:ss"); + titleText = $"[{timestamp}] {titleText}"; } + var titleSize = ImGui.CalcTextSize(titleText, true, contentWidth); + height += titleSize.Y + 4f; // Title height + spacing + + // Calculate message height + if (!string.IsNullOrEmpty(notification.Message)) + { + var messageSize = ImGui.CalcTextSize(notification.Message, true, contentWidth); + height += messageSize.Y + 4f; // Message height + spacing + } + + // Add height for progress bar if (notification.ShowProgress) { height += 12f; } + // Add height for action buttons if (notification.Actions.Count > 0) { height += 28f; } - return Math.Max(height, NotificationMinHeight); + // Allow notifications to grow taller but cap at maximum height + return Math.Clamp(height, NotificationMinHeight, NotificationMaxHeight); } private Vector4 GetNotificationAccentColor(NotificationType type) diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index f8249ac..2573a18 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -1,4 +1,4 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Game.Text; using Dalamud.Interface; using Dalamud.Interface.Colors; @@ -61,6 +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 (int, int, FileCacheEntity) _currentProgress; private bool _deleteAccountPopupModalShown = false; private bool _deleteFilesPopupModalShown = false; @@ -105,7 +106,8 @@ public class SettingsUi : WindowMediatorSubscriberBase IpcManager ipcManager, CacheMonitor cacheMonitor, DalamudUtilService dalamudUtilService, HttpClient httpClient, NameplateService nameplateService, - NameplateHandler nameplateHandler) : base(logger, mediator, "Lightless Sync Settings", performanceCollector) + NameplateHandler nameplateHandler, + LightlessNotificationService lightlessNotificationService) : base(logger, mediator, "Lightless Sync Settings", performanceCollector) { _configService = configService; _pairManager = pairManager; @@ -125,6 +127,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared = uiShared; _nameplateService = nameplateService; _nameplateHandler = nameplateHandler; + _lightlessNotificationService = lightlessNotificationService; AllowClickthrough = false; AllowPinning = true; _validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v); @@ -351,13 +354,8 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.UnderlinedBigText("Transfer UI", UIColors.Get("LightlessBlue")); ImGuiHelpers.ScaledDummy(5); - bool useNotificationsForDownloads = _configService.Current.UseNotificationsForDownloads; - if (ImGui.Checkbox("Use notifications for download progress", ref useNotificationsForDownloads)) - { - _configService.Current.UseNotificationsForDownloads = useNotificationsForDownloads; - _configService.Save(); - } - _uiShared.DrawHelpText("Show download progress as clean notifications instead of overlay text. Notifications update in real-time."); + _uiShared.DrawHelpText("Download progress notification settings have been moved to the 'Enhanced Notifications' tab for better organization."); + ImGuiHelpers.ScaledDummy(5); bool showTransferWindow = _configService.Current.ShowTransferWindow; if (ImGui.Checkbox("Show separate transfer window", ref showTransferWindow)) @@ -1645,48 +1643,10 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Dummy(new Vector2(10)); _uiShared.BigText("Notifications"); - var disableOptionalPluginWarnings = _configService.Current.DisableOptionalPluginWarnings; - var onlineNotifs = _configService.Current.ShowOnlineNotifications; - var onlineNotifsPairsOnly = _configService.Current.ShowOnlineNotificationsOnlyForIndividualPairs; - var onlineNotifsNamedOnly = _configService.Current.ShowOnlineNotificationsOnlyForNamedPairs; if (_uiShared.MediumTreeNode("Display", UIColors.Get("LightlessPurple"))) { - _uiShared.DrawCombo("Info Notification Display##settingsUi", (NotificationLocation[])Enum.GetValues(typeof(NotificationLocation)), (i) => i.ToString(), - (i) => - { - _configService.Current.InfoNotification = i; - _configService.Save(); - }, _configService.Current.InfoNotification); - _uiShared.DrawHelpText("The location where \"Info\" notifications will display." - + Environment.NewLine + "'Nowhere' will not show any Info notifications" - + Environment.NewLine + "'Chat' will print Info notifications in chat" - + Environment.NewLine + "'Toast' will show Warning toast notifications in the bottom right corner" - + Environment.NewLine + "'Both' will show chat as well as the toast notification"); - - _uiShared.DrawCombo("Warning Notification Display##settingsUi", (NotificationLocation[])Enum.GetValues(typeof(NotificationLocation)), (i) => i.ToString(), - (i) => - { - _configService.Current.WarningNotification = i; - _configService.Save(); - }, _configService.Current.WarningNotification); - _uiShared.DrawHelpText("The location where \"Warning\" notifications will display." - + Environment.NewLine + "'Nowhere' will not show any Warning notifications" - + Environment.NewLine + "'Chat' will print Warning notifications in chat" - + Environment.NewLine + "'Toast' will show Warning toast notifications in the bottom right corner" - + Environment.NewLine + "'Both' will show chat as well as the toast notification"); - - _uiShared.DrawCombo("Error Notification Display##settingsUi", (NotificationLocation[])Enum.GetValues(typeof(NotificationLocation)), (i) => i.ToString(), - (i) => - { - _configService.Current.ErrorNotification = i; - _configService.Save(); - }, _configService.Current.ErrorNotification); - _uiShared.DrawHelpText("The location where \"Error\" notifications will display." - + Environment.NewLine + "'Nowhere' will not show any Error notifications" - + Environment.NewLine + "'Chat' will print Error notifications in chat" - + Environment.NewLine + "'Toast' will show Error toast notifications in the bottom right corner" - + Environment.NewLine + "'Both' will show chat as well as the toast notification"); + _uiShared.DrawHelpText("Notification settings have been moved to the 'Enhanced Notifications' tab for better organization. You can configure where all notifications appear from there."); _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); @@ -1694,38 +1654,6 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Separator(); - if (_uiShared.MediumTreeNode("Toggles", UIColors.Get("LightlessPurple"))) - { - if (ImGui.Checkbox("Disable optional plugin warnings", ref disableOptionalPluginWarnings)) - { - _configService.Current.DisableOptionalPluginWarnings = disableOptionalPluginWarnings; - _configService.Save(); - } - _uiShared.DrawHelpText("Enabling this will not show any \"Warning\" labeled messages for missing optional plugins."); - if (ImGui.Checkbox("Enable online notifications", ref onlineNotifs)) - { - _configService.Current.ShowOnlineNotifications = onlineNotifs; - _configService.Save(); - } - _uiShared.DrawHelpText("Enabling this will show a small notification (type: Info) in the bottom right corner when pairs go online."); - - using var disabled = ImRaii.Disabled(!onlineNotifs); - if (ImGui.Checkbox("Notify only for individual pairs", ref onlineNotifsPairsOnly)) - { - _configService.Current.ShowOnlineNotificationsOnlyForIndividualPairs = onlineNotifsPairsOnly; - _configService.Save(); - } - _uiShared.DrawHelpText("Enabling this will only show online notifications (type: Info) for individual pairs."); - if (ImGui.Checkbox("Notify only for named pairs", ref onlineNotifsNamedOnly)) - { - _configService.Current.ShowOnlineNotificationsOnlyForNamedPairs = onlineNotifsNamedOnly; - _configService.Save(); - } - _uiShared.DrawHelpText("Enabling this will only show online notifications (type: Info) for pairs where you have set an individual note."); - - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); - ImGui.TreePop(); - } } private void DrawPerformance() @@ -2661,6 +2589,12 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.EndTabItem(); } + if (ImGui.BeginTabItem("Notifications")) + { + DrawNotificationSettings(); + ImGui.EndTabItem(); + } + if (ImGui.BeginTabItem("Debug")) { DrawDebug(); @@ -2681,4 +2615,568 @@ public class SettingsUi : WindowMediatorSubscriberBase _wasOpen = IsOpen; IsOpen = false; } + + private void DrawNotificationSettings() + { + _lastTab = "Notifications"; + _uiShared.UnderlinedBigText("Notification System", UIColors.Get("LightlessBlue")); + ImGuiHelpers.ScaledDummy(5); + + bool useLightlessNotifications = _configService.Current.UseLightlessNotifications; + if (ImGui.Checkbox("Use Enhanced Lightless Notifications", ref useLightlessNotifications)) + { + _configService.Current.UseLightlessNotifications = useLightlessNotifications; + _configService.Save(); + } + _uiShared.DrawHelpText("Enable the new enhanced notification system with interactive buttons, animations, and better visual design."); + ImGui.Separator(); + _uiShared.UnderlinedBigText("Notification Locations", UIColors.Get("LightlessBlue")); + ImGuiHelpers.ScaledDummy(5); + + UiSharedService.ColorTextWrapped("Configure where different types of notifications appear. Enhanced notifications provide modern interactive notifications with backwards compatibility support for classic toast/chat notifications.", ImGuiColors.DalamudGrey); + ImGuiHelpers.ScaledDummy(5); + + if (useLightlessNotifications) + { + // Enhanced notification locations (primary) + _uiShared.BigText("Enhanced Notification Locations"); + ImGuiHelpers.ScaledDummy(3); + + var lightlessLocations = GetLightlessNotificationLocations(); + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Info Notifications:"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + _uiShared.DrawCombo("###enhanced_info", lightlessLocations, GetNotificationLocationLabel, (location) => + { + _configService.Current.LightlessInfoNotification = location; + _configService.Save(); + }, _configService.Current.LightlessInfoNotification); + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Warning Notifications:"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + _uiShared.DrawCombo("###enhanced_warning", lightlessLocations, GetNotificationLocationLabel, (location) => + { + _configService.Current.LightlessWarningNotification = location; + _configService.Save(); + }, _configService.Current.LightlessWarningNotification); + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Error Notifications:"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + _uiShared.DrawCombo("###enhanced_error", lightlessLocations, GetNotificationLocationLabel, (location) => + { + _configService.Current.LightlessErrorNotification = location; + _configService.Save(); + }, _configService.Current.LightlessErrorNotification); + + ImGuiHelpers.ScaledDummy(5); + + // Classic notification locations (backwards compatibility) + if (ImGui.CollapsingHeader("Classic Notification Settings (Backwards Compatibility)")) + { + _uiShared.DrawHelpText("These settings provide backwards compatibility. They will also be used if they're different from the enhanced settings above and don't conflict."); + ImGuiHelpers.ScaledDummy(3); + + var classicLocations = GetClassicNotificationLocations(); + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Info Fallback:"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + _uiShared.DrawCombo("###classic_info", classicLocations, GetNotificationLocationLabel, (location) => + { + _configService.Current.InfoNotification = location; + _configService.Save(); + }, _configService.Current.InfoNotification); + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Warning Fallback:"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + _uiShared.DrawCombo("###classic_warning", classicLocations, GetNotificationLocationLabel, (location) => + { + _configService.Current.WarningNotification = location; + _configService.Save(); + }, _configService.Current.WarningNotification); + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Error Fallback:"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + _uiShared.DrawCombo("###classic_error", classicLocations, GetNotificationLocationLabel, (location) => + { + _configService.Current.ErrorNotification = location; + _configService.Save(); + }, _configService.Current.ErrorNotification); + } + } + else + { + // Only classic notifications when enhanced is disabled + var classicLocations = GetClassicNotificationLocations(); + _uiShared.BigText("Classic Notification Locations"); + ImGuiHelpers.ScaledDummy(3); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Info Notifications:"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + _uiShared.DrawCombo("###info", classicLocations, GetNotificationLocationLabel, (location) => + { + _configService.Current.InfoNotification = location; + _configService.Save(); + }, _configService.Current.InfoNotification); + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Warning Notifications:"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + _uiShared.DrawCombo("###warning", classicLocations, GetNotificationLocationLabel, (location) => + { + _configService.Current.WarningNotification = location; + _configService.Save(); + }, _configService.Current.WarningNotification); + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Error Notifications:"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + _uiShared.DrawCombo("###error", classicLocations, GetNotificationLocationLabel, (location) => + { + _configService.Current.ErrorNotification = location; + _configService.Save(); + }, _configService.Current.ErrorNotification); + } + ImGui.Separator(); + if (useLightlessNotifications) + { + UiSharedService.ColorTextWrapped("• Lightless Notifications: Modern animated notifications with interactive buttons", ImGuiColors.DalamudGrey); + UiSharedService.ColorTextWrapped("• Chat: Traditional chat messages with colored text", ImGuiColors.DalamudGrey); + UiSharedService.ColorTextWrapped("• Combined options: Show in multiple locations simultaneously", ImGuiColors.DalamudGrey); + } + else + { + UiSharedService.ColorTextWrapped("• Toast: Dalamud's built-in notification toasts", ImGuiColors.DalamudGrey); + UiSharedService.ColorTextWrapped("• Chat: Traditional chat messages with colored text", ImGuiColors.DalamudGrey); + UiSharedService.ColorTextWrapped("• Both: Show in both toast and chat", ImGuiColors.DalamudGrey); + } + ImGui.Separator(); + if (useLightlessNotifications) + { + ImGui.Indent(); + + // Test notification buttons + if (_uiShared.IconTextButton(FontAwesomeIcon.Bell, "Test Info")) + { + Mediator.Publish(new NotificationMessage("Test Info", "This is a test info notification with the current settings!", NotificationType.Info)); + } + ImGui.SameLine(); + if (_uiShared.IconTextButton(FontAwesomeIcon.ExclamationTriangle, "Test Warning")) + { + Mediator.Publish(new NotificationMessage("Test Warning", "This is a test warning notification!", NotificationType.Warning)); + } + ImGui.SameLine(); + if (_uiShared.IconTextButton(FontAwesomeIcon.ExclamationCircle, "Test Error")) + { + Mediator.Publish(new NotificationMessage("Test Error", "This is a test error notification!", NotificationType.Error)); + } + _uiShared.DrawHelpText("Click to preview different notification types with your current settings."); + + ImGui.Unindent(); + + ImGui.Separator(); + _uiShared.UnderlinedBigText("Basic Settings", UIColors.Get("LightlessBlue")); + ImGuiHelpers.ScaledDummy(5); + + int defaultDuration = _configService.Current.DefaultNotificationDurationSeconds; + if (ImGui.SliderInt("Default Duration (seconds)", ref defaultDuration, 3, 60)) + { + _configService.Current.DefaultNotificationDurationSeconds = defaultDuration; + _configService.Save(); + } + _uiShared.DrawHelpText("How long notifications stay visible by default."); + + bool showProgress = _configService.Current.ShowNotificationProgress; + if (ImGui.Checkbox("Show Progress Bars", ref showProgress)) + { + _configService.Current.ShowNotificationProgress = showProgress; + _configService.Save(); + } + _uiShared.DrawHelpText("Display progress bars for download and other progress-based notifications."); + + bool showTimestamp = _configService.Current.ShowNotificationTimestamp; + if (ImGui.Checkbox("Show Timestamps", ref showTimestamp)) + { + _configService.Current.ShowNotificationTimestamp = showTimestamp; + _configService.Save(); + } + _uiShared.DrawHelpText("Display the time when each notification was created."); + + bool autoDismiss = _configService.Current.AutoDismissOnAction; + if (ImGui.Checkbox("Auto-dismiss on Action", ref autoDismiss)) + { + _configService.Current.AutoDismissOnAction = autoDismiss; + _configService.Save(); + } + _uiShared.DrawHelpText("Automatically close notifications when you click an action button."); + + if (useLightlessNotifications) + { + ImGui.Separator(); + _uiShared.UnderlinedBigText("Appearance & Animation", UIColors.Get("LightlessBlue")); + ImGuiHelpers.ScaledDummy(5); + + float opacity = _configService.Current.NotificationOpacity; + if (ImGui.SliderFloat("Notification Opacity", ref opacity, 0.1f, 1.0f, "%.2f")) + { + _configService.Current.NotificationOpacity = opacity; + _configService.Save(); + } + _uiShared.DrawHelpText("Transparency level of notification windows."); + + bool enableAnimations = _configService.Current.EnableNotificationAnimations; + if (ImGui.Checkbox("Enable Animations", ref enableAnimations)) + { + _configService.Current.EnableNotificationAnimations = enableAnimations; + _configService.Save(); + } + _uiShared.DrawHelpText("Enable slide-in/out animations for notifications."); + + int maxNotifications = _configService.Current.MaxSimultaneousNotifications; + if (ImGui.SliderInt("Max Simultaneous Notifications", ref maxNotifications, 1, 10)) + { + _configService.Current.MaxSimultaneousNotifications = maxNotifications; + _configService.Save(); + } + _uiShared.DrawHelpText("Maximum number of notifications that can be shown at once."); + + bool enableHistory = _configService.Current.EnableNotificationHistory; + if (ImGui.Checkbox("Enable Notification History", ref enableHistory)) + { + _configService.Current.EnableNotificationHistory = enableHistory; + _configService.Save(); + } + _uiShared.DrawHelpText("Keep a history of recent notifications that you can review."); + + if (enableHistory) + { + ImGui.Indent(); + int historySize = _configService.Current.NotificationHistorySize; + if (ImGui.SliderInt("History Size", ref historySize, 10, 200)) + { + _configService.Current.NotificationHistorySize = historySize; + _configService.Save(); + } + _uiShared.DrawHelpText("Number of notifications to keep in history."); + ImGui.Unindent(); + } + } + + ImGui.Separator(); + _uiShared.UnderlinedBigText("Sound Settings", UIColors.Get("LightlessBlue")); + ImGuiHelpers.ScaledDummy(5); + + bool enableSounds = _configService.Current.EnableNotificationSounds; + if (ImGui.Checkbox("Enable Notification Sounds", ref enableSounds)) + { + _configService.Current.EnableNotificationSounds = enableSounds; + _configService.Save(); + } + _uiShared.DrawHelpText("Play FFXIV sound effects when notifications appear."); + + if (enableSounds) + { + ImGui.Indent(); + + // float soundVolume = _configService.Current.NotificationSoundVolume; + // if (ImGui.SliderFloat("Sound Volume", ref soundVolume, 0.0f, 1.0f, "%.2f")) + // { + // _configService.Current.NotificationSoundVolume = soundVolume; + // _configService.Save(); + // } + // _uiShared.DrawHelpText("Volume level for notification sounds (Note: FFXIV doesn't support volume control for SE sounds)."); + + // bool useCustomSounds = _configService.Current.UseCustomSounds; + // if (ImGui.Checkbox("Use Custom Sound Effects", ref useCustomSounds)) + // { + // _configService.Current.UseCustomSounds = useCustomSounds; + // _configService.Save(); + // } + // _uiShared.DrawHelpText("Override default sounds with custom FFXIV sound effects."); + DrawSoundCustomization(); + + // if (useCustomSounds) + // { + // ImGui.Indent(); + // DrawSoundCustomization(); + // ImGui.Unindent(); + // } + + ImGui.Unindent(); + } + + + ImGui.Separator(); + _uiShared.UnderlinedBigText("Specific Notification Types", UIColors.Get("LightlessBlue")); + ImGuiHelpers.ScaledDummy(5); + + UiSharedService.ColorTextWrapped("Configure specific types of notifications and their behavior.", ImGuiColors.DalamudGrey); + ImGuiHelpers.ScaledDummy(3); + + // Online Notifications Section + if (_uiShared.MediumTreeNode("Online Status Notifications", UIColors.Get("LightlessGreen"))) + { + var onlineNotifs = _configService.Current.ShowOnlineNotifications; + var onlineNotifsPairsOnly = _configService.Current.ShowOnlineNotificationsOnlyForIndividualPairs; + var onlineNotifsNamedOnly = _configService.Current.ShowOnlineNotificationsOnlyForNamedPairs; + + if (ImGui.Checkbox("Enable online notifications", ref onlineNotifs)) + { + _configService.Current.ShowOnlineNotifications = onlineNotifs; + _configService.Save(); + } + _uiShared.DrawHelpText("Show notifications when pairs come online. These will use the Info notification location settings above."); + + using var disabled = ImRaii.Disabled(!onlineNotifs); + ImGui.Indent(); + if (ImGui.Checkbox("Notify only for individual pairs", ref onlineNotifsPairsOnly)) + { + _configService.Current.ShowOnlineNotificationsOnlyForIndividualPairs = onlineNotifsPairsOnly; + _configService.Save(); + } + _uiShared.DrawHelpText("Only show online notifications for individual pairs (not syncshell members)."); + + if (ImGui.Checkbox("Notify only for named pairs", ref onlineNotifsNamedOnly)) + { + _configService.Current.ShowOnlineNotificationsOnlyForNamedPairs = onlineNotifsNamedOnly; + _configService.Save(); + } + _uiShared.DrawHelpText("Only show online notifications for pairs where you have set an individual note."); + ImGui.Unindent(); + + _uiShared.ColoredSeparator(UIColors.Get("LightlessGreen"), 1.5f); + ImGui.TreePop(); + } + + // Pairing Request Notifications Section + if (_uiShared.MediumTreeNode("Pairing Request Notifications", UIColors.Get("LightlessBlue"))) + { + UiSharedService.ColorTextWrapped("Pairing requests always show as interactive notifications with Accept/Decline buttons. These settings control additional behavior.", ImGuiColors.DalamudGrey); + ImGuiHelpers.ScaledDummy(3); + + // Note: Pairing requests are always shown as interactive notifications + // This section can be expanded later with additional pairing notification settings + + _uiShared.ColoredSeparator(UIColors.Get("LightlessBlue"), 1.5f); + ImGui.TreePop(); + } + + // Download Progress Notifications Section + if (_uiShared.MediumTreeNode("Download Progress Notifications", UIColors.Get("LightlessPurple"))) + { + var useNotificationsForDownloads = _configService.Current.UseNotificationsForDownloads; + if (ImGui.Checkbox("Show download progress as notifications", ref useNotificationsForDownloads)) + { + _configService.Current.UseNotificationsForDownloads = useNotificationsForDownloads; + _configService.Save(); + } + _uiShared.DrawHelpText("Show download progress as clean notifications instead of overlay text. Notifications update in real-time and use the Info notification location settings above."); + + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + } + + // System Notifications Section + if (_uiShared.MediumTreeNode("System Notifications", UIColors.Get("LightlessYellow"))) + { + var disableOptionalPluginWarnings = _configService.Current.DisableOptionalPluginWarnings; + if (ImGui.Checkbox("Disable optional plugin warnings", ref disableOptionalPluginWarnings)) + { + _configService.Current.DisableOptionalPluginWarnings = disableOptionalPluginWarnings; + _configService.Save(); + } + _uiShared.DrawHelpText("Disable warning notifications for missing optional plugins."); + + _uiShared.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); + ImGui.TreePop(); + } + + ImGui.Separator(); + _uiShared.UnderlinedBigText("Location Descriptions", UIColors.Get("LightlessBlue")); + ImGuiHelpers.ScaledDummy(5); + } + } + + private NotificationLocation[] GetLightlessNotificationLocations() + { + return new[] + { + NotificationLocation.LightlessUI, + NotificationLocation.ChatAndLightlessUI, + NotificationLocation.Nowhere + }; + } + + private NotificationLocation[] GetClassicNotificationLocations() + { + return new[] + { + NotificationLocation.Toast, + NotificationLocation.Chat, + NotificationLocation.Both, + NotificationLocation.Nowhere + }; + } + + private string GetNotificationLocationLabel(NotificationLocation location) + { + return location switch + { + NotificationLocation.Nowhere => "Nowhere", + NotificationLocation.Chat => "Chat", + NotificationLocation.Toast => "Toast", + NotificationLocation.Both => "Toast + Chat", + NotificationLocation.LightlessUI => "Lightless Notifications", + NotificationLocation.ChatAndLightlessUI => "Chat + Lightless Notifications", + _ => location.ToString() + }; + } + + private void DrawSoundCustomization() + { + var soundEffects = new[] + { + (1u, "Se1 - Soft chime"), + (2u, "Se2 - Higher chime"), + (3u, "Se3 - Bell tone"), + (4u, "Se4 - Harp tone"), + (5u, "Se5 - Drum/percussion"), + (6u, "Se6 - Mechanical click"), + (7u, "Se7 - Metallic chime"), + (8u, "Se8 - Wooden tone"), + (9u, "Se9 - Wind/flute tone"), + (10u, "Se10 - Magical sparkle"), + (11u, "Se11 - Metallic ring"), + (12u, "Se12 - Deep thud"), + (13u, "Se13 - Tell received ping"), + (14u, "Se14 - Success fanfare"), + (15u, "Se15 - System warning"), + (16u, "Se16 - Error/failure") + }; + + // Info Sound + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Info Sound:"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + var currentInfoSound = _configService.Current.CustomInfoSoundId; + var currentInfoIndex = Array.FindIndex(soundEffects, s => s.Item1 == currentInfoSound); + if (currentInfoIndex == -1) currentInfoIndex = 1; // Default to Se2 + + if (ImGui.Combo("###info_sound", ref currentInfoIndex, soundEffects.Select(s => s.Item2).ToArray(), soundEffects.Length)) + { + _configService.Current.CustomInfoSoundId = soundEffects[currentInfoIndex].Item1; + _configService.Save(); + } + ImGui.SameLine(); + ImGui.PushID("test_info"); + if (_uiShared.IconButton(FontAwesomeIcon.Play)) + { + try + { + FFXIVClientStructs.FFXIV.Client.UI.UIGlobals.PlayChatSoundEffect(_configService.Current.CustomInfoSoundId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to play test sound"); + } + } + ImGui.PopID(); + UiSharedService.AttachToolTip("Test this sound"); + + // Warning Sound + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Warning Sound:"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + var currentWarningSound = _configService.Current.CustomWarningSoundId; + var currentWarningIndex = Array.FindIndex(soundEffects, s => s.Item1 == currentWarningSound); + if (currentWarningIndex == -1) currentWarningIndex = 14; // Default to Se15 + + if (ImGui.Combo("###warning_sound", ref currentWarningIndex, soundEffects.Select(s => s.Item2).ToArray(), soundEffects.Length)) + { + _configService.Current.CustomWarningSoundId = soundEffects[currentWarningIndex].Item1; + _configService.Save(); + } + ImGui.SameLine(); + ImGui.PushID("test_warning"); + if (_uiShared.IconButton(FontAwesomeIcon.Play)) + { + try + { + FFXIVClientStructs.FFXIV.Client.UI.UIGlobals.PlayChatSoundEffect(_configService.Current.CustomWarningSoundId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to play test sound"); + } + } + ImGui.PopID(); + UiSharedService.AttachToolTip("Test this sound"); + + // Error Sound + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Error Sound:"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + var currentErrorSound = _configService.Current.CustomErrorSoundId; + var currentErrorIndex = Array.FindIndex(soundEffects, s => s.Item1 == currentErrorSound); + if (currentErrorIndex == -1) currentErrorIndex = 15; // Default to Se16 + + if (ImGui.Combo("###error_sound", ref currentErrorIndex, soundEffects.Select(s => s.Item2).ToArray(), soundEffects.Length)) + { + _configService.Current.CustomErrorSoundId = soundEffects[currentErrorIndex].Item1; + _configService.Save(); + } + ImGui.SameLine(); + ImGui.PushID("test_error"); + if (_uiShared.IconButton(FontAwesomeIcon.Play)) + { + try + { + FFXIVClientStructs.FFXIV.Client.UI.UIGlobals.PlayChatSoundEffect(_configService.Current.CustomErrorSoundId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to play test sound"); + } + } + ImGui.PopID(); + UiSharedService.AttachToolTip("Test this sound"); + + ImGuiHelpers.ScaledDummy(5); + if (_uiShared.IconTextButton(FontAwesomeIcon.VolumeUp, "Test All Sounds")) + { + Task.Run(async () => + { + try + { + FFXIVClientStructs.FFXIV.Client.UI.UIGlobals.PlayChatSoundEffect(_configService.Current.CustomInfoSoundId); + await Task.Delay(800); + FFXIVClientStructs.FFXIV.Client.UI.UIGlobals.PlayChatSoundEffect(_configService.Current.CustomWarningSoundId); + await Task.Delay(800); + FFXIVClientStructs.FFXIV.Client.UI.UIGlobals.PlayChatSoundEffect(_configService.Current.CustomErrorSoundId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to play test sounds"); + } + }); + } + _uiShared.DrawHelpText("Play all custom sounds in sequence: Info → Warning → Error"); + } } diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index 05ff04f..782e211 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -1,4 +1,4 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; @@ -204,19 +204,19 @@ public class TopTabMenu "debug-user-id", onAccept: () => { - _lightlessNotificationService.ShowNotification( + _lightlessMediator.Publish(new NotificationMessage( "Pair Accepted", "Debug pair request was accepted!", NotificationType.Info, - TimeSpan.FromSeconds(3)); + TimeSpan.FromSeconds(3))); }, onDecline: () => { - _lightlessNotificationService.ShowNotification( + _lightlessMediator.Publish(new NotificationMessage( "Pair Declined", "Debug pair request was declined.", NotificationType.Warning, - TimeSpan.FromSeconds(3)); + TimeSpan.FromSeconds(3))); } ); } @@ -224,31 +224,31 @@ public class TopTabMenu ImGui.SameLine(); if (ImGui.Button("Test Info")) { - _lightlessNotificationService.ShowNotification( + _lightlessMediator.Publish(new NotificationMessage( "Information", "This is a test ifno notification with some longer text to see how it wraps. This is a test ifno notification with some longer text to see how it wraps. This is a test ifno notification with some longer text to see how it wraps. This is a test ifno notification with some longer text to see how it wraps.", NotificationType.Info, - TimeSpan.FromSeconds(5)); + TimeSpan.FromSeconds(5))); } ImGui.SameLine(); if (ImGui.Button("Test Warning")) { - _lightlessNotificationService.ShowNotification( + _lightlessMediator.Publish(new NotificationMessage( "Warning", "This is a test warning notification.", NotificationType.Warning, - TimeSpan.FromSeconds(7)); + TimeSpan.FromSeconds(7))); } ImGui.SameLine(); if (ImGui.Button("Test Error")) { - _lightlessNotificationService.ShowNotification( + _lightlessMediator.Publish(new NotificationMessage( "Error", "This is a test error notification erp police", NotificationType.Error, - TimeSpan.FromSeconds(10)); + TimeSpan.FromSeconds(10))); } if (ImGui.Button("Test Download Progress")) diff --git a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs index 7a27fe0..3227940 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs @@ -1,4 +1,4 @@ -using LightlessSync.API.Data; +using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Dto; using LightlessSync.API.Dto.CharaData; @@ -122,36 +122,38 @@ public partial class ApiController // Fire and forget async operation _ = Task.Run(async () => { - try { var myCidHash = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); - await TryPairWithContentId(request.HashedCid, myCidHash).ConfigureAwait(false); - _pairRequestService.RemoveRequest(request.HashedCid); + try + { + await TryPairWithContentId(request.HashedCid, myCidHash).ConfigureAwait(false); + _pairRequestService.RemoveRequest(request.HashedCid); - _lightlessNotificationService.ShowNotification( - "Pair Request Accepted", - $"Sent a pair request back to {senderName}.", - NotificationType.Info, - TimeSpan.FromSeconds(3)); - } - catch (Exception ex) - { - _lightlessNotificationService.ShowNotification( - "Failed to Accept Pair Request", - ex.Message, - NotificationType.Error, - TimeSpan.FromSeconds(5)); + Mediator.Publish(new NotificationMessage( + "Pair Request Accepted", + $"Sent a pair request back to {senderName}.", + NotificationType.Info, + TimeSpan.FromSeconds(3))); + } + catch (Exception ex) + { + Mediator.Publish(new NotificationMessage( + "Failed to Accept Pair Request", + ex.Message, + NotificationType.Error, + TimeSpan.FromSeconds(5))); + } } }); }, onDecline: () => { _pairRequestService.RemoveRequest(request.HashedCid); - _lightlessNotificationService.ShowNotification( + Mediator.Publish(new NotificationMessage( "Pair Request Declined", $"Declined {senderName}'s pair request.", NotificationType.Info, - TimeSpan.FromSeconds(3)); + TimeSpan.FromSeconds(3))); }); return Task.CompletedTask;