diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs index edb493c..dca6232 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs @@ -63,6 +63,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; @@ -76,6 +77,49 @@ 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 + public bool UseLightlessNotifications { get; set; } = true; + 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 LightlessPairRequestNotification { get; set; } = NotificationLocation.LightlessUi; + public NotificationLocation LightlessDownloadNotification { get; set; } = NotificationLocation.TextOverlay; + + // Basic Settings + public float NotificationOpacity { get; set; } = 0.95f; + public int MaxSimultaneousNotifications { get; set; } = 5; + public bool AutoDismissOnAction { get; set; } = true; + public bool DismissNotificationOnClick { get; set; } = false; + public bool ShowNotificationTimestamp { get; set; } = false; + + // 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; + + // Animation & Effects + public float NotificationAnimationSpeed { get; set; } = 10f; + public float NotificationAccentBarWidth { get; set; } = 3f; + + // 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; + public uint CustomInfoSoundId { get; set; } = 2; // Se2 + 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; + 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/LightlessConfiguration/Models/NotificationLocation.cs b/LightlessSync/LightlessConfiguration/Models/NotificationLocation.cs index a33e727..2815986 100644 --- a/LightlessSync/LightlessConfiguration/Models/NotificationLocation.cs +++ b/LightlessSync/LightlessConfiguration/Models/NotificationLocation.cs @@ -1,16 +1,21 @@ -namespace LightlessSync.LightlessConfiguration.Models; +namespace LightlessSync.LightlessConfiguration.Models; public enum NotificationLocation { Nowhere, Chat, Toast, - Both + Both, + LightlessUi, + ChatAndLightlessUi, + TextOverlay, } public enum NotificationType { Info, Warning, - Error + Error, + PairRequest, + Download } \ No newline at end of file diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 7a54ce9..531522b 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -182,9 +182,13 @@ 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 NotificationService( + s.GetRequiredService>(), + s.GetRequiredService(), + s.GetRequiredService(), + notificationManager, + chatGui, + s.GetRequiredService())); collection.AddSingleton((s) => { var httpClient = new HttpClient(); @@ -248,6 +252,12 @@ 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(), + s.GetRequiredService())); collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); @@ -255,7 +265,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())); diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index dba8427..4745d23 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; @@ -54,6 +54,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/NotificationService.cs b/LightlessSync/Services/NotificationService.cs index 4ee361b..621b816 100644 --- a/LightlessSync/Services/NotificationService.cs +++ b/LightlessSync/Services/NotificationService.cs @@ -1,42 +1,492 @@ -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, + public NotificationService( + ILogger logger, + LightlessConfigService configService, DalamudUtilService dalamudUtilService, INotificationManager notificationManager, - IChatGui chatGui, LightlessConfigService configurationService) : base(logger, mediator) + IChatGui chatGui, + LightlessMediator mediator) : base(logger, mediator) { + _logger = logger; + _configService = configService; _dalamudUtilService = dalamudUtilService; _notificationManager = notificationManager; _chatGui = chatGui; - _configurationService = configurationService; } - public Task StartAsync(CancellationToken cancellationToken) { - Mediator.Subscribe(this, ShowNotification); + Mediator.Subscribe(this, HandleNotificationMessage); return Task.CompletedTask; } - - public Task StopAsync(CancellationToken cancellationToken) + 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) { - return Task.CompletedTask; + 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, + Type = type, + Duration = duration ?? GetDefaultDurationForType(type), + Actions = actions ?? new List(), + SoundEffectId = GetSoundEffectId(type, soundEffectId), + ShowProgress = _configService.Current.ShowNotificationProgress, + CreatedAt = DateTime.UtcNow + }; + } + + private void WrapActionsWithAutoDismiss(LightlessNotification notification) + { + foreach (var action in notification.Actions) + { + var originalOnClick = action.OnClick; + action.OnClick = (n) => + { + originalOnClick(n); + if (_configService.Current.AutoDismissOnAction) + { + DismissNotification(n); + } + }; + } + } + + private void DismissNotification(LightlessNotification notification) + { + notification.IsDismissed = true; + notification.IsAnimatingOut = true; + } + public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline) + { + var notification = new LightlessNotification + { + Title = "Pair Request Received", + Message = $"{senderName} wants to directly pair with you.", + Type = NotificationType.PairRequest, + Duration = TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds), + SoundEffectId = GetPairRequestSoundId(), + Actions = CreatePairRequestActions(onAccept, onDecline) + }; + + if (notification.SoundEffectId.HasValue) + { + PlayNotificationSound(notification.SoundEffectId.Value); + } + + 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(); + + if (onOpenFolder != null) + { + actions.Add(new LightlessNotificationAction + { + Id = "open_folder", + Label = "Open Folder", + Icon = FontAwesomeIcon.FolderOpen, + Color = UIColors.Get("LightlessBlue"), + OnClick = (n) => + { + onOpenFolder(); + 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 = title, + Message = FormatErrorMessage(message, exception), + Type = NotificationType.Error, + Duration = TimeSpan.FromSeconds(15), + Actions = CreateErrorActions(onRetry, onViewLog), + SoundEffectId = NotificationSounds.Error + }; + + if (notification.SoundEffectId.HasValue) + { + PlayNotificationSound(notification.SoundEffectId.Value); + } + + Mediator.Publish(new LightlessNotificationMessage(notification)); + } + + 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 + { + Id = "retry", + Label = "Retry", + Icon = FontAwesomeIcon.Redo, + Color = UIColors.Get("LightlessBlue"), + OnClick = (n) => + { + onRetry(); + DismissNotification(n); + } + }); + } + + if (onViewLog != null) + { + actions.Add(new LightlessNotificationAction + { + Id = "view_log", + Label = "View Log", + Icon = FontAwesomeIcon.FileAlt, + Color = UIColors.Get("LightlessYellow"), + OnClick = (n) => onViewLog() + }); + } + + 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 message = BuildPairDownloadMessage(userDownloads, queueWaiting); + + var notification = new LightlessNotification + { + Id = "pair_download_progress", + Title = "Downloading Pair Data", + Message = message, + Type = NotificationType.Download, + Duration = TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds), + ShowProgress = true, + Progress = totalProgress + }; + + Mediator.Publish(new LightlessNotificationMessage(notification)); + + if (AreAllDownloadsCompleted(userDownloads)) + { + DismissPairDownloadNotification(); + } + } + + private string BuildPairDownloadMessage(List<(string playerName, float progress, string status)> userDownloads, int queueWaiting) + { + var messageParts = new List(); + + if (queueWaiting > 0) + { + 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; + 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) + { + try + { + UIGlobals.PlayChatSoundEffect(soundEffectId); + _logger.LogDebug("Played notification sound effect {SoundId} via ChatGui", 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; + + 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) + { + 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 ?? GetDefaultDurationForType(msg.Type); + ShowNotification(msg.Title ?? "Lightless Sync", msg.Message ?? string.Empty, msg.Type, duration, null, null); + } + + private void ShowToast(NotificationMessage msg) + { + var dalamudType = ConvertToDalamudNotificationType(msg.Type); + + _notificationManager.AddNotification(new Notification() + { + Content = msg.Message ?? string.Empty, + Title = msg.Title, + Type = dalamudType, + Minimized = false, + 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) + { + 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) @@ -57,85 +507,4 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ _chatGui.Print(se.BuiltString); } - 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 ShowNotification(NotificationMessage msg) - { - Logger.LogInformation("{msg}", msg.ToString()); - - if (!_dalamudUtilService.IsLoggedIn) return; - - switch (msg.Type) - { - case NotificationType.Info: - ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification); - break; - - case NotificationType.Warning: - ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification); - break; - - case NotificationType.Error: - ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification); - break; - } - } - - 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.Nowhere: - break; - } - } - - 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) - }); - } } \ No newline at end of file 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/Services/UiService.cs b/LightlessSync/Services/UiService.cs index 4071b61..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; @@ -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, + NotificationService notificationService) : base(logger, lightlessMediator) { _logger = logger; _logger.LogTrace("Creating {type}", GetType().Name); diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index b02594f..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) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService) + PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService 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..301f177 100644 --- a/LightlessSync/UI/DownloadUi.cs +++ b/LightlessSync/UI/DownloadUi.cs @@ -1,7 +1,7 @@ -using System; 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; @@ -22,9 +22,12 @@ public class DownloadUi : WindowMediatorSubscriberBase private readonly UiSharedService _uiShared; private readonly PairProcessingLimiter _pairProcessingLimiter; private readonly ConcurrentDictionary _uploadingPlayers = new(); + 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) + PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared, + PerformanceCollectorService performanceCollectorService, NotificationService 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) => @@ -77,19 +88,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) @@ -122,28 +121,64 @@ public class DownloadUi : WindowMediatorSubscriberBase try { - foreach (var item in _currentDownloads.ToList()) + // 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) { - 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 notification system + if (_currentDownloads.Any()) + { + UpdateDownloadNotification(limiterSnapshot); + _notificationDismissed = false; + } + else if (!_notificationDismissed) + { + _notificationService.DismissPairDownloadNotification(); + _notificationDismissed = true; + } + } + else + { + 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); + 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 +297,40 @@ public class DownloadUi : WindowMediatorSubscriberBase MaximumSize = new Vector2(300, maxHeight), }; } + + private void UpdateDownloadNotification(PairProcessingLimiterSnapshot limiterSnapshot) + { + 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)); + } + + // 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, queueWaiting); + } + } + } \ No newline at end of file diff --git a/LightlessSync/UI/LightlessNotificationUI.cs b/LightlessSync/UI/LightlessNotificationUI.cs new file mode 100644 index 0000000..139aa15 --- /dev/null +++ b/LightlessSync/UI/LightlessNotificationUI.cs @@ -0,0 +1,598 @@ +using Dalamud.Interface; +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; + +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; + + 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 | + ImGuiWindowFlags.NoSavedSettings | + ImGuiWindowFlags.NoFocusOnAppearing | + ImGuiWindowFlags.NoNav | + ImGuiWindowFlags.NoBackground | + ImGuiWindowFlags.NoCollapse | + ImGuiWindowFlags.AlwaysAutoResize; + + PositionCondition = ImGuiCond.Always; + + Size = new Vector2(_configService.Current.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) + { + UpdateExistingNotification(existingNotification, notification); + } + else + { + _notifications.Add(notification); + _logger.LogDebug("Added new notification: {Title}", notification.Title); + } + + 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) + { + lock (_notificationLock) + { + var notification = _notifications.FirstOrDefault(n => n.Id == id); + if (notification != null) + { + StartOutAnimation(notification); + } + } + } + + private void StartOutAnimation(LightlessNotification notification) + { + notification.IsAnimatingOut = true; + notification.IsAnimatingIn = false; + } + + 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(); + 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) + { + var oldestNotification = _notifications + .Where(n => !n.IsAnimatingOut) + .OrderBy(n => n.CreatedAt) + .FirstOrDefault(); + + if (oldestNotification != null) + { + StartOutAnimation(oldestNotification); + } + } + } + + private void UpdateAnimationsAndRemoveExpired(float deltaTime) + { + for (int i = _notifications.Count - 1; i >= 0; i--) + { + var notification = _notifications[i]; + UpdateNotificationAnimation(notification, deltaTime); + + 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) * 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(notificationWidth, notificationHeight), + false, ImGuiWindowFlags.NoScrollbar); + + if (child.Success) + { + DrawNotificationContent(notification, alpha); + } + + ImGui.PopStyleVar(); + } + + private void DrawNotificationContent(LightlessNotification notification, float alpha) + { + var drawList = ImGui.GetWindowDrawList(); + 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); + + if (isHovered) + { + bgColor *= 1.1f; + bgColor.W = Math.Min(bgColor.W, 0.98f); + } + + return bgColor; + } + + private void DrawShadow(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float 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 + ); + } + + private void HandleClickToDismiss(LightlessNotification notification) + { + if (ImGui.IsWindowHovered() && + _configService.Current.DismissNotificationOnClick && + !notification.Actions.Any() && + ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + { + 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 + ); + } + + private void DrawAccentBar(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 accentColor) + { + var accentWidth = _configService.Current.NotificationAccentBarWidth; + if (accentWidth > 0f) + { + drawList.AddRectFilled( + windowPos, + windowPos + new Vector2(accentWidth, windowSize.Y), + ImGui.ColorConvertFloat4ToU32(accentColor), + 3f + ); + } + } + + private void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList) + { + 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), + new Vector2(windowPos.X + windowSize.X, progressY + progressHeight), + ImGui.ColorConvertFloat4ToU32(bgProgressColor), + 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); + + var titleHeight = DrawTitle(notification, contentSize.X, alpha); + DrawMessage(notification, contentPos, contentSize.X, titleHeight, alpha); + + if (notification.Actions.Count > 0) + { + 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) + { + 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 contentWidth = _configService.Current.NotificationWidth - 35f; + var height = 12f; + + height += CalculateTitleHeight(notification, contentWidth); + height += CalculateMessageHeight(notification, contentWidth); + + if (notification.ShowProgress) + { + height += 12f; + } + + if (notification.Actions.Count > 0) + { + height += ImGui.GetStyle().ItemSpacing.Y; + height += ImGui.GetFrameHeight(); + height += 12f; + } + + 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) + { + return type switch + { + 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") + }; + } +} \ 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..3c6edea --- /dev/null +++ b/LightlessSync/UI/Models/LightlessNotification.cs @@ -0,0 +1,32 @@ +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 float AnimationProgress { get; set; } = 0f; + public bool IsAnimatingIn { get; set; } = true; + public bool IsAnimatingOut { get; set; } = false; + public uint? SoundEffectId { get; set; } = null; +} +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/Models/NotificationSounds.cs b/LightlessSync/UI/Models/NotificationSounds.cs new file mode 100644 index 0000000..fc74256 --- /dev/null +++ b/LightlessSync/UI/Models/NotificationSounds.cs @@ -0,0 +1,72 @@ +using LightlessSync.LightlessConfiguration.Models; + +namespace LightlessSync.UI.Models; + +/// +/// 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; // 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 = 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 () + /// + public const uint Info = Se2; + + /// + /// Warning/alert sound () + /// + public const uint Warning = Se15; + + /// + /// Error sound ( - System warning, used for errors) + /// + public const uint Error = Se15; + + /// + /// Success sound () + /// + public const uint Success = Se14; + + /// + /// Pair request sound (, same as tell notification) + /// + public const uint PairRequest = Se13; + + /// + /// Download complete sound (, a clean sparkle tone) + /// + public const uint DownloadComplete = Se10; + + /// + /// 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/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 1b3038a..ee6db90 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -63,6 +63,7 @@ public class SettingsUi : WindowMediatorSubscriberBase private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress; private readonly NameplateService _nameplateService; private readonly NameplateHandler _nameplateHandler; + private readonly NotificationService _lightlessNotificationService; private (int, int, FileCacheEntity) _currentProgress; private bool _deleteAccountPopupModalShown = false; private bool _deleteFilesPopupModalShown = false; @@ -78,17 +79,13 @@ public class SettingsUi : WindowMediatorSubscriberBase private bool _selectGeneralTabOnNextDraw = false; private bool _openLightfinderSectionOnNextDraw = false; private static readonly LightlessConfig DefaultConfig = new(); + 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) }; @@ -110,7 +107,9 @@ 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, + NotificationService lightlessNotificationService) : base(logger, mediator, "Lightless Sync Settings", + performanceCollector) { _configService = configService; _themeConfigService = themeConfigService; @@ -131,14 +130,14 @@ 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); 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()); @@ -152,7 +151,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; } @@ -545,9 +545,10 @@ public class SettingsUi : WindowMediatorSubscriberBase 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)) @@ -569,9 +570,11 @@ public class SettingsUi : WindowMediatorSubscriberBase { ImGui.TextUnformatted(item.Hash); } + ImGui.TableNextColumn(); ImGui.TextUnformatted(item.ForbiddenBy); } + ImGui.EndTable(); } } @@ -599,6 +602,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], @@ -624,6 +628,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)) @@ -631,6 +636,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); @@ -641,7 +647,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) @@ -655,6 +663,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) @@ -667,7 +676,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 @@ -682,19 +693,27 @@ 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."); + ImGuiHelpers.ScaledDummy(5); + bool showTransferWindow = _configService.Current.ShowTransferWindow; if (ImGui.Checkbox("Show separate transfer window", ref showTransferWindow)) { _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}" + @@ -706,6 +725,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { _uiShared.EditTrackerPosition = editTransferWindowPosition; } + ImGui.Unindent(); if (!_configService.Current.ShowTransferWindow) ImGui.EndDisabled(); @@ -715,7 +735,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(); @@ -725,6 +747,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)) @@ -732,21 +755,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(); @@ -757,6 +786,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(); @@ -772,30 +802,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(); @@ -803,6 +844,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _speedTestCts = new(); } } + if (_speedTestTask != null && _speedTestTask.IsCompleted) { if (_speedTestTask.Result != null && _speedTestTask.Result.Count != 0) @@ -814,11 +856,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); } @@ -853,6 +897,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { ImGui.TextUnformatted(transfer.Hash); } + ImGui.TableNextColumn(); ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Transferred)); ImGui.TableNextColumn(); @@ -861,6 +906,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.EndTable(); } + ImGui.Separator(); ImGui.TextUnformatted("Downloads"); if (ImGui.BeginTable("DownloadsTable", 4)) @@ -876,7 +922,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(); @@ -885,7 +932,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(); @@ -921,7 +969,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)); @@ -944,8 +994,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"); } @@ -971,6 +1023,7 @@ public class SettingsUi : WindowMediatorSubscriberBase st?.Stop(); } } + return speedTestResults; } @@ -978,9 +1031,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) { @@ -998,7 +1055,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}"); } @@ -1010,13 +1069,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) => @@ -1031,7 +1092,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)) { @@ -1039,6 +1102,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { _performanceCollector.PrintPerformanceStats(); } + ImGui.SameLine(); if (_uiShared.IconTextButton(FontAwesomeIcon.StickyNote, "Print Performance Stats (last 60s) to /xllog")) { @@ -1052,7 +1116,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); @@ -1065,12 +1132,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(); @@ -1082,7 +1151,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(); @@ -1092,6 +1162,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _cacheMonitor.StartLightlessWatcher(_configService.Current.CacheFolder); } } + if (_cacheMonitor.LightlessWatcher == null || _cacheMonitor.PenumbraWatcher == null) { if (_uiShared.IconTextButton(FontAwesomeIcon.Play, "Resume Monitoring")) @@ -1100,9 +1171,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 { @@ -1113,32 +1186,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) @@ -1151,8 +1234,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")) { @@ -1162,26 +1247,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")) @@ -1190,9 +1281,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(); @@ -1208,12 +1301,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}"); } } @@ -1231,12 +1326,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(() => { @@ -1253,9 +1351,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(); @@ -1285,8 +1388,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; @@ -1296,14 +1402,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); @@ -1320,7 +1429,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)) @@ -1328,7 +1439,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(); @@ -1369,6 +1482,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)) @@ -1376,7 +1490,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)) { @@ -1599,6 +1715,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { _configService.Current.LightfinderLabelOffsetX = 0; @@ -1607,10 +1724,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)) @@ -1621,6 +1740,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { _configService.Current.LightfinderLabelOffsetY = 0; @@ -1629,6 +1749,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."); @@ -1641,6 +1762,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { _configService.Current.LightfinderLabelScale = 1.0f; @@ -1649,6 +1771,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."); @@ -1663,7 +1786,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) { @@ -1715,6 +1840,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } + _uiShared.DrawHelpText("Toggles your own Lightfinder label."); var showPaired = _configService.Current.LightfinderLabelShowPaired; @@ -1726,6 +1852,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } + _uiShared.DrawHelpText("Toggles paired player(s) Lightfinder label."); var showHidden = _configService.Current.LightfinderLabelShowHidden; @@ -1761,6 +1888,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _lightfinderIconPresetIndex = -1; } } + _uiShared.DrawHelpText("Switch between the Lightfinder text label and an icon on nameplates."); if (useIcon) @@ -1835,7 +1963,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 { @@ -1859,17 +1988,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); @@ -1879,30 +2005,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)) @@ -1913,9 +2041,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(); } @@ -1924,6 +2055,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { UIColors.ResetAll(); } + _uiShared.DrawHelpText("This will reset all theme colors to their default values"); ImGui.Spacing(); @@ -1937,7 +2069,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."); ImGui.BeginDisabled(!useColorsInDtr); const ImGuiTableFlags serverInfoTableFlags = ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit; @@ -1994,6 +2128,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)) @@ -2005,18 +2140,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; @@ -2044,6 +2182,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.useColoredUIDs = usePairColoredUIDs; _configService.Save(); } + _uiShared.DrawHelpText("This changes the vanity colored UID's in pair list."); DrawThemeOverridesSection(); @@ -2062,7 +2201,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)) { @@ -2081,7 +2222,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)) { @@ -2100,7 +2243,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)) { @@ -2108,6 +2253,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)) @@ -2116,7 +2262,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(); @@ -2126,6 +2274,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(); @@ -2135,6 +2284,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.UseFocusTarget = useFocusTarget; _configService.Save(); } + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } @@ -2148,6 +2298,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(); @@ -2157,12 +2308,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(); @@ -2172,6 +2325,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); @@ -2182,48 +2336,11 @@ 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(); @@ -2231,45 +2348,14 @@ 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() { _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; @@ -2280,14 +2366,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(); @@ -2297,8 +2389,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; @@ -2309,9 +2404,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)) @@ -2319,9 +2417,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"); } @@ -2332,7 +2433,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; @@ -2344,40 +2446,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; @@ -2387,9 +2508,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)) @@ -2397,9 +2521,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"); } @@ -2413,7 +2540,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); @@ -2422,14 +2550,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); @@ -2447,6 +2578,7 @@ public class SettingsUi : WindowMediatorSubscriberBase } } } + using (ImRaii.Disabled(_selectedEntry == -1)) { if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete selected UID")) @@ -2477,7 +2609,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."); @@ -2486,7 +2619,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))) { @@ -2504,6 +2637,7 @@ public class SettingsUi : WindowMediatorSubscriberBase UiSharedService.SetScaledWindowSize(325); ImGui.EndPopup(); } + ImGui.SameLine(); if (ImGui.Button("Delete account")) { @@ -2513,7 +2647,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."); @@ -2560,14 +2695,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(); @@ -2584,7 +2723,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; @@ -2595,30 +2736,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; @@ -2626,10 +2778,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) { @@ -2639,49 +2793,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; @@ -2705,24 +2880,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; @@ -2749,11 +2931,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) @@ -2761,20 +2948,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."); @@ -2790,13 +2985,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(); } @@ -2807,7 +3004,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(); @@ -2824,24 +3022,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()) @@ -2851,10 +3054,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(); } @@ -2872,6 +3074,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { selectedServer.ServerUri = serverUri; } + if (isMain) { _uiShared.DrawHelpText("You cannot edit the URI of the main service."); @@ -2882,6 +3085,7 @@ public class SettingsUi : WindowMediatorSubscriberBase selectedServer.ServerName = serverName; _serverConfigurationManager.Save(); } + if (isMain) { _uiShared.DrawHelpText("You cannot edit the name of the main service."); @@ -2889,12 +3093,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) @@ -2905,7 +3113,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."); @@ -2918,20 +3128,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); } } @@ -2939,10 +3155,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"); } @@ -2954,26 +3172,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); @@ -2982,18 +3209,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; @@ -3004,28 +3234,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(); } @@ -3036,10 +3274,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) @@ -3060,7 +3301,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://"); @@ -3071,8 +3314,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."); @@ -3110,7 +3355,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() @@ -3151,7 +3397,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(); @@ -3165,6 +3412,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { Util.OpenLink("https://discord.gg/Lightless"); } + ImGui.Separator(); if (ImGui.BeginTabBar("mainTabBar")) { @@ -3205,6 +3453,12 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.EndTabItem(); } + if (ImGui.BeginTabItem("Notifications")) + { + DrawNotificationSettings(); + ImGui.EndTabItem(); + } + if (ImGui.BeginTabItem("Debug")) { DrawDebug(); @@ -3225,4 +3479,743 @@ 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 Lightless Notifications", ref useLightlessNotifications)) + { + _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."); + + ImGuiHelpers.ScaledDummy(5); + ImGui.Separator(); + + if (_uiShared.MediumTreeNode("Notification Locations", UIColors.Get("LightlessPurple"))) + { + UiSharedService.ColorTextWrapped("Choose where each notification type appears.", ImGuiColors.DalamudGrey); + ImGuiHelpers.ScaledDummy(5); + + if (useLightlessNotifications) + { + // 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(); + } + + _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 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."); + + 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."); + + ImGui.Spacing(); + ImGui.TextUnformatted("Size & Layout"); + + float notifWidth = _configService.Current.NotificationWidth; + if (ImGui.SliderFloat("Notification Width", ref notifWidth, 250f, 600f, "%.0f")) + { + _configService.Current.NotificationWidth = notifWidth; + _configService.Save(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _configService.Current.NotificationWidth = 350f; + _configService.Save(); + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Right click to reset to default (350)."); + _uiShared.DrawHelpText("Width of notification windows."); + + 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"); + + 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."); + + 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"))) + { + 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); + 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. Configure the sound in the Sound Settings table above.", + ImGuiColors.DalamudGrey); + ImGuiHelpers.ScaledDummy(3); + + _uiShared.ColoredSeparator(UIColors.Get("LightlessBlue"), 1.5f); + ImGui.TreePop(); + } + + 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(); + // Location descriptions removed - information is now inline with each setting + } + } + + private NotificationLocation[] GetLightlessNotificationLocations() + { + return new[] + { + NotificationLocation.LightlessUi, NotificationLocation.ChatAndLightlessUi, NotificationLocation.Nowhere + }; + } + + private NotificationLocation[] GetDownloadNotificationLocations() + { + return new[] + { + NotificationLocation.LightlessUi, NotificationLocation.ChatAndLightlessUi, + NotificationLocation.TextOverlay, 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", + NotificationLocation.TextOverlay => "Text Overlay", + _ => location.ToString() + }; + } + + private void DrawSoundTable() + { + var soundEffects = new[] + { + (1u, "Se1 - Soft chime"), (2u, "Se2 - Higher chime"), (3u, "Se3 - Bell tone"), (4u, "Se4 - Harp tone"), + (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, + 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, 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, 16u), + ("Error", 2, _configService.Current.CustomErrorSoundId, _configService.Current.DisableErrorSound, 16u), + ("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) + { + ImGui.TableNextRow(); + + // Type column + ImGui.TableSetColumnIndex(0); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(typeName); + + // Sound picker column + ImGui.TableSetColumnIndex(1); + using (ImRaii.Disabled(isDisabled)) + { + 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; + case 4: _configService.Current.DownloadSoundId = newSoundId; break; + } + + _configService.Save(); + } + } + + // Actions column + ImGui.TableSetColumnIndex(2); + var availableWidth = ImGui.GetContentRegionAvail().X; + var buttonWidth = (availableWidth - ImGui.GetStyle().ItemSpacing.X * 2) / 3; + + // Play button + using var playId = ImRaii.PushId($"Play_{typeIndex}"); + using (ImRaii.Disabled(isDisabled)) + { + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + if (ImGui.Button(FontAwesomeIcon.Play.ToIconString(), new Vector2(buttonWidth, 0))) + { + try + { + FFXIVClientStructs.FFXIV.Client.UI.UIGlobals.PlayChatSoundEffect(currentSoundId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to play test sound"); + } + } + } + } + UiSharedService.AttachToolTip("Test this 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; + case 4: _configService.Current.DisableDownloadSound = newDisabled; break; + } + _configService.Save(); + } + + 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/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index 47f8f86..2a6a236 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; @@ -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; @@ -33,13 +29,14 @@ public class TopTabMenu private bool _pairRequestsExpanded; // useless for now private int _lastRequestCount; private readonly UiSharedService _uiSharedService; + 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) + public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService) { _lightlessMediator = lightlessMediator; _apiController = apiController; @@ -47,6 +44,7 @@ public class TopTabMenu _pairRequestService = pairRequestService; _dalamudUtilService = dalamudUtilService; _uiSharedService = uiSharedService; + _lightlessNotificationService = lightlessNotificationService; } private enum SelectedTab @@ -199,16 +197,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")) + { + _lightlessNotificationService.ShowPairRequestNotification( + "Debug User", + "debug-user-id", + onAccept: () => + { + _lightlessMediator.Publish(new NotificationMessage( + "Pair Accepted", + "Debug pair request was accepted!", + NotificationType.Info, + TimeSpan.FromSeconds(3))); + }, + onDecline: () => + { + _lightlessMediator.Publish(new NotificationMessage( + "Pair Declined", + "Debug pair request was declined.", + NotificationType.Warning, + TimeSpan.FromSeconds(3))); + } + ); + } + + ImGui.SameLine(); + if (ImGui.Button("Test Info")) { - var fakeCid = Guid.NewGuid().ToString("N"); - var display = _pairRequestService.RegisterIncomingRequest(fakeCid, "Debug pair request"); _lightlessMediator.Publish(new NotificationMessage( - "Pair request received (debug)", - display.Message, + "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))); } + + ImGui.SameLine(); + if (ImGui.Button("Test Warning")) + { + _lightlessMediator.Publish(new NotificationMessage( + "Warning", + "This is a test warning notification.", + NotificationType.Warning, + TimeSpan.FromSeconds(7))); + } + + ImGui.SameLine(); + if (ImGui.Button("Test Error")) + { + _lightlessMediator.Publish(new NotificationMessage( + "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 DrawIncomingPairRequests(availableWidth); @@ -850,4 +911,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..e3c1fbc 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; @@ -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,25 +105,27 @@ public partial class ApiController return Task.CompletedTask; } - public Task Client_ReceiveBroadcastPairRequest(UserPairNotificationDto dto) { if (dto == null) return Task.CompletedTask; var request = _pairRequestService.RegisterIncomingRequest(dto.myHashedCid, dto.message ?? string.Empty); + var senderName = string.IsNullOrEmpty(request.DisplayName) ? "Unknown User" : request.DisplayName; - Mediator.Publish(new NotificationMessage( - "Pair request received", - request.Message, - NotificationType.Info, - TimeSpan.FromSeconds(5))); + _lightlessNotificationService.ShowPairRequestNotification( + senderName, + request.HashedCid, + onAccept: () => _pairRequestService.AcceptPairRequest(request.HashedCid), + onDecline: () => _pairRequestService.DeclinePairRequest(request.HashedCid)); 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..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,6 +32,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL private readonly ServerConfigurationManager _serverManager; private readonly TokenProvider _tokenProvider; private readonly LightlessConfigService _lightlessConfigService; + private readonly NotificationService _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, NotificationService 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());