From 090b81c989151357c9edc431839dcd41b701bf5f Mon Sep 17 00:00:00 2001 From: choco Date: Mon, 6 Oct 2025 16:14:34 +0200 Subject: [PATCH 01/15] added notification system for file downloads and pair requests --- .../Configurations/LightlessConfig.cs | 1 + LightlessSync/Plugin.cs | 17 +- .../Services/LightlessNotificationService.cs | 212 ++++++++ LightlessSync/Services/Mediator/Messages.cs | 4 +- LightlessSync/Services/UiService.cs | 9 +- LightlessSync/UI/CompactUI.cs | 4 +- LightlessSync/UI/DownloadUi.cs | 108 +++- LightlessSync/UI/LightlessNotificationUI.cs | 469 ++++++++++++++++++ .../UI/Models/LightlessNotification.cs | 34 ++ LightlessSync/UI/SettingsUi.cs | 10 +- LightlessSync/UI/TopTabMenu.cs | 84 +++- .../ApiController.Functions.Callbacks.cs | 50 +- LightlessSync/WebAPI/SignalR/ApiController.cs | 4 +- 13 files changed, 960 insertions(+), 46 deletions(-) create mode 100644 LightlessSync/Services/LightlessNotificationService.cs create mode 100644 LightlessSync/UI/LightlessNotificationUI.cs create mode 100644 LightlessSync/UI/Models/LightlessNotification.cs diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs index f90472a..c7359e9 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs @@ -56,6 +56,7 @@ public class LightlessConfig : ILightlessConfiguration public bool ShowOnlineNotificationsOnlyForNamedPairs { get; set; } = false; public bool ShowTransferBars { get; set; } = true; public bool ShowTransferWindow { get; set; } = false; + public bool UseNotificationsForDownloads { get; set; } = true; public bool ShowUploading { get; set; } = true; public bool ShowUploadingBigText { get; set; } = true; public bool ShowVisibleUsersSeparately { get; set; } = true; diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index c1648ca..745afd3 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; @@ -175,6 +175,11 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton((s) => new NotificationService(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), notificationManager, chatGui, s.GetRequiredService())); + collection.AddSingleton((s) => new LightlessNotificationService( + s.GetRequiredService>(), + s.GetRequiredService(), + s.GetRequiredService(), + s.GetRequiredService())); collection.AddSingleton((s) => { var httpClient = new HttpClient(); @@ -235,6 +240,11 @@ public sealed class Plugin : IDalamudPlugin collection.AddScoped((s) => new BroadcastUI(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddScoped((s) => new SyncshellFinderUI(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddScoped(); + collection.AddScoped((s) => + new LightlessNotificationUI( + s.GetRequiredService>(), + s.GetRequiredService(), + s.GetRequiredService())); collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); @@ -242,7 +252,9 @@ public sealed class Plugin : IDalamudPlugin collection.AddScoped((s) => new UiService(s.GetRequiredService>(), pluginInterface.UiBuilder, s.GetRequiredService(), s.GetRequiredService(), s.GetServices(), s.GetRequiredService(), - 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())); @@ -261,6 +273,7 @@ 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 new file mode 100644 index 0000000..7dabe71 --- /dev/null +++ b/LightlessSync/Services/LightlessNotificationService.cs @@ -0,0 +1,212 @@ +using Dalamud.Interface; +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 System.Numerics; +namespace LightlessSync.Services; +public class LightlessNotificationService : DisposableMediatorSubscriberBase, IHostedService +{ + private readonly ILogger _logger; + private readonly LightlessConfigService _configService; + private readonly DalamudUtilService _dalamudUtilService; + private LightlessNotificationUI? _notificationUI; + public LightlessNotificationService( + ILogger logger, + LightlessConfigService configService, + DalamudUtilService dalamudUtilService, + LightlessMediator mediator) : base(logger, mediator) + { + _logger = logger; + _configService = configService; + _dalamudUtilService = dalamudUtilService; + } + public Task StartAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + public void SetNotificationUI(LightlessNotificationUI notificationUI) + { + _notificationUI = notificationUI; + } + public void ShowNotification(string title, string message, NotificationType type = NotificationType.Info, + TimeSpan? duration = null, List? actions = null) + { + var notification = new LightlessNotification + { + Title = title, + Message = message, + Type = type, + Duration = duration ?? TimeSpan.FromSeconds(10), + Actions = actions ?? new List() + }; + 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), + 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; + } + } + } + }; + 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 + }; + 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 + }; + Mediator.Publish(new LightlessNotificationMessage(notification)); + } + public void ShowPairDownloadNotification(List<(string playerName, float progress, string status)> downloadStatus) + { + var totalProgress = downloadStatus.Count > 0 ? downloadStatus.Average(x => x.progress) : 0f; + var completedCount = downloadStatus.Count(x => x.progress >= 1.0f); + var totalCount = downloadStatus.Count; + + var message = $"Progress: {completedCount}/{totalCount} completed"; + if (downloadStatus.Any(x => x.progress < 1.0f)) + { + var activeDownloads = downloadStatus.Where(x => x.progress < 1.0f).Take(3); + message += "\n" + 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}"; + })); + + if (downloadStatus.Count(x => x.progress < 1.0f) > 3) + { + message += $"\n• ... and {downloadStatus.Count(x => x.progress < 1.0f) - 3} more"; + } + } + 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)); + } + public void DismissPairDownloadNotification() + { + Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress")); + } +} \ No newline at end of file diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index 963bcbf..a7ee439 100644 --- a/LightlessSync/Services/Mediator/Messages.cs +++ b/LightlessSync/Services/Mediator/Messages.cs @@ -1,4 +1,4 @@ -using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.ClientState.Objects.Types; using LightlessSync.API.Data; using LightlessSync.API.Dto; using LightlessSync.API.Dto.CharaData; @@ -53,6 +53,8 @@ public record NotificationMessage public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage; public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage; public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage; +public record LightlessNotificationMessage(LightlessSync.UI.Models.LightlessNotification Notification) : MessageBase; +public record LightlessNotificationDismissMessage(string NotificationId) : MessageBase; public record CharacterDataAnalyzedMessage : MessageBase; public record PenumbraStartRedrawMessage(IntPtr Address) : MessageBase; public record PenumbraEndRedrawMessage(IntPtr Address) : MessageBase; diff --git a/LightlessSync/Services/UiService.cs b/LightlessSync/Services/UiService.cs index 4071b61..73fc289 100644 --- a/LightlessSync/Services/UiService.cs +++ b/LightlessSync/Services/UiService.cs @@ -22,7 +22,8 @@ public sealed class UiService : DisposableMediatorSubscriberBase LightlessConfigService lightlessConfigService, WindowSystem windowSystem, IEnumerable windows, UiFactory uiFactory, FileDialogManager fileDialogManager, - LightlessMediator lightlessMediator) : base(logger, lightlessMediator) + LightlessMediator lightlessMediator, + LightlessNotificationService lightlessNotificationService) : base(logger, lightlessMediator) { _logger = logger; _logger.LogTrace("Creating {type}", GetType().Name); @@ -40,6 +41,12 @@ 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 b02594f..c724124 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -87,7 +87,7 @@ public class CompactUi : WindowMediatorSubscriberBase IpcManager ipcManager, BroadcastService broadcastService, CharacterAnalyzer characterAnalyzer, - PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService) + PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, LightlessNotificationService lightlessNotificationService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService) { _uiSharedService = uiShared; _configService = configService; @@ -105,7 +105,7 @@ public class CompactUi : WindowMediatorSubscriberBase _renamePairTagUi = renameTagUi; _ipcManager = ipcManager; _broadcastService = broadcastService; - _tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService); + _tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService, lightlessNotificationService); AllowPinning = true; AllowClickthrough = false; diff --git a/LightlessSync/UI/DownloadUi.cs b/LightlessSync/UI/DownloadUi.cs index 2e9f366..eb38bc1 100644 --- a/LightlessSync/UI/DownloadUi.cs +++ b/LightlessSync/UI/DownloadUi.cs @@ -1,4 +1,4 @@ -using System; +using System; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Colors; using LightlessSync.LightlessConfiguration; @@ -22,9 +22,12 @@ public class DownloadUi : WindowMediatorSubscriberBase private readonly UiSharedService _uiShared; private readonly PairProcessingLimiter _pairProcessingLimiter; private readonly ConcurrentDictionary _uploadingPlayers = new(); + private readonly LightlessNotificationService _notificationService; + private bool _notificationDismissed = true; public DownloadUi(ILogger logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService, - PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared, PerformanceCollectorService performanceCollectorService) + PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared, + PerformanceCollectorService performanceCollectorService, LightlessNotificationService notificationService) : base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService) { _dalamudUtilService = dalamudUtilService; @@ -32,6 +35,7 @@ public class DownloadUi : WindowMediatorSubscriberBase _pairProcessingLimiter = pairProcessingLimiter; _fileTransferManager = fileTransferManager; _uiShared = uiShared; + _notificationService = notificationService; SizeConstraints = new WindowSizeConstraints() { @@ -56,7 +60,14 @@ public class DownloadUi : WindowMediatorSubscriberBase IsOpen = true; Mediator.Subscribe(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus); - Mediator.Subscribe(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _)); + Mediator.Subscribe(this, (msg) => + { + _currentDownloads.TryRemove(msg.DownloadId, out _); + if (!_currentDownloads.Any()) + { + _notificationService.DismissPairDownloadNotification(); + } + }); Mediator.Subscribe(this, (_) => IsOpen = false); Mediator.Subscribe(this, (_) => IsOpen = true); Mediator.Subscribe(this, (msg) => @@ -122,28 +133,46 @@ public class DownloadUi : WindowMediatorSubscriberBase try { - foreach (var item in _currentDownloads.ToList()) + if (_configService.Current.UseNotificationsForDownloads) { - var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot); - var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue); - var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading); - var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing); - var totalFiles = item.Value.Sum(c => c.Value.TotalFiles); - var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles); - var totalBytes = item.Value.Sum(c => c.Value.TotalBytes); - var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes); + // Use new notification stuff + if (_currentDownloads.Any()) + { + UpdateDownloadNotification(); + _notificationDismissed = false; + } + else if (!_notificationDismissed) + { + _notificationService.DismissPairDownloadNotification(); + _notificationDismissed = true; + } + } + else + { + // text overlay + foreach (var item in _currentDownloads.ToList()) + { + var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot); + var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue); + var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading); + var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing); + var totalFiles = item.Value.Sum(c => c.Value.TotalFiles); + var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles); + var totalBytes = item.Value.Sum(c => c.Value.TotalBytes); + var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes); - UiSharedService.DrawOutlinedFont($"▼", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); - ImGui.SameLine(); - var xDistance = ImGui.GetCursorPosX(); - UiSharedService.DrawOutlinedFont( - $"{item.Key.Name} [W:{dlSlot}/Q:{dlQueue}/P:{dlProg}/D:{dlDecomp}]", - ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); - ImGui.NewLine(); - ImGui.SameLine(xDistance); - UiSharedService.DrawOutlinedFont( - $"{transferredFiles}/{totalFiles} ({UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)})", - ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); + UiSharedService.DrawOutlinedFont($"▼", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); + ImGui.SameLine(); + var xDistance = ImGui.GetCursorPosX(); + UiSharedService.DrawOutlinedFont( + $"{item.Key.Name} [W:{dlSlot}/Q:{dlQueue}/P:{dlProg}/D:{dlDecomp}]", + ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); + ImGui.NewLine(); + ImGui.SameLine(xDistance); + UiSharedService.DrawOutlinedFont( + $"{transferredFiles}/{totalFiles} ({UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)})", + ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); + } } } catch @@ -262,4 +291,37 @@ public class DownloadUi : WindowMediatorSubscriberBase MaximumSize = new Vector2(300, maxHeight), }; } + + private void UpdateDownloadNotification() + { + var downloadStatus = new List<(string playerName, float progress, string status)>(); + + foreach (var item in _currentDownloads.ToList()) + { + var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot); + var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue); + var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading); + var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing); + var totalFiles = item.Value.Sum(c => c.Value.TotalFiles); + var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles); + var totalBytes = item.Value.Sum(c => c.Value.TotalBytes); + var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes); + + var progress = totalBytes > 0 ? (float)transferredBytes / totalBytes : 0f; + + string status; + if (dlDecomp > 0) status = "decompressing"; + else if (dlProg > 0) status = "downloading"; + else if (dlQueue > 0) status = "queued"; + else if (dlSlot > 0) status = "waiting"; + else status = "completed"; + + downloadStatus.Add((item.Key.Name, progress, status)); + } + + if (downloadStatus.Any()) + { + _notificationService.ShowPairDownloadNotification(downloadStatus); + } + } } \ No newline at end of file diff --git a/LightlessSync/UI/LightlessNotificationUI.cs b/LightlessSync/UI/LightlessNotificationUI.cs new file mode 100644 index 0000000..f1c8455 --- /dev/null +++ b/LightlessSync/UI/LightlessNotificationUI.cs @@ -0,0 +1,469 @@ +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Interface.Windowing; +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; + +namespace LightlessSync.UI; + +public class LightlessNotificationUI : WindowMediatorSubscriberBase +{ + private readonly List _notifications = new(); + private readonly object _notificationLock = new(); + + private const float NotificationWidth = 350f; + private const float NotificationMinHeight = 60f; + 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) + : base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector) + { + Flags = ImGuiWindowFlags.NoDecoration | + ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoResize | + ImGuiWindowFlags.NoSavedSettings | + ImGuiWindowFlags.NoFocusOnAppearing | + ImGuiWindowFlags.NoNav | + ImGuiWindowFlags.NoBackground | + ImGuiWindowFlags.NoCollapse | + ImGuiWindowFlags.AlwaysAutoResize; + + var viewport = ImGui.GetMainViewport(); + if (viewport.WorkSize.X > 0) + { + Position = new Vector2(viewport.WorkPos.X + viewport.WorkSize.X - NotificationWidth - EdgeXMargin, + viewport.WorkPos.Y + EdgeYMargin); + PositionCondition = ImGuiCond.Always; + } + + Size = new Vector2(NotificationWidth, 100); + SizeCondition = ImGuiCond.FirstUseEver; + IsOpen = false; + RespectCloseHotkey = false; + DisableWindowSounds = true; + + Mediator.Subscribe(this, HandleNotificationMessage); + Mediator.Subscribe(this, HandleNotificationDismissMessage); + } + private void HandleNotificationMessage(LightlessNotificationMessage message) + { + AddNotification(message.Notification); + } + + private void HandleNotificationDismissMessage(LightlessNotificationDismissMessage message) + { + RemoveNotification(message.NotificationId); + } + + public void AddNotification(LightlessNotification notification) + { + lock (_notificationLock) + { + var existingNotification = _notifications.FirstOrDefault(n => n.Id == notification.Id); + if (existingNotification != null) + { + // Update existing notification without restarting animation + existingNotification.Message = notification.Message; + existingNotification.Progress = notification.Progress; + existingNotification.ShowProgress = notification.ShowProgress; + existingNotification.Title = notification.Title; + _logger.LogDebug("Updated existing notification: {Title}", notification.Title); + } + else + { + _notifications.Add(notification); + _logger.LogDebug("Added new notification: {Title}", notification.Title); + } + + if (!IsOpen) + { + IsOpen = true; + } + } + } + + public void RemoveNotification(string id) + { + lock (_notificationLock) + { + var notification = _notifications.FirstOrDefault(n => n.Id == id); + if (notification != null) + { + notification.IsAnimatingOut = true; + notification.IsAnimatingIn = false; + } + } + } + + protected override void DrawInternal() + { + lock (_notificationLock) + { + UpdateNotifications(); + + if (_notifications.Count == 0) + { + IsOpen = false; + return; + } + + var viewport = ImGui.GetMainViewport(); + var windowPos = new Vector2( + viewport.WorkPos.X + viewport.WorkSize.X - NotificationWidth - EdgeXMargin, + viewport.WorkPos.Y + EdgeYMargin + ); + ImGui.SetWindowPos(windowPos); + + for (int i = 0; i < _notifications.Count; i++) + { + DrawNotification(_notifications[i], i); + + if (i < _notifications.Count - 1) + { + ImGui.Dummy(new Vector2(0, NotificationSpacing)); + } + } + } + } + + private void UpdateNotifications() + { + var deltaTime = ImGui.GetIO().DeltaTime; + + for (int i = _notifications.Count - 1; i >= 0; i--) + { + var notification = _notifications[i]; + + if (notification.IsAnimatingIn && notification.AnimationProgress < 1f) + { + notification.AnimationProgress = Math.Min(1f, notification.AnimationProgress + deltaTime * AnimationSpeed); + } + else if (notification.IsAnimatingOut && notification.AnimationProgress > 0f) + { + notification.AnimationProgress = Math.Max(0f, notification.AnimationProgress - deltaTime * (AnimationSpeed * 0.7f)); + } + else if (!notification.IsAnimatingOut && !notification.IsDismissed) + { + notification.IsAnimatingIn = false; + + if (notification.IsExpired && !notification.IsAnimatingOut) + { + notification.IsAnimatingOut = true; + notification.IsAnimatingIn = false; + } + } + + if (notification.IsAnimatingOut && notification.AnimationProgress <= 0.01f) + { + _notifications.RemoveAt(i); + } + } + } + + private void DrawNotification(LightlessNotification notification, int index) + { + var alpha = notification.AnimationProgress; + if (alpha <= 0f) return; + + var slideOffset = (1f - alpha) * SlideDistance; + var originalCursorPos = ImGui.GetCursorPos(); + ImGui.SetCursorPosX(originalCursorPos.X + slideOffset); + + var notificationHeight = CalculateNotificationHeight(notification); + + using var child = ImRaii.Child($"notification_{notification.Id}", + new Vector2(NotificationWidth - slideOffset, notificationHeight), + false, ImGuiWindowFlags.NoScrollbar); + + if (child.Success) + { + DrawNotificationContent(notification, alpha); + } + } + + private void DrawNotificationContent(LightlessNotification notification, float alpha) + { + var drawList = ImGui.GetWindowDrawList(); + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + + var bgColor = new Vector4(30f/255f, 30f/255f, 30f/255f, 0.95f * alpha); + var accentColor = GetNotificationAccentColor(notification.Type); + var progressBarColor = UIColors.Get("LightlessBlue"); + accentColor.W *= alpha; + + var shadowOffset = new Vector2(1f, 1f); + var shadowColor = new Vector4(0f, 0f, 0f, 0.4f * alpha); + drawList.AddRectFilled( + windowPos + shadowOffset, + windowPos + windowSize + shadowOffset, + ImGui.ColorConvertFloat4ToU32(shadowColor), + 3f + ); + + var isHovered = ImGui.IsWindowHovered(); + if (isHovered) + { + bgColor = bgColor * 1.1f; + bgColor.W = Math.Min(bgColor.W, 0.98f); + } + + drawList.AddRectFilled( + windowPos, + windowPos + windowSize, + ImGui.ColorConvertFloat4ToU32(bgColor), + 3f + ); + + var accentWidth = 3f; + drawList.AddRectFilled( + windowPos, + windowPos + new Vector2(accentWidth, windowSize.Y), + ImGui.ColorConvertFloat4ToU32(accentColor), + 3f + ); + + DrawDurationProgressBar(notification, alpha, windowPos, windowSize, progressBarColor, drawList); + + DrawNotificationText(notification, alpha); + } + + private void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, Vector4 progressBarColor, ImDrawListPtr drawList) + { + var elapsed = DateTime.UtcNow - notification.CreatedAt; + var progress = Math.Min(1.0f, (float)(elapsed.TotalSeconds / notification.Duration.TotalSeconds)); + + var progressHeight = 2f; + var progressY = windowPos.Y + windowSize.Y - progressHeight; + var progressWidth = windowSize.X * progress; + + var bgProgressColor = new Vector4(progressBarColor.X * 0.3f, progressBarColor.Y * 0.3f, progressBarColor.Z * 0.3f, 0.5f * alpha); + drawList.AddRectFilled( + new Vector2(windowPos.X, progressY), + new Vector2(windowPos.X + windowSize.X, progressY + progressHeight), + ImGui.ColorConvertFloat4ToU32(bgProgressColor), + 0f + ); + + if (progress > 0) + { + var progressColor = progressBarColor; + progressColor.W *= alpha; + drawList.AddRectFilled( + new Vector2(windowPos.X, progressY), + new Vector2(windowPos.X + progressWidth, progressY + progressHeight), + ImGui.ColorConvertFloat4ToU32(progressColor), + 0f + ); + } + } + + private void DrawNotificationText(LightlessNotification notification, float alpha) + { + var padding = new Vector2(10f, 6f); + var contentPos = new Vector2(6f, 0f) + padding; + var windowSize = ImGui.GetWindowSize(); + var contentSize = windowSize - padding * 2 - new Vector2(6f, 0f); + + ImGui.SetCursorPos(contentPos); + + using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1f, 1f, 1f, alpha))) + { + ImGui.Text(notification.Title); + } + + if (!string.IsNullOrEmpty(notification.Message)) + { + ImGui.SetCursorPos(contentPos + new Vector2(0f, 18f)); + ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + contentSize.X); + using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(0.9f, 0.9f, 0.9f, alpha))) + { + ImGui.TextWrapped(notification.Message); + } + ImGui.PopTextWrapPos(); + } + + if (notification.ShowProgress) + { + ImGui.Spacing(); + var progressColor = GetNotificationAccentColor(notification.Type); + progressColor.W *= alpha; + using (ImRaii.PushColor(ImGuiCol.PlotHistogram, progressColor)) + { + ImGui.ProgressBar(notification.Progress, new Vector2(contentSize.X, 2f), ""); + } + } + + if (notification.Actions.Count > 0) + { + ImGui.Spacing(); + ImGui.SetCursorPosX(contentPos.X); + DrawNotificationActions(notification, contentSize.X, alpha); + } + } + + private void DrawNotificationActions(LightlessNotification notification, float availableWidth, float alpha) + { + var buttonSpacing = 8f; + var rightPadding = 10f; + var usableWidth = availableWidth - rightPadding; + var totalSpacing = (notification.Actions.Count - 1) * buttonSpacing; + var buttonWidth = (usableWidth - totalSpacing) / notification.Actions.Count; + + _logger.LogDebug("Drawing {ActionCount} notification actions, buttonWidth: {ButtonWidth}, availableWidth: {AvailableWidth}", + notification.Actions.Count, buttonWidth, availableWidth); + + var startCursorPos = ImGui.GetCursorPos(); + + for (int i = 0; i < notification.Actions.Count; i++) + { + var action = notification.Actions[i]; + + if (i > 0) + { + ImGui.SameLine(); + var currentX = startCursorPos.X + i * (buttonWidth + buttonSpacing); + ImGui.SetCursorPosX(currentX); + } + DrawActionButton(action, notification, alpha, buttonWidth); + } + } + + private void DrawActionButton(LightlessNotificationAction action, LightlessNotification notification, float alpha, float buttonWidth) + { + _logger.LogDebug("Drawing action button: {ActionId} - {ActionLabel}, width: {ButtonWidth}", action.Id, action.Label, buttonWidth); + + var buttonColor = action.Color; + buttonColor.W *= alpha; + + var hoveredColor = buttonColor * 1.1f; + hoveredColor.W = buttonColor.W; + + var activeColor = buttonColor * 0.9f; + activeColor.W = buttonColor.W; + + using (ImRaii.PushColor(ImGuiCol.Button, buttonColor)) + using (ImRaii.PushColor(ImGuiCol.ButtonHovered, hoveredColor)) + using (ImRaii.PushColor(ImGuiCol.ButtonActive, activeColor)) + using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1f, 1f, 1f, alpha))) + { + var buttonPressed = false; + + if (action.Icon != FontAwesomeIcon.None) + { + buttonPressed = DrawIconTextButton(action.Icon, action.Label, buttonWidth, alpha); + } + else + { + buttonPressed = ImGui.Button(action.Label, new Vector2(buttonWidth, 0)); + } + + _logger.LogDebug("Button {ActionId} pressed: {ButtonPressed}", action.Id, buttonPressed); + + if (buttonPressed) + { + try + { + _logger.LogDebug("Executing action: {ActionId}", action.Id); + action.OnClick(notification); + _logger.LogDebug("Action executed successfully: {ActionId}", action.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error executing notification action: {ActionId}", action.Id); + } + } + } + } + + private bool DrawIconTextButton(FontAwesomeIcon icon, string text, float width, float alpha) + { + var drawList = ImGui.GetWindowDrawList(); + var cursorPos = ImGui.GetCursorScreenPos(); + var frameHeight = ImGui.GetFrameHeight(); + + Vector2 iconSize; + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + iconSize = ImGui.CalcTextSize(icon.ToIconString()); + } + + var textSize = ImGui.CalcTextSize(text); + var spacing = 3f * ImGuiHelpers.GlobalScale; + var totalTextWidth = iconSize.X + spacing + textSize.X; + + var buttonPressed = ImGui.InvisibleButton($"btn_{icon}_{text}", new Vector2(width, frameHeight)); + + var buttonMin = ImGui.GetItemRectMin(); + var buttonMax = ImGui.GetItemRectMax(); + var buttonSize = buttonMax - buttonMin; + + var buttonColor = ImGui.GetColorU32(ImGuiCol.Button); + if (ImGui.IsItemHovered()) + buttonColor = ImGui.GetColorU32(ImGuiCol.ButtonHovered); + if (ImGui.IsItemActive()) + buttonColor = ImGui.GetColorU32(ImGuiCol.ButtonActive); + + drawList.AddRectFilled(buttonMin, buttonMax, buttonColor, 3f); + + var iconPos = buttonMin + new Vector2((buttonSize.X - totalTextWidth) / 2f, (buttonSize.Y - iconSize.Y) / 2f); + var textPos = iconPos + new Vector2(iconSize.X + spacing, (iconSize.Y - textSize.Y) / 2f); + + var textColor = ImGui.GetColorU32(ImGuiCol.Text); + + // Draw icon + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + drawList.AddText(iconPos, textColor, icon.ToIconString()); + } + + // Draw text + drawList.AddText(textPos, textColor, text); + + return buttonPressed; + } + + private float CalculateNotificationHeight(LightlessNotification notification) + { + var height = 40f; + if (!string.IsNullOrEmpty(notification.Message)) + { + var textSize = ImGui.CalcTextSize(notification.Message, true, NotificationWidth - 35f); + height += textSize.Y + 4f; + } + + if (notification.ShowProgress) + { + height += 12f; + } + + if (notification.Actions.Count > 0) + { + height += 28f; + } + + return Math.Max(height, NotificationMinHeight); + } + + private Vector4 GetNotificationAccentColor(NotificationType type) + { + return type switch + { + NotificationType.Info => UIColors.Get("LightlessPurple"), + NotificationType.Warning => UIColors.Get("LightlessYellow"), + NotificationType.Error => UIColors.Get("DimRed"), + _ => UIColors.Get("LightlessPurple") + }; + } +} \ No newline at end of file diff --git a/LightlessSync/UI/Models/LightlessNotification.cs b/LightlessSync/UI/Models/LightlessNotification.cs new file mode 100644 index 0000000..c9000da --- /dev/null +++ b/LightlessSync/UI/Models/LightlessNotification.cs @@ -0,0 +1,34 @@ +using Dalamud.Interface; +using LightlessSync.LightlessConfiguration.Models; +using System.Numerics; +namespace LightlessSync.UI.Models; +public class LightlessNotification +{ + public string Id { get; set; } = Guid.NewGuid().ToString(); + public string Title { get; set; } = string.Empty; + public string Message { get; set; } = string.Empty; + public NotificationType Type { get; set; } = NotificationType.Info; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public TimeSpan Duration { get; set; } = TimeSpan.FromSeconds(5); + public bool IsExpired => DateTime.UtcNow - CreatedAt > Duration; + public bool IsDismissed { get; set; } = false; + public List Actions { get; set; } = new(); + public bool ShowProgress { get; set; } = false; + public float Progress { get; set; } = 0f; + public bool IsMinimized { get; set; } = false; + + // Animation properties + public float AnimationProgress { get; set; } = 0f; + public bool IsAnimatingIn { get; set; } = true; + public bool IsAnimatingOut { get; set; } = false; +} +public class LightlessNotificationAction +{ + public string Id { get; set; } = Guid.NewGuid().ToString(); + public string Label { get; set; } = string.Empty; + public FontAwesomeIcon Icon { get; set; } = FontAwesomeIcon.None; + public Vector4 Color { get; set; } = Vector4.One; + public Action OnClick { get; set; } = _ => { }; + public bool IsPrimary { get; set; } = false; + public bool IsDestructive { get; set; } = false; +} \ No newline at end of file diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 0a592f0..c54e912 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; @@ -351,6 +351,14 @@ 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."); + bool showTransferWindow = _configService.Current.ShowTransferWindow; if (ImGui.Checkbox("Show separate transfer window", ref showTransferWindow)) { diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index 8a9e6c9..52b15a6 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -33,13 +33,14 @@ public class TopTabMenu private bool _pairRequestsExpanded; // useless for now private int _lastRequestCount; private readonly UiSharedService _uiSharedService; + private readonly LightlessNotificationService _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) + public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, LightlessNotificationService lightlessNotificationService) { _lightlessMediator = lightlessMediator; _apiController = apiController; @@ -47,6 +48,7 @@ public class TopTabMenu _pairRequestService = pairRequestService; _dalamudUtilService = dalamudUtilService; _uiSharedService = uiSharedService; + _lightlessNotificationService = lightlessNotificationService; } private enum SelectedTab @@ -199,15 +201,79 @@ public class TopTabMenu if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f); #if DEBUG - if (ImGui.Button("Add Test Pair Request")) + if (ImGui.Button("Test Pair Request")) { - var fakeCid = Guid.NewGuid().ToString("N"); - var display = _pairRequestService.RegisterIncomingRequest(fakeCid, "Debug pair request"); - _lightlessMediator.Publish(new NotificationMessage( - "Pair request received (debug)", - display.Message, + _lightlessNotificationService.ShowPairRequestNotification( + "Debug User", + "debug-user-id", + onAccept: () => + { + _lightlessNotificationService.ShowNotification( + "Pair Accepted", + "Debug pair request was accepted!", + NotificationType.Info, + TimeSpan.FromSeconds(3)); + }, + onDecline: () => + { + _lightlessNotificationService.ShowNotification( + "Pair Declined", + "Debug pair request was declined.", + NotificationType.Warning, + TimeSpan.FromSeconds(3)); + } + ); + } + + ImGui.SameLine(); + if (ImGui.Button("Test Info")) + { + _lightlessNotificationService.ShowNotification( + "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( + "Warning", + "This is a test warning notification.", + NotificationType.Warning, + TimeSpan.FromSeconds(7)); + } + + ImGui.SameLine(); + if (ImGui.Button("Test Error")) + { + _lightlessNotificationService.ShowNotification( + "Error", + "This is a test error notification erp police", + NotificationType.Error, + TimeSpan.FromSeconds(10)); + } + + if (ImGui.Button("Test Download Progress")) + { + var downloadStatus = new List<(string playerName, float progress, string status)> + { + ("Mauwmauw Nekochan", 0.85f, "downloading"), + ("Raelynn Kitsune", 0.34f, "downloading"), + ("Jaina Elraeth", 0.67f, "downloading"), + ("Vaelstra Bloodthorn", 0.19f, "downloading"), + ("Lydia Hera Moondrop", 0.86f, "downloading"), + ("C'liina Star", 1.0f, "completed") + }; + + _lightlessNotificationService.ShowPairDownloadNotification(downloadStatus); + } + + ImGui.SameLine(); + if (ImGui.Button("Dismiss Download")) + { + _lightlessNotificationService.DismissPairDownloadNotification(); } #endif @@ -850,4 +916,4 @@ public class TopTabMenu ImGui.EndPopup(); } } -} +} \ No newline at end of file diff --git a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs index 263c87a..7a27fe0 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs @@ -6,6 +6,7 @@ using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.User; using LightlessSync.LightlessConfiguration.Models; using LightlessSync.Services.Mediator; +using LightlessSync.Utils; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Logging; @@ -104,7 +105,6 @@ public partial class ApiController return Task.CompletedTask; } - public Task Client_ReceiveBroadcastPairRequest(UserPairNotificationDto dto) { if (dto == null) @@ -112,17 +112,55 @@ public partial class ApiController var request = _pairRequestService.RegisterIncomingRequest(dto.myHashedCid, dto.message ?? string.Empty); - Mediator.Publish(new NotificationMessage( - "Pair request received", - request.Message, - NotificationType.Info, - TimeSpan.FromSeconds(5))); + // Use the new interactive notification system for pair requests + var senderName = string.IsNullOrEmpty(request.DisplayName) ? "Unknown User" : request.DisplayName; + _lightlessNotificationService.ShowPairRequestNotification( + senderName, + request.HashedCid, + onAccept: () => + { + // 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); + + _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)); + } + }); + }, + onDecline: () => + { + _pairRequestService.RemoveRequest(request.HashedCid); + _lightlessNotificationService.ShowNotification( + "Pair Request Declined", + $"Declined {senderName}'s pair request.", + NotificationType.Info, + TimeSpan.FromSeconds(3)); + }); return Task.CompletedTask; } + public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo) { SystemInfoDto = systemInfo; + //Mediator.Publish(new UpdateSystemInfoMessage(systemInfo)); return Task.CompletedTask; } diff --git a/LightlessSync/WebAPI/SignalR/ApiController.cs b/LightlessSync/WebAPI/SignalR/ApiController.cs index 90be67f..fb21d2a 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.cs @@ -32,6 +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 CancellationTokenSource _connectionCancellationTokenSource; private ConnectionDto? _connectionDto; private bool _doNotNotifyOnNextInfo = false; @@ -44,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) : base(logger, mediator) + TokenProvider tokenProvider, LightlessConfigService lightlessConfigService, LightlessNotificationService lightlessNotificationService) : base(logger, mediator) { _hubFactory = hubFactory; _dalamudUtil = dalamudUtil; @@ -53,6 +54,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL _serverManager = serverManager; _tokenProvider = tokenProvider; _lightlessConfigService = lightlessConfigService; + _lightlessNotificationService = lightlessNotificationService; _connectionCancellationTokenSource = new CancellationTokenSource(); Mediator.Subscribe(this, (_) => DalamudUtilOnLogIn()); From 83e4555e4b99363f5446b88ad5d56686f14c0bd1 Mon Sep 17 00:00:00 2001 From: choco Date: Mon, 6 Oct 2025 20:25:47 +0200 Subject: [PATCH 02/15] notifications improvement, working pairs incoming request feature and working user logging in notif --- LightlessSync/PlayerData/Pairs/PairManager.cs | 7 +- LightlessSync/Plugin.cs | 2 +- .../Services/LightlessNotificationService.cs | 108 +++++++++++++++--- LightlessSync/UI/DownloadUi.cs | 46 ++++---- .../UI/Models/LightlessNotification.cs | 5 +- LightlessSync/UI/Models/NotificationSounds.cs | 50 ++++++++ LightlessSync/UI/TopTabMenu.cs | 7 +- 7 files changed, 176 insertions(+), 49 deletions(-) create mode 100644 LightlessSync/UI/Models/NotificationSounds.cs 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")) { From 9b6d00570ece6d109572c9cf65ba43d0d8410bca Mon Sep 17 00:00:00 2001 From: choco Date: Mon, 6 Oct 2025 21:55:45 +0200 Subject: [PATCH 03/15] implemened game sound effects for notifs --- .../Services/LightlessNotificationService.cs | 30 ++++----- LightlessSync/UI/Models/NotificationSounds.cs | 62 +++++++++++++------ 2 files changed, 55 insertions(+), 37 deletions(-) diff --git a/LightlessSync/Services/LightlessNotificationService.cs b/LightlessSync/Services/LightlessNotificationService.cs index c926d34..1afd9a5 100644 --- a/LightlessSync/Services/LightlessNotificationService.cs +++ b/LightlessSync/Services/LightlessNotificationService.cs @@ -1,4 +1,5 @@ using Dalamud.Interface; +using Dalamud.Plugin.Services; using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration.Models; using LightlessSync.Services.Mediator; @@ -6,6 +7,8 @@ using LightlessSync.UI; using LightlessSync.UI.Models; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using FFXIVClientStructs.FFXIV.Client.UI; + namespace LightlessSync.Services; public class LightlessNotificationService : DisposableMediatorSubscriberBase, IHostedService { @@ -48,7 +51,6 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH SoundEffectId = soundEffectId ?? NotificationSounds.GetDefaultSound(type) }; - // Play sound effect if specified if (notification.SoundEffectId.HasValue) { PlayNotificationSound(notification.SoundEffectId.Value); @@ -100,7 +102,6 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH } }; - // Play sound effect if (notification.SoundEffectId.HasValue) { PlayNotificationSound(notification.SoundEffectId.Value); @@ -140,7 +141,6 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH SoundEffectId = NotificationSounds.DownloadComplete }; - // Play sound effect if (notification.SoundEffectId.HasValue) { PlayNotificationSound(notification.SoundEffectId.Value); @@ -188,7 +188,6 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH SoundEffectId = NotificationSounds.Error }; - // Play sound effect if (notification.SoundEffectId.HasValue) { PlayNotificationSound(notification.SoundEffectId.Value); @@ -198,7 +197,6 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH } public void ShowPairDownloadNotification(List<(string playerName, float progress, string status)> downloadStatus, int queueWaiting = 0) { - // Filter out queue status from user downloads var userDownloads = downloadStatus.Where(x => x.playerName != "Pair Queue").ToList(); var totalProgress = userDownloads.Count > 0 ? userDownloads.Average(x => x.progress) : 0f; @@ -207,13 +205,11 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH var message = ""; - // Add queue status at the top if there are waiting items if (queueWaiting > 0) { message = $"Queue: {queueWaiting} waiting"; } - // Add download progress if there are downloads if (totalCount > 0) { var progressMessage = $"Progress: {completedCount}/{totalCount} completed"; @@ -240,7 +236,6 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH 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 @@ -259,6 +254,7 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH DismissPairDownloadNotification(); } } + public void DismissPairDownloadNotification() { Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress")); @@ -268,15 +264,15 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH { 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 + 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) { diff --git a/LightlessSync/UI/Models/NotificationSounds.cs b/LightlessSync/UI/Models/NotificationSounds.cs index 962263a..8b60532 100644 --- a/LightlessSync/UI/Models/NotificationSounds.cs +++ b/LightlessSync/UI/Models/NotificationSounds.cs @@ -1,42 +1,64 @@ -using LightlessSync.LightlessConfiguration.Models; +using LightlessSync.LightlessConfiguration.Models; namespace LightlessSync.UI.Models; /// -/// Common FFXIV sound effect IDs for notifications +/// Common FFXIV sound effect IDs for notifications. +/// These correspond to the same sound IDs used in macros (1–16). /// public static class NotificationSounds { + // ───────────────────────────────────────────── + // Base IDs (1–16) + // https://ffxiv.consolegameswiki.com/wiki/Macros#Sound_Effects + // ───────────────────────────────────────────── + public const uint Se1 = 1; // Soft chime + public const uint Se2 = 2; // Higher chime + public const uint Se3 = 3; // Bell tone + public const uint Se4 = 4; // Harp tone + public const uint Se5 = 5; // Drum / percussion + public const uint Se6 = 6; // Mechanical click + public const uint Se7 = 7; // Metallic chime + public const uint Se8 = 8; // Wooden tone + public const uint Se9 = 9; // Wind / flute tone + public const uint Se10 = 10; // Magical sparkle + public const uint Se11 = 11; // Metallic ring + public const uint Se12 = 12; // Deep thud + public const uint Se13 = 13; // "Tell received" ping + public const uint Se14 = 14; // Success fanfare short + public const uint Se15 = 15; // System warning + public const uint Se16 = 16; // Error / failure────────────────────────────────────────── + /// - /// General notification sound (quest complete) + /// General notification sound () /// - public const uint Info = 37; - + public const uint Info = Se2; + /// - /// Warning/alert sound (system error) + /// Warning/alert sound () /// - public const uint Warning = 15; - + public const uint Warning = Se15; + /// - /// Error sound (action failed) + /// Error sound () /// - public const uint Error = 16; - + public const uint Error = Se16; + /// - /// Success sound (level up) + /// Success sound () /// - public const uint Success = 25; - + public const uint Success = Se14; + /// - /// Pair request sound (tell received) + /// Pair request sound (, same as tell notification) /// - public const uint PairRequest = 13; - + public const uint PairRequest = Se13; + /// - /// Download complete sound (item obtained) + /// Download complete sound (, a clean sparkle tone) /// - public const uint DownloadComplete = 30; - + public const uint DownloadComplete = Se10; + /// /// Get default sound for notification type /// From 27e7fb7ed91bfb89f5cf4d6ffa43701d307dc864 Mon Sep 17 00:00:00 2001 From: choco Date: Wed, 8 Oct 2025 23:20:58 +0200 Subject: [PATCH 04/15] 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; From 2b118df892c291fd4aa6a99ab24b81eed433b889 Mon Sep 17 00:00:00 2001 From: choco Date: Thu, 9 Oct 2025 11:13:47 +0200 Subject: [PATCH 05/15] notifications refactor with duplication bugfix --- LightlessSync/PlayerData/Pairs/PairManager.cs | 4 +- LightlessSync/Plugin.cs | 17 +- .../Services/LightlessNotificationService.cs | 501 ------------------ LightlessSync/Services/NotificationService.cs | 463 ++++++++++++++-- LightlessSync/Services/UiService.cs | 10 +- LightlessSync/UI/CompactUI.cs | 4 +- LightlessSync/UI/DownloadUi.cs | 6 +- LightlessSync/UI/SettingsUi.cs | 4 +- LightlessSync/UI/TopTabMenu.cs | 4 +- LightlessSync/WebAPI/SignalR/ApiController.cs | 6 +- 10 files changed, 434 insertions(+), 585 deletions(-) delete mode 100644 LightlessSync/Services/LightlessNotificationService.cs 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; From 0dfa667ed3857468f26282cd97e455992fe8bb54 Mon Sep 17 00:00:00 2001 From: choco Date: Thu, 9 Oct 2025 11:31:35 +0200 Subject: [PATCH 06/15] removed fallback logic in NotificationService and some settings cleanup --- LightlessSync/Services/NotificationService.cs | 60 ++++---------- LightlessSync/UI/SettingsUi.cs | 83 +++++-------------- 2 files changed, 36 insertions(+), 107 deletions(-) diff --git a/LightlessSync/Services/NotificationService.cs b/LightlessSync/Services/NotificationService.cs index 990e2b6..c15fd3f 100644 --- a/LightlessSync/Services/NotificationService.cs +++ b/LightlessSync/Services/NotificationService.cs @@ -336,43 +336,24 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ 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 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)) + // Get notification location based on type and system pref + var location = _configService.Current.UseLightlessNotifications + ? msg.Type switch { - ShowNotificationLocationBased(msg, oldLocation); + NotificationType.Info => _configService.Current.LightlessInfoNotification, + NotificationType.Warning => _configService.Current.LightlessWarningNotification, + NotificationType.Error => _configService.Current.LightlessErrorNotification, + _ => NotificationLocation.LightlessUI } - } + : msg.Type switch + { + NotificationType.Info => _configService.Current.InfoNotification, + NotificationType.Warning => _configService.Current.WarningNotification, + NotificationType.Error => _configService.Current.ErrorNotification, + _ => NotificationLocation.Nowhere + }; + + ShowNotificationLocationBased(msg, location); } private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location) @@ -488,13 +469,4 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ _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/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index ce7e63a..074eb60 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -2623,24 +2623,25 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGuiHelpers.ScaledDummy(5); bool useLightlessNotifications = _configService.Current.UseLightlessNotifications; - if (ImGui.Checkbox("Use Enhanced Lightless Notifications", ref useLightlessNotifications)) + if (ImGui.Checkbox("Use 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."); + _uiShared.DrawHelpText("Enable modern notifications with interactive buttons, animations, and progress tracking. Disable for classic Dalamud toast/chat notifications."); + + ImGuiHelpers.ScaledDummy(5); 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); + UiSharedService.ColorTextWrapped("Choose where each notification type appears.", ImGuiColors.DalamudGrey); ImGuiHelpers.ScaledDummy(5); if (useLightlessNotifications) { - // Enhanced notification locations (primary) - _uiShared.BigText("Enhanced Notification Locations"); - ImGuiHelpers.ScaledDummy(3); + // Lightless notification locations + ImGui.Indent(); var lightlessLocations = GetLightlessNotificationLocations(); @@ -2674,53 +2675,14 @@ public class SettingsUi : WindowMediatorSubscriberBase _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); - } + + ImGui.Unindent(); } else { - // Only classic notifications when enhanced is disabled + // Classic notifications when lightless notifs is disabled var classicLocations = GetClassicNotificationLocations(); - _uiShared.BigText("Classic Notification Locations"); - ImGuiHelpers.ScaledDummy(3); + ImGui.Indent(); ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted("Info Notifications:"); ImGui.SameLine(); @@ -2750,23 +2712,17 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.ErrorNotification = location; _configService.Save(); }, _configService.Current.ErrorNotification); + + ImGui.Unindent(); } + + ImGuiHelpers.ScaledDummy(3); 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) - { + _uiShared.UnderlinedBigText("Test Notifications", UIColors.Get("LightlessBlue")); + ImGuiHelpers.ScaledDummy(5); + ImGui.Indent(); // Test notification buttons @@ -2784,10 +2740,11 @@ public class SettingsUi : WindowMediatorSubscriberBase { 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."); + _uiShared.DrawHelpText("Preview how notifications will appear with your current settings."); ImGui.Unindent(); + ImGuiHelpers.ScaledDummy(5); ImGui.Separator(); _uiShared.UnderlinedBigText("Basic Settings", UIColors.Get("LightlessBlue")); ImGuiHelpers.ScaledDummy(5); From d295f3e22d615723cb9d108b00cb9244bd365548 Mon Sep 17 00:00:00 2001 From: choco Date: Thu, 9 Oct 2025 13:56:40 +0200 Subject: [PATCH 07/15] pair/downloads notif changes + more settings options --- .../Configurations/LightlessConfig.cs | 9 +-- .../Models/NotificationLocation.cs | 9 ++- LightlessSync/Services/NotificationService.cs | 19 +++--- LightlessSync/UI/DownloadUi.cs | 13 +++- LightlessSync/UI/LightlessNotificationUI.cs | 2 + LightlessSync/UI/SettingsUi.cs | 59 +++++++++++++------ 6 files changed, 74 insertions(+), 37 deletions(-) diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs index b102cf0..80193d4 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs @@ -77,9 +77,11 @@ public class LightlessConfig : ILightlessConfiguration 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 NotificationLocation LightlessInfoNotification { get; set; } = NotificationLocation.LightlessUi; + public NotificationLocation LightlessWarningNotification { get; set; } = NotificationLocation.LightlessUi; + public NotificationLocation LightlessErrorNotification { get; set; } = NotificationLocation.ChatAndLightlessUi; + public NotificationLocation LightlessPairRequestNotification { get; set; } = NotificationLocation.LightlessUi; + public NotificationLocation LightlessDownloadNotification { get; set; } = NotificationLocation.TextOverlay; public float NotificationOpacity { get; set; } = 0.95f; public bool EnableNotificationAnimations { get; set; } = true; @@ -93,7 +95,6 @@ public class LightlessConfig : ILightlessConfiguration 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; diff --git a/LightlessSync/LightlessConfiguration/Models/NotificationLocation.cs b/LightlessSync/LightlessConfiguration/Models/NotificationLocation.cs index 657715f..2815986 100644 --- a/LightlessSync/LightlessConfiguration/Models/NotificationLocation.cs +++ b/LightlessSync/LightlessConfiguration/Models/NotificationLocation.cs @@ -6,13 +6,16 @@ public enum NotificationLocation Chat, Toast, Both, - LightlessUI, - ChatAndLightlessUI, + LightlessUi, + ChatAndLightlessUi, + TextOverlay, } public enum NotificationType { Info, Warning, - Error + Error, + PairRequest, + Download } \ No newline at end of file diff --git a/LightlessSync/Services/NotificationService.cs b/LightlessSync/Services/NotificationService.cs index c15fd3f..bfec6e6 100644 --- a/LightlessSync/Services/NotificationService.cs +++ b/LightlessSync/Services/NotificationService.cs @@ -92,8 +92,8 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ { Title = "Pair Request Received", Message = $"{senderName} wants to pair with you.", - Type = NotificationType.Info, - Duration = TimeSpan.FromSeconds(60), + Type = NotificationType.PairRequest, + Duration = TimeSpan.FromSeconds(180), SoundEffectId = NotificationSounds.PairRequest, Actions = new List { @@ -271,7 +271,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ Id = "pair_download_progress", Title = "Downloading Pair Data", Message = message, - Type = NotificationType.Info, + Type = NotificationType.Download, Duration = TimeSpan.FromMinutes(5), ShowProgress = true, Progress = totalProgress @@ -336,20 +336,24 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ if (!_dalamudUtilService.IsLoggedIn) return; - // Get notification location based on type and system pref + // Get notification location based on type and system preference var location = _configService.Current.UseLightlessNotifications ? msg.Type switch { NotificationType.Info => _configService.Current.LightlessInfoNotification, NotificationType.Warning => _configService.Current.LightlessWarningNotification, NotificationType.Error => _configService.Current.LightlessErrorNotification, - _ => NotificationLocation.LightlessUI + NotificationType.PairRequest => _configService.Current.LightlessPairRequestNotification, + NotificationType.Download => _configService.Current.LightlessDownloadNotification, + _ => NotificationLocation.LightlessUi } : msg.Type switch { NotificationType.Info => _configService.Current.InfoNotification, NotificationType.Warning => _configService.Current.WarningNotification, NotificationType.Error => _configService.Current.ErrorNotification, + NotificationType.PairRequest => NotificationLocation.Toast, + NotificationType.Download => NotificationLocation.Toast, _ => NotificationLocation.Nowhere }; @@ -373,11 +377,11 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ ShowChat(msg); break; - case NotificationLocation.LightlessUI: + case NotificationLocation.LightlessUi: ShowLightlessNotification(msg); break; - case NotificationLocation.ChatAndLightlessUI: + case NotificationLocation.ChatAndLightlessUi: ShowChat(msg); ShowLightlessNotification(msg); break; @@ -419,7 +423,6 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ { 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 }; diff --git a/LightlessSync/UI/DownloadUi.cs b/LightlessSync/UI/DownloadUi.cs index 45fce1a..301f177 100644 --- a/LightlessSync/UI/DownloadUi.cs +++ b/LightlessSync/UI/DownloadUi.cs @@ -1,6 +1,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface.Colors; using LightlessSync.LightlessConfiguration; +using LightlessSync.LightlessConfiguration.Models; using LightlessSync.PlayerData.Handlers; using LightlessSync.Services; using LightlessSync.Services.Mediator; @@ -120,9 +121,14 @@ public class DownloadUi : WindowMediatorSubscriberBase try { - if (_configService.Current.UseNotificationsForDownloads) + // Check if download notifications are enabled (not set to TextOverlay) + var useNotifications = _configService.Current.UseLightlessNotifications + ? _configService.Current.LightlessDownloadNotification != NotificationLocation.TextOverlay + : _configService.Current.UseNotificationsForDownloads; + + if (useNotifications) { - // Use new notification stuff + // Use notification system if (_currentDownloads.Any()) { UpdateDownloadNotification(limiterSnapshot); @@ -132,7 +138,8 @@ public class DownloadUi : WindowMediatorSubscriberBase { _notificationService.DismissPairDownloadNotification(); _notificationDismissed = true; - } } + } + } else { if (limiterSnapshot.IsEnabled) diff --git a/LightlessSync/UI/LightlessNotificationUI.cs b/LightlessSync/UI/LightlessNotificationUI.cs index c74c5b1..8a34a84 100644 --- a/LightlessSync/UI/LightlessNotificationUI.cs +++ b/LightlessSync/UI/LightlessNotificationUI.cs @@ -527,6 +527,8 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase NotificationType.Info => UIColors.Get("LightlessPurple"), NotificationType.Warning => UIColors.Get("LightlessYellow"), NotificationType.Error => UIColors.Get("DimRed"), + NotificationType.PairRequest => UIColors.Get("LightlessBlue"), + NotificationType.Download => UIColors.Get("LightlessGreen"), _ => UIColors.Get("LightlessPurple") }; } diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 074eb60..45876ba 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -2675,6 +2675,30 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Save(); }, _configService.Current.LightlessErrorNotification); + ImGuiHelpers.ScaledDummy(3); + _uiShared.DrawHelpText("Special notification types:"); + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Pair Request Notifications:"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + _uiShared.DrawCombo("###enhanced_pairrequest", lightlessLocations, GetNotificationLocationLabel, (location) => + { + _configService.Current.LightlessPairRequestNotification = location; + _configService.Save(); + }, _configService.Current.LightlessPairRequestNotification); + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Download Progress Notifications:"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + var downloadLocations = GetDownloadNotificationLocations(); + _uiShared.DrawCombo("###enhanced_download", downloadLocations, GetNotificationLocationLabel, (location) => + { + _configService.Current.LightlessDownloadNotification = location; + _configService.Save(); + }, _configService.Current.LightlessDownloadNotification); + ImGui.Unindent(); } @@ -2932,21 +2956,6 @@ public class SettingsUi : WindowMediatorSubscriberBase 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"))) { @@ -2972,8 +2981,19 @@ public class SettingsUi : WindowMediatorSubscriberBase { return new[] { - NotificationLocation.LightlessUI, - NotificationLocation.ChatAndLightlessUI, + NotificationLocation.LightlessUi, + NotificationLocation.ChatAndLightlessUi, + NotificationLocation.Nowhere + }; + } + + private NotificationLocation[] GetDownloadNotificationLocations() + { + return new[] + { + NotificationLocation.LightlessUi, + NotificationLocation.ChatAndLightlessUi, + NotificationLocation.TextOverlay, NotificationLocation.Nowhere }; } @@ -2997,8 +3017,9 @@ public class SettingsUi : WindowMediatorSubscriberBase NotificationLocation.Chat => "Chat", NotificationLocation.Toast => "Toast", NotificationLocation.Both => "Toast + Chat", - NotificationLocation.LightlessUI => "Lightless Notifications", - NotificationLocation.ChatAndLightlessUI => "Chat + Lightless Notifications", + NotificationLocation.LightlessUi => "Lightless Notifications", + NotificationLocation.ChatAndLightlessUi => "Chat + Lightless Notifications", + NotificationLocation.TextOverlay => "Text Overlay", _ => location.ToString() }; } From f50b622f0a83bed685e743a618a7e3fb08d630db Mon Sep 17 00:00:00 2001 From: choco Date: Thu, 9 Oct 2025 15:50:59 +0200 Subject: [PATCH 08/15] service cleanup --- LightlessSync/PlayerData/Pairs/PairManager.cs | 7 ++----- LightlessSync/Plugin.cs | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/LightlessSync/PlayerData/Pairs/PairManager.cs b/LightlessSync/PlayerData/Pairs/PairManager.cs index 5a92649..512c7ad 100644 --- a/LightlessSync/PlayerData/Pairs/PairManager.cs +++ b/LightlessSync/PlayerData/Pairs/PairManager.cs @@ -25,7 +25,6 @@ public sealed class PairManager : DisposableMediatorSubscriberBase private readonly LightlessConfigService _configurationService; private readonly IContextMenu _dalamudContextMenu; private readonly PairFactory _pairFactory; - private readonly NotificationService _lightlessNotificationService; private Lazy> _directPairsInternal; private Lazy>> _groupPairsInternal; private Lazy>> _pairsWithGroupsInternal; @@ -36,14 +35,12 @@ public sealed class PairManager : DisposableMediatorSubscriberBase public PairManager(ILogger logger, PairFactory pairFactory, LightlessConfigService configurationService, LightlessMediator mediator, - IContextMenu dalamudContextMenu, PairProcessingLimiter pairProcessingLimiter, - NotificationService lightlessNotificationService) : base(logger, mediator) + IContextMenu dalamudContextMenu, PairProcessingLimiter pairProcessingLimiter) : base(logger, mediator) { _pairFactory = pairFactory; _configurationService = configurationService; _dalamudContextMenu = dalamudContextMenu; _pairProcessingLimiter = pairProcessingLimiter; - _lightlessNotificationService = lightlessNotificationService; Mediator.Subscribe(this, (_) => ClearPairs()); Mediator.Subscribe(this, (_) => ReapplyPairData()); _directPairsInternal = DirectPairsLazy(); @@ -171,7 +168,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase var msg = !string.IsNullOrEmpty(note) ? $"{note} ({pair.UserData.AliasOrUID}) is now online" : $"{pair.UserData.AliasOrUID} is now online"; - _lightlessNotificationService.ShowNotification("User Online", msg, NotificationType.Info, TimeSpan.FromSeconds(5)); + Mediator.Publish(new NotificationMessage("User online", msg, NotificationType.Info, TimeSpan.FromSeconds(5))); } QueuePairCreation(pair, dto); diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index d370dd3..862c777 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(), s.GetRequiredService(), contextMenu, s.GetRequiredService())); collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(addonLifecycle); From cd817487e443154debd26ff09dc4348a3973dca5 Mon Sep 17 00:00:00 2001 From: choco Date: Thu, 9 Oct 2025 11:13:47 +0200 Subject: [PATCH 09/15] scoped service crash fix --- LightlessSync/Plugin.cs | 3 +-- LightlessSync/Services/NotificationService.cs | 6 +----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 862c777..3a6e638 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -178,8 +178,7 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService(), notificationManager, chatGui, - s.GetRequiredService(), - s.GetServices())); + s.GetRequiredService())); collection.AddSingleton((s) => { var httpClient = new HttpClient(); diff --git a/LightlessSync/Services/NotificationService.cs b/LightlessSync/Services/NotificationService.cs index bfec6e6..ea5151e 100644 --- a/LightlessSync/Services/NotificationService.cs +++ b/LightlessSync/Services/NotificationService.cs @@ -20,23 +20,19 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ private readonly DalamudUtilService _dalamudUtilService; private readonly INotificationManager _notificationManager; private readonly IChatGui _chatGui; - private readonly LightlessNotificationUI? _notificationUI; - public NotificationService( ILogger logger, LightlessConfigService configService, DalamudUtilService dalamudUtilService, INotificationManager notificationManager, IChatGui chatGui, - LightlessMediator mediator, - IEnumerable windows) : base(logger, mediator) + LightlessMediator mediator) : base(logger, mediator) { _logger = logger; _configService = configService; _dalamudUtilService = dalamudUtilService; _notificationManager = notificationManager; _chatGui = chatGui; - _notificationUI = windows.OfType().FirstOrDefault(); } public Task StartAsync(CancellationToken cancellationToken) { From 85ecea6391e74be41c7cd3f32721ffa899df936f Mon Sep 17 00:00:00 2001 From: choco Date: Thu, 9 Oct 2025 20:21:01 +0200 Subject: [PATCH 10/15] settings styling and sound disabled not working bugfix --- .../Configurations/LightlessConfig.cs | 6 + LightlessSync/Services/NotificationService.cs | 73 +- LightlessSync/UI/LightlessNotificationUI.cs | 9 + LightlessSync/UI/SettingsUi.cs | 1542 ++++++++++------- 4 files changed, 990 insertions(+), 640 deletions(-) diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs index 80193d4..9fa662f 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs @@ -87,6 +87,7 @@ public class LightlessConfig : ILightlessConfiguration public bool EnableNotificationAnimations { get; set; } = true; public int MaxSimultaneousNotifications { get; set; } = 5; public bool AutoDismissOnAction { get; set; } = true; + public bool DismissNotificationOnClick { get; set; } = false; public bool ShowNotificationTimestamp { get; set; } = false; public bool EnableNotificationHistory { get; set; } = true; public int NotificationHistorySize { get; set; } = 50; @@ -95,6 +96,11 @@ public class LightlessConfig : ILightlessConfiguration public uint CustomWarningSoundId { get; set; } = 15; // Se15 public uint CustomErrorSoundId { get; set; } = 16; // Se16 public bool UseCustomSounds { get; set; } = false; + public uint PairRequestSoundId { get; set; } = 5; // Se5 + public bool DisableInfoSound { get; set; } = false; + public bool DisableWarningSound { get; set; } = false; + public bool DisableErrorSound { get; set; } = false; + public bool DisablePairRequestSound { get; set; } = false; // till here c: public bool UseFocusTarget { get; set; } = false; public bool overrideFriendColor { get; set; } = false; diff --git a/LightlessSync/Services/NotificationService.cs b/LightlessSync/Services/NotificationService.cs index ea5151e..91f2cbc 100644 --- a/LightlessSync/Services/NotificationService.cs +++ b/LightlessSync/Services/NotificationService.cs @@ -75,7 +75,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ } } - if (notification.SoundEffectId.HasValue && _configService.Current.EnableNotificationSounds) + if (notification.SoundEffectId.HasValue) { PlayNotificationSound(notification.SoundEffectId.Value); } @@ -90,7 +90,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ Message = $"{senderName} wants to pair with you.", Type = NotificationType.PairRequest, Duration = TimeSpan.FromSeconds(180), - SoundEffectId = NotificationSounds.PairRequest, + SoundEffectId = !_configService.Current.DisablePairRequestSound ? _configService.Current.PairRequestSoundId : null, Actions = new List { new() @@ -286,39 +286,37 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ 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); + // Check if this specific notification type is disabled + bool isDisabled = type switch + { + NotificationType.Info => _configService.Current.DisableInfoSound, + NotificationType.Warning => _configService.Current.DisableWarningSound, + NotificationType.Error => _configService.Current.DisableErrorSound, + _ => false + }; + + if (isDisabled) + return null; + + // Return the configured sound for this type + return type switch + { + NotificationType.Info => _configService.Current.CustomInfoSoundId, + NotificationType.Warning => _configService.Current.CustomWarningSoundId, + NotificationType.Error => _configService.Current.CustomErrorSoundId, + _ => 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); - } + UIGlobals.PlayChatSoundEffect(soundEffectId); + _logger.LogDebug("Played notification sound effect {SoundId} via ChatGui", soundEffectId); } catch (Exception ex) { @@ -390,27 +388,8 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ 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); + // GetSoundEffectId will handle checking if the sound is disabled + ShowNotification(msg.Title ?? "Lightless Sync", msg.Message ?? string.Empty, msg.Type, duration, null, null); } private void ShowToast(NotificationMessage msg) diff --git a/LightlessSync/UI/LightlessNotificationUI.cs b/LightlessSync/UI/LightlessNotificationUI.cs index 8a34a84..7706142 100644 --- a/LightlessSync/UI/LightlessNotificationUI.cs +++ b/LightlessSync/UI/LightlessNotificationUI.cs @@ -248,6 +248,15 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase { bgColor = bgColor * 1.1f; bgColor.W = Math.Min(bgColor.W, 0.98f); + + // Handle click-to-dismiss for notifications without actions + if (_configService.Current.DismissNotificationOnClick && + !notification.Actions.Any() && + ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + { + notification.IsDismissed = true; + notification.IsAnimatingOut = true; + } } drawList.AddRectFilled( diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 45876ba..1964431 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -44,7 +44,10 @@ public class SettingsUi : WindowMediatorSubscriberBase private readonly ApiController _apiController; private readonly CacheMonitor _cacheMonitor; private readonly LightlessConfigService _configService; - private readonly ConcurrentDictionary> _currentDownloads = new(); + + private readonly ConcurrentDictionary> _currentDownloads = + new(); + private readonly DalamudUtilService _dalamudUtilService; private readonly HttpClient _httpClient; private readonly FileCacheManager _fileCacheManager; @@ -74,17 +77,13 @@ public class SettingsUi : WindowMediatorSubscriberBase private string _lightfinderIconInput = string.Empty; private bool _lightfinderIconInputInitialized = false; private int _lightfinderIconPresetIndex = -1; + private static readonly (string Label, SeIconChar Icon)[] LightfinderIconPresets = new[] { - ("Link Marker", SeIconChar.LinkMarker), - ("Hyadelyn", SeIconChar.Hyadelyn), - ("Gil", SeIconChar.Gil), - ("Quest Sync", SeIconChar.QuestSync), - ("Glamoured", SeIconChar.Glamoured), - ("Glamoured (Dyed)", SeIconChar.GlamouredDyed), - ("Auto-Translate Open", SeIconChar.AutoTranslateOpen), - ("Auto-Translate Close", SeIconChar.AutoTranslateClose), - ("Boxed Star", SeIconChar.BoxedStar), + ("Link Marker", SeIconChar.LinkMarker), ("Hyadelyn", SeIconChar.Hyadelyn), ("Gil", SeIconChar.Gil), + ("Quest Sync", SeIconChar.QuestSync), ("Glamoured", SeIconChar.Glamoured), + ("Glamoured (Dyed)", SeIconChar.GlamouredDyed), ("Auto-Translate Open", SeIconChar.AutoTranslateOpen), + ("Auto-Translate Close", SeIconChar.AutoTranslateClose), ("Boxed Star", SeIconChar.BoxedStar), ("Boxed Plus", SeIconChar.BoxedPlus) }; @@ -107,7 +106,8 @@ public class SettingsUi : WindowMediatorSubscriberBase DalamudUtilService dalamudUtilService, HttpClient httpClient, NameplateService nameplateService, NameplateHandler nameplateHandler, - NotificationService lightlessNotificationService) : base(logger, mediator, "Lightless Sync Settings", performanceCollector) + NotificationService lightlessNotificationService) : base(logger, mediator, "Lightless Sync Settings", + performanceCollector) { _configService = configService; _pairManager = pairManager; @@ -134,8 +134,7 @@ public class SettingsUi : WindowMediatorSubscriberBase SizeConstraints = new WindowSizeConstraints() { - MinimumSize = new Vector2(800, 400), - MaximumSize = new Vector2(800, 2000), + MinimumSize = new Vector2(800, 400), MaximumSize = new Vector2(800, 2000), }; Mediator.Subscribe(this, (_) => Toggle()); @@ -143,7 +142,8 @@ public class SettingsUi : WindowMediatorSubscriberBase Mediator.Subscribe(this, (_) => UiSharedService_GposeStart()); Mediator.Subscribe(this, (_) => UiSharedService_GposeEnd()); Mediator.Subscribe(this, (msg) => LastCreatedCharacterData = msg.CharacterData); - Mediator.Subscribe(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus); + Mediator.Subscribe(this, + (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus); Mediator.Subscribe(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _)); _nameplateService = nameplateService; } @@ -177,6 +177,7 @@ public class SettingsUi : WindowMediatorSubscriberBase DrawSettingsContent(); } + private static bool InputDtrColors(string label, ref DtrEntry.Colors colors) { using var id = ImRaii.PushId(label); @@ -184,12 +185,14 @@ public class SettingsUi : WindowMediatorSubscriberBase var foregroundColor = ConvertColor(colors.Foreground); var glowColor = ConvertColor(colors.Glow); - var ret = ImGui.ColorEdit3("###foreground", ref foregroundColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel | ImGuiColorEditFlags.Uint8); + var ret = ImGui.ColorEdit3("###foreground", ref foregroundColor, + ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel | ImGuiColorEditFlags.Uint8); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Foreground Color - Set to pure black (#000000) to use the default color"); ImGui.SameLine(0.0f, innerSpacing); - ret |= ImGui.ColorEdit3("###glow", ref glowColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel | ImGuiColorEditFlags.Uint8); + ret |= ImGui.ColorEdit3("###glow", ref glowColor, + ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel | ImGuiColorEditFlags.Uint8); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Glow Color - Set to pure black (#000000) to use the default color"); @@ -205,15 +208,17 @@ public class SettingsUi : WindowMediatorSubscriberBase => unchecked(new((byte)color / 255.0f, (byte)(color >> 8) / 255.0f, (byte)(color >> 16) / 255.0f)); static uint ConvertBackColor(Vector3 color) - => byte.CreateSaturating(color.X * 255.0f) | ((uint)byte.CreateSaturating(color.Y * 255.0f) << 8) | ((uint)byte.CreateSaturating(color.Z * 255.0f) << 16); + => byte.CreateSaturating(color.X * 255.0f) | ((uint)byte.CreateSaturating(color.Y * 255.0f) << 8) | + ((uint)byte.CreateSaturating(color.Z * 255.0f) << 16); } private void DrawBlockedTransfers() { _lastTab = "BlockedTransfers"; - UiSharedService.ColorTextWrapped("Files that you attempted to upload or download that were forbidden to be transferred by their creators will appear here. " + - "If you see file paths from your drive here, then those files were not allowed to be uploaded. If you see hashes, those files were not allowed to be downloaded. " + - "Ask your paired friend to send you the mod in question through other means, acquire the mod yourself or pester the mod creator to allow it to be sent over Lightless.", + UiSharedService.ColorTextWrapped( + "Files that you attempted to upload or download that were forbidden to be transferred by their creators will appear here. " + + "If you see file paths from your drive here, then those files were not allowed to be uploaded. If you see hashes, those files were not allowed to be downloaded. " + + "Ask your paired friend to send you the mod in question through other means, acquire the mod yourself or pester the mod creator to allow it to be sent over Lightless.", ImGuiColors.DalamudGrey); if (ImGui.BeginTable("TransfersTable", 2, ImGuiTableFlags.SizingStretchProp)) @@ -235,9 +240,11 @@ public class SettingsUi : WindowMediatorSubscriberBase { ImGui.TextUnformatted(item.Hash); } + ImGui.TableNextColumn(); ImGui.TextUnformatted(item.ForbiddenBy); } + ImGui.EndTable(); } } @@ -265,6 +272,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Save(); Mediator.Publish(new DownloadLimitChangedMessage()); } + ImGui.SameLine(); ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); _uiShared.DrawCombo("###speed", [DownloadSpeeds.Bps, DownloadSpeeds.KBps, DownloadSpeeds.MBps], @@ -290,6 +298,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Save(); Mediator.Publish(new DownloadLimitChangedMessage()); } + _uiShared.DrawHelpText("Controls how many download slots can be active at once."); if (ImGui.SliderInt("Maximum Parallel Uploads", ref maxParallelUploads, 1, 8)) @@ -297,6 +306,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.ParallelUploads = maxParallelUploads; _configService.Save(); } + _uiShared.DrawHelpText("Controls how many uploads can run at once."); _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); @@ -307,7 +317,9 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Save(); Mediator.Publish(new PairProcessingLimitChangedMessage()); } - _uiShared.DrawHelpText("When enabled we stagger pair downloads to avoid large network and game lag caused by attempting to download everyone at once."); + + _uiShared.DrawHelpText( + "When enabled we stagger pair downloads to avoid large network and game lag caused by attempting to download everyone at once."); var limiterDisabledScope = !limitPairApplications; if (limiterDisabledScope) @@ -321,6 +333,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Save(); Mediator.Publish(new PairProcessingLimitChangedMessage()); } + _uiShared.DrawHelpText("How many pair downloads/applications can run simultaneously when the limit is on."); if (limiterDisabledScope) @@ -333,7 +346,9 @@ public class SettingsUi : WindowMediatorSubscriberBase { 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)"; + queueText += limiterSnapshot.Waiting > 0 + ? $" ({limiterSnapshot.Waiting} waiting, {limiterSnapshot.Remaining} free)" + : $" ({limiterSnapshot.Remaining} free)"; ImGui.TextColored(queueColor, queueText); } else @@ -348,13 +363,16 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.UseAlternativeFileUpload = useAlternativeUpload; _configService.Save(); } - _uiShared.DrawHelpText("This will attempt to upload files in one go instead of a stream. Typically not necessary to enable. Use if you have upload issues."); + + _uiShared.DrawHelpText( + "This will attempt to upload files in one go instead of a stream. Typically not necessary to enable. Use if you have upload issues."); ImGui.Separator(); _uiShared.UnderlinedBigText("Transfer UI", UIColors.Get("LightlessBlue")); ImGuiHelpers.ScaledDummy(5); - _uiShared.DrawHelpText("Download progress notification settings have been moved to the 'Enhanced Notifications' tab for better organization."); + _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; @@ -363,7 +381,9 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.ShowTransferWindow = showTransferWindow; _configService.Save(); } - _uiShared.DrawHelpText($"The download window will show the current progress of outstanding downloads.{Environment.NewLine}{Environment.NewLine}" + + + _uiShared.DrawHelpText( + $"The download window will show the current progress of outstanding downloads.{Environment.NewLine}{Environment.NewLine}" + $"What do W/Q/P/D stand for?{Environment.NewLine}W = Waiting for Slot (see Maximum Parallel Downloads){Environment.NewLine}" + $"Q = Queued on Server, waiting for queue ready signal{Environment.NewLine}" + $"P = Processing download (aka downloading){Environment.NewLine}" + @@ -375,6 +395,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { _uiShared.EditTrackerPosition = editTransferWindowPosition; } + ImGui.Unindent(); if (!_configService.Current.ShowTransferWindow) ImGui.EndDisabled(); @@ -384,7 +405,9 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.ShowTransferBars = showTransferBars; _configService.Save(); } - _uiShared.DrawHelpText("This will render a progress bar during the download at the feet of the player you are downloading from."); + + _uiShared.DrawHelpText( + "This will render a progress bar during the download at the feet of the player you are downloading from."); if (!showTransferBars) ImGui.BeginDisabled(); ImGui.Indent(); @@ -394,6 +417,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.TransferBarsShowText = transferBarShowText; _configService.Save(); } + _uiShared.DrawHelpText("Shows download text (amount of MiB downloaded) in the transfer bars"); int transferBarWidth = _configService.Current.TransferBarsWidth; if (ImGui.SliderInt("Transfer Bar Width", ref transferBarWidth, 10, 500)) @@ -401,21 +425,27 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.TransferBarsWidth = transferBarWidth; _configService.Save(); } - _uiShared.DrawHelpText("Width of the displayed transfer bars (will never be less wide than the displayed text)"); + + _uiShared.DrawHelpText( + "Width of the displayed transfer bars (will never be less wide than the displayed text)"); int transferBarHeight = _configService.Current.TransferBarsHeight; if (ImGui.SliderInt("Transfer Bar Height", ref transferBarHeight, 2, 50)) { _configService.Current.TransferBarsHeight = transferBarHeight; _configService.Save(); } - _uiShared.DrawHelpText("Height of the displayed transfer bars (will never be less tall than the displayed text)"); + + _uiShared.DrawHelpText( + "Height of the displayed transfer bars (will never be less tall than the displayed text)"); bool showUploading = _configService.Current.ShowUploading; if (ImGui.Checkbox("Show 'Uploading' text below players that are currently uploading", ref showUploading)) { _configService.Current.ShowUploading = showUploading; _configService.Save(); } - _uiShared.DrawHelpText("This will render an 'Uploading' text at the feet of the player that is in progress of uploading data."); + + _uiShared.DrawHelpText( + "This will render an 'Uploading' text at the feet of the player that is in progress of uploading data."); ImGui.Unindent(); if (!showUploading) ImGui.BeginDisabled(); @@ -426,6 +456,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.ShowUploadingBigText = showUploadingBigText; _configService.Save(); } + _uiShared.DrawHelpText("This will render an 'Uploading' text in a larger font."); ImGui.Unindent(); @@ -441,30 +472,41 @@ public class SettingsUi : WindowMediatorSubscriberBase using var tree = ImRaii.TreeNode("Speed Test to Servers"); if (tree) { - if (_downloadServersTask == null || ((_downloadServersTask?.IsCompleted ?? false) && (!_downloadServersTask?.IsCompletedSuccessfully ?? false))) + if (_downloadServersTask == null || ((_downloadServersTask?.IsCompleted ?? false) && + (!_downloadServersTask?.IsCompletedSuccessfully ?? false))) { if (_uiShared.IconTextButton(FontAwesomeIcon.GroupArrowsRotate, "Update Download Server List")) { _downloadServersTask = GetDownloadServerList(); } } - if (_downloadServersTask != null && _downloadServersTask.IsCompleted && !_downloadServersTask.IsCompletedSuccessfully) + + if (_downloadServersTask != null && _downloadServersTask.IsCompleted && + !_downloadServersTask.IsCompletedSuccessfully) { - UiSharedService.ColorTextWrapped("Failed to get download servers from service, see /xllog for more information", ImGuiColors.DalamudRed); + UiSharedService.ColorTextWrapped( + "Failed to get download servers from service, see /xllog for more information", + ImGuiColors.DalamudRed); } - if (_downloadServersTask != null && _downloadServersTask.IsCompleted && _downloadServersTask.IsCompletedSuccessfully) + + if (_downloadServersTask != null && _downloadServersTask.IsCompleted && + _downloadServersTask.IsCompletedSuccessfully) { if (_speedTestTask == null || _speedTestTask.IsCompleted) { if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowRight, "Start Speedtest")) { - _speedTestTask = RunSpeedTest(_downloadServersTask.Result!, _speedTestCts?.Token ?? CancellationToken.None); + _speedTestTask = RunSpeedTest(_downloadServersTask.Result!, + _speedTestCts?.Token ?? CancellationToken.None); } } else if (!_speedTestTask.IsCompleted) { - UiSharedService.ColorTextWrapped("Running Speedtest to File Servers...", UIColors.Get("LightlessYellow")); - UiSharedService.ColorTextWrapped("Please be patient, depending on usage and load this can take a while.", UIColors.Get("LightlessYellow")); + UiSharedService.ColorTextWrapped("Running Speedtest to File Servers...", + UIColors.Get("LightlessYellow")); + UiSharedService.ColorTextWrapped( + "Please be patient, depending on usage and load this can take a while.", + UIColors.Get("LightlessYellow")); if (_uiShared.IconTextButton(FontAwesomeIcon.Ban, "Cancel speedtest")) { _speedTestCts?.Cancel(); @@ -472,6 +514,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _speedTestCts = new(); } } + if (_speedTestTask != null && _speedTestTask.IsCompleted) { if (_speedTestTask.Result != null && _speedTestTask.Result.Count != 0) @@ -483,11 +526,13 @@ public class SettingsUi : WindowMediatorSubscriberBase } else { - UiSharedService.ColorTextWrapped("Speedtest completed with no results", UIColors.Get("LightlessYellow")); + UiSharedService.ColorTextWrapped("Speedtest completed with no results", + UIColors.Get("LightlessYellow")); } } } } + ImGuiHelpers.ScaledDummy(10); } @@ -522,6 +567,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { ImGui.TextUnformatted(transfer.Hash); } + ImGui.TableNextColumn(); ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Transferred)); ImGui.TableNextColumn(); @@ -530,6 +576,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.EndTable(); } + ImGui.Separator(); ImGui.TextUnformatted("Downloads"); if (ImGui.BeginTable("DownloadsTable", 4)) @@ -545,7 +592,8 @@ public class SettingsUi : WindowMediatorSubscriberBase var userName = transfer.Key.Name; foreach (var entry in transfer.Value) { - var color = UiSharedService.UploadColor((entry.Value.TransferredBytes, entry.Value.TotalBytes)); + var color = UiSharedService.UploadColor((entry.Value.TransferredBytes, + entry.Value.TotalBytes)); ImGui.TableNextColumn(); ImGui.TextUnformatted(userName); ImGui.TableNextColumn(); @@ -554,7 +602,8 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.TableNextColumn(); ImGui.TextUnformatted(entry.Value.TransferredFiles + "/" + entry.Value.TotalFiles); ImGui.TableNextColumn(); - ImGui.TextUnformatted(UiSharedService.ByteToString(entry.Value.TransferredBytes) + "/" + UiSharedService.ByteToString(entry.Value.TotalBytes)); + ImGui.TextUnformatted(UiSharedService.ByteToString(entry.Value.TransferredBytes) + "/" + + UiSharedService.ByteToString(entry.Value.TotalBytes)); ImGui.TableNextColumn(); col.Dispose(); ImGui.TableNextRow(); @@ -590,7 +639,9 @@ public class SettingsUi : WindowMediatorSubscriberBase Stopwatch? st = null; try { - result = await _fileTransferOrchestrator.SendRequestAsync(HttpMethod.Get, new Uri(new Uri(server), "speedtest/run"), token, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); + result = await _fileTransferOrchestrator.SendRequestAsync(HttpMethod.Get, + new Uri(new Uri(server), "speedtest/run"), token, HttpCompletionOption.ResponseHeadersRead) + .ConfigureAwait(false); result.EnsureSuccessStatusCode(); using CancellationTokenSource speedtestTimeCts = new(); speedtestTimeCts.CancelAfter(TimeSpan.FromSeconds(10)); @@ -613,8 +664,10 @@ public class SettingsUi : WindowMediatorSubscriberBase { _logger.LogWarning("Speedtest to {server} cancelled", server); } + st.Stop(); - _logger.LogInformation("Downloaded {bytes} from {server} in {time}", UiSharedService.ByteToString(readBytes), server, st.Elapsed); + _logger.LogInformation("Downloaded {bytes} from {server} in {time}", + UiSharedService.ByteToString(readBytes), server, st.Elapsed); var bps = (long)((readBytes) / st.Elapsed.TotalSeconds); speedTestResults.Add($"{server}: ~{UiSharedService.ByteToString(bps)}/s"); } @@ -640,6 +693,7 @@ public class SettingsUi : WindowMediatorSubscriberBase st?.Stop(); } } + return speedTestResults; } @@ -647,9 +701,13 @@ public class SettingsUi : WindowMediatorSubscriberBase { try { - var result = await _fileTransferOrchestrator.SendRequestAsync(HttpMethod.Get, new Uri(_fileTransferOrchestrator.FilesCdnUri!, "files/downloadServers"), CancellationToken.None).ConfigureAwait(false); + var result = await _fileTransferOrchestrator.SendRequestAsync(HttpMethod.Get, + new Uri(_fileTransferOrchestrator.FilesCdnUri!, "files/downloadServers"), CancellationToken.None) + .ConfigureAwait(false); result.EnsureSuccessStatusCode(); - return await JsonSerializer.DeserializeAsync>(await result.Content.ReadAsStreamAsync().ConfigureAwait(false)).ConfigureAwait(false); + return await JsonSerializer + .DeserializeAsync>(await result.Content.ReadAsStreamAsync().ConfigureAwait(false)) + .ConfigureAwait(false); } catch (Exception ex) { @@ -667,7 +725,9 @@ public class SettingsUi : WindowMediatorSubscriberBase #if DEBUG if (LastCreatedCharacterData != null && ImGui.TreeNode("Last created character data")) { - foreach (var l in JsonSerializer.Serialize(LastCreatedCharacterData, new JsonSerializerOptions() { WriteIndented = true }).Split('\n')) + foreach (var l in JsonSerializer + .Serialize(LastCreatedCharacterData, new JsonSerializerOptions() { WriteIndented = true }) + .Split('\n')) { ImGui.TextUnformatted($"{l}"); } @@ -679,13 +739,15 @@ public class SettingsUi : WindowMediatorSubscriberBase { if (LastCreatedCharacterData != null) { - ImGui.SetClipboardText(JsonSerializer.Serialize(LastCreatedCharacterData, new JsonSerializerOptions() { WriteIndented = true })); + ImGui.SetClipboardText(JsonSerializer.Serialize(LastCreatedCharacterData, + new JsonSerializerOptions() { WriteIndented = true })); } else { ImGui.SetClipboardText("ERROR: No created character data, cannot copy."); } } + UiSharedService.AttachToolTip("Use this when reporting mods being rejected from the server."); _uiShared.DrawCombo("Log Level", Enum.GetValues(), (l) => l.ToString(), (l) => @@ -700,7 +762,9 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.LogPerformance = logPerformance; _configService.Save(); } - _uiShared.DrawHelpText("Enabling this can incur a (slight) performance impact. Enabling this for extended periods of time is not recommended."); + + _uiShared.DrawHelpText( + "Enabling this can incur a (slight) performance impact. Enabling this for extended periods of time is not recommended."); using (ImRaii.Disabled(!logPerformance)) { @@ -708,6 +772,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { _performanceCollector.PrintPerformanceStats(); } + ImGui.SameLine(); if (_uiShared.IconTextButton(FontAwesomeIcon.StickyNote, "Print Performance Stats (last 60s) to /xllog")) { @@ -721,7 +786,10 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.DebugStopWhining = stopWhining; _configService.Save(); } - _uiShared.DrawHelpText("Having modified game files will still mark your logs with UNSUPPORTED and you will not receive support, message shown or not." + UiSharedService.TooltipSeparator + + _uiShared.DrawHelpText( + "Having modified game files will still mark your logs with UNSUPPORTED and you will not receive support, message shown or not." + + UiSharedService.TooltipSeparator + "Keeping LOD enabled can lead to more crashes. Use at your own risk."); _uiShared.ColoredSeparator(UIColors.Get("LightlessYellow"), 2f); @@ -734,12 +802,14 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.UnderlinedBigText("Storage", UIColors.Get("LightlessBlue")); ImGuiHelpers.ScaledDummy(5); - UiSharedService.TextWrapped("Lightless stores downloaded files from paired people permanently. This is to improve loading performance and requiring less downloads. " + + UiSharedService.TextWrapped( + "Lightless stores downloaded files from paired people permanently. This is to improve loading performance and requiring less downloads. " + "The storage governs itself by clearing data beyond the set storage size. Please set the storage size accordingly. It is not necessary to manually clear the storage."); _uiShared.DrawFileScanState(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("Monitoring Penumbra Folder: " + (_cacheMonitor.PenumbraWatcher?.Path ?? "Not monitoring")); + ImGui.TextUnformatted( + "Monitoring Penumbra Folder: " + (_cacheMonitor.PenumbraWatcher?.Path ?? "Not monitoring")); if (string.IsNullOrEmpty(_cacheMonitor.PenumbraWatcher?.Path)) { ImGui.SameLine(); @@ -751,7 +821,8 @@ public class SettingsUi : WindowMediatorSubscriberBase } ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("Monitoring Lightless Storage Folder: " + (_cacheMonitor.LightlessWatcher?.Path ?? "Not monitoring")); + ImGui.TextUnformatted("Monitoring Lightless Storage Folder: " + + (_cacheMonitor.LightlessWatcher?.Path ?? "Not monitoring")); if (string.IsNullOrEmpty(_cacheMonitor.LightlessWatcher?.Path)) { ImGui.SameLine(); @@ -761,6 +832,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _cacheMonitor.StartLightlessWatcher(_configService.Current.CacheFolder); } } + if (_cacheMonitor.LightlessWatcher == null || _cacheMonitor.PenumbraWatcher == null) { if (_uiShared.IconTextButton(FontAwesomeIcon.Play, "Resume Monitoring")) @@ -769,9 +841,11 @@ public class SettingsUi : WindowMediatorSubscriberBase _cacheMonitor.StartPenumbraWatcher(_ipcManager.Penumbra.ModDirectory); _cacheMonitor.InvokeScan(); } + UiSharedService.AttachToolTip("Attempts to resume monitoring for both Penumbra and Lightless Storage. " - + "Resuming the monitoring will also force a full scan to run." + Environment.NewLine - + "If the button remains present after clicking it, consult /xllog for errors"); + + "Resuming the monitoring will also force a full scan to run." + + Environment.NewLine + + "If the button remains present after clicking it, consult /xllog for errors"); } else { @@ -782,32 +856,42 @@ public class SettingsUi : WindowMediatorSubscriberBase _cacheMonitor.StopMonitoring(); } } + UiSharedService.AttachToolTip("Stops the monitoring for both Penumbra and Lightless Storage. " - + "Do not stop the monitoring, unless you plan to move the Penumbra and Lightless Storage folders, to ensure correct functionality of Lightless." + Environment.NewLine - + "If you stop the monitoring to move folders around, resume it after you are finished moving the files." - + UiSharedService.TooltipSeparator + "Hold CTRL to enable this button"); + + "Do not stop the monitoring, unless you plan to move the Penumbra and Lightless Storage folders, to ensure correct functionality of Lightless." + + Environment.NewLine + + "If you stop the monitoring to move folders around, resume it after you are finished moving the files." + + UiSharedService.TooltipSeparator + "Hold CTRL to enable this button"); } _uiShared.DrawCacheDirectorySetting(); ImGui.AlignTextToFramePadding(); if (_cacheMonitor.FileCacheSize >= 0) - ImGui.TextUnformatted($"Currently utilized local storage: {UiSharedService.ByteToString(_cacheMonitor.FileCacheSize)}"); + ImGui.TextUnformatted( + $"Currently utilized local storage: {UiSharedService.ByteToString(_cacheMonitor.FileCacheSize)}"); else ImGui.TextUnformatted($"Currently utilized local storage: Calculating..."); - ImGui.TextUnformatted($"Remaining space free on drive: {UiSharedService.ByteToString(_cacheMonitor.FileCacheDriveFree)}"); + ImGui.TextUnformatted( + $"Remaining space free on drive: {UiSharedService.ByteToString(_cacheMonitor.FileCacheDriveFree)}"); bool useFileCompactor = _configService.Current.UseCompactor; bool isLinux = _dalamudUtilService.IsWine; if (!useFileCompactor && !isLinux) { - UiSharedService.ColorTextWrapped("Hint: To free up space when using Lightless consider enabling the File Compactor", UIColors.Get("LightlessYellow")); + UiSharedService.ColorTextWrapped( + "Hint: To free up space when using Lightless consider enabling the File Compactor", + UIColors.Get("LightlessYellow")); } + if (isLinux || !_cacheMonitor.StorageisNTFS) ImGui.BeginDisabled(); if (ImGui.Checkbox("Use file compactor", ref useFileCompactor)) { _configService.Current.UseCompactor = useFileCompactor; _configService.Save(); } - _uiShared.DrawHelpText("The file compactor can massively reduce your saved files. It might incur a minor penalty on loading files on a slow CPU." + Environment.NewLine + + _uiShared.DrawHelpText( + "The file compactor can massively reduce your saved files. It might incur a minor penalty on loading files on a slow CPU." + + Environment.NewLine + "It is recommended to leave it enabled to save on space."); ImGui.SameLine(); if (!_fileCompactor.MassCompactRunning) @@ -820,8 +904,10 @@ public class SettingsUi : WindowMediatorSubscriberBase _cacheMonitor.RecalculateFileCacheSize(CancellationToken.None); }); } - UiSharedService.AttachToolTip("This will run compression on all files in your current Lightless Storage." + Environment.NewLine - + "You do not need to run this manually if you keep the file compactor enabled."); + + UiSharedService.AttachToolTip("This will run compression on all files in your current Lightless Storage." + + Environment.NewLine + + "You do not need to run this manually if you keep the file compactor enabled."); ImGui.SameLine(); if (_uiShared.IconTextButton(FontAwesomeIcon.File, "Decompact all files in storage")) { @@ -831,26 +917,32 @@ public class SettingsUi : WindowMediatorSubscriberBase _cacheMonitor.RecalculateFileCacheSize(CancellationToken.None); }); } - UiSharedService.AttachToolTip("This will run decompression on all files in your current Lightless Storage."); + + UiSharedService.AttachToolTip( + "This will run decompression on all files in your current Lightless Storage."); } else { - UiSharedService.ColorText($"File compactor currently running ({_fileCompactor.Progress})", UIColors.Get("LightlessYellow")); + UiSharedService.ColorText($"File compactor currently running ({_fileCompactor.Progress})", + UIColors.Get("LightlessYellow")); } + if (isLinux || !_cacheMonitor.StorageisNTFS) { ImGui.EndDisabled(); ImGui.TextUnformatted("The file compactor is only available on Windows and NTFS drives."); } + ImGuiHelpers.ScaledDummy(new Vector2(10, 10)); ImGui.Separator(); if (_uiShared.MediumTreeNode("Storage Validation", UIColors.Get("LightlessYellow"))) { - UiSharedService.TextWrapped("File Storage validation can make sure that all files in your local Lightless Storage are valid. " + - "Run the validation before you clear the Storage for no reason. " + Environment.NewLine + - "This operation, depending on how many files you have in your storage, can take a while and will be CPU and drive intensive."); + UiSharedService.TextWrapped( + "File Storage validation can make sure that all files in your local Lightless Storage are valid. " + + "Run the validation before you clear the Storage for no reason. " + Environment.NewLine + + "This operation, depending on how many files you have in your storage, can take a while and will be CPU and drive intensive."); using (ImRaii.Disabled(_validationTask != null && !_validationTask.IsCompleted)) { if (_uiShared.IconTextButton(FontAwesomeIcon.Check, "Start File Storage Validation")) @@ -859,9 +951,11 @@ public class SettingsUi : WindowMediatorSubscriberBase _validationCts?.Dispose(); _validationCts = new(); var token = _validationCts.Token; - _validationTask = Task.Run(() => _fileCacheManager.ValidateLocalIntegrity(_validationProgress, token)); + _validationTask = Task.Run(() => + _fileCacheManager.ValidateLocalIntegrity(_validationProgress, token)); } } + if (_validationTask != null && !_validationTask.IsCompleted) { ImGui.SameLine(); @@ -877,12 +971,14 @@ public class SettingsUi : WindowMediatorSubscriberBase { if (_validationTask.IsCompleted) { - UiSharedService.TextWrapped($"The storage validation has completed and removed {_validationTask.Result.Count} invalid files from storage."); + UiSharedService.TextWrapped( + $"The storage validation has completed and removed {_validationTask.Result.Count} invalid files from storage."); } else { - UiSharedService.TextWrapped($"Storage validation is running: {_currentProgress.Item1}/{_currentProgress.Item2}"); + UiSharedService.TextWrapped( + $"Storage validation is running: {_currentProgress.Item1}/{_currentProgress.Item2}"); UiSharedService.TextWrapped($"Current item: {_currentProgress.Item3.ResolvedFilepath}"); } } @@ -900,12 +996,15 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Indent(); ImGui.Checkbox("##readClearCache", ref _readClearCache); ImGui.SameLine(); - UiSharedService.TextWrapped("I understand that: " + Environment.NewLine + "- By clearing the local storage I put the file servers of my connected service under extra strain by having to redownload all data." - + Environment.NewLine + "- This is not a step to try to fix sync issues." - + Environment.NewLine + "- This can make the situation of not getting other players data worse in situations of heavy file server load."); + UiSharedService.TextWrapped("I understand that: " + Environment.NewLine + + "- By clearing the local storage I put the file servers of my connected service under extra strain by having to redownload all data." + + Environment.NewLine + "- This is not a step to try to fix sync issues." + + Environment.NewLine + + "- This can make the situation of not getting other players data worse in situations of heavy file server load."); if (!_readClearCache) ImGui.BeginDisabled(); - if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Clear local storage") && UiSharedService.CtrlPressed() && _readClearCache) + if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Clear local storage") && + UiSharedService.CtrlPressed() && _readClearCache) { _ = Task.Run(() => { @@ -922,9 +1021,14 @@ public class SettingsUi : WindowMediatorSubscriberBase } }); } - UiSharedService.AttachToolTip("You normally do not need to do this. THIS IS NOT SOMETHING YOU SHOULD BE DOING TO TRY TO FIX SYNC ISSUES." + Environment.NewLine - + "This will solely remove all downloaded data from all players and will require you to re-download everything again." + Environment.NewLine - + "Lightless storage is self-clearing and will not surpass the limit you have set it to." + Environment.NewLine + + UiSharedService.AttachToolTip( + "You normally do not need to do this. THIS IS NOT SOMETHING YOU SHOULD BE DOING TO TRY TO FIX SYNC ISSUES." + + Environment.NewLine + + "This will solely remove all downloaded data from all players and will require you to re-download everything again." + + Environment.NewLine + + "Lightless storage is self-clearing and will not surpass the limit you have set it to." + + Environment.NewLine + "If you still think you need to do this hold CTRL while pressing the button."); if (!_readClearCache) ImGui.EndDisabled(); @@ -954,8 +1058,11 @@ public class SettingsUi : WindowMediatorSubscriberBase { if (_uiShared.IconTextButton(FontAwesomeIcon.StickyNote, "Export all your user notes to clipboard")) { - ImGui.SetClipboardText(UiSharedService.GetNotes(_pairManager.DirectPairs.UnionBy(_pairManager.GroupPairs.SelectMany(p => p.Value), p => p.UserData, UserDataComparer.Instance).ToList())); + ImGui.SetClipboardText(UiSharedService.GetNotes(_pairManager.DirectPairs + .UnionBy(_pairManager.GroupPairs.SelectMany(p => p.Value), p => p.UserData, + UserDataComparer.Instance).ToList())); } + if (_uiShared.IconTextButton(FontAwesomeIcon.FileImport, "Import notes from clipboard")) { _notesSuccessfullyApplied = null; @@ -965,14 +1072,17 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.SameLine(); ImGui.Checkbox("Overwrite existing notes", ref _overwriteExistingLabels); - _uiShared.DrawHelpText("If this option is selected all already existing notes for UIDs will be overwritten by the imported notes."); + _uiShared.DrawHelpText( + "If this option is selected all already existing notes for UIDs will be overwritten by the imported notes."); if (_notesSuccessfullyApplied.HasValue && _notesSuccessfullyApplied.Value) { UiSharedService.ColorTextWrapped("User Notes successfully imported", UIColors.Get("LightlessBlue")); } else if (_notesSuccessfullyApplied.HasValue && !_notesSuccessfullyApplied.Value) { - UiSharedService.ColorTextWrapped("Attempt to import notes from clipboard failed. Check formatting and try again", ImGuiColors.DalamudRed); + UiSharedService.ColorTextWrapped( + "Attempt to import notes from clipboard failed. Check formatting and try again", + ImGuiColors.DalamudRed); } _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); @@ -989,7 +1099,9 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.OpenPopupOnAdd = openPopupOnAddition; _configService.Save(); } - _uiShared.DrawHelpText("This will open a popup that allows you to set the notes for a user after successfully adding them to your individual pairs."); + + _uiShared.DrawHelpText( + "This will open a popup that allows you to set the notes for a user after successfully adding them to your individual pairs."); var autoPopulateNotes = _configService.Current.AutoPopulateEmptyNotesFromCharaName; if (ImGui.Checkbox("Automatically populate notes using player names", ref autoPopulateNotes)) @@ -997,7 +1109,9 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.AutoPopulateEmptyNotesFromCharaName = autoPopulateNotes; _configService.Save(); } - _uiShared.DrawHelpText("This will automatically populate user notes using the first encountered player name if the note was not set prior"); + + _uiShared.DrawHelpText( + "This will automatically populate user notes using the first encountered player name if the note was not set prior"); _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); @@ -1038,6 +1152,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.EnableRightClickMenus = enableRightClickMenu; _configService.Save(); } + _uiShared.DrawHelpText("This will add all Lightless related right click menu entries in the game UI."); if (ImGui.Checkbox("Display status and visible pair count in Server Info Bar", ref enableDtrEntry)) @@ -1045,7 +1160,9 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.EnableDtrEntry = enableDtrEntry; _configService.Save(); } - _uiShared.DrawHelpText("This will add Lightless connection status and visible pair count in the Server Info Bar.\nYou can further configure this through your Dalamud Settings."); + + _uiShared.DrawHelpText( + "This will add Lightless connection status and visible pair count in the Server Info Bar.\nYou can further configure this through your Dalamud Settings."); using (ImRaii.Disabled(!enableDtrEntry)) { @@ -1087,6 +1204,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { _configService.Current.LightfinderLabelOffsetX = 0; @@ -1095,10 +1213,12 @@ public class SettingsUi : WindowMediatorSubscriberBase _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } + if (ImGui.IsItemHovered()) ImGui.SetTooltip("Right click to reset to default."); ImGui.EndDisabled(); - _uiShared.DrawHelpText("Moves the Lightfinder label horizontally on player nameplates.\nUnavailable when automatic alignment is enabled."); + _uiShared.DrawHelpText( + "Moves the Lightfinder label horizontally on player nameplates.\nUnavailable when automatic alignment is enabled."); if (ImGui.SliderInt("Label Offset Y", ref offsetY, -200, 200)) @@ -1109,6 +1229,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { _configService.Current.LightfinderLabelOffsetY = 0; @@ -1117,6 +1238,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } + if (ImGui.IsItemHovered()) ImGui.SetTooltip("Right click to reset to default."); _uiShared.DrawHelpText("Moves the Lightfinder label vertically on player nameplates."); @@ -1129,6 +1251,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { _configService.Current.LightfinderLabelScale = 1.0f; @@ -1137,6 +1260,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } + if (ImGui.IsItemHovered()) ImGui.SetTooltip("Right click to reset to default."); _uiShared.DrawHelpText("Adjusts the Lightfinder label size for both text and icon modes."); @@ -1151,7 +1275,9 @@ public class SettingsUi : WindowMediatorSubscriberBase _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } - _uiShared.DrawHelpText("Automatically position the label relative to the in-game nameplate. Turn off to rely entirely on manual offsets."); + + _uiShared.DrawHelpText( + "Automatically position the label relative to the in-game nameplate. Turn off to rely entirely on manual offsets."); if (autoAlign) { @@ -1203,6 +1329,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } + _uiShared.DrawHelpText("Toggles your own Lightfinder label."); var showPaired = _configService.Current.LightfinderLabelShowPaired; @@ -1214,6 +1341,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } + _uiShared.DrawHelpText("Toggles paired player(s) Lightfinder label."); _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); @@ -1238,6 +1366,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _lightfinderIconPresetIndex = -1; } } + _uiShared.DrawHelpText("Switch between the Lightfinder text label and an icon on nameplates."); if (useIcon) @@ -1311,7 +1440,8 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.SameLine(); ImGui.AlignTextToFramePadding(); ImGui.Text($"Preview: {previewGlyph}"); - _uiShared.DrawHelpText("Enter a hex code (e.g. E0BB), pick a preset, or paste an icon character directly."); + _uiShared.DrawHelpText( + "Enter a hex code (e.g. E0BB), pick a preset, or paste an icon character directly."); } else { @@ -1335,17 +1465,14 @@ public class SettingsUi : WindowMediatorSubscriberBase ("LightlessPurpleActive", "Primary Purple (Active)", "Active tabs and hover highlights"), ("LightlessPurpleDefault", "Primary Purple (Inactive)", "Inactive tabs and default dividers"), ("LightlessBlue", "Secondary Blue", "Secondary title colors, visable pairs"), - ("LightlessGreen", "Success Green", "Join buttons and success messages"), - ("LightlessYellow", "Warning Yellow", "Warning colors"), ("LightlessYellow2", "Warning Yellow (Alt)", "Warning colors"), - ("PairBlue", "Syncshell Blue", "Syncshell headers, toggle highlights, and moderator actions"), - ("DimRed", "Error Red", "Error and offline colors") }; - if (ImGui.BeginTable("##ColorTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) + if (ImGui.BeginTable("##ColorTable", 3, + ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) { ImGui.TableSetupColumn("Color", ImGuiTableColumnFlags.WidthFixed); ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); @@ -1355,30 +1482,32 @@ public class SettingsUi : WindowMediatorSubscriberBase foreach (var (colorKey, displayName, description) in colorNames) { ImGui.TableNextRow(); - + // color column ImGui.TableSetColumnIndex(0); var currentColor = UIColors.Get(colorKey); var colorToEdit = currentColor; - if (ImGui.ColorEdit4($"##color_{colorKey}", ref colorToEdit, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf)) + if (ImGui.ColorEdit4($"##color_{colorKey}", ref colorToEdit, + ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf)) { UIColors.Set(colorKey, colorToEdit); } + ImGui.SameLine(); ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(displayName); - + // description column ImGui.TableSetColumnIndex(1); ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(description); - + // actions column ImGui.TableSetColumnIndex(2); using var resetId = ImRaii.PushId($"Reset_{colorKey}"); var availableWidth = ImGui.GetContentRegionAvail().X; var isCustom = UIColors.IsCustom(colorKey); - + using (ImRaii.Disabled(!isCustom)) { using (ImRaii.PushFont(UiBuilder.IconFont)) @@ -1389,9 +1518,12 @@ public class SettingsUi : WindowMediatorSubscriberBase } } } - UiSharedService.AttachToolTip(isCustom ? "Reset this color to default" : "Color is already at default value"); + + UiSharedService.AttachToolTip(isCustom + ? "Reset this color to default" + : "Color is already at default value"); } - + ImGui.EndTable(); } @@ -1400,6 +1532,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { UIColors.ResetAll(); } + _uiShared.DrawHelpText("This will reset all theme colors to their default values"); ImGui.Spacing(); @@ -1413,7 +1546,9 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.UseColorsInDtr = useColorsInDtr; _configService.Save(); } - _uiShared.DrawHelpText("This will color the Server Info Bar entry based on connection status and visible pairs."); + + _uiShared.DrawHelpText( + "This will color the Server Info Bar entry based on connection status and visible pairs."); using (ImRaii.Disabled(!useColorsInDtr)) { @@ -1457,6 +1592,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Save(); _nameplateService.RequestRedraw(); } + _uiShared.DrawHelpText("This will override the nameplate colors for visible paired players in-game."); using (ImRaii.Disabled(!nameColorsEnabled)) @@ -1468,18 +1604,21 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Save(); _nameplateService.RequestRedraw(); } + if (ImGui.Checkbox("Override friend color", ref isFriendOverride)) { _configService.Current.overrideFriendColor = isFriendOverride; _configService.Save(); _nameplateService.RequestRedraw(); } + if (ImGui.Checkbox("Override party color", ref isPartyOverride)) { _configService.Current.overridePartyColor = isPartyOverride; _configService.Save(); _nameplateService.RequestRedraw(); } + if (ImGui.Checkbox("Override FC tag color", ref isFcTagOverride)) { _configService.Current.overrideFcTagColor = isFcTagOverride; @@ -1507,6 +1646,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.useColoredUIDs = usePairColoredUIDs; _configService.Save(); } + _uiShared.DrawHelpText("This changes the vanity colored UID's in pair list."); _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); @@ -1523,7 +1663,9 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Save(); Mediator.Publish(new RefreshUiMessage()); } - _uiShared.DrawHelpText("This will show all currently visible users in a special 'Visible' group in the main UI."); + + _uiShared.DrawHelpText( + "This will show all currently visible users in a special 'Visible' group in the main UI."); using (ImRaii.Disabled(!showVisibleSeparate)) { @@ -1542,7 +1684,9 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Save(); Mediator.Publish(new RefreshUiMessage()); } - _uiShared.DrawHelpText("This will show all currently offline users in a special 'Offline' group in the main UI."); + + _uiShared.DrawHelpText( + "This will show all currently offline users in a special 'Offline' group in the main UI."); using (ImRaii.Disabled(!showOfflineSeparate)) { @@ -1561,7 +1705,9 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Save(); Mediator.Publish(new RefreshUiMessage()); } - _uiShared.DrawHelpText("This will group up all Syncshells in a special 'All Syncshells' folder in the main UI."); + + _uiShared.DrawHelpText( + "This will group up all Syncshells in a special 'All Syncshells' folder in the main UI."); if (ImGui.Checkbox("Show grouped syncshells in main screen/all syncshells", ref groupedSyncshells)) { @@ -1569,6 +1715,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Save(); Mediator.Publish(new RefreshUiMessage()); } + _uiShared.DrawHelpText("This will show grouped syncshells in main screen or group 'All Syncshells'."); if (ImGui.Checkbox("Show player name for visible players", ref showNameInsteadOfNotes)) @@ -1577,7 +1724,9 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Save(); Mediator.Publish(new RefreshUiMessage()); } - _uiShared.DrawHelpText("This will show the character name instead of custom set note when a character is visible"); + + _uiShared.DrawHelpText( + "This will show the character name instead of custom set note when a character is visible"); ImGui.Indent(); if (!_configService.Current.ShowCharacterNameInsteadOfNotesForVisible) ImGui.BeginDisabled(); @@ -1587,6 +1736,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Save(); Mediator.Publish(new RefreshUiMessage()); } + _uiShared.DrawHelpText("If you set a note for a player it will be shown instead of the player name"); if (!_configService.Current.ShowCharacterNameInsteadOfNotesForVisible) ImGui.EndDisabled(); ImGui.Unindent(); @@ -1596,6 +1746,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.UseFocusTarget = useFocusTarget; _configService.Save(); } + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } @@ -1609,6 +1760,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.ProfilesShow = showProfiles; _configService.Save(); } + _uiShared.DrawHelpText("This will show the configured user profile after a set delay"); ImGui.Indent(); if (!showProfiles) ImGui.BeginDisabled(); @@ -1618,12 +1770,14 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Save(); Mediator.Publish(new CompactUiChange(Vector2.Zero, Vector2.Zero)); } + _uiShared.DrawHelpText("Will show profiles on the right side of the main UI"); if (ImGui.SliderFloat("Hover Delay", ref profileDelay, 1, 10)) { _configService.Current.ProfileDelay = profileDelay; _configService.Save(); } + _uiShared.DrawHelpText("Delay until the profile should be displayed"); if (!showProfiles) ImGui.EndDisabled(); ImGui.Unindent(); @@ -1633,6 +1787,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.ProfilesAllowNsfw = showNsfwProfiles; _configService.Save(); } + _uiShared.DrawHelpText("Will show profiles that have the NSFW tag enabled"); _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); @@ -1646,7 +1801,8 @@ public class SettingsUi : WindowMediatorSubscriberBase if (_uiShared.MediumTreeNode("Display", UIColors.Get("LightlessPurple"))) { - _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.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(); @@ -1660,7 +1816,8 @@ public class SettingsUi : WindowMediatorSubscriberBase { _uiShared.UnderlinedBigText("Performance Settings", UIColors.Get("LightlessBlue")); ImGui.Dummy(new Vector2(10)); - UiSharedService.TextWrapped("The configuration options here are to give you more informed warnings and automation when it comes to other performance-intensive synced players."); + UiSharedService.TextWrapped( + "The configuration options here are to give you more informed warnings and automation when it comes to other performance-intensive synced players."); bool showPerformanceIndicator = _playerPerformanceConfigService.Current.ShowPerformanceIndicator; @@ -1671,14 +1828,20 @@ public class SettingsUi : WindowMediatorSubscriberBase _playerPerformanceConfigService.Current.ShowPerformanceIndicator = showPerformanceIndicator; _playerPerformanceConfigService.Save(); } - _uiShared.DrawHelpText("Will show a performance indicator when players exceed defined thresholds in Lightless UI." + Environment.NewLine + "Will use warning thresholds."); + + _uiShared.DrawHelpText( + "Will show a performance indicator when players exceed defined thresholds in Lightless UI." + + Environment.NewLine + "Will use warning thresholds."); bool warnOnExceedingThresholds = _playerPerformanceConfigService.Current.WarnOnExceedingThresholds; - if (ImGui.Checkbox("Warn on loading in players exceeding performance thresholds", ref warnOnExceedingThresholds)) + if (ImGui.Checkbox("Warn on loading in players exceeding performance thresholds", + ref warnOnExceedingThresholds)) { _playerPerformanceConfigService.Current.WarnOnExceedingThresholds = warnOnExceedingThresholds; _playerPerformanceConfigService.Save(); } - _uiShared.DrawHelpText("Lightless will print a warning in chat once per session of meeting those people. Will not warn on players with preferred permissions."); + + _uiShared.DrawHelpText( + "Lightless will print a warning in chat once per session of meeting those people. Will not warn on players with preferred permissions."); using (ImRaii.Disabled(!warnOnExceedingThresholds && !showPerformanceIndicator)) { using var indent = ImRaii.PushIndent(); @@ -1688,8 +1851,11 @@ public class SettingsUi : WindowMediatorSubscriberBase _playerPerformanceConfigService.Current.WarnOnPreferredPermissionsExceedingThresholds = warnOnPref; _playerPerformanceConfigService.Save(); } - _uiShared.DrawHelpText("Lightless will also print warnings and show performance indicator for players where you enabled preferred permissions. If warning in general is disabled, this will not produce any warnings."); + + _uiShared.DrawHelpText( + "Lightless will also print warnings and show performance indicator for players where you enabled preferred permissions. If warning in general is disabled, this will not produce any warnings."); } + using (ImRaii.Disabled(!showPerformanceIndicator && !warnOnExceedingThresholds)) { var vram = _playerPerformanceConfigService.Current.VRAMSizeWarningThresholdMiB; @@ -1700,9 +1866,12 @@ public class SettingsUi : WindowMediatorSubscriberBase _playerPerformanceConfigService.Current.VRAMSizeWarningThresholdMiB = vram; _playerPerformanceConfigService.Save(); } + ImGui.SameLine(); ImGui.Text("(MiB)"); - _uiShared.DrawHelpText("Limit in MiB of approximate VRAM usage to trigger warning or performance indicator on UI." + UiSharedService.TooltipSeparator + _uiShared.DrawHelpText( + "Limit in MiB of approximate VRAM usage to trigger warning or performance indicator on UI." + + UiSharedService.TooltipSeparator + "Default: 375 MiB"); ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); if (ImGui.InputInt("Warning Triangle threshold", ref tris)) @@ -1710,9 +1879,12 @@ public class SettingsUi : WindowMediatorSubscriberBase _playerPerformanceConfigService.Current.TrisWarningThresholdThousands = tris; _playerPerformanceConfigService.Save(); } + ImGui.SameLine(); ImGui.Text("(thousand triangles)"); - _uiShared.DrawHelpText("Limit in approximate used triangles from mods to trigger warning or performance indicator on UI." + UiSharedService.TooltipSeparator + _uiShared.DrawHelpText( + "Limit in approximate used triangles from mods to trigger warning or performance indicator on UI." + + UiSharedService.TooltipSeparator + "Default: 165 thousand"); } @@ -1723,7 +1895,8 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Separator(); bool autoPause = _playerPerformanceConfigService.Current.AutoPausePlayersExceedingThresholds; - bool autoPauseEveryone = _playerPerformanceConfigService.Current.AutoPausePlayersWithPreferredPermissionsExceedingThresholds; + bool autoPauseEveryone = _playerPerformanceConfigService.Current + .AutoPausePlayersWithPreferredPermissionsExceedingThresholds; bool autoPauseInDuty = _playerPerformanceConfigService.Current.PauseInInstanceDuty; bool autoPauseInCombat = _playerPerformanceConfigService.Current.PauseInCombat; bool autoPauseWhilePerforming = _playerPerformanceConfigService.Current.PauseWhilePerforming; @@ -1735,40 +1908,59 @@ public class SettingsUi : WindowMediatorSubscriberBase _playerPerformanceConfigService.Current.PauseInCombat = autoPauseInCombat; _playerPerformanceConfigService.Save(); } - _uiShared.DrawHelpText("AUTO-ENABLED: Your risk of crashing during a fight increases when this is disabled. For example: VFX mods Loading mid fight can cause a crash." + Environment.NewLine + + _uiShared.DrawHelpText( + "AUTO-ENABLED: Your risk of crashing during a fight increases when this is disabled. For example: VFX mods Loading mid fight can cause a crash." + + Environment.NewLine + UiSharedService.TooltipSeparator + "WARNING: DISABLE AT YOUR OWN RISK."); if (ImGui.Checkbox("Auto pause sync while in Perfomance as Bard", ref autoPauseWhilePerforming)) { _playerPerformanceConfigService.Current.PauseWhilePerforming = autoPauseWhilePerforming; _playerPerformanceConfigService.Save(); } - _uiShared.DrawHelpText("AUTO-ENABLED: Your risk of crashing during a performance increases when this is disabled. For example: Some mods can crash you mid performance" + Environment.NewLine + + _uiShared.DrawHelpText( + "AUTO-ENABLED: Your risk of crashing during a performance increases when this is disabled. For example: Some mods can crash you mid performance" + + Environment.NewLine + UiSharedService.TooltipSeparator + "WARNING: DISABLE AT YOUR OWN RISK."); if (ImGui.Checkbox("Auto pause sync while in instances and duties", ref autoPauseInDuty)) { _playerPerformanceConfigService.Current.PauseInInstanceDuty = autoPauseInDuty; _playerPerformanceConfigService.Save(); } - _uiShared.DrawHelpText("When enabled, it will automatically pause all players while you are in an instance, such as a dungeon or raid." + Environment.NewLine - + UiSharedService.TooltipSeparator + "Warning: You may have to leave the dungeon to resync with people again"); + + _uiShared.DrawHelpText( + "When enabled, it will automatically pause all players while you are in an instance, such as a dungeon or raid." + + Environment.NewLine + + UiSharedService.TooltipSeparator + + "Warning: You may have to leave the dungeon to resync with people again"); if (ImGui.Checkbox("Automatically pause players exceeding thresholds", ref autoPause)) { _playerPerformanceConfigService.Current.AutoPausePlayersExceedingThresholds = autoPause; _playerPerformanceConfigService.Save(); } - _uiShared.DrawHelpText("When enabled, it will automatically pause all players without preferred permissions that exceed the thresholds defined below." + Environment.NewLine + + _uiShared.DrawHelpText( + "When enabled, it will automatically pause all players without preferred permissions that exceed the thresholds defined below." + + Environment.NewLine + "Will print a warning in chat when a player got paused automatically." - + UiSharedService.TooltipSeparator + "Warning: this will not automatically unpause those people again, you will have to do this manually."); + + UiSharedService.TooltipSeparator + + "Warning: this will not automatically unpause those people again, you will have to do this manually."); using (ImRaii.Disabled(!autoPause)) { using var indent = ImRaii.PushIndent(); - if (ImGui.Checkbox("Automatically pause also players with preferred permissions", ref autoPauseEveryone)) + if (ImGui.Checkbox("Automatically pause also players with preferred permissions", + ref autoPauseEveryone)) { - _playerPerformanceConfigService.Current.AutoPausePlayersWithPreferredPermissionsExceedingThresholds = autoPauseEveryone; + _playerPerformanceConfigService.Current + .AutoPausePlayersWithPreferredPermissionsExceedingThresholds = autoPauseEveryone; _playerPerformanceConfigService.Save(); } - _uiShared.DrawHelpText("When enabled, will automatically pause all players regardless of preferred permissions that exceed thresholds defined below." + UiSharedService.TooltipSeparator + + + _uiShared.DrawHelpText( + "When enabled, will automatically pause all players regardless of preferred permissions that exceed thresholds defined below." + + UiSharedService.TooltipSeparator + "Warning: this will not automatically unpause those people again, you will have to do this manually."); var vramAuto = _playerPerformanceConfigService.Current.VRAMSizeAutoPauseThresholdMiB; var trisAuto = _playerPerformanceConfigService.Current.TrisAutoPauseThresholdThousands; @@ -1778,9 +1970,12 @@ public class SettingsUi : WindowMediatorSubscriberBase _playerPerformanceConfigService.Current.VRAMSizeAutoPauseThresholdMiB = vramAuto; _playerPerformanceConfigService.Save(); } + ImGui.SameLine(); ImGui.Text("(MiB)"); - _uiShared.DrawHelpText("When a loading in player and their VRAM usage exceeds this amount, automatically pauses the synced player." + UiSharedService.TooltipSeparator + _uiShared.DrawHelpText( + "When a loading in player and their VRAM usage exceeds this amount, automatically pauses the synced player." + + UiSharedService.TooltipSeparator + "Default: 550 MiB"); ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); if (ImGui.InputInt("Auto Pause Triangle threshold", ref trisAuto)) @@ -1788,9 +1983,12 @@ public class SettingsUi : WindowMediatorSubscriberBase _playerPerformanceConfigService.Current.TrisAutoPauseThresholdThousands = trisAuto; _playerPerformanceConfigService.Save(); } + ImGui.SameLine(); ImGui.Text("(thousand triangles)"); - _uiShared.DrawHelpText("When a loading in player and their triangle count exceeds this amount, automatically pauses the synced player." + UiSharedService.TooltipSeparator + _uiShared.DrawHelpText( + "When a loading in player and their triangle count exceeds this amount, automatically pauses the synced player." + + UiSharedService.TooltipSeparator + "Default: 250 thousand"); } @@ -1804,7 +2002,8 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.UnderlinedBigText("Whitelisted UIDs", UIColors.Get("LightlessBlue")); ImGuiHelpers.ScaledDummy(5); - UiSharedService.TextWrapped("The entries in the list below will be ignored for all warnings and auto pause operations."); + UiSharedService.TextWrapped( + "The entries in the list below will be ignored for all warnings and auto pause operations."); ImGui.Dummy(new Vector2(10)); ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); ImGui.InputText("##ignoreuid", ref _uidToAddForIgnore, 20); @@ -1813,14 +2012,17 @@ public class SettingsUi : WindowMediatorSubscriberBase { if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Add UID/Vanity ID to whitelist")) { - if (!_playerPerformanceConfigService.Current.UIDsToIgnore.Contains(_uidToAddForIgnore, StringComparer.Ordinal)) + if (!_playerPerformanceConfigService.Current.UIDsToIgnore.Contains(_uidToAddForIgnore, + StringComparer.Ordinal)) { _playerPerformanceConfigService.Current.UIDsToIgnore.Add(_uidToAddForIgnore); _playerPerformanceConfigService.Save(); } + _uidToAddForIgnore = string.Empty; } } + _uiShared.DrawHelpText("Hint: UIDs are case sensitive."); var playerList = _playerPerformanceConfigService.Current.UIDsToIgnore; ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); @@ -1838,6 +2040,7 @@ public class SettingsUi : WindowMediatorSubscriberBase } } } + using (ImRaii.Disabled(_selectedEntry == -1)) { if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete selected UID")) @@ -1868,7 +2071,8 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.DrawHelpText("Completely deletes all your uploaded files on the service."); - if (ImGui.BeginPopupModal("Delete all your files?", ref _deleteFilesPopupModalShown, UiSharedService.PopupWindowFlags)) + if (ImGui.BeginPopupModal("Delete all your files?", ref _deleteFilesPopupModalShown, + UiSharedService.PopupWindowFlags)) { UiSharedService.TextWrapped( "All your own uploaded files on the service will be deleted.\nThis operation cannot be undone."); @@ -1877,7 +2081,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Spacing(); var buttonSize = (ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - - ImGui.GetStyle().ItemSpacing.X) / 2; + ImGui.GetStyle().ItemSpacing.X) / 2; if (ImGui.Button("Delete everything", new Vector2(buttonSize, 0))) { @@ -1895,6 +2099,7 @@ public class SettingsUi : WindowMediatorSubscriberBase UiSharedService.SetScaledWindowSize(325); ImGui.EndPopup(); } + ImGui.SameLine(); if (ImGui.Button("Delete account")) { @@ -1904,7 +2109,8 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.DrawHelpText("Completely deletes your account and all uploaded files to the service."); - if (ImGui.BeginPopupModal("Delete your account?", ref _deleteAccountPopupModalShown, UiSharedService.PopupWindowFlags)) + if (ImGui.BeginPopupModal("Delete your account?", ref _deleteAccountPopupModalShown, + UiSharedService.PopupWindowFlags)) { UiSharedService.TextWrapped( "Your account and all associated files and data on the service will be deleted."); @@ -1951,14 +2157,18 @@ public class SettingsUi : WindowMediatorSubscriberBase { _serverConfigurationManager.SendCensusData = sendCensus; } - _uiShared.DrawHelpText("This will allow sending census data to the currently connected service." + UiSharedService.TooltipSeparator - + "Census data contains:" + Environment.NewLine - + "- Current World" + Environment.NewLine - + "- Current Gender" + Environment.NewLine - + "- Current Race" + Environment.NewLine - + "- Current Clan (this is not your Free Company, this is e.g. Keeper or Seeker for Miqo'te)" + UiSharedService.TooltipSeparator - + "The census data is only saved temporarily and will be removed from the server on disconnect. It is stored temporarily associated with your UID while you are connected." + UiSharedService.TooltipSeparator - + "If you do not wish to participate in the statistical census, untick this box and reconnect to the server."); + + _uiShared.DrawHelpText("This will allow sending census data to the currently connected service." + + UiSharedService.TooltipSeparator + + "Census data contains:" + Environment.NewLine + + "- Current World" + Environment.NewLine + + "- Current Gender" + Environment.NewLine + + "- Current Race" + Environment.NewLine + + "- Current Clan (this is not your Free Company, this is e.g. Keeper or Seeker for Miqo'te)" + + UiSharedService.TooltipSeparator + + "The census data is only saved temporarily and will be removed from the server on disconnect. It is stored temporarily associated with your UID while you are connected." + + UiSharedService.TooltipSeparator + + "If you do not wish to participate in the statistical census, untick this box and reconnect to the server."); ImGuiHelpers.ScaledDummy(new Vector2(10, 10)); var idx = _uiShared.DrawServiceSelection(); @@ -1975,7 +2185,9 @@ public class SettingsUi : WindowMediatorSubscriberBase var selectedServer = _serverConfigurationManager.GetServerByIndex(idx); if (selectedServer == _serverConfigurationManager.CurrentServer) { - UiSharedService.ColorTextWrapped("For any changes to be applied to the current service you need to reconnect to the service.", UIColors.Get("LightlessYellow")); + UiSharedService.ColorTextWrapped( + "For any changes to be applied to the current service you need to reconnect to the service.", + UIColors.Get("LightlessYellow")); } bool useOauth = selectedServer.UseOAuth2; @@ -1986,30 +2198,41 @@ public class SettingsUi : WindowMediatorSubscriberBase { if (selectedServer.SecretKeys.Any() || useOauth) { - UiSharedService.ColorTextWrapped("Characters listed here will automatically connect to the selected Lightless service with the settings as provided below." + - " Make sure to enter the character names correctly or use the 'Add current character' button at the bottom.", UIColors.Get("LightlessYellow")); + UiSharedService.ColorTextWrapped( + "Characters listed here will automatically connect to the selected Lightless service with the settings as provided below." + + " Make sure to enter the character names correctly or use the 'Add current character' button at the bottom.", + UIColors.Get("LightlessYellow")); int i = 0; _uiShared.DrawUpdateOAuthUIDsButton(selectedServer); if (selectedServer.UseOAuth2 && !string.IsNullOrEmpty(selectedServer.OAuthToken)) { - bool hasSetSecretKeysButNoUid = selectedServer.Authentications.Exists(u => u.SecretKeyIdx != -1 && string.IsNullOrEmpty(u.UID)); + bool hasSetSecretKeysButNoUid = + selectedServer.Authentications.Exists(u => + u.SecretKeyIdx != -1 && string.IsNullOrEmpty(u.UID)); if (hasSetSecretKeysButNoUid) { ImGui.Dummy(new(5f, 5f)); - UiSharedService.TextWrapped("Some entries have been detected that have previously been assigned secret keys but not UIDs. " + + UiSharedService.TextWrapped( + "Some entries have been detected that have previously been assigned secret keys but not UIDs. " + "Press this button below to attempt to convert those entries."); - using (ImRaii.Disabled(_secretKeysConversionTask != null && !_secretKeysConversionTask.IsCompleted)) + using (ImRaii.Disabled(_secretKeysConversionTask != null && + !_secretKeysConversionTask.IsCompleted)) { - if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowsLeftRight, "Try to Convert Secret Keys to UIDs")) + if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowsLeftRight, + "Try to Convert Secret Keys to UIDs")) { - _secretKeysConversionTask = ConvertSecretKeysToUIDs(selectedServer, _secretKeysConversionCts.Token); + _secretKeysConversionTask = + ConvertSecretKeysToUIDs(selectedServer, _secretKeysConversionCts.Token); } } + if (_secretKeysConversionTask != null && !_secretKeysConversionTask.IsCompleted) { - UiSharedService.ColorTextWrapped("Converting Secret Keys to UIDs", UIColors.Get("LightlessYellow")); + UiSharedService.ColorTextWrapped("Converting Secret Keys to UIDs", + UIColors.Get("LightlessYellow")); } + if (_secretKeysConversionTask != null && _secretKeysConversionTask.IsCompletedSuccessfully) { Vector4? textColor = null; @@ -2017,10 +2240,12 @@ public class SettingsUi : WindowMediatorSubscriberBase { textColor = UIColors.Get("LightlessYellow"); } + if (!_secretKeysConversionTask.Result.Success) { textColor = ImGuiColors.DalamudRed; } + string text = $"Conversion has completed: {_secretKeysConversionTask.Result.Result}"; if (textColor == null) { @@ -2030,49 +2255,70 @@ public class SettingsUi : WindowMediatorSubscriberBase { UiSharedService.ColorTextWrapped(text, textColor!.Value); } - if (!_secretKeysConversionTask.Result.Success || _secretKeysConversionTask.Result.PartialSuccess) + + if (!_secretKeysConversionTask.Result.Success || + _secretKeysConversionTask.Result.PartialSuccess) { - UiSharedService.TextWrapped("In case of conversion failures, please set the UIDs for the failed conversions manually."); + UiSharedService.TextWrapped( + "In case of conversion failures, please set the UIDs for the failed conversions manually."); } } } } + ImGui.Separator(); string youName = _dalamudUtilService.GetPlayerName(); uint youWorld = _dalamudUtilService.GetHomeWorldId(); ulong youCid = _dalamudUtilService.GetCID(); - if (!selectedServer.Authentications.Exists(a => string.Equals(a.CharacterName, youName, StringComparison.Ordinal) && a.WorldId == youWorld)) + if (!selectedServer.Authentications.Exists(a => + string.Equals(a.CharacterName, youName, StringComparison.Ordinal) && a.WorldId == youWorld)) { _uiShared.BigText("Your Character is not Configured", ImGuiColors.DalamudRed); - UiSharedService.ColorTextWrapped("You have currently no character configured that corresponds to your current name and world.", ImGuiColors.DalamudRed); + UiSharedService.ColorTextWrapped( + "You have currently no character configured that corresponds to your current name and world.", + ImGuiColors.DalamudRed); var authWithCid = selectedServer.Authentications.Find(f => f.LastSeenCID == youCid); if (authWithCid != null) { ImGuiHelpers.ScaledDummy(5); - UiSharedService.ColorText("A potential rename/world change from this character was detected:", UIColors.Get("LightlessYellow")); + UiSharedService.ColorText( + "A potential rename/world change from this character was detected:", + UIColors.Get("LightlessYellow")); using (ImRaii.PushIndent(10f)) - UiSharedService.ColorText("Entry: " + authWithCid.CharacterName + " - " + _dalamudUtilService.WorldData.Value[(ushort)authWithCid.WorldId], UIColors.Get("LightlessBlue")); - UiSharedService.ColorText("Press the button below to adjust that entry to your current character:", UIColors.Get("LightlessYellow")); + UiSharedService.ColorText( + "Entry: " + authWithCid.CharacterName + " - " + + _dalamudUtilService.WorldData.Value[(ushort)authWithCid.WorldId], + UIColors.Get("LightlessBlue")); + UiSharedService.ColorText( + "Press the button below to adjust that entry to your current character:", + UIColors.Get("LightlessYellow")); using (ImRaii.PushIndent(10f)) - UiSharedService.ColorText("Current: " + youName + " - " + _dalamudUtilService.WorldData.Value[(ushort)youWorld], UIColors.Get("LightlessBlue")); + UiSharedService.ColorText( + "Current: " + youName + " - " + + _dalamudUtilService.WorldData.Value[(ushort)youWorld], + UIColors.Get("LightlessBlue")); ImGuiHelpers.ScaledDummy(5); - if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowRight, "Update Entry to Current Character")) + if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowRight, + "Update Entry to Current Character")) { authWithCid.CharacterName = youName; authWithCid.WorldId = youWorld; _serverConfigurationManager.Save(); } } + ImGuiHelpers.ScaledDummy(5); ImGui.Separator(); ImGuiHelpers.ScaledDummy(5); } + foreach (var item in selectedServer.Authentications.ToList()) { using var charaId = ImRaii.PushId("selectedChara" + i); var worldIdx = (ushort)item.WorldId; - var data = _uiShared.WorldData.OrderBy(u => u.Value, StringComparer.Ordinal).ToDictionary(k => k.Key, k => k.Value); + var data = _uiShared.WorldData.OrderBy(u => u.Value, StringComparer.Ordinal) + .ToDictionary(k => k.Key, k => k.Value); if (!data.TryGetValue(worldIdx, out string? worldPreview)) { worldPreview = data.First().Value; @@ -2096,24 +2342,31 @@ public class SettingsUi : WindowMediatorSubscriberBase { thisIsYou = true; } + bool misManaged = false; - if (selectedServer.UseOAuth2 && !string.IsNullOrEmpty(selectedServer.OAuthToken) && string.IsNullOrEmpty(item.UID)) + if (selectedServer.UseOAuth2 && !string.IsNullOrEmpty(selectedServer.OAuthToken) && + string.IsNullOrEmpty(item.UID)) { misManaged = true; } + if (!selectedServer.UseOAuth2 && item.SecretKeyIdx == -1) { misManaged = true; } + Vector4 color = UIColors.Get("LightlessBlue"); string text = thisIsYou ? "Your Current Character" : string.Empty; if (misManaged) { - text += " [MISMANAGED (" + (selectedServer.UseOAuth2 ? "No UID Set" : "No Secret Key Set") + ")]"; + text += " [MISMANAGED (" + (selectedServer.UseOAuth2 ? "No UID Set" : "No Secret Key Set") + + ")]"; color = ImGuiColors.DalamudRed; } - if (selectedServer.Authentications.Where(e => e != item).Any(e => string.Equals(e.CharacterName, item.CharacterName, StringComparison.Ordinal) - && e.WorldId == item.WorldId)) + + if (selectedServer.Authentications.Where(e => e != item).Any(e => + string.Equals(e.CharacterName, item.CharacterName, StringComparison.Ordinal) + && e.WorldId == item.WorldId)) { text += " [DUPLICATE]"; color = ImGuiColors.DalamudRed; @@ -2140,11 +2393,16 @@ public class SettingsUi : WindowMediatorSubscriberBase item.WorldId = w.Key; _serverConfigurationManager.Save(); } - }, EqualityComparer>.Default.Equals(data.FirstOrDefault(f => f.Key == worldIdx), default) ? data.First() : data.First(f => f.Key == worldIdx)); + }, + EqualityComparer>.Default.Equals( + data.FirstOrDefault(f => f.Key == worldIdx), default) + ? data.First() + : data.First(f => f.Key == worldIdx)); if (!useOauth) { - _uiShared.DrawCombo("Secret Key###" + item.CharacterName + i, keys, (w) => w.Value.FriendlyName, + _uiShared.DrawCombo("Secret Key###" + item.CharacterName + i, keys, + (w) => w.Value.FriendlyName, (w) => { if (w.Key != item.SecretKeyIdx) @@ -2152,20 +2410,28 @@ public class SettingsUi : WindowMediatorSubscriberBase item.SecretKeyIdx = w.Key; _serverConfigurationManager.Save(); } - }, EqualityComparer>.Default.Equals(keys.FirstOrDefault(f => f.Key == item.SecretKeyIdx), default) ? keys.First() : keys.First(f => f.Key == item.SecretKeyIdx)); + }, + EqualityComparer>.Default.Equals( + keys.FirstOrDefault(f => f.Key == item.SecretKeyIdx), default) + ? keys.First() + : keys.First(f => f.Key == item.SecretKeyIdx)); } else { _uiShared.DrawUIDComboForAuthentication(i, item, selectedServer.ServerUri, _logger); } + bool isAutoLogin = item.AutoLogin; if (ImGui.Checkbox("Automatically login to Lightless", ref isAutoLogin)) { item.AutoLogin = isAutoLogin; _serverConfigurationManager.Save(); } - _uiShared.DrawHelpText("When enabled and logging into this character in XIV, Lightless will automatically connect to the current service."); - if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Character") && UiSharedService.CtrlPressed()) + + _uiShared.DrawHelpText( + "When enabled and logging into this character in XIV, Lightless will automatically connect to the current service."); + if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Character") && + UiSharedService.CtrlPressed()) _serverConfigurationManager.RemoveCharacterFromServer(idx, item); UiSharedService.AttachToolTip("Hold CTRL to delete this entry."); @@ -2181,13 +2447,15 @@ public class SettingsUi : WindowMediatorSubscriberBase if (selectedServer.Authentications.Any()) ImGui.Separator(); - if (!selectedServer.Authentications.Exists(c => string.Equals(c.CharacterName, youName, StringComparison.Ordinal) - && c.WorldId == youWorld)) + if (!selectedServer.Authentications.Exists(c => + string.Equals(c.CharacterName, youName, StringComparison.Ordinal) + && c.WorldId == youWorld)) { if (_uiShared.IconTextButton(FontAwesomeIcon.User, "Add current character")) { _serverConfigurationManager.AddCurrentCharacterToServer(idx); } + ImGui.SameLine(); } @@ -2198,7 +2466,8 @@ public class SettingsUi : WindowMediatorSubscriberBase } else { - UiSharedService.ColorTextWrapped("You need to add a Secret Key first before adding Characters.", UIColors.Get("LightlessYellow")); + UiSharedService.ColorTextWrapped("You need to add a Secret Key first before adding Characters.", + UIColors.Get("LightlessYellow")); } ImGui.EndTabItem(); @@ -2215,24 +2484,29 @@ public class SettingsUi : WindowMediatorSubscriberBase item.Value.FriendlyName = friendlyName; _serverConfigurationManager.Save(); } + var key = item.Value.Key; if (ImGui.InputText("Secret Key", ref key, 64)) { item.Value.Key = key; _serverConfigurationManager.Save(); } + if (!selectedServer.Authentications.Exists(p => p.SecretKeyIdx == item.Key)) { - if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Secret Key") && UiSharedService.CtrlPressed()) + if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Secret Key") && + UiSharedService.CtrlPressed()) { selectedServer.SecretKeys.Remove(item.Key); _serverConfigurationManager.Save(); } + UiSharedService.AttachToolTip("Hold CTRL to delete this secret key entry"); } else { - UiSharedService.ColorTextWrapped("This key is in use and cannot be deleted", UIColors.Get("LightlessYellow")); + UiSharedService.ColorTextWrapped("This key is in use and cannot be deleted", + UIColors.Get("LightlessYellow")); } if (item.Key != selectedServer.SecretKeys.Keys.LastOrDefault()) @@ -2242,10 +2516,9 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Separator(); if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Add new Secret Key")) { - selectedServer.SecretKeys.Add(selectedServer.SecretKeys.Any() ? selectedServer.SecretKeys.Max(p => p.Key) + 1 : 0, new SecretKey() - { - FriendlyName = "New Secret Key", - }); + selectedServer.SecretKeys.Add( + selectedServer.SecretKeys.Any() ? selectedServer.SecretKeys.Max(p => p.Key) + 1 : 0, + new SecretKey() { FriendlyName = "New Secret Key", }); _serverConfigurationManager.Save(); } @@ -2263,6 +2536,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { selectedServer.ServerUri = serverUri; } + if (isMain) { _uiShared.DrawHelpText("You cannot edit the URI of the main service."); @@ -2273,6 +2547,7 @@ public class SettingsUi : WindowMediatorSubscriberBase selectedServer.ServerName = serverName; _serverConfigurationManager.Save(); } + if (isMain) { _uiShared.DrawHelpText("You cannot edit the name of the main service."); @@ -2280,12 +2555,16 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.SetNextItemWidth(200); var serverTransport = _serverConfigurationManager.GetTransport(); - _uiShared.DrawCombo("Server Transport Type", Enum.GetValues().Where(t => t != HttpTransportType.None), + _uiShared.DrawCombo("Server Transport Type", + Enum.GetValues().Where(t => t != HttpTransportType.None), (v) => v.ToString(), onSelected: (t) => _serverConfigurationManager.SetTransportType(t), serverTransport); - _uiShared.DrawHelpText("You normally do not need to change this, if you don't know what this is or what it's for, keep it to WebSockets." + Environment.NewLine - + "If you run into connection issues with e.g. VPNs, try ServerSentEvents first before trying out LongPolling." + UiSharedService.TooltipSeparator + _uiShared.DrawHelpText( + "You normally do not need to change this, if you don't know what this is or what it's for, keep it to WebSockets." + + Environment.NewLine + + "If you run into connection issues with e.g. VPNs, try ServerSentEvents first before trying out LongPolling." + + UiSharedService.TooltipSeparator + "Note: if the server does not support a specific Transport Type it will fall through to the next automatically: WebSockets > ServerSentEvents > LongPolling"); if (_dalamudUtilService.IsWine) @@ -2296,7 +2575,9 @@ public class SettingsUi : WindowMediatorSubscriberBase selectedServer.ForceWebSockets = forceWebSockets; _serverConfigurationManager.Save(); } - _uiShared.DrawHelpText("On wine, Lightless will automatically fall back to ServerSentEvents/LongPolling, even if WebSockets is selected. " + + _uiShared.DrawHelpText( + "On wine, Lightless will automatically fall back to ServerSentEvents/LongPolling, even if WebSockets is selected. " + "WebSockets are known to crash XIV entirely on wine 8.5 shipped with Dalamud. " + "Only enable this if you are not running wine 8.5." + Environment.NewLine + "Note: If the issue gets resolved at some point this option will be removed."); @@ -2309,20 +2590,26 @@ public class SettingsUi : WindowMediatorSubscriberBase selectedServer.UseOAuth2 = useOauth; _serverConfigurationManager.Save(); } - _uiShared.DrawHelpText("Use Discord OAuth2 Authentication to identify with this server instead of secret keys"); + + _uiShared.DrawHelpText( + "Use Discord OAuth2 Authentication to identify with this server instead of secret keys"); if (useOauth) { _uiShared.DrawOAuth(selectedServer); if (string.IsNullOrEmpty(_serverConfigurationManager.GetDiscordUserFromToken(selectedServer))) { ImGuiHelpers.ScaledDummy(10f); - UiSharedService.ColorTextWrapped("You have enabled OAuth2 but it is not linked. Press the buttons Check, then Authenticate to link properly.", ImGuiColors.DalamudRed); + UiSharedService.ColorTextWrapped( + "You have enabled OAuth2 but it is not linked. Press the buttons Check, then Authenticate to link properly.", + ImGuiColors.DalamudRed); } + if (!string.IsNullOrEmpty(_serverConfigurationManager.GetDiscordUserFromToken(selectedServer)) && selectedServer.Authentications.TrueForAll(u => string.IsNullOrEmpty(u.UID))) { ImGuiHelpers.ScaledDummy(10f); - UiSharedService.ColorTextWrapped("You have enabled OAuth2 but no characters configured. Set the correct UIDs for your characters in \"Character Management\".", + UiSharedService.ColorTextWrapped( + "You have enabled OAuth2 but no characters configured. Set the correct UIDs for your characters in \"Character Management\".", ImGuiColors.DalamudRed); } } @@ -2330,10 +2617,12 @@ public class SettingsUi : WindowMediatorSubscriberBase if (!isMain && selectedServer != _serverConfigurationManager.CurrentServer) { ImGui.Separator(); - if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Service") && UiSharedService.CtrlPressed()) + if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Service") && + UiSharedService.CtrlPressed()) { _serverConfigurationManager.DeleteServer(selectedServer); } + _uiShared.DrawHelpText("Hold CTRL to delete this service"); } @@ -2345,26 +2634,35 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.BigText("Default Permission Settings"); if (selectedServer == _serverConfigurationManager.CurrentServer && _apiController.IsConnected) { - UiSharedService.TextWrapped("Note: The default permissions settings here are not applied retroactively to existing pairs or joined Syncshells."); - UiSharedService.TextWrapped("Note: The default permissions settings here are sent and stored on the connected service."); + UiSharedService.TextWrapped( + "Note: The default permissions settings here are not applied retroactively to existing pairs or joined Syncshells."); + UiSharedService.TextWrapped( + "Note: The default permissions settings here are sent and stored on the connected service."); ImGuiHelpers.ScaledDummy(5f); var perms = _apiController.DefaultPermissions!; bool individualIsSticky = perms.IndividualIsSticky; bool disableIndividualSounds = perms.DisableIndividualSounds; bool disableIndividualAnimations = perms.DisableIndividualAnimations; bool disableIndividualVFX = perms.DisableIndividualVFX; - if (ImGui.Checkbox("Individually set permissions become preferred permissions", ref individualIsSticky)) + if (ImGui.Checkbox("Individually set permissions become preferred permissions", + ref individualIsSticky)) { perms.IndividualIsSticky = individualIsSticky; _ = _apiController.UserUpdateDefaultPermissions(perms); } - _uiShared.DrawHelpText("The preferred attribute means that the permissions to that user will never change through any of your permission changes to Syncshells " + + + _uiShared.DrawHelpText( + "The preferred attribute means that the permissions to that user will never change through any of your permission changes to Syncshells " + "(i.e. if you have paused one specific user in a Syncshell and they become preferred permissions, then pause and unpause the same Syncshell, the user will remain paused - " + - "if a user does not have preferred permissions, it will follow the permissions of the Syncshell and be unpaused)." + Environment.NewLine + Environment.NewLine + + "if a user does not have preferred permissions, it will follow the permissions of the Syncshell and be unpaused)." + + Environment.NewLine + Environment.NewLine + "This setting means:" + Environment.NewLine + - " - All new individual pairs get their permissions defaulted to preferred permissions." + Environment.NewLine + - " - All individually set permissions for any pair will also automatically become preferred permissions. This includes pairs in Syncshells." + Environment.NewLine + Environment.NewLine + - "It is possible to remove or set the preferred permission state for any pair at any time." + Environment.NewLine + Environment.NewLine + + " - All new individual pairs get their permissions defaulted to preferred permissions." + + Environment.NewLine + + " - All individually set permissions for any pair will also automatically become preferred permissions. This includes pairs in Syncshells." + + Environment.NewLine + Environment.NewLine + + "It is possible to remove or set the preferred permission state for any pair at any time." + + Environment.NewLine + Environment.NewLine + "If unsure, leave this setting off."); ImGuiHelpers.ScaledDummy(3f); @@ -2373,18 +2671,21 @@ public class SettingsUi : WindowMediatorSubscriberBase perms.DisableIndividualSounds = disableIndividualSounds; _ = _apiController.UserUpdateDefaultPermissions(perms); } + _uiShared.DrawHelpText("This setting will disable sound sync for all new individual pairs."); if (ImGui.Checkbox("Disable individual pair animations", ref disableIndividualAnimations)) { perms.DisableIndividualAnimations = disableIndividualAnimations; _ = _apiController.UserUpdateDefaultPermissions(perms); } + _uiShared.DrawHelpText("This setting will disable animation sync for all new individual pairs."); if (ImGui.Checkbox("Disable individual pair VFX", ref disableIndividualVFX)) { perms.DisableIndividualVFX = disableIndividualVFX; _ = _apiController.UserUpdateDefaultPermissions(perms); } + _uiShared.DrawHelpText("This setting will disable VFX sync for all new individual pairs."); ImGuiHelpers.ScaledDummy(5f); bool disableGroundSounds = perms.DisableGroupSounds; @@ -2395,28 +2696,36 @@ public class SettingsUi : WindowMediatorSubscriberBase perms.DisableGroupSounds = disableGroundSounds; _ = _apiController.UserUpdateDefaultPermissions(perms); } - _uiShared.DrawHelpText("This setting will disable sound sync for all non-sticky pairs in newly joined syncshells."); + + _uiShared.DrawHelpText( + "This setting will disable sound sync for all non-sticky pairs in newly joined syncshells."); if (ImGui.Checkbox("Disable Syncshell pair animations", ref disableGroupAnimations)) { perms.DisableGroupAnimations = disableGroupAnimations; _ = _apiController.UserUpdateDefaultPermissions(perms); } - _uiShared.DrawHelpText("This setting will disable animation sync for all non-sticky pairs in newly joined syncshells."); + + _uiShared.DrawHelpText( + "This setting will disable animation sync for all non-sticky pairs in newly joined syncshells."); if (ImGui.Checkbox("Disable Syncshell pair VFX", ref disableGroupVFX)) { perms.DisableGroupVFX = disableGroupVFX; _ = _apiController.UserUpdateDefaultPermissions(perms); } - _uiShared.DrawHelpText("This setting will disable VFX sync for all non-sticky pairs in newly joined syncshells."); + + _uiShared.DrawHelpText( + "This setting will disable VFX sync for all non-sticky pairs in newly joined syncshells."); } else { UiSharedService.ColorTextWrapped("Default Permission Settings unavailable for this service. " + - "You need to connect to this service to change the default permissions since they are stored on the service.", UIColors.Get("LightlessYellow")); + "You need to connect to this service to change the default permissions since they are stored on the service.", + UIColors.Get("LightlessYellow")); } ImGui.EndTabItem(); } + ImGui.EndTabBar(); } @@ -2427,10 +2736,13 @@ public class SettingsUi : WindowMediatorSubscriberBase private Task<(bool Success, bool PartialSuccess, string Result)>? _secretKeysConversionTask = null; private CancellationTokenSource _secretKeysConversionCts = new CancellationTokenSource(); - private async Task<(bool Success, bool partialSuccess, string Result)> ConvertSecretKeysToUIDs(ServerStorage serverStorage, CancellationToken token) + private async Task<(bool Success, bool partialSuccess, string Result)> ConvertSecretKeysToUIDs( + ServerStorage serverStorage, CancellationToken token) { - List failedConversions = serverStorage.Authentications.Where(u => u.SecretKeyIdx == -1 && string.IsNullOrEmpty(u.UID)).ToList(); - List conversionsToAttempt = serverStorage.Authentications.Where(u => u.SecretKeyIdx != -1 && string.IsNullOrEmpty(u.UID)).ToList(); + List failedConversions = serverStorage.Authentications + .Where(u => u.SecretKeyIdx == -1 && string.IsNullOrEmpty(u.UID)).ToList(); + List conversionsToAttempt = serverStorage.Authentications + .Where(u => u.SecretKeyIdx != -1 && string.IsNullOrEmpty(u.UID)).ToList(); List successfulConversions = []; Dictionary> secretKeyMapping = new(StringComparer.Ordinal); foreach (var authEntry in conversionsToAttempt) @@ -2451,7 +2763,9 @@ public class SettingsUi : WindowMediatorSubscriberBase if (secretKeyMapping.Count == 0) { - return (false, false, $"Failed to convert {failedConversions.Count} entries: " + string.Join(", ", failedConversions.Select(k => k.CharacterName))); + return (false, false, + $"Failed to convert {failedConversions.Count} entries: " + + string.Join(", ", failedConversions.Select(k => k.CharacterName))); } var baseUri = serverStorage.ServerUri.Replace("wss://", "https://").Replace("ws://", "http://"); @@ -2462,8 +2776,10 @@ public class SettingsUi : WindowMediatorSubscriberBase requestMessage.Content = requestContent; using var response = await _httpClient.SendAsync(requestMessage, token).ConfigureAwait(false); - Dictionary? secretKeyUidMapping = await JsonSerializer.DeserializeAsync> - (await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false), cancellationToken: token).ConfigureAwait(false); + Dictionary? secretKeyUidMapping = await JsonSerializer + .DeserializeAsync> + (await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false), cancellationToken: token) + .ConfigureAwait(false); if (secretKeyUidMapping == null) { return (false, false, $"Failed to parse the server response. Failed to convert all entries."); @@ -2501,7 +2817,8 @@ public class SettingsUi : WindowMediatorSubscriberBase private static string GetLightfinderPresetGlyph(int index) { - return NameplateHandler.NormalizeIconGlyph(SeIconCharExtensions.ToIconString(LightfinderIconPresets[index].Icon)); + return NameplateHandler.NormalizeIconGlyph( + SeIconCharExtensions.ToIconString(LightfinderIconPresets[index].Icon)); } private void RefreshLightfinderIconState() @@ -2542,7 +2859,8 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.SameLine(); ImGui.TextUnformatted("("); ImGui.SameLine(); - ImGui.TextColored(UIColors.Get("LightlessBlue"), _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture)); + ImGui.TextColored(UIColors.Get("LightlessBlue"), + _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture)); ImGui.SameLine(); ImGui.TextUnformatted("Users Online"); ImGui.SameLine(); @@ -2556,6 +2874,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { Util.OpenLink("https://discord.gg/mpNdkrTRjW"); } + ImGui.Separator(); if (ImGui.BeginTabBar("mainTabBar")) { @@ -2628,284 +2947,317 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.UseLightlessNotifications = useLightlessNotifications; _configService.Save(); } - _uiShared.DrawHelpText("Enable modern notifications with interactive buttons, animations, and progress tracking. Disable for classic Dalamud toast/chat notifications."); - + + _uiShared.DrawHelpText( + "Enable modern notifications with interactive buttons, animations, and progress tracking. Disable for classic Dalamud toast/chat notifications."); + ImGuiHelpers.ScaledDummy(5); ImGui.Separator(); - _uiShared.UnderlinedBigText("Notification Locations", UIColors.Get("LightlessBlue")); - ImGuiHelpers.ScaledDummy(5); - UiSharedService.ColorTextWrapped("Choose where each notification type appears.", ImGuiColors.DalamudGrey); - ImGuiHelpers.ScaledDummy(5); - - if (useLightlessNotifications) + if (_uiShared.MediumTreeNode("Notification Locations", UIColors.Get("LightlessPurple"))) { - // Lightless notification locations - ImGui.Indent(); - - 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(3); - _uiShared.DrawHelpText("Special notification types:"); - - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("Pair Request Notifications:"); - ImGui.SameLine(); - ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); - _uiShared.DrawCombo("###enhanced_pairrequest", lightlessLocations, GetNotificationLocationLabel, (location) => - { - _configService.Current.LightlessPairRequestNotification = location; - _configService.Save(); - }, _configService.Current.LightlessPairRequestNotification); - - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("Download Progress Notifications:"); - ImGui.SameLine(); - ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); - var downloadLocations = GetDownloadNotificationLocations(); - _uiShared.DrawCombo("###enhanced_download", downloadLocations, GetNotificationLocationLabel, (location) => - { - _configService.Current.LightlessDownloadNotification = location; - _configService.Save(); - }, _configService.Current.LightlessDownloadNotification); - - - ImGui.Unindent(); - } - else - { - // Classic notifications when lightless notifs is disabled - var classicLocations = GetClassicNotificationLocations(); - ImGui.Indent(); - 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.Unindent(); - } - - ImGuiHelpers.ScaledDummy(3); - ImGui.Separator(); - if (useLightlessNotifications) - { - _uiShared.UnderlinedBigText("Test Notifications", UIColors.Get("LightlessBlue")); + UiSharedService.ColorTextWrapped("Choose where each notification type appears.", ImGuiColors.DalamudGrey); ImGuiHelpers.ScaledDummy(5); - - 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("Preview how notifications will appear with your current settings."); - - ImGui.Unindent(); - - ImGuiHelpers.ScaledDummy(5); - 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) - { + // Lightless notification locations 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)."); + var lightlessLocations = GetLightlessNotificationLocations(); - // 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(); + 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); - // if (useCustomSounds) - // { - // ImGui.Indent(); - // DrawSoundCustomization(); - // ImGui.Unindent(); - // } + 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(3); + _uiShared.DrawHelpText("Special notification types:"); + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Pair Request Notifications:"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + _uiShared.DrawCombo("###enhanced_pairrequest", lightlessLocations, GetNotificationLocationLabel, + (location) => + { + _configService.Current.LightlessPairRequestNotification = location; + _configService.Save(); + }, _configService.Current.LightlessPairRequestNotification); + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Download Progress Notifications:"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + var downloadLocations = GetDownloadNotificationLocations(); + _uiShared.DrawCombo("###enhanced_download", downloadLocations, GetNotificationLocationLabel, + (location) => + { + _configService.Current.LightlessDownloadNotification = location; + _configService.Save(); + }, _configService.Current.LightlessDownloadNotification); + + + ImGui.Unindent(); + } + else + { + // Classic notifications when lightless notifs is disabled + var classicLocations = GetClassicNotificationLocations(); + ImGui.Indent(); + 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.Unindent(); } + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + } + + ImGui.Separator(); + if (useLightlessNotifications) + { + if (_uiShared.MediumTreeNode("Test Notifications", UIColors.Get("LightlessPurple"))) + { + ImGui.Indent(); + + // Test notification buttons + if (_uiShared.IconTextButton(FontAwesomeIcon.Bell, "Test Info")) + { + Mediator.Publish(new NotificationMessage("Test Info", + "This is a test info notification to let you know Chocola is cute :3", 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)); + } + + ImGuiHelpers.ScaledDummy(3); + if (_uiShared.IconTextButton(FontAwesomeIcon.UserPlus, "Test Pair Request")) + { + _lightlessNotificationService.ShowPairRequestNotification( + "Test User", + "test-uid-123", + () => + { + Mediator.Publish(new NotificationMessage("Accepted", "You accepted the test pair request.", + NotificationType.Info)); + }, + () => + { + Mediator.Publish(new NotificationMessage("Declined", "You declined the test pair request.", + NotificationType.Info)); + } + ); + } + + ImGui.SameLine(); + if (_uiShared.IconTextButton(FontAwesomeIcon.Download, "Test Download Progress")) + { + _lightlessNotificationService.ShowPairDownloadNotification( + new List<(string playerName, float progress, string status)> + { + ("Player One", 0.35f, "downloading"), + ("Player Two", 0.75f, "downloading"), + ("Player Three", 1.0f, "downloading") + }, + queueWaiting: 2 + ); + } + + _uiShared.DrawHelpText("Preview how notifications will appear with your current settings."); + + ImGui.Unindent(); + + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + } + + ImGui.Separator(); + if (_uiShared.MediumTreeNode("Basic Settings", UIColors.Get("LightlessPurple"))) + { + + int defaultDuration = _configService.Current.DefaultNotificationDurationSeconds; + if (ImGui.SliderInt("Default Duration (seconds)", ref defaultDuration, 3, 60)) + { + _configService.Current.DefaultNotificationDurationSeconds = defaultDuration; + _configService.Save(); + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _configService.Current.DefaultNotificationDurationSeconds = 10; + _configService.Save(); + } + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Right click to reset to default (10 seconds)."); + + _uiShared.DrawHelpText("How long notifications stay visible by default."); + + 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 dismissOnClick = _configService.Current.DismissNotificationOnClick; + if (ImGui.Checkbox("Dismiss on Click", ref dismissOnClick)) + { + _configService.Current.DismissNotificationOnClick = dismissOnClick; + _configService.Save(); + } + + _uiShared.DrawHelpText("Click anywhere on a notification to dismiss it. Notifications with action buttons (like pair requests) are excluded."); + + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + } + + if (useLightlessNotifications) + { + ImGui.Separator(); + if (_uiShared.MediumTreeNode("Appearance & Animation", UIColors.Get("LightlessPurple"))) + { + + float opacity = _configService.Current.NotificationOpacity; + if (ImGui.SliderFloat("Notification Opacity", ref opacity, 0.1f, 1.0f, "%.2f")) + { + _configService.Current.NotificationOpacity = opacity; + _configService.Save(); + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _configService.Current.NotificationOpacity = 0.95f; + _configService.Save(); + } + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Right click to reset to default (0.95)."); + + _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(); + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _configService.Current.MaxSimultaneousNotifications = 5; + _configService.Save(); + } + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Right click to reset to default (5)."); + + _uiShared.DrawHelpText("Maximum number of notifications that can be shown at once."); + + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + } + } + + ImGui.Separator(); + if (_uiShared.MediumTreeNode("Sound Settings", UIColors.Get("LightlessPurple"))) + { + ImGui.TextUnformatted("Notification Sounds"); + ImGuiHelpers.ScaledDummy(5); + + DrawSoundTable(); + + _uiShared.DrawHelpText( + "Configure which sounds play for each notification type. Use the play button to preview sounds."); + + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + } 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); + UiSharedService.ColorTextWrapped("Configure specific types of notifications and their behavior.", + ImGuiColors.DalamudGrey); ImGuiHelpers.ScaledDummy(3); // Online Notifications Section @@ -2920,7 +3272,9 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.ShowOnlineNotifications = onlineNotifs; _configService.Save(); } - _uiShared.DrawHelpText("Show notifications when pairs come online. These will use the Info notification location settings above."); + + _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(); @@ -2929,6 +3283,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _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)) @@ -2936,7 +3291,9 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.ShowOnlineNotificationsOnlyForNamedPairs = onlineNotifsNamedOnly; _configService.Save(); } - _uiShared.DrawHelpText("Only show online notifications for pairs where you have set an individual note."); + + _uiShared.DrawHelpText( + "Only show online notifications for pairs where you have set an individual note."); ImGui.Unindent(); _uiShared.ColoredSeparator(UIColors.Get("LightlessGreen"), 1.5f); @@ -2946,17 +3303,15 @@ public class SettingsUi : WindowMediatorSubscriberBase // 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); + UiSharedService.ColorTextWrapped( + "Pairing requests always show as interactive notifications with Accept/Decline buttons. Configure the sound in the Sound Settings table above.", + 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(); } - // System Notifications Section if (_uiShared.MediumTreeNode("System Notifications", UIColors.Get("LightlessYellow"))) { var disableOptionalPluginWarnings = _configService.Current.DisableOptionalPluginWarnings; @@ -2965,6 +3320,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.DisableOptionalPluginWarnings = disableOptionalPluginWarnings; _configService.Save(); } + _uiShared.DrawHelpText("Disable warning notifications for missing optional plugins."); _uiShared.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); @@ -2972,8 +3328,7 @@ public class SettingsUi : WindowMediatorSubscriberBase } ImGui.Separator(); - _uiShared.UnderlinedBigText("Location Descriptions", UIColors.Get("LightlessBlue")); - ImGuiHelpers.ScaledDummy(5); + // Location descriptions removed - information is now inline with each setting } } @@ -2981,9 +3336,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { return new[] { - NotificationLocation.LightlessUi, - NotificationLocation.ChatAndLightlessUi, - NotificationLocation.Nowhere + NotificationLocation.LightlessUi, NotificationLocation.ChatAndLightlessUi, NotificationLocation.Nowhere }; } @@ -2991,10 +3344,8 @@ public class SettingsUi : WindowMediatorSubscriberBase { return new[] { - NotificationLocation.LightlessUi, - NotificationLocation.ChatAndLightlessUi, - NotificationLocation.TextOverlay, - NotificationLocation.Nowhere + NotificationLocation.LightlessUi, NotificationLocation.ChatAndLightlessUi, + NotificationLocation.TextOverlay, NotificationLocation.Nowhere }; } @@ -3002,9 +3353,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { return new[] { - NotificationLocation.Toast, - NotificationLocation.Chat, - NotificationLocation.Both, + NotificationLocation.Toast, NotificationLocation.Chat, NotificationLocation.Both, NotificationLocation.Nowhere }; } @@ -3024,137 +3373,144 @@ public class SettingsUi : WindowMediatorSubscriberBase }; } - private void DrawSoundCustomization() + private void DrawSoundTable() { 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") + (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.BeginTable("##SoundTable", 3, + ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) + { + ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed, 120 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Sound", ImGuiTableColumnFlags.WidthStretch, 280 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, 80 * ImGuiHelpers.GlobalScale); + ImGui.TableHeadersRow(); - 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 + var soundTypes = new[] { - 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"); + ("Info", 0, _configService.Current.CustomInfoSoundId, _configService.Current.DisableInfoSound, 2u), + ("Warning", 1, _configService.Current.CustomWarningSoundId, _configService.Current.DisableWarningSound, 15u), + ("Error", 2, _configService.Current.CustomErrorSoundId, _configService.Current.DisableErrorSound, 16u), + ("Pair Request", 3, _configService.Current.PairRequestSoundId, _configService.Current.DisablePairRequestSound, 5u) + }; - // 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 + foreach (var (typeName, typeIndex, currentSoundId, isDisabled, defaultSoundId) in soundTypes) + { + ImGui.TableNextRow(); - 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"); + // Type column + ImGui.TableSetColumnIndex(0); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(typeName); - // 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 + // Sound picker column + ImGui.TableSetColumnIndex(1); + using (ImRaii.Disabled(isDisabled)) { - 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); + var currentIndex = Array.FindIndex(soundEffects, s => s.Item1 == currentSoundId); + if (currentIndex == -1) currentIndex = 1; + + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + if (ImGui.Combo($"##sound_{typeIndex}", ref currentIndex, + soundEffects.Select(s => s.Item2).ToArray(), soundEffects.Length)) + { + var newSoundId = soundEffects[currentIndex].Item1; + switch (typeIndex) + { + case 0: _configService.Current.CustomInfoSoundId = newSoundId; break; + case 1: _configService.Current.CustomWarningSoundId = newSoundId; break; + case 2: _configService.Current.CustomErrorSoundId = newSoundId; break; + case 3: _configService.Current.PairRequestSoundId = newSoundId; break; + } + + _configService.Save(); + } + + ImGui.SameLine(); + ImGui.PushID($"test_{typeIndex}"); + if (_uiShared.IconButton(FontAwesomeIcon.Play)) + { + try + { + FFXIVClientStructs.FFXIV.Client.UI.UIGlobals.PlayChatSoundEffect(currentSoundId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to play test sound"); + } + } + + ImGui.PopID(); + UiSharedService.AttachToolTip("Test this sound"); } - catch (Exception ex) + + // Actions column + ImGui.TableSetColumnIndex(2); + var availableWidth = ImGui.GetContentRegionAvail().X; + var buttonWidth = (availableWidth - ImGui.GetStyle().ItemSpacing.X) / 2; + + // Reset button + using var resetId = ImRaii.PushId($"Reset_{typeIndex}"); + bool isDefault = currentSoundId == defaultSoundId; + + using (ImRaii.Disabled(isDefault)) { - _logger.LogWarning(ex, "Failed to play test sounds"); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + if (ImGui.Button(FontAwesomeIcon.Undo.ToIconString(), new Vector2(buttonWidth, 0))) + { + switch (typeIndex) + { + case 0: _configService.Current.CustomInfoSoundId = defaultSoundId; break; + case 1: _configService.Current.CustomWarningSoundId = defaultSoundId; break; + case 2: _configService.Current.CustomErrorSoundId = defaultSoundId; break; + case 3: _configService.Current.PairRequestSoundId = defaultSoundId; break; + } + _configService.Save(); + } + } } - }); + UiSharedService.AttachToolTip(isDefault ? "Sound is already at default value" : "Reset to default sound"); + + // Disable toggle button + ImGui.SameLine(); + using var disableId = ImRaii.PushId($"Disable_{typeIndex}"); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + var icon = isDisabled ? FontAwesomeIcon.VolumeOff : FontAwesomeIcon.VolumeUp; + var color = isDisabled ? UIColors.Get("DimRed") : UIColors.Get("LightlessGreen"); + + ImGui.PushStyleColor(ImGuiCol.Button, color); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, color * new Vector4(1.2f, 1.2f, 1.2f, 1f)); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, color * new Vector4(0.8f, 0.8f, 0.8f, 1f)); + + if (ImGui.Button(icon.ToIconString(), new Vector2(buttonWidth, 0))) + { + bool newDisabled = !isDisabled; + switch (typeIndex) + { + case 0: _configService.Current.DisableInfoSound = newDisabled; break; + case 1: _configService.Current.DisableWarningSound = newDisabled; break; + case 2: _configService.Current.DisableErrorSound = newDisabled; break; + case 3: _configService.Current.DisablePairRequestSound = newDisabled; break; + } + _configService.Save(); + } + + ImGui.PopStyleColor(3); + } + UiSharedService.AttachToolTip(isDisabled ? "Sound is disabled - click to enable" : "Sound is enabled - click to disable"); + } + + ImGui.EndTable(); } - _uiShared.DrawHelpText("Play all custom sounds in sequence: Info → Warning → Error"); } } + + From f5339dc1d2cffceb62e7d0aa86ee92108b49338e Mon Sep 17 00:00:00 2001 From: choco Date: Thu, 9 Oct 2025 22:53:01 +0200 Subject: [PATCH 11/15] notif offset placement, default slider yoinked from abel --- .../Configurations/LightlessConfig.cs | 5 +-- LightlessSync/UI/LightlessNotificationUI.cs | 43 ++++++++++++------- LightlessSync/UI/SettingsUi.cs | 21 +++++++++ 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs index 9fa662f..694c325 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs @@ -74,7 +74,6 @@ public class LightlessConfig : ILightlessConfiguration // 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; @@ -89,13 +88,11 @@ public class LightlessConfig : ILightlessConfiguration public bool AutoDismissOnAction { get; set; } = true; public bool DismissNotificationOnClick { get; set; } = false; public bool ShowNotificationTimestamp { get; set; } = false; - public bool EnableNotificationHistory { get; set; } = true; - public int NotificationHistorySize { get; set; } = 50; + public int NotificationOffsetY { 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 uint PairRequestSoundId { get; set; } = 5; // Se5 public bool DisableInfoSound { get; set; } = false; public bool DisableWarningSound { get; set; } = false; diff --git a/LightlessSync/UI/LightlessNotificationUI.cs b/LightlessSync/UI/LightlessNotificationUI.cs index 7706142..3f714a0 100644 --- a/LightlessSync/UI/LightlessNotificationUI.cs +++ b/LightlessSync/UI/LightlessNotificationUI.cs @@ -43,15 +43,9 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoCollapse | - ImGuiWindowFlags.AlwaysAutoResize; + ImGuiWindowFlags.AlwaysAutoResize; - var viewport = ImGui.GetMainViewport(); - if (viewport.WorkSize.X > 0) - { - Position = new Vector2(viewport.WorkPos.X + viewport.WorkSize.X - NotificationWidth - EdgeXMargin, - viewport.WorkPos.Y + EdgeYMargin); - PositionCondition = ImGuiCond.Always; - } + PositionCondition = ImGuiCond.Always; Size = new Vector2(NotificationWidth, 100); SizeCondition = ImGuiCond.FirstUseEver; @@ -114,22 +108,30 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase protected override void DrawInternal() { + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); + lock (_notificationLock) { UpdateNotifications(); if (_notifications.Count == 0) { + ImGui.PopStyleVar(); IsOpen = false; return; } var viewport = ImGui.GetMainViewport(); - var windowPos = new Vector2( - viewport.WorkPos.X + viewport.WorkSize.X - NotificationWidth - EdgeXMargin, - viewport.WorkPos.Y + EdgeYMargin - ); - ImGui.SetWindowPos(windowPos); + + // Always position at top (choco doesnt know how to handle top positions how fitting) + var baseX = viewport.WorkPos.X + viewport.WorkSize.X - NotificationWidth; + var baseY = viewport.WorkPos.Y; + + // Apply Y offset + var finalY = baseY + _configService.Current.NotificationOffsetY; + + // Update position + Position = new Vector2(baseX, finalY); for (int i = 0; i < _notifications.Count; i++) { @@ -141,6 +143,8 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase } } } + + ImGui.PopStyleVar(); } private void UpdateNotifications() @@ -208,6 +212,8 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase var notificationHeight = CalculateNotificationHeight(notification); + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); + using var child = ImRaii.Child($"notification_{notification.Id}", new Vector2(NotificationWidth - slideOffset, notificationHeight), false, ImGuiWindowFlags.NoScrollbar); @@ -216,6 +222,8 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase { DrawNotificationContent(notification, alpha); } + + ImGui.PopStyleVar(); } private void DrawNotificationContent(LightlessNotification notification, float alpha) @@ -266,6 +274,7 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase 3f ); + // Draw accent bar on left side of the notif var accentWidth = 3f; drawList.AddRectFilled( windowPos, @@ -312,9 +321,10 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase private void DrawNotificationText(LightlessNotification notification, float alpha) { var padding = new Vector2(10f, 6f); - var contentPos = new Vector2(6f, 0f) + padding; + + var contentPos = new Vector2(padding.X, padding.Y); var windowSize = ImGui.GetWindowSize(); - var contentSize = windowSize - padding * 2 - new Vector2(6f, 0f); + var contentSize = new Vector2(windowSize.X - padding.X, windowSize.Y - padding.Y * 2); ImGui.SetCursorPos(contentPos); @@ -358,7 +368,8 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase progressColor.W *= alpha; using (ImRaii.PushColor(ImGuiCol.PlotHistogram, progressColor)) { - ImGui.ProgressBar(notification.Progress, new Vector2(contentSize.X, 2f), ""); + // Use full window width for progress bar + ImGui.ProgressBar(notification.Progress, new Vector2(windowSize.X - padding.X * 2 - 6f, 2f), ""); } } diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 1964431..6184a92 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -3232,6 +3232,27 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.DrawHelpText("Maximum number of notifications that can be shown at once."); + ImGui.Spacing(); + ImGui.TextUnformatted("Position"); + + int offsetY = _configService.Current.NotificationOffsetY; + if (ImGui.SliderInt("Vertical Offset", ref offsetY, 0, 500)) + { + _configService.Current.NotificationOffsetY = offsetY; + _configService.Save(); + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _configService.Current.NotificationOffsetY = 50; + _configService.Save(); + } + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Right click to reset to default (50)."); + + _uiShared.DrawHelpText("Move notifications down from the top-right corner. 0 aligns to the very top."); + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } From e13fde3d431297ff1cb2e307cda2862d58b85736 Mon Sep 17 00:00:00 2001 From: choco Date: Sat, 11 Oct 2025 00:46:18 +0200 Subject: [PATCH 12/15] improved settings with sounds bug fix --- .../Configurations/LightlessConfig.cs | 34 ++- LightlessSync/Services/NotificationService.cs | 25 +- LightlessSync/UI/LightlessNotificationUI.cs | 100 +++---- .../UI/Models/LightlessNotification.cs | 2 - LightlessSync/UI/Models/NotificationSounds.cs | 24 +- LightlessSync/UI/SettingsUi.cs | 275 +++++++++++++----- .../ApiController.Functions.Callbacks.cs | 37 +-- 7 files changed, 325 insertions(+), 172 deletions(-) diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs index 694c325..f2b9f12 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs @@ -72,9 +72,7 @@ public class LightlessConfig : ILightlessConfiguration public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both; // Lightless Notification Configuration - // TODO: clean these public bool UseLightlessNotifications { 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; @@ -82,23 +80,45 @@ public class LightlessConfig : ILightlessConfiguration public NotificationLocation LightlessPairRequestNotification { get; set; } = NotificationLocation.LightlessUi; public NotificationLocation LightlessDownloadNotification { get; set; } = NotificationLocation.TextOverlay; + // Basic Settings 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 DismissNotificationOnClick { get; set; } = false; public bool ShowNotificationTimestamp { get; set; } = false; - public int NotificationOffsetY { get; set; } = 50; + // Position & Layout + public int NotificationOffsetY { get; set; } = 50; + public int NotificationOffsetX { get; set; } = 0; + public float NotificationWidth { get; set; } = 350f; + public float NotificationSpacing { get; set; } = 8f; + public bool NotificationStackUpwards { get; set; } = false; + + // Animation & Effects + public float NotificationAnimationSpeed { get; set; } = 10f; + public float NotificationAccentBarWidth { get; set; } = 3f; + + // Typography + public float NotificationFontScale { get; set; } = 1.0f; + + // Duration per Type + public int InfoNotificationDurationSeconds { get; set; } = 10; + public int WarningNotificationDurationSeconds { get; set; } = 15; + public int ErrorNotificationDurationSeconds { get; set; } = 20; + public int PairRequestDurationSeconds { get; set; } = 180; + public int DownloadNotificationDurationSeconds { get; set; } = 300; + + // Sound Settings public uint CustomInfoSoundId { get; set; } = 2; // Se2 - public uint CustomWarningSoundId { get; set; } = 15; // Se15 - public uint CustomErrorSoundId { get; set; } = 16; // Se16 + public uint CustomWarningSoundId { get; set; } = 16; // Se15 + public uint CustomErrorSoundId { get; set; } = 16; // Se15 public uint PairRequestSoundId { get; set; } = 5; // Se5 + public uint DownloadSoundId { get; set; } = 15; // Se14 public bool DisableInfoSound { get; set; } = false; public bool DisableWarningSound { get; set; } = false; public bool DisableErrorSound { get; set; } = false; public bool DisablePairRequestSound { get; set; } = false; - // till here c: + public bool DisableDownloadSound { get; set; } = true; // Disabled by default public bool UseFocusTarget { get; set; } = false; public bool overrideFriendColor { get; set; } = false; public bool overridePartyColor { get; set; } = false; diff --git a/LightlessSync/Services/NotificationService.cs b/LightlessSync/Services/NotificationService.cs index 91f2cbc..1279089 100644 --- a/LightlessSync/Services/NotificationService.cs +++ b/LightlessSync/Services/NotificationService.cs @@ -51,7 +51,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ Title = title, Message = message, Type = type, - Duration = duration ?? TimeSpan.FromSeconds(_configService.Current.DefaultNotificationDurationSeconds), + Duration = duration ?? GetDefaultDurationForType(type), Actions = actions ?? new List(), SoundEffectId = GetSoundEffectId(type, soundEffectId), ShowProgress = _configService.Current.ShowNotificationProgress, @@ -87,9 +87,9 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ var notification = new LightlessNotification { Title = "Pair Request Received", - Message = $"{senderName} wants to pair with you.", + Message = $"{senderName} wants to directly pair with you.", Type = NotificationType.PairRequest, - Duration = TimeSpan.FromSeconds(180), + Duration = TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds), SoundEffectId = !_configService.Current.DisablePairRequestSound ? _configService.Current.PairRequestSoundId : null, Actions = new List { @@ -268,7 +268,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ Title = "Downloading Pair Data", Message = message, Type = NotificationType.Download, - Duration = TimeSpan.FromMinutes(5), + Duration = TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds), ShowProgress = true, Progress = totalProgress }; @@ -284,6 +284,19 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress")); } + private TimeSpan GetDefaultDurationForType(NotificationType type) + { + return type switch + { + NotificationType.Info => TimeSpan.FromSeconds(_configService.Current.InfoNotificationDurationSeconds), + NotificationType.Warning => TimeSpan.FromSeconds(_configService.Current.WarningNotificationDurationSeconds), + NotificationType.Error => TimeSpan.FromSeconds(_configService.Current.ErrorNotificationDurationSeconds), + NotificationType.PairRequest => TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds), + NotificationType.Download => TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds), + _ => TimeSpan.FromSeconds(10) // Fallback for any unknown types + }; + } + private uint? GetSoundEffectId(NotificationType type, uint? overrideSoundId) { if (overrideSoundId.HasValue) @@ -295,6 +308,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ NotificationType.Info => _configService.Current.DisableInfoSound, NotificationType.Warning => _configService.Current.DisableWarningSound, NotificationType.Error => _configService.Current.DisableErrorSound, + NotificationType.Download => _configService.Current.DisableDownloadSound, _ => false }; @@ -307,6 +321,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ NotificationType.Info => _configService.Current.CustomInfoSoundId, NotificationType.Warning => _configService.Current.CustomWarningSoundId, NotificationType.Error => _configService.Current.CustomErrorSoundId, + NotificationType.Download => _configService.Current.DownloadSoundId, _ => NotificationSounds.GetDefaultSound(type) }; } @@ -387,7 +402,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ private void ShowLightlessNotification(NotificationMessage msg) { - var duration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(_configService.Current.DefaultNotificationDurationSeconds); + var duration = msg.TimeShownOnScreen ?? GetDefaultDurationForType(msg.Type); // GetSoundEffectId will handle checking if the sound is disabled ShowNotification(msg.Title ?? "Lightless Sync", msg.Message ?? string.Empty, msg.Type, duration, null, null); } diff --git a/LightlessSync/UI/LightlessNotificationUI.cs b/LightlessSync/UI/LightlessNotificationUI.cs index 3f714a0..34fe884 100644 --- a/LightlessSync/UI/LightlessNotificationUI.cs +++ b/LightlessSync/UI/LightlessNotificationUI.cs @@ -21,15 +21,8 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase 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; + private const float NotificationMaxHeight = 250f; public LightlessNotificationUI(ILogger logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService) : base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector) @@ -47,7 +40,7 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase PositionCondition = ImGuiCond.Always; - Size = new Vector2(NotificationWidth, 100); + Size = new Vector2(_configService.Current.NotificationWidth, 100); SizeCondition = ImGuiCond.FirstUseEver; IsOpen = false; RespectCloseHotkey = false; @@ -124,7 +117,7 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase var viewport = ImGui.GetMainViewport(); // Always position at top (choco doesnt know how to handle top positions how fitting) - var baseX = viewport.WorkPos.X + viewport.WorkSize.X - NotificationWidth; + var baseX = viewport.WorkPos.X + viewport.WorkSize.X - _configService.Current.NotificationWidth - _configService.Current.NotificationOffsetX; var baseY = viewport.WorkPos.Y; // Apply Y offset @@ -139,7 +132,7 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase if (i < _notifications.Count - 1) { - ImGui.Dummy(new Vector2(0, NotificationSpacing)); + ImGui.Dummy(new Vector2(0, _configService.Current.NotificationSpacing)); } } } @@ -170,11 +163,11 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase if (notification.IsAnimatingIn && notification.AnimationProgress < 1f) { - notification.AnimationProgress = Math.Min(1f, notification.AnimationProgress + deltaTime * AnimationSpeed); + notification.AnimationProgress = Math.Min(1f, notification.AnimationProgress + deltaTime * _configService.Current.NotificationAnimationSpeed); } else if (notification.IsAnimatingOut && notification.AnimationProgress > 0f) { - notification.AnimationProgress = Math.Max(0f, notification.AnimationProgress - deltaTime * (AnimationSpeed * 0.7f)); + notification.AnimationProgress = Math.Max(0f, notification.AnimationProgress - deltaTime * (_configService.Current.NotificationAnimationSpeed * 0.7f)); } else if (!notification.IsAnimatingOut && !notification.IsDismissed) { @@ -198,14 +191,10 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase { var alpha = notification.AnimationProgress; - if (_configService.Current.EnableNotificationAnimations && alpha <= 0f) + if (alpha <= 0f) return; - var slideOffset = 0f; - if (_configService.Current.EnableNotificationAnimations) - { - slideOffset = (1f - alpha) * SlideDistance; - } + var slideOffset = (1f - alpha) * 100f; // Fixed slide distance var originalCursorPos = ImGui.GetCursorPos(); ImGui.SetCursorPosX(originalCursorPos.X + slideOffset); @@ -215,7 +204,7 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); using var child = ImRaii.Child($"notification_{notification.Id}", - new Vector2(NotificationWidth - slideOffset, notificationHeight), + new Vector2(_configService.Current.NotificationWidth - slideOffset, notificationHeight), false, ImGuiWindowFlags.NoScrollbar); if (child.Success) @@ -233,16 +222,16 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase var windowSize = ImGui.GetWindowSize(); var baseOpacity = _configService.Current.NotificationOpacity; - var finalOpacity = _configService.Current.EnableNotificationAnimations ? baseOpacity * alpha : baseOpacity; + var finalOpacity = baseOpacity * alpha; var bgColor = new Vector4(30f/255f, 30f/255f, 30f/255f, finalOpacity); var accentColor = GetNotificationAccentColor(notification.Type); var progressBarColor = UIColors.Get("LightlessBlue"); - var finalAccentAlpha = _configService.Current.EnableNotificationAnimations ? alpha : 1f; - accentColor.W *= finalAccentAlpha; + accentColor.W *= alpha; + // Draw shadow with fixed intensity var shadowOffset = new Vector2(1f, 1f); - var shadowAlpha = _configService.Current.EnableNotificationAnimations ? 0.4f * alpha : 0.4f; + var shadowAlpha = 0.4f * alpha; var shadowColor = new Vector4(0f, 0f, 0f, shadowAlpha); drawList.AddRectFilled( windowPos + shadowOffset, @@ -252,6 +241,7 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase ); var isHovered = ImGui.IsWindowHovered(); + if (isHovered) { bgColor = bgColor * 1.1f; @@ -274,14 +264,17 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase 3f ); - // Draw accent bar on left side of the notif - var accentWidth = 3f; - drawList.AddRectFilled( - windowPos, - windowPos + new Vector2(accentWidth, windowSize.Y), - ImGui.ColorConvertFloat4ToU32(accentColor), - 3f - ); + // Draw accent bar on left side of the notif (only if width > 0) + var accentWidth = _configService.Current.NotificationAccentBarWidth; + if (accentWidth > 0f) + { + drawList.AddRectFilled( + windowPos, + windowPos + new Vector2(accentWidth, windowSize.Y), + ImGui.ColorConvertFloat4ToU32(accentColor), + 3f + ); + } DrawDurationProgressBar(notification, alpha, windowPos, windowSize, progressBarColor, drawList); @@ -290,8 +283,17 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase private void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, Vector4 progressBarColor, ImDrawListPtr drawList) { - var elapsed = DateTime.UtcNow - notification.CreatedAt; - var progress = Math.Min(1.0f, (float)(elapsed.TotalSeconds / notification.Duration.TotalSeconds)); + // For download notifications, use download progress instead of duration + float progress; + if (notification.Type == NotificationType.Download && notification.ShowProgress) + { + progress = Math.Clamp(notification.Progress, 0f, 1f); + } + else + { + var elapsed = DateTime.UtcNow - notification.CreatedAt; + progress = Math.Min(1.0f, (float)(elapsed.TotalSeconds / notification.Duration.TotalSeconds)); + } var progressHeight = 2f; var progressY = windowPos.Y + windowSize.Y - progressHeight; @@ -361,21 +363,10 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase ImGui.PopTextWrapPos(); } - if (notification.ShowProgress) - { - ImGui.Spacing(); - var progressColor = GetNotificationAccentColor(notification.Type); - progressColor.W *= alpha; - using (ImRaii.PushColor(ImGuiCol.PlotHistogram, progressColor)) - { - // Use full window width for progress bar - ImGui.ProgressBar(notification.Progress, new Vector2(windowSize.X - padding.X * 2 - 6f, 2f), ""); - } - } - if (notification.Actions.Count > 0) { - ImGui.Spacing(); + var spacingHeight = ImGui.GetStyle().ItemSpacing.Y; + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + spacingHeight); ImGui.SetCursorPosX(contentPos.X); DrawNotificationActions(notification, contentSize.X, alpha); } @@ -504,8 +495,8 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase private float CalculateNotificationHeight(LightlessNotification notification) { - var contentWidth = NotificationWidth - 35f; // Account for padding and accent bar - var height = 20f; // Base height for padding + var contentWidth = _configService.Current.NotificationWidth - 35f; // Account for padding and accent bar + var height = 12f; // Base height for padding (top + bottom) var titleText = notification.Title; if (_configService.Current.ShowNotificationTimestamp) @@ -515,13 +506,14 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase } var titleSize = ImGui.CalcTextSize(titleText, true, contentWidth); - height += titleSize.Y + 4f; // Title height + spacing + height += titleSize.Y; // Title height // Calculate message height if (!string.IsNullOrEmpty(notification.Message)) { - var messageSize = ImGui.CalcTextSize(notification.Message, true, contentWidth); - height += messageSize.Y + 4f; // Message height + spacing + height += 4f; // Spacing between title and message + var messageSize = ImGui.CalcTextSize(notification.Message, true, contentWidth); // This is cringe + height += messageSize.Y; // Message height } // Add height for progress bar @@ -533,7 +525,9 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase // Add height for action buttons if (notification.Actions.Count > 0) { - height += 28f; + height += ImGui.GetStyle().ItemSpacing.Y; // Spacing before buttons + height += ImGui.GetFrameHeight(); // Button height + height += 12f; // Bottom padding for buttons } // Allow notifications to grow taller but cap at maximum height diff --git a/LightlessSync/UI/Models/LightlessNotification.cs b/LightlessSync/UI/Models/LightlessNotification.cs index 7013a97..4516dde 100644 --- a/LightlessSync/UI/Models/LightlessNotification.cs +++ b/LightlessSync/UI/Models/LightlessNotification.cs @@ -16,8 +16,6 @@ public class LightlessNotification public bool ShowProgress { get; set; } = false; public float Progress { get; set; } = 0f; public bool IsMinimized { get; set; } = false; - - // Animation properties public float AnimationProgress { get; set; } = 0f; public bool IsAnimatingIn { get; set; } = true; public bool IsAnimatingOut { get; set; } = false; diff --git a/LightlessSync/UI/Models/NotificationSounds.cs b/LightlessSync/UI/Models/NotificationSounds.cs index 8b60532..fc74256 100644 --- a/LightlessSync/UI/Models/NotificationSounds.cs +++ b/LightlessSync/UI/Models/NotificationSounds.cs @@ -1,4 +1,4 @@ -using LightlessSync.LightlessConfiguration.Models; +using LightlessSync.LightlessConfiguration.Models; namespace LightlessSync.UI.Models; @@ -16,18 +16,18 @@ public static class NotificationSounds public const uint Se2 = 2; // Higher chime public const uint Se3 = 3; // Bell tone public const uint Se4 = 4; // Harp tone - public const uint Se5 = 5; // Drum / percussion - public const uint Se6 = 6; // Mechanical click + public const uint Se5 = 5; // Mechanical click + public const uint Se6 = 6; // Drum / percussion public const uint Se7 = 7; // Metallic chime public const uint Se8 = 8; // Wooden tone public const uint Se9 = 9; // Wind / flute tone - public const uint Se10 = 10; // Magical sparkle - public const uint Se11 = 11; // Metallic ring - public const uint Se12 = 12; // Deep thud - public const uint Se13 = 13; // "Tell received" ping - public const uint Se14 = 14; // Success fanfare short - public const uint Se15 = 15; // System warning - public const uint Se16 = 16; // Error / failure────────────────────────────────────────── + public const uint Se10 = 11; // Magical sparkle (ID 10 is skipped in game) + public const uint Se11 = 12; // Metallic ring + public const uint Se12 = 13; // Deep thud + public const uint Se13 = 14; // "Tell received" ping + public const uint Se14 = 15; // Success fanfare + public const uint Se15 = 16; // System warning + // Note: Se16 doesn't exist - Se15 is the last available sound /// /// General notification sound () @@ -40,9 +40,9 @@ public static class NotificationSounds public const uint Warning = Se15; /// - /// Error sound () + /// Error sound ( - System warning, used for errors) /// - public const uint Error = Se16; + public const uint Error = Se15; /// /// Success sound () diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 6184a92..38c4186 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -3140,24 +3140,20 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Separator(); if (_uiShared.MediumTreeNode("Basic Settings", UIColors.Get("LightlessPurple"))) { - - int defaultDuration = _configService.Current.DefaultNotificationDurationSeconds; - if (ImGui.SliderInt("Default Duration (seconds)", ref defaultDuration, 3, 60)) + int maxNotifications = _configService.Current.MaxSimultaneousNotifications; + if (ImGui.SliderInt("Max Simultaneous Notifications", ref maxNotifications, 1, 10)) { - _configService.Current.DefaultNotificationDurationSeconds = defaultDuration; + _configService.Current.MaxSimultaneousNotifications = maxNotifications; _configService.Save(); } - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { - _configService.Current.DefaultNotificationDurationSeconds = 10; + _configService.Current.MaxSimultaneousNotifications = 5; _configService.Save(); } - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Right click to reset to default (10 seconds)."); - - _uiShared.DrawHelpText("How long notifications stay visible by default."); + ImGui.SetTooltip("Right click to reset to default (5)."); + _uiShared.DrawHelpText("Maximum number of notifications that can be shown at once."); bool showTimestamp = _configService.Current.ShowNotificationTimestamp; if (ImGui.Checkbox("Show Timestamps", ref showTimestamp)) @@ -3205,32 +3201,38 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.DrawHelpText("Transparency level of notification windows."); - bool enableAnimations = _configService.Current.EnableNotificationAnimations; - if (ImGui.Checkbox("Enable Animations", ref enableAnimations)) + ImGui.Spacing(); + ImGui.TextUnformatted("Size & Layout"); + + float notifWidth = _configService.Current.NotificationWidth; + if (ImGui.SliderFloat("Notification Width", ref notifWidth, 250f, 600f, "%.0f")) { - _configService.Current.EnableNotificationAnimations = enableAnimations; + _configService.Current.NotificationWidth = notifWidth; _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(); - } - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { - _configService.Current.MaxSimultaneousNotifications = 5; + _configService.Current.NotificationWidth = 350f; _configService.Save(); } - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Right click to reset to default (5)."); + ImGui.SetTooltip("Right click to reset to default (350)."); + _uiShared.DrawHelpText("Width of notification windows."); - _uiShared.DrawHelpText("Maximum number of notifications that can be shown at once."); + float notifSpacing = _configService.Current.NotificationSpacing; + if (ImGui.SliderFloat("Notification Spacing", ref notifSpacing, 0f, 30f, "%.0f")) + { + _configService.Current.NotificationSpacing = notifSpacing; + _configService.Save(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _configService.Current.NotificationSpacing = 8f; + _configService.Save(); + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Right click to reset to default (8)."); + _uiShared.DrawHelpText("Gap between stacked notifications."); ImGui.Spacing(); ImGui.TextUnformatted("Position"); @@ -3241,23 +3243,151 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.NotificationOffsetY = offsetY; _configService.Save(); } - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { _configService.Current.NotificationOffsetY = 50; _configService.Save(); } - if (ImGui.IsItemHovered()) ImGui.SetTooltip("Right click to reset to default (50)."); + _uiShared.DrawHelpText("Move notifications down from the top-right corner."); - _uiShared.DrawHelpText("Move notifications down from the top-right corner. 0 aligns to the very top."); + int offsetX = _configService.Current.NotificationOffsetX; + if (ImGui.SliderInt("Horizontal Offset", ref offsetX, 0, 500)) + { + _configService.Current.NotificationOffsetX = offsetX; + _configService.Save(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _configService.Current.NotificationOffsetX = 0; + _configService.Save(); + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Right click to reset to default (0)."); + _uiShared.DrawHelpText("Move notifications left from the right edge."); + + ImGui.Spacing(); + ImGui.TextUnformatted("Animation Settings"); + + float animSpeed = _configService.Current.NotificationAnimationSpeed; + if (ImGui.SliderFloat("Animation Speed", ref animSpeed, 1f, 30f, "%.1f")) + { + _configService.Current.NotificationAnimationSpeed = animSpeed; + _configService.Save(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _configService.Current.NotificationAnimationSpeed = 10f; + _configService.Save(); + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Right click to reset to default (10)."); + _uiShared.DrawHelpText("How fast notifications slide in/out. Higher = faster."); + + ImGui.Spacing(); + ImGui.TextUnformatted("Visual Effects"); + + float accentWidth = _configService.Current.NotificationAccentBarWidth; + if (ImGui.SliderFloat("Accent Bar Width", ref accentWidth, 0f, 10f, "%.1f")) + { + _configService.Current.NotificationAccentBarWidth = accentWidth; + _configService.Save(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _configService.Current.NotificationAccentBarWidth = 3f; + _configService.Save(); + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Right click to reset to default (3)."); + _uiShared.DrawHelpText("Width of the colored accent bar on the left side."); _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } } + ImGui.Separator(); + if (_uiShared.MediumTreeNode("Duration Settings", UIColors.Get("LightlessPurple"))) + { + UiSharedService.ColorTextWrapped("Configure how long each notification type stays visible.", ImGuiColors.DalamudGrey); + ImGuiHelpers.ScaledDummy(5); + + int infoDuration = _configService.Current.InfoNotificationDurationSeconds; + if (ImGui.SliderInt("Info Duration (seconds)", ref infoDuration, 3, 60)) + { + _configService.Current.InfoNotificationDurationSeconds = infoDuration; + _configService.Save(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _configService.Current.InfoNotificationDurationSeconds = 10; + _configService.Save(); + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Right click to reset to default (10)."); + + int warningDuration = _configService.Current.WarningNotificationDurationSeconds; + if (ImGui.SliderInt("Warning Duration (seconds)", ref warningDuration, 3, 60)) + { + _configService.Current.WarningNotificationDurationSeconds = warningDuration; + _configService.Save(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _configService.Current.WarningNotificationDurationSeconds = 15; + _configService.Save(); + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Right click to reset to default (15)."); + + int errorDuration = _configService.Current.ErrorNotificationDurationSeconds; + if (ImGui.SliderInt("Error Duration (seconds)", ref errorDuration, 3, 120)) + { + _configService.Current.ErrorNotificationDurationSeconds = errorDuration; + _configService.Save(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _configService.Current.ErrorNotificationDurationSeconds = 20; + _configService.Save(); + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Right click to reset to default (20)."); + + int pairRequestDuration = _configService.Current.PairRequestDurationSeconds; + if (ImGui.SliderInt("Pair Request Duration (seconds)", ref pairRequestDuration, 30, 600)) + { + _configService.Current.PairRequestDurationSeconds = pairRequestDuration; + _configService.Save(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _configService.Current.PairRequestDurationSeconds = 180; + _configService.Save(); + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Right click to reset to default (180)."); + + int downloadDuration = _configService.Current.DownloadNotificationDurationSeconds; + if (ImGui.SliderInt("Download Duration (seconds)", ref downloadDuration, 60, 600)) + { + _configService.Current.DownloadNotificationDurationSeconds = downloadDuration; + _configService.Save(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _configService.Current.DownloadNotificationDurationSeconds = 300; + _configService.Save(); + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Right click to reset to default (300)."); + + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + } + ImGui.Separator(); if (_uiShared.MediumTreeNode("Sound Settings", UIColors.Get("LightlessPurple"))) { @@ -3399,10 +3529,10 @@ public class SettingsUi : WindowMediatorSubscriberBase 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") + (5u, "Se5 - Mechanical click"), (6u, "Se6 - Drum/percussion"), (7u, "Se7 - Metallic chime"), + (8u, "Se8 - Wooden tone"), (9u, "Se9 - Wind/flute tone"), (11u, "Se10 - Magical sparkle"), + (12u, "Se11 - Metallic ring"), (13u, "Se12 - Deep thud"), (14u, "Se13 - Tell received ping"), + (15u, "Se14 - Success fanfare"), (16u, "Se15 - System warning") }; if (ImGui.BeginTable("##SoundTable", 3, @@ -3410,15 +3540,16 @@ public class SettingsUi : WindowMediatorSubscriberBase { ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed, 120 * ImGuiHelpers.GlobalScale); ImGui.TableSetupColumn("Sound", ImGuiTableColumnFlags.WidthStretch, 280 * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, 80 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, 120 * ImGuiHelpers.GlobalScale); ImGui.TableHeadersRow(); var soundTypes = new[] { ("Info", 0, _configService.Current.CustomInfoSoundId, _configService.Current.DisableInfoSound, 2u), - ("Warning", 1, _configService.Current.CustomWarningSoundId, _configService.Current.DisableWarningSound, 15u), + ("Warning", 1, _configService.Current.CustomWarningSoundId, _configService.Current.DisableWarningSound, 16u), ("Error", 2, _configService.Current.CustomErrorSoundId, _configService.Current.DisableErrorSound, 16u), - ("Pair Request", 3, _configService.Current.PairRequestSoundId, _configService.Current.DisablePairRequestSound, 5u) + ("Pair Request", 3, _configService.Current.PairRequestSoundId, _configService.Current.DisablePairRequestSound, 5u), + ("Download", 4, _configService.Current.DownloadSoundId, _configService.Current.DisableDownloadSound, 15u) }; foreach (var (typeName, typeIndex, currentSoundId, isDisabled, defaultSoundId) in soundTypes) @@ -3448,56 +3579,38 @@ public class SettingsUi : WindowMediatorSubscriberBase case 1: _configService.Current.CustomWarningSoundId = newSoundId; break; case 2: _configService.Current.CustomErrorSoundId = newSoundId; break; case 3: _configService.Current.PairRequestSoundId = newSoundId; break; + case 4: _configService.Current.DownloadSoundId = newSoundId; break; } _configService.Save(); } - - ImGui.SameLine(); - ImGui.PushID($"test_{typeIndex}"); - if (_uiShared.IconButton(FontAwesomeIcon.Play)) - { - try - { - FFXIVClientStructs.FFXIV.Client.UI.UIGlobals.PlayChatSoundEffect(currentSoundId); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to play test sound"); - } - } - - ImGui.PopID(); - UiSharedService.AttachToolTip("Test this sound"); } // Actions column ImGui.TableSetColumnIndex(2); var availableWidth = ImGui.GetContentRegionAvail().X; - var buttonWidth = (availableWidth - ImGui.GetStyle().ItemSpacing.X) / 2; + var buttonWidth = (availableWidth - ImGui.GetStyle().ItemSpacing.X * 2) / 3; - // Reset button - using var resetId = ImRaii.PushId($"Reset_{typeIndex}"); - bool isDefault = currentSoundId == defaultSoundId; - - using (ImRaii.Disabled(isDefault)) + // Play button + using var playId = ImRaii.PushId($"Play_{typeIndex}"); + using (ImRaii.Disabled(isDisabled)) { using (ImRaii.PushFont(UiBuilder.IconFont)) { - if (ImGui.Button(FontAwesomeIcon.Undo.ToIconString(), new Vector2(buttonWidth, 0))) + if (ImGui.Button(FontAwesomeIcon.Play.ToIconString(), new Vector2(buttonWidth, 0))) { - switch (typeIndex) + try { - case 0: _configService.Current.CustomInfoSoundId = defaultSoundId; break; - case 1: _configService.Current.CustomWarningSoundId = defaultSoundId; break; - case 2: _configService.Current.CustomErrorSoundId = defaultSoundId; break; - case 3: _configService.Current.PairRequestSoundId = defaultSoundId; break; + FFXIVClientStructs.FFXIV.Client.UI.UIGlobals.PlayChatSoundEffect(currentSoundId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to play test sound"); } - _configService.Save(); } } } - UiSharedService.AttachToolTip(isDefault ? "Sound is already at default value" : "Reset to default sound"); + UiSharedService.AttachToolTip("Test this sound"); // Disable toggle button ImGui.SameLine(); @@ -3520,6 +3633,7 @@ public class SettingsUi : WindowMediatorSubscriberBase case 1: _configService.Current.DisableWarningSound = newDisabled; break; case 2: _configService.Current.DisableErrorSound = newDisabled; break; case 3: _configService.Current.DisablePairRequestSound = newDisabled; break; + case 4: _configService.Current.DisableDownloadSound = newDisabled; break; } _configService.Save(); } @@ -3527,6 +3641,31 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.PopStyleColor(3); } UiSharedService.AttachToolTip(isDisabled ? "Sound is disabled - click to enable" : "Sound is enabled - click to disable"); + + // Reset button + ImGui.SameLine(); + using var resetId = ImRaii.PushId($"Reset_{typeIndex}"); + bool isDefault = currentSoundId == defaultSoundId; + + using (ImRaii.Disabled(isDefault)) + { + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + if (ImGui.Button(FontAwesomeIcon.Undo.ToIconString(), new Vector2(buttonWidth, 0))) + { + switch (typeIndex) + { + case 0: _configService.Current.CustomInfoSoundId = defaultSoundId; break; + case 1: _configService.Current.CustomWarningSoundId = defaultSoundId; break; + case 2: _configService.Current.CustomErrorSoundId = defaultSoundId; break; + case 3: _configService.Current.PairRequestSoundId = defaultSoundId; break; + case 4: _configService.Current.DownloadSoundId = defaultSoundId; break; + } + _configService.Save(); + } + } + } + UiSharedService.AttachToolTip(isDefault ? "Sound is already at default value" : "Reset to default sound"); } ImGui.EndTable(); diff --git a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs index 3227940..38df25a 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs @@ -122,38 +122,25 @@ public partial class ApiController // Fire and forget async operation _ = Task.Run(async () => { + var myCidHash = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); + try { - var myCidHash = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); - try - { - await TryPairWithContentId(request.HashedCid, myCidHash).ConfigureAwait(false); - _pairRequestService.RemoveRequest(request.HashedCid); - - 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))); - } + await TryPairWithContentId(request.HashedCid, myCidHash).ConfigureAwait(false); + _pairRequestService.RemoveRequest(request.HashedCid); + } + catch (Exception ex) + { + Mediator.Publish(new NotificationMessage( + "Failed to Accept Pair Request", + ex.Message, + NotificationType.Error, + TimeSpan.FromSeconds(5))); } }); }, onDecline: () => { _pairRequestService.RemoveRequest(request.HashedCid); - Mediator.Publish(new NotificationMessage( - "Pair Request Declined", - $"Declined {senderName}'s pair request.", - NotificationType.Info, - TimeSpan.FromSeconds(3))); }); return Task.CompletedTask; From c545ccea52d6bc2598641cac2901f9a9ecacfb5f Mon Sep 17 00:00:00 2001 From: choco Date: Sat, 11 Oct 2025 20:19:24 +0200 Subject: [PATCH 13/15] X offset --- LightlessSync/UI/LightlessNotificationUI.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LightlessSync/UI/LightlessNotificationUI.cs b/LightlessSync/UI/LightlessNotificationUI.cs index 34fe884..fd49055 100644 --- a/LightlessSync/UI/LightlessNotificationUI.cs +++ b/LightlessSync/UI/LightlessNotificationUI.cs @@ -28,7 +28,7 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase : base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector) { _configService = configService; - Flags = ImGuiWindowFlags.NoDecoration | + Flags = ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoSavedSettings | @@ -117,7 +117,7 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase var viewport = ImGui.GetMainViewport(); // Always position at top (choco doesnt know how to handle top positions how fitting) - var baseX = viewport.WorkPos.X + viewport.WorkSize.X - _configService.Current.NotificationWidth - _configService.Current.NotificationOffsetX; + var baseX = viewport.WorkPos.X + viewport.WorkSize.X - _configService.Current.NotificationWidth - _configService.Current.NotificationOffsetX - 6f ; var baseY = viewport.WorkPos.Y; // Apply Y offset From a441bbfcc8aa1537b316589740a4c4b4b9a97f5f Mon Sep 17 00:00:00 2001 From: choco Date: Sat, 11 Oct 2025 21:24:39 +0200 Subject: [PATCH 14/15] notifcation refactor for better readability --- .../Configurations/LightlessConfig.cs | 8 +- LightlessSync/Services/NotificationService.cs | 437 ++++++++++-------- LightlessSync/UI/LightlessNotificationUI.cs | 415 +++++++++-------- .../UI/Models/LightlessNotification.cs | 3 - 4 files changed, 474 insertions(+), 389 deletions(-) diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs index f2b9f12..6cd92bf 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs @@ -92,23 +92,17 @@ public class LightlessConfig : ILightlessConfiguration public int NotificationOffsetX { get; set; } = 0; public float NotificationWidth { get; set; } = 350f; public float NotificationSpacing { get; set; } = 8f; - public bool NotificationStackUpwards { get; set; } = false; // Animation & Effects public float NotificationAnimationSpeed { get; set; } = 10f; public float NotificationAccentBarWidth { get; set; } = 3f; - // Typography - public float NotificationFontScale { get; set; } = 1.0f; - // Duration per Type public int InfoNotificationDurationSeconds { get; set; } = 10; public int WarningNotificationDurationSeconds { get; set; } = 15; public int ErrorNotificationDurationSeconds { get; set; } = 20; public int PairRequestDurationSeconds { get; set; } = 180; public int DownloadNotificationDurationSeconds { get; set; } = 300; - - // Sound Settings public uint CustomInfoSoundId { get; set; } = 2; // Se2 public uint CustomWarningSoundId { get; set; } = 16; // Se15 public uint CustomErrorSoundId { get; set; } = 16; // Se15 @@ -118,7 +112,7 @@ public class LightlessConfig : ILightlessConfiguration public bool DisableWarningSound { get; set; } = false; public bool DisableErrorSound { get; set; } = false; public bool DisablePairRequestSound { get; set; } = false; - public bool DisableDownloadSound { get; set; } = true; // Disabled by default + public bool DisableDownloadSound { get; set; } = true; // Disabled by default (annoying) public bool UseFocusTarget { get; set; } = false; public bool overrideFriendColor { get; set; } = false; public bool overridePartyColor { get; set; } = false; diff --git a/LightlessSync/Services/NotificationService.cs b/LightlessSync/Services/NotificationService.cs index 1279089..621b816 100644 --- a/LightlessSync/Services/NotificationService.cs +++ b/LightlessSync/Services/NotificationService.cs @@ -39,14 +39,29 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ Mediator.Subscribe(this, HandleNotificationMessage); return Task.CompletedTask; } - public Task StopAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; public void ShowNotification(string title, string message, NotificationType type = NotificationType.Info, TimeSpan? duration = null, List? actions = null, uint? soundEffectId = null) { - var notification = new LightlessNotification + var notification = CreateNotification(title, message, type, duration, actions, soundEffectId); + + if (_configService.Current.AutoDismissOnAction && notification.Actions.Any()) + { + WrapActionsWithAutoDismiss(notification); + } + + if (notification.SoundEffectId.HasValue) + { + PlayNotificationSound(notification.SoundEffectId.Value); + } + + Mediator.Publish(new LightlessNotificationMessage(notification)); + } + + private LightlessNotification CreateNotification(string title, string message, NotificationType type, + TimeSpan? duration, List? actions, uint? soundEffectId) + { + return new LightlessNotification { Title = title, Message = message, @@ -57,30 +72,28 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ ShowProgress = _configService.Current.ShowNotificationProgress, CreatedAt = DateTime.UtcNow }; - - if (_configService.Current.AutoDismissOnAction && notification.Actions.Any()) + } + + private void WrapActionsWithAutoDismiss(LightlessNotification notification) + { + foreach (var action in notification.Actions) { - foreach (var action in notification.Actions) + var originalOnClick = action.OnClick; + action.OnClick = (n) => { - var originalOnClick = action.OnClick; - action.OnClick = (n) => + originalOnClick(n); + if (_configService.Current.AutoDismissOnAction) { - originalOnClick(n); - if (_configService.Current.AutoDismissOnAction) - { - n.IsDismissed = true; - n.IsAnimatingOut = true; - } - }; - } + DismissNotification(n); + } + }; } - - if (notification.SoundEffectId.HasValue) - { - PlayNotificationSound(notification.SoundEffectId.Value); - } - - Mediator.Publish(new LightlessNotificationMessage(notification)); + } + + private void DismissNotification(LightlessNotification notification) + { + notification.IsDismissed = true; + notification.IsAnimatingOut = true; } public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline) { @@ -90,40 +103,8 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ Message = $"{senderName} wants to directly pair with you.", Type = NotificationType.PairRequest, Duration = TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds), - SoundEffectId = !_configService.Current.DisablePairRequestSound ? _configService.Current.PairRequestSoundId : null, - 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; - } - } - } + SoundEffectId = GetPairRequestSoundId(), + Actions = CreatePairRequestActions(onAccept, onDecline) }; if (notification.SoundEffectId.HasValue) @@ -133,7 +114,70 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ Mediator.Publish(new LightlessNotificationMessage(notification)); } + + private uint? GetPairRequestSoundId() => + !_configService.Current.DisablePairRequestSound ? _configService.Current.PairRequestSoundId : null; + + private List CreatePairRequestActions(Action onAccept, Action onDecline) + { + return new List + { + new() + { + Id = "accept", + Label = "Accept", + Icon = FontAwesomeIcon.Check, + Color = UIColors.Get("LightlessGreen"), + IsPrimary = true, + OnClick = (n) => + { + _logger.LogInformation("Pair request accepted"); + onAccept(); + DismissNotification(n); + } + }, + new() + { + Id = "decline", + Label = "Decline", + Icon = FontAwesomeIcon.Times, + Color = UIColors.Get("DimRed"), + IsDestructive = true, + OnClick = (n) => + { + _logger.LogInformation("Pair request declined"); + onDecline(); + DismissNotification(n); + } + } + }; + } public void ShowDownloadCompleteNotification(string fileName, int fileCount, Action? onOpenFolder = null) + { + var notification = new LightlessNotification + { + Title = "Download Complete", + Message = FormatDownloadCompleteMessage(fileName, fileCount), + Type = NotificationType.Info, + Duration = TimeSpan.FromSeconds(8), + Actions = CreateDownloadCompleteActions(onOpenFolder), + SoundEffectId = NotificationSounds.DownloadComplete + }; + + if (notification.SoundEffectId.HasValue) + { + PlayNotificationSound(notification.SoundEffectId.Value); + } + + Mediator.Publish(new LightlessNotificationMessage(notification)); + } + + private string FormatDownloadCompleteMessage(string fileName, int fileCount) => + fileCount > 1 + ? $"Downloaded {fileCount} files successfully." + : $"Downloaded {fileName} successfully."; + + private List CreateDownloadCompleteActions(Action? onOpenFolder) { var actions = new List(); @@ -148,21 +192,23 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ OnClick = (n) => { onOpenFolder(); - n.IsDismissed = true; - n.IsAnimatingOut = true; + DismissNotification(n); } }); } + + return actions; + } + public void ShowErrorNotification(string title, string message, Exception? exception = null, Action? onRetry = null, Action? onViewLog = null) + { 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 + Title = title, + Message = FormatErrorMessage(message, exception), + Type = NotificationType.Error, + Duration = TimeSpan.FromSeconds(15), + Actions = CreateErrorActions(onRetry, onViewLog), + SoundEffectId = NotificationSounds.Error }; if (notification.SoundEffectId.HasValue) @@ -172,9 +218,14 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ Mediator.Publish(new LightlessNotificationMessage(notification)); } - public void ShowErrorNotification(string title, string message, Exception? exception = null, Action? onRetry = null, Action? onViewLog = null) + + private string FormatErrorMessage(string message, Exception? exception) => + exception != null ? $"{message}\n\nError: {exception.Message}" : message; + + private List CreateErrorActions(Action? onRetry, Action? onViewLog) { var actions = new List(); + if (onRetry != null) { actions.Add(new LightlessNotificationAction @@ -186,11 +237,11 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ OnClick = (n) => { onRetry(); - n.IsDismissed = true; - n.IsAnimatingOut = true; + DismissNotification(n); } }); } + if (onViewLog != null) { actions.Add(new LightlessNotificationAction @@ -202,65 +253,14 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ 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)); + + return actions; } 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 message = BuildPairDownloadMessage(userDownloads, queueWaiting); var notification = new LightlessNotification { @@ -272,59 +272,99 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ ShowProgress = true, Progress = totalProgress }; + Mediator.Publish(new LightlessNotificationMessage(notification)); - if (allDownloadsCompleted) + + if (AreAllDownloadsCompleted(userDownloads)) { DismissPairDownloadNotification(); } } - - public void DismissPairDownloadNotification() + + private string BuildPairDownloadMessage(List<(string playerName, float progress, string status)> userDownloads, int queueWaiting) { - Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress")); - } - - private TimeSpan GetDefaultDurationForType(NotificationType type) - { - return type switch + var messageParts = new List(); + + if (queueWaiting > 0) { - NotificationType.Info => TimeSpan.FromSeconds(_configService.Current.InfoNotificationDurationSeconds), - NotificationType.Warning => TimeSpan.FromSeconds(_configService.Current.WarningNotificationDurationSeconds), - NotificationType.Error => TimeSpan.FromSeconds(_configService.Current.ErrorNotificationDurationSeconds), - NotificationType.PairRequest => TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds), - NotificationType.Download => TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds), - _ => TimeSpan.FromSeconds(10) // Fallback for any unknown types - }; + messageParts.Add($"Queue: {queueWaiting} waiting"); + } + + if (userDownloads.Count > 0) + { + var completedCount = userDownloads.Count(x => x.progress >= 1.0f); + messageParts.Add($"Progress: {completedCount}/{userDownloads.Count} completed"); + } + + var activeDownloadLines = BuildActiveDownloadLines(userDownloads); + if (!string.IsNullOrEmpty(activeDownloadLines)) + { + messageParts.Add(activeDownloadLines); + } + + return string.Join("\n", messageParts); } + + private string BuildActiveDownloadLines(List<(string playerName, float progress, string status)> userDownloads) + { + var activeDownloads = userDownloads + .Where(x => x.progress < 1.0f) + .Take(_configService.Current.MaxConcurrentPairApplications); + + if (!activeDownloads.Any()) return string.Empty; + + return string.Join("\n", activeDownloads.Select(x => $"• {x.playerName}: {FormatDownloadStatus(x)}")); + } + + private string FormatDownloadStatus((string playerName, float progress, string status) download) => download.status switch + { + "downloading" => $"{download.progress:P0}", + "decompressing" => "decompressing", + "queued" => "queued", + "waiting" => "waiting for slot", + _ => download.status + }; + + private bool AreAllDownloadsCompleted(List<(string playerName, float progress, string status)> userDownloads) => + userDownloads.Any() && userDownloads.All(x => x.progress >= 1.0f); + + public void DismissPairDownloadNotification() => + Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress")); + + private TimeSpan GetDefaultDurationForType(NotificationType type) => type switch + { + NotificationType.Info => TimeSpan.FromSeconds(_configService.Current.InfoNotificationDurationSeconds), + NotificationType.Warning => TimeSpan.FromSeconds(_configService.Current.WarningNotificationDurationSeconds), + NotificationType.Error => TimeSpan.FromSeconds(_configService.Current.ErrorNotificationDurationSeconds), + NotificationType.PairRequest => TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds), + NotificationType.Download => TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds), + _ => TimeSpan.FromSeconds(10) + }; private uint? GetSoundEffectId(NotificationType type, uint? overrideSoundId) { - if (overrideSoundId.HasValue) - return overrideSoundId; - - // Check if this specific notification type is disabled - bool isDisabled = type switch - { - NotificationType.Info => _configService.Current.DisableInfoSound, - NotificationType.Warning => _configService.Current.DisableWarningSound, - NotificationType.Error => _configService.Current.DisableErrorSound, - NotificationType.Download => _configService.Current.DisableDownloadSound, - _ => false - }; - - if (isDisabled) - return null; - - // Return the configured sound for this type - return type switch - { - NotificationType.Info => _configService.Current.CustomInfoSoundId, - NotificationType.Warning => _configService.Current.CustomWarningSoundId, - NotificationType.Error => _configService.Current.CustomErrorSoundId, - NotificationType.Download => _configService.Current.DownloadSoundId, - _ => NotificationSounds.GetDefaultSound(type) - }; + if (overrideSoundId.HasValue) return overrideSoundId; + if (IsSoundDisabledForType(type)) return null; + return GetConfiguredSoundForType(type); } + + private bool IsSoundDisabledForType(NotificationType type) => type switch + { + NotificationType.Info => _configService.Current.DisableInfoSound, + NotificationType.Warning => _configService.Current.DisableWarningSound, + NotificationType.Error => _configService.Current.DisableErrorSound, + NotificationType.Download => _configService.Current.DisableDownloadSound, + _ => false + }; + + private uint GetConfiguredSoundForType(NotificationType type) => type switch + { + NotificationType.Info => _configService.Current.CustomInfoSoundId, + NotificationType.Warning => _configService.Current.CustomWarningSoundId, + NotificationType.Error => _configService.Current.CustomErrorSoundId, + NotificationType.Download => _configService.Current.DownloadSoundId, + _ => NotificationSounds.GetDefaultSound(type) + }; private void PlayNotificationSound(uint soundEffectId) { @@ -342,32 +382,36 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ private void HandleNotificationMessage(NotificationMessage msg) { _logger.LogInformation("{msg}", msg.ToString()); - if (!_dalamudUtilService.IsLoggedIn) return; - // Get notification location based on type and system preference - var location = _configService.Current.UseLightlessNotifications - ? msg.Type switch - { - NotificationType.Info => _configService.Current.LightlessInfoNotification, - NotificationType.Warning => _configService.Current.LightlessWarningNotification, - NotificationType.Error => _configService.Current.LightlessErrorNotification, - NotificationType.PairRequest => _configService.Current.LightlessPairRequestNotification, - NotificationType.Download => _configService.Current.LightlessDownloadNotification, - _ => NotificationLocation.LightlessUi - } - : msg.Type switch - { - NotificationType.Info => _configService.Current.InfoNotification, - NotificationType.Warning => _configService.Current.WarningNotification, - NotificationType.Error => _configService.Current.ErrorNotification, - NotificationType.PairRequest => NotificationLocation.Toast, - NotificationType.Download => NotificationLocation.Toast, - _ => NotificationLocation.Nowhere - }; - + var location = GetNotificationLocation(msg.Type); ShowNotificationLocationBased(msg, location); } + + private NotificationLocation GetNotificationLocation(NotificationType type) => + _configService.Current.UseLightlessNotifications + ? GetLightlessNotificationLocation(type) + : GetClassicNotificationLocation(type); + + private NotificationLocation GetLightlessNotificationLocation(NotificationType type) => type switch + { + NotificationType.Info => _configService.Current.LightlessInfoNotification, + NotificationType.Warning => _configService.Current.LightlessWarningNotification, + NotificationType.Error => _configService.Current.LightlessErrorNotification, + NotificationType.PairRequest => _configService.Current.LightlessPairRequestNotification, + NotificationType.Download => _configService.Current.LightlessDownloadNotification, + _ => NotificationLocation.LightlessUi + }; + + private NotificationLocation GetClassicNotificationLocation(NotificationType type) => type switch + { + NotificationType.Info => _configService.Current.InfoNotification, + NotificationType.Warning => _configService.Current.WarningNotification, + NotificationType.Error => _configService.Current.ErrorNotification, + NotificationType.PairRequest => NotificationLocation.Toast, + NotificationType.Download => NotificationLocation.Toast, + _ => NotificationLocation.Nowhere + }; private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location) { @@ -403,18 +447,12 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ private void ShowLightlessNotification(NotificationMessage msg) { var duration = msg.TimeShownOnScreen ?? GetDefaultDurationForType(msg.Type); - // GetSoundEffectId will handle checking if the sound is disabled ShowNotification(msg.Title ?? "Lightless Sync", msg.Message ?? string.Empty, msg.Type, duration, null, null); } 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, - _ => Dalamud.Interface.ImGuiNotification.NotificationType.Info - }; + var dalamudType = ConvertToDalamudNotificationType(msg.Type); _notificationManager.AddNotification(new Notification() { @@ -425,6 +463,13 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3) }); } + + private Dalamud.Interface.ImGuiNotification.NotificationType ConvertToDalamudNotificationType(NotificationType type) => type switch + { + NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error, + NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning, + _ => Dalamud.Interface.ImGuiNotification.NotificationType.Info + }; private void ShowChat(NotificationMessage msg) { diff --git a/LightlessSync/UI/LightlessNotificationUI.cs b/LightlessSync/UI/LightlessNotificationUI.cs index fd49055..139aa15 100644 --- a/LightlessSync/UI/LightlessNotificationUI.cs +++ b/LightlessSync/UI/LightlessNotificationUI.cs @@ -17,12 +17,15 @@ namespace LightlessSync.UI; public class LightlessNotificationUI : WindowMediatorSubscriberBase { + private const float NotificationMinHeight = 60f; + private const float NotificationMaxHeight = 250f; + private const float WindowPaddingOffset = 6f; + private const float SlideAnimationDistance = 100f; + private const float OutAnimationSpeedMultiplier = 0.7f; + private readonly List _notifications = new(); private readonly object _notificationLock = new(); private readonly LightlessConfigService _configService; - - private const float NotificationMinHeight = 60f; - private const float NotificationMaxHeight = 250f; public LightlessNotificationUI(ILogger logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService) : base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector) @@ -49,15 +52,11 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase Mediator.Subscribe(this, HandleNotificationMessage); Mediator.Subscribe(this, HandleNotificationDismissMessage); } - private void HandleNotificationMessage(LightlessNotificationMessage message) - { + private void HandleNotificationMessage(LightlessNotificationMessage message) => AddNotification(message.Notification); - } - private void HandleNotificationDismissMessage(LightlessNotificationDismissMessage message) - { + private void HandleNotificationDismissMessage(LightlessNotificationDismissMessage message) => RemoveNotification(message.NotificationId); - } public void AddNotification(LightlessNotification notification) { @@ -66,12 +65,7 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase var existingNotification = _notifications.FirstOrDefault(n => n.Id == notification.Id); if (existingNotification != null) { - // Update existing notification without restarting animation - existingNotification.Message = notification.Message; - existingNotification.Progress = notification.Progress; - existingNotification.ShowProgress = notification.ShowProgress; - existingNotification.Title = notification.Title; - _logger.LogDebug("Updated existing notification: {Title}", notification.Title); + UpdateExistingNotification(existingNotification, notification); } else { @@ -79,12 +73,18 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase _logger.LogDebug("Added new notification: {Title}", notification.Title); } - if (!IsOpen) - { - IsOpen = true; - } + if (!IsOpen) IsOpen = true; } } + + private void UpdateExistingNotification(LightlessNotification existing, LightlessNotification updated) + { + existing.Message = updated.Message; + existing.Progress = updated.Progress; + existing.ShowProgress = updated.ShowProgress; + existing.Title = updated.Title; + _logger.LogDebug("Updated existing notification: {Title}", updated.Title); + } public void RemoveNotification(string id) { @@ -93,11 +93,16 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase var notification = _notifications.FirstOrDefault(n => n.Id == id); if (notification != null) { - notification.IsAnimatingOut = true; - notification.IsAnimatingIn = false; + StartOutAnimation(notification); } } } + + private void StartOutAnimation(LightlessNotification notification) + { + notification.IsAnimatingOut = true; + notification.IsAnimatingIn = false; + } protected override void DrawInternal() { @@ -115,35 +120,45 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase } var viewport = ImGui.GetMainViewport(); - - // Always position at top (choco doesnt know how to handle top positions how fitting) - var baseX = viewport.WorkPos.X + viewport.WorkSize.X - _configService.Current.NotificationWidth - _configService.Current.NotificationOffsetX - 6f ; - var baseY = viewport.WorkPos.Y; - - // Apply Y offset - var finalY = baseY + _configService.Current.NotificationOffsetY; - - // Update position - Position = new Vector2(baseX, finalY); - - for (int i = 0; i < _notifications.Count; i++) - { - DrawNotification(_notifications[i], i); - - if (i < _notifications.Count - 1) - { - ImGui.Dummy(new Vector2(0, _configService.Current.NotificationSpacing)); - } - } + Position = CalculateWindowPosition(viewport); + DrawAllNotifications(); } ImGui.PopStyleVar(); } + + private Vector2 CalculateWindowPosition(ImGuiViewportPtr viewport) + { + var x = viewport.WorkPos.X + viewport.WorkSize.X - + _configService.Current.NotificationWidth - + _configService.Current.NotificationOffsetX - + WindowPaddingOffset; + var y = viewport.WorkPos.Y + _configService.Current.NotificationOffsetY; + return new Vector2(x, y); + } + + private void DrawAllNotifications() + { + for (int i = 0; i < _notifications.Count; i++) + { + DrawNotification(_notifications[i], i); + + if (i < _notifications.Count - 1) + { + ImGui.Dummy(new Vector2(0, _configService.Current.NotificationSpacing)); + } + } + } private void UpdateNotifications() { var deltaTime = ImGui.GetIO().DeltaTime; - + EnforceMaxNotificationLimit(); + UpdateAnimationsAndRemoveExpired(deltaTime); + } + + private void EnforceMaxNotificationLimit() + { var maxNotifications = _configService.Current.MaxSimultaneousNotifications; while (_notifications.Count(n => !n.IsAnimatingOut) > maxNotifications) { @@ -151,60 +166,70 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase .Where(n => !n.IsAnimatingOut) .OrderBy(n => n.CreatedAt) .FirstOrDefault(); + if (oldestNotification != null) { - oldestNotification.IsAnimatingOut = true; - oldestNotification.IsAnimatingIn = false; + StartOutAnimation(oldestNotification); } } + } + + private void UpdateAnimationsAndRemoveExpired(float deltaTime) + { for (int i = _notifications.Count - 1; i >= 0; i--) { var notification = _notifications[i]; + UpdateNotificationAnimation(notification, deltaTime); - if (notification.IsAnimatingIn && notification.AnimationProgress < 1f) - { - notification.AnimationProgress = Math.Min(1f, notification.AnimationProgress + deltaTime * _configService.Current.NotificationAnimationSpeed); - } - else if (notification.IsAnimatingOut && notification.AnimationProgress > 0f) - { - notification.AnimationProgress = Math.Max(0f, notification.AnimationProgress - deltaTime * (_configService.Current.NotificationAnimationSpeed * 0.7f)); - } - else if (!notification.IsAnimatingOut && !notification.IsDismissed) - { - notification.IsAnimatingIn = false; - - if (notification.IsExpired && !notification.IsAnimatingOut) - { - notification.IsAnimatingOut = true; - notification.IsAnimatingIn = false; - } - } - - if (notification.IsAnimatingOut && notification.AnimationProgress <= 0.01f) + if (ShouldRemoveNotification(notification)) { _notifications.RemoveAt(i); } } } + + private void UpdateNotificationAnimation(LightlessNotification notification, float deltaTime) + { + if (notification.IsAnimatingIn && notification.AnimationProgress < 1f) + { + notification.AnimationProgress = Math.Min(1f, + notification.AnimationProgress + deltaTime * _configService.Current.NotificationAnimationSpeed); + } + else if (notification.IsAnimatingOut && notification.AnimationProgress > 0f) + { + notification.AnimationProgress = Math.Max(0f, + notification.AnimationProgress - deltaTime * _configService.Current.NotificationAnimationSpeed * OutAnimationSpeedMultiplier); + } + else if (!notification.IsAnimatingOut && !notification.IsDismissed) + { + notification.IsAnimatingIn = false; + + if (notification.IsExpired) + { + StartOutAnimation(notification); + } + } + } + + private bool ShouldRemoveNotification(LightlessNotification notification) => + notification.IsAnimatingOut && notification.AnimationProgress <= 0.01f; private void DrawNotification(LightlessNotification notification, int index) { var alpha = notification.AnimationProgress; - - if (alpha <= 0f) - return; - - var slideOffset = (1f - alpha) * 100f; // Fixed slide distance + if (alpha <= 0f) return; + var slideOffset = (1f - alpha) * SlideAnimationDistance; var originalCursorPos = ImGui.GetCursorPos(); ImGui.SetCursorPosX(originalCursorPos.X + slideOffset); var notificationHeight = CalculateNotificationHeight(notification); + var notificationWidth = _configService.Current.NotificationWidth - slideOffset; ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); using var child = ImRaii.Child($"notification_{notification.Id}", - new Vector2(_configService.Current.NotificationWidth - slideOffset, notificationHeight), + new Vector2(notificationWidth, notificationHeight), false, ImGuiWindowFlags.NoScrollbar); if (child.Success) @@ -221,50 +246,69 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); + var bgColor = CalculateBackgroundColor(alpha, ImGui.IsWindowHovered()); + var accentColor = GetNotificationAccentColor(notification.Type); + accentColor.W *= alpha; + + DrawShadow(drawList, windowPos, windowSize, alpha); + HandleClickToDismiss(notification); + DrawBackground(drawList, windowPos, windowSize, bgColor); + DrawAccentBar(drawList, windowPos, windowSize, accentColor); + DrawDurationProgressBar(notification, alpha, windowPos, windowSize, drawList); + DrawNotificationText(notification, alpha); + } + + private Vector4 CalculateBackgroundColor(float alpha, bool isHovered) + { var baseOpacity = _configService.Current.NotificationOpacity; var finalOpacity = baseOpacity * alpha; var bgColor = new Vector4(30f/255f, 30f/255f, 30f/255f, finalOpacity); - var accentColor = GetNotificationAccentColor(notification.Type); - var progressBarColor = UIColors.Get("LightlessBlue"); - accentColor.W *= alpha; + if (isHovered) + { + bgColor *= 1.1f; + bgColor.W = Math.Min(bgColor.W, 0.98f); + } - // Draw shadow with fixed intensity + return bgColor; + } + + private void DrawShadow(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float alpha) + { var shadowOffset = new Vector2(1f, 1f); - var shadowAlpha = 0.4f * alpha; - var shadowColor = new Vector4(0f, 0f, 0f, shadowAlpha); + var shadowColor = new Vector4(0f, 0f, 0f, 0.4f * alpha); drawList.AddRectFilled( windowPos + shadowOffset, windowPos + windowSize + shadowOffset, ImGui.ColorConvertFloat4ToU32(shadowColor), 3f ); - - var isHovered = ImGui.IsWindowHovered(); - - if (isHovered) + } + + private void HandleClickToDismiss(LightlessNotification notification) + { + if (ImGui.IsWindowHovered() && + _configService.Current.DismissNotificationOnClick && + !notification.Actions.Any() && + ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { - bgColor = bgColor * 1.1f; - bgColor.W = Math.Min(bgColor.W, 0.98f); - - // Handle click-to-dismiss for notifications without actions - if (_configService.Current.DismissNotificationOnClick && - !notification.Actions.Any() && - ImGui.IsMouseClicked(ImGuiMouseButton.Left)) - { - notification.IsDismissed = true; - notification.IsAnimatingOut = true; - } + notification.IsDismissed = true; + StartOutAnimation(notification); } - + } + + private void DrawBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 bgColor) + { drawList.AddRectFilled( windowPos, windowPos + windowSize, ImGui.ColorConvertFloat4ToU32(bgColor), 3f ); - - // Draw accent bar on left side of the notif (only if width > 0) + } + + private void DrawAccentBar(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 accentColor) + { var accentWidth = _configService.Current.NotificationAccentBarWidth; if (accentWidth > 0f) { @@ -275,30 +319,37 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase 3f ); } - - DrawDurationProgressBar(notification, alpha, windowPos, windowSize, progressBarColor, drawList); - - DrawNotificationText(notification, alpha); } - private void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, Vector4 progressBarColor, ImDrawListPtr drawList) + private void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList) { - // For download notifications, use download progress instead of duration - float progress; - if (notification.Type == NotificationType.Download && notification.ShowProgress) - { - progress = Math.Clamp(notification.Progress, 0f, 1f); - } - else - { - var elapsed = DateTime.UtcNow - notification.CreatedAt; - progress = Math.Min(1.0f, (float)(elapsed.TotalSeconds / notification.Duration.TotalSeconds)); - } - + var progress = CalculateProgress(notification); + var progressBarColor = UIColors.Get("LightlessBlue"); var progressHeight = 2f; var progressY = windowPos.Y + windowSize.Y - progressHeight; var progressWidth = windowSize.X * progress; + DrawProgressBackground(drawList, windowPos, windowSize, progressY, progressHeight, progressBarColor, alpha); + + if (progress > 0) + { + DrawProgressForeground(drawList, windowPos, progressY, progressHeight, progressWidth, progressBarColor, alpha); + } + } + + private float CalculateProgress(LightlessNotification notification) + { + if (notification.Type == NotificationType.Download && notification.ShowProgress) + { + return Math.Clamp(notification.Progress, 0f, 1f); + } + + var elapsed = DateTime.UtcNow - notification.CreatedAt; + return Math.Min(1.0f, (float)(elapsed.TotalSeconds / notification.Duration.TotalSeconds)); + } + + private void DrawProgressBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float progressY, float progressHeight, Vector4 progressBarColor, float alpha) + { var bgProgressColor = new Vector4(progressBarColor.X * 0.3f, progressBarColor.Y * 0.3f, progressBarColor.Z * 0.3f, 0.5f * alpha); drawList.AddRectFilled( new Vector2(windowPos.X, progressY), @@ -306,71 +357,70 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase ImGui.ColorConvertFloat4ToU32(bgProgressColor), 0f ); - - if (progress > 0) - { - var progressColor = progressBarColor; - progressColor.W *= alpha; - drawList.AddRectFilled( - new Vector2(windowPos.X, progressY), - new Vector2(windowPos.X + progressWidth, progressY + progressHeight), - ImGui.ColorConvertFloat4ToU32(progressColor), - 0f - ); - } + } + + private void DrawProgressForeground(ImDrawListPtr drawList, Vector2 windowPos, float progressY, float progressHeight, float progressWidth, Vector4 progressBarColor, float alpha) + { + var progressColor = progressBarColor; + progressColor.W *= alpha; + drawList.AddRectFilled( + new Vector2(windowPos.X, progressY), + new Vector2(windowPos.X + progressWidth, progressY + progressHeight), + ImGui.ColorConvertFloat4ToU32(progressColor), + 0f + ); } private void DrawNotificationText(LightlessNotification notification, float alpha) { var padding = new Vector2(10f, 6f); - var contentPos = new Vector2(padding.X, padding.Y); var windowSize = ImGui.GetWindowSize(); var contentSize = new Vector2(windowSize.X - padding.X, windowSize.Y - padding.Y * 2); ImGui.SetCursorPos(contentPos); - float titleHeight = 0f; - using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1f, 1f, 1f, alpha))) - { - // 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, titleHeight + 4f)); - ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + contentSize.X); - using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(0.9f, 0.9f, 0.9f, alpha))) - { - ImGui.TextWrapped(notification.Message); - } - ImGui.PopTextWrapPos(); - } + var titleHeight = DrawTitle(notification, contentSize.X, alpha); + DrawMessage(notification, contentPos, contentSize.X, titleHeight, alpha); if (notification.Actions.Count > 0) { - var spacingHeight = ImGui.GetStyle().ItemSpacing.Y; - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + spacingHeight); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetStyle().ItemSpacing.Y); ImGui.SetCursorPosX(contentPos.X); DrawNotificationActions(notification, contentSize.X, alpha); } } + + private float DrawTitle(LightlessNotification notification, float contentWidth, float alpha) + { + using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1f, 1f, 1f, alpha))) + { + ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + contentWidth); + var titleStartY = ImGui.GetCursorPosY(); + + var titleText = _configService.Current.ShowNotificationTimestamp + ? $"[{notification.CreatedAt.ToLocalTime():HH:mm:ss}] {notification.Title}" + : notification.Title; + + ImGui.TextWrapped(titleText); + var titleHeight = ImGui.GetCursorPosY() - titleStartY; + ImGui.PopTextWrapPos(); + return titleHeight; + } + } + + private void DrawMessage(LightlessNotification notification, Vector2 contentPos, float contentWidth, float titleHeight, float alpha) + { + if (string.IsNullOrEmpty(notification.Message)) return; + + ImGui.SetCursorPos(contentPos + new Vector2(0f, titleHeight + 4f)); + ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + contentWidth); + using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(0.9f, 0.9f, 0.9f, alpha))) + { + ImGui.TextWrapped(notification.Message); + } + ImGui.PopTextWrapPos(); + } private void DrawNotificationActions(LightlessNotification notification, float availableWidth, float alpha) { @@ -495,44 +545,43 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase private float CalculateNotificationHeight(LightlessNotification notification) { - var contentWidth = _configService.Current.NotificationWidth - 35f; // Account for padding and accent bar - var height = 12f; // Base height for padding (top + bottom) + var contentWidth = _configService.Current.NotificationWidth - 35f; + var height = 12f; - var titleText = notification.Title; - if (_configService.Current.ShowNotificationTimestamp) - { - var timestamp = notification.CreatedAt.ToLocalTime().ToString("HH:mm:ss"); - titleText = $"[{timestamp}] {titleText}"; - } + height += CalculateTitleHeight(notification, contentWidth); + height += CalculateMessageHeight(notification, contentWidth); - var titleSize = ImGui.CalcTextSize(titleText, true, contentWidth); - height += titleSize.Y; // Title height - - // Calculate message height - if (!string.IsNullOrEmpty(notification.Message)) - { - height += 4f; // Spacing between title and message - var messageSize = ImGui.CalcTextSize(notification.Message, true, contentWidth); // This is cringe - height += messageSize.Y; // Message height - } - - // Add height for progress bar if (notification.ShowProgress) { height += 12f; } - // Add height for action buttons if (notification.Actions.Count > 0) { - height += ImGui.GetStyle().ItemSpacing.Y; // Spacing before buttons - height += ImGui.GetFrameHeight(); // Button height - height += 12f; // Bottom padding for buttons + height += ImGui.GetStyle().ItemSpacing.Y; + height += ImGui.GetFrameHeight(); + height += 12f; } - // Allow notifications to grow taller but cap at maximum height return Math.Clamp(height, NotificationMinHeight, NotificationMaxHeight); } + + private float CalculateTitleHeight(LightlessNotification notification, float contentWidth) + { + var titleText = _configService.Current.ShowNotificationTimestamp + ? $"[{notification.CreatedAt.ToLocalTime():HH:mm:ss}] {notification.Title}" + : notification.Title; + + return ImGui.CalcTextSize(titleText, true, contentWidth).Y; + } + + private float CalculateMessageHeight(LightlessNotification notification, float contentWidth) + { + if (string.IsNullOrEmpty(notification.Message)) return 0f; + + var messageHeight = ImGui.CalcTextSize(notification.Message, true, contentWidth).Y; + return 4f + messageHeight; + } private Vector4 GetNotificationAccentColor(NotificationType type) { diff --git a/LightlessSync/UI/Models/LightlessNotification.cs b/LightlessSync/UI/Models/LightlessNotification.cs index 4516dde..3c6edea 100644 --- a/LightlessSync/UI/Models/LightlessNotification.cs +++ b/LightlessSync/UI/Models/LightlessNotification.cs @@ -15,12 +15,9 @@ public class LightlessNotification public List Actions { get; set; } = new(); public bool ShowProgress { get; set; } = false; public float Progress { get; set; } = 0f; - public bool IsMinimized { get; set; } = false; 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 From bb779904f7a9475ba053638a8675b29813f227ec Mon Sep 17 00:00:00 2001 From: choco Date: Sat, 11 Oct 2025 21:52:14 +0200 Subject: [PATCH 15/15] removed temp accept/decline logic from the api layer --- LightlessSync/Services/PairRequestService.cs | 43 ++++++++++++++++++- .../ApiController.Functions.Callbacks.cs | 30 ++----------- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/LightlessSync/Services/PairRequestService.cs b/LightlessSync/Services/PairRequestService.cs index 998ea42..9d87244 100644 --- a/LightlessSync/Services/PairRequestService.cs +++ b/LightlessSync/Services/PairRequestService.cs @@ -1,6 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using LightlessSync.LightlessConfiguration.Models; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; @@ -11,16 +13,18 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase { private readonly DalamudUtilService _dalamudUtil; private readonly PairManager _pairManager; + private readonly Lazy _apiController; private readonly object _syncRoot = new(); private readonly List _requests = []; private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(5); - public PairRequestService(ILogger logger, LightlessMediator mediator, DalamudUtilService dalamudUtil, PairManager pairManager) + public PairRequestService(ILogger logger, LightlessMediator mediator, DalamudUtilService dalamudUtil, PairManager pairManager, Lazy apiController) : base(logger, mediator) { _dalamudUtil = dalamudUtil; _pairManager = pairManager; + _apiController = apiController; Mediator.Subscribe(this, _ => { @@ -183,6 +187,41 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase return _requests.RemoveAll(r => now - r.ReceivedAt > Expiration) > 0; } + public void AcceptPairRequest(string hashedCid) + { + _ = Task.Run(async () => + { + try + { + await _apiController.Value.TryPairWithContentId(hashedCid).ConfigureAwait(false); + RemoveRequest(hashedCid); + + var display = ResolveDisplayName(hashedCid); + var displayText = string.IsNullOrEmpty(display) ? hashedCid : display; + Mediator.Publish(new NotificationMessage( + "Pair request accepted", + $"Sent a pair request back to {displayText}.", + NotificationType.Info, + TimeSpan.FromSeconds(3))); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to accept pair request for {HashedCid}", hashedCid); + Mediator.Publish(new NotificationMessage( + "Failed to Accept Pair Request", + ex.Message, + NotificationType.Error, + TimeSpan.FromSeconds(5))); + } + }); + } + + public void DeclinePairRequest(string hashedCid) + { + RemoveRequest(hashedCid); + Logger.LogDebug("Declined pair request from {HashedCid}", hashedCid); + } + private record struct PairRequestEntry(string HashedCid, string MessageTemplate, DateTime ReceivedAt); public readonly record struct PairRequestDisplay(string HashedCid, string DisplayName, string Message, DateTime ReceivedAt); diff --git a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs index 38df25a..e3c1fbc 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs @@ -111,37 +111,13 @@ public partial class ApiController return Task.CompletedTask; var request = _pairRequestService.RegisterIncomingRequest(dto.myHashedCid, dto.message ?? string.Empty); - - // Use the new interactive notification system for pair requests var senderName = string.IsNullOrEmpty(request.DisplayName) ? "Unknown User" : request.DisplayName; + _lightlessNotificationService.ShowPairRequestNotification( senderName, request.HashedCid, - onAccept: () => - { - // Fire and forget async operation - _ = Task.Run(async () => - { - var myCidHash = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); - try - { - await TryPairWithContentId(request.HashedCid, myCidHash).ConfigureAwait(false); - _pairRequestService.RemoveRequest(request.HashedCid); - } - catch (Exception ex) - { - Mediator.Publish(new NotificationMessage( - "Failed to Accept Pair Request", - ex.Message, - NotificationType.Error, - TimeSpan.FromSeconds(5))); - } - }); - }, - onDecline: () => - { - _pairRequestService.RemoveRequest(request.HashedCid); - }); + onAccept: () => _pairRequestService.AcceptPairRequest(request.HashedCid), + onDecline: () => _pairRequestService.DeclinePairRequest(request.HashedCid)); return Task.CompletedTask; }