diff --git a/LightlessSync/Changelog/changelog.yaml b/LightlessSync/Changelog/changelog.yaml index db2397d..7055a90 100644 --- a/LightlessSync/Changelog/changelog.yaml +++ b/LightlessSync/Changelog/changelog.yaml @@ -25,6 +25,7 @@ changelog: - "More customizable notification options." - "Perfomance limiter shows as notifications." - "All notifications can be configured or disabled in Settings → Notifications." + - "Cleaning up notifications implementation" - number: "Bugfixes" icon: "" items: diff --git a/LightlessSync/Changelog/credits.yaml b/LightlessSync/Changelog/credits.yaml index b04b1e6..d685978 100644 --- a/LightlessSync/Changelog/credits.yaml +++ b/LightlessSync/Changelog/credits.yaml @@ -51,12 +51,12 @@ credits: role: "Height offset integration" - name: "Honorific Team" role: "Title system integration" - - name: "Moodles Team" - role: "Status effect integration" - - name: "PetNicknames Team" - role: "Pet naming integration" - - name: "Brio Team" - role: "GPose enhancement integration" + - name: "Glyceri" + role: "Moodles - Status effect integration" + - name: "Glyceri" + role: "PetNicknames - Pet naming integration" + - name: "Minmoose" + role: "Brio - GPose enhancement integration" - category: "Special Thanks" items: diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 9ec4bed..01a4de4 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -255,9 +255,9 @@ 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>(), + collection.AddScoped((s) => + new LightlessNotificationUi( + s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); @@ -269,8 +269,7 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService(), s.GetServices(), 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/CharacterAnalyzer.cs b/LightlessSync/Services/CharacterAnalyzer.cs index c35fd01..27235f6 100644 --- a/LightlessSync/Services/CharacterAnalyzer.cs +++ b/LightlessSync/Services/CharacterAnalyzer.cs @@ -6,7 +6,11 @@ using LightlessSync.UI; using LightlessSync.Utils; using Lumina.Data.Files; using Microsoft.Extensions.Logging; - +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace LightlessSync.Services; public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable @@ -16,6 +20,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable private CancellationTokenSource? _analysisCts; private CancellationTokenSource _baseAnalysisCts = new(); private string _lastDataHash = string.Empty; + private CharacterAnalysisSummary _latestSummary = CharacterAnalysisSummary.Empty; public CharacterAnalyzer(ILogger logger, LightlessMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer) : base(logger, mediator) @@ -34,6 +39,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable public bool IsAnalysisRunning => _analysisCts != null; public int TotalFiles { get; internal set; } internal Dictionary> LastAnalysis { get; } = []; + public CharacterAnalysisSummary LatestSummary => _latestSummary; public void CancelAnalyze() { @@ -80,6 +86,8 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable } } + RecalculateSummary(); + Mediator.Publish(new CharacterDataAnalyzedMessage()); _analysisCts.CancelDispose(); @@ -137,11 +145,39 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable LastAnalysis[obj.Key] = data; } + RecalculateSummary(); + Mediator.Publish(new CharacterDataAnalyzedMessage()); _lastDataHash = charaData.DataHash.Value; } + private void RecalculateSummary() + { + var builder = ImmutableDictionary.CreateBuilder(); + + foreach (var (objectKind, entries) in LastAnalysis) + { + long totalTriangles = 0; + long texOriginalBytes = 0; + long texCompressedBytes = 0; + + foreach (var entry in entries.Values) + { + totalTriangles += entry.Triangles; + if (string.Equals(entry.FileType, "tex", StringComparison.OrdinalIgnoreCase)) + { + texOriginalBytes += entry.OriginalSize; + texCompressedBytes += entry.CompressedSize; + } + } + + builder[objectKind] = new CharacterAnalysisObjectSummary(entries.Count, totalTriangles, texOriginalBytes, texCompressedBytes); + } + + _latestSummary = new CharacterAnalysisSummary(builder.ToImmutable()); + } + private void PrintAnalysis() { if (LastAnalysis.Count == 0) return; @@ -232,4 +268,24 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable } }); } +} + +public readonly record struct CharacterAnalysisObjectSummary(int EntryCount, long TotalTriangles, long TexOriginalBytes, long TexCompressedBytes) +{ + public bool HasEntries => EntryCount > 0; +} + +public sealed class CharacterAnalysisSummary +{ + public static CharacterAnalysisSummary Empty { get; } = + new(ImmutableDictionary.Empty); + + internal CharacterAnalysisSummary(IImmutableDictionary objects) + { + Objects = objects; + } + + public IImmutableDictionary Objects { get; } + + public bool HasData => Objects.Any(kvp => kvp.Value.HasEntries); } \ No newline at end of file diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index 8a724b4..79434c2 100644 --- a/LightlessSync/Services/Mediator/Messages.cs +++ b/LightlessSync/Services/Mediator/Messages.cs @@ -108,7 +108,9 @@ public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase; public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase; public record SyncshellBroadcastsUpdatedMessage : MessageBase; +public record PairRequestReceivedMessage(string HashedCid, string Message) : MessageBase; public record PairRequestsUpdatedMessage : MessageBase; +public record PairDownloadStatusMessage(List<(string PlayerName, float Progress, string Status)> DownloadStatus, int QueueWaiting) : MessageBase; public record VisibilityChange : MessageBase; #pragma warning restore S2094 #pragma warning restore MA0048 // File name must match type name \ No newline at end of file diff --git a/LightlessSync/Services/NotificationService.cs b/LightlessSync/Services/NotificationService.cs index 755e756..72f4a16 100644 --- a/LightlessSync/Services/NotificationService.cs +++ b/LightlessSync/Services/NotificationService.cs @@ -45,7 +45,9 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ public Task StartAsync(CancellationToken cancellationToken) { Mediator.Subscribe(this, HandleNotificationMessage); + Mediator.Subscribe(this, HandlePairRequestReceived); Mediator.Subscribe(this, HandlePairRequestsUpdated); + Mediator.Subscribe(this, HandlePairDownloadStatus); Mediator.Subscribe(this, HandlePerformanceNotification); return Task.CompletedTask; } @@ -293,33 +295,8 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ 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, + private string BuildPairDownloadMessage(List<(string PlayerName, float Progress, string Status)> userDownloads, int queueWaiting) { var messageParts = new List(); @@ -331,7 +308,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ if (userDownloads.Count > 0) { - var completedCount = userDownloads.Count(x => x.progress >= 1.0f); + var completedCount = userDownloads.Count(x => x.Progress >= 1.0f); messageParts.Add($"Progress: {completedCount}/{userDownloads.Count} completed"); } @@ -344,29 +321,29 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ return string.Join("\n", messageParts); } - private string BuildActiveDownloadLines(List<(string playerName, float progress, string status)> userDownloads) + private string BuildActiveDownloadLines(List<(string PlayerName, float Progress, string Status)> userDownloads) { var activeDownloads = userDownloads - .Where(x => x.progress < 1.0f) + .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)}")); + return string.Join("\n", activeDownloads.Select(x => $"• {x.PlayerName}: {FormatDownloadStatus(x)}")); } - private string FormatDownloadStatus((string playerName, float progress, string status) download) => - download.status switch + private string FormatDownloadStatus((string PlayerName, float Progress, string Status) download) => + download.Status switch { - "downloading" => $"{download.progress:P0}", + "downloading" => $"{download.Progress:P0}", "decompressing" => "decompressing", "queued" => "queued", "waiting" => "waiting for slot", - _ => download.status + _ => download.Status }; - private bool AreAllDownloadsCompleted(List<(string playerName, float progress, string status)> userDownloads) => - userDownloads.Any() && userDownloads.All(x => x.progress >= 1.0f); + 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")); @@ -581,12 +558,25 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ _chatGui.Print(se.BuiltString); } + private void HandlePairRequestReceived(PairRequestReceivedMessage msg) + { + var request = _pairRequestService.RegisterIncomingRequest(msg.HashedCid, msg.Message); + var senderName = string.IsNullOrEmpty(request.DisplayName) ? "Unknown User" : request.DisplayName; + + _shownPairRequestNotifications.Add(request.HashedCid); + ShowPairRequestNotification( + senderName, + request.HashedCid, + onAccept: () => _pairRequestService.AcceptPairRequest(request.HashedCid, senderName), + onDecline: () => _pairRequestService.DeclinePairRequest(request.HashedCid, senderName)); + } + private void HandlePairRequestsUpdated(PairRequestsUpdatedMessage _) { var activeRequests = _pairRequestService.GetActiveRequests(); var activeRequestIds = activeRequests.Select(r => r.HashedCid).ToHashSet(); - // Dismiss notifications for requests that are no longer active + // Dismiss notifications for requests that are no longer active (expired) var notificationsToRemove = _shownPairRequestNotifications .Where(hashedCid => !activeRequestIds.Contains(hashedCid)) .ToList(); @@ -597,17 +587,30 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ Mediator.Publish(new LightlessNotificationDismissMessage(notificationId)); _shownPairRequestNotifications.Remove(hashedCid); } + } - // Show/update notifications for all active requests - foreach (var request in activeRequests) + private void HandlePairDownloadStatus(PairDownloadStatusMessage msg) + { + var userDownloads = msg.DownloadStatus.Where(x => x.PlayerName != "Pair Queue").ToList(); + var totalProgress = userDownloads.Count > 0 ? userDownloads.Average(x => x.Progress) : 0f; + var message = BuildPairDownloadMessage(userDownloads, msg.QueueWaiting); + + var notification = new LightlessNotification { - _shownPairRequestNotifications.Add(request.HashedCid); - ShowPairRequestNotification( - request.DisplayName, - request.HashedCid, - () => _pairRequestService.AcceptPairRequest(request.HashedCid, request.DisplayName), - () => _pairRequestService.DeclinePairRequest(request.HashedCid) - ); + 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 (userDownloads.Count == 0 || AreAllDownloadsCompleted(userDownloads)) + { + Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress")); } } diff --git a/LightlessSync/Services/PairRequestService.cs b/LightlessSync/Services/PairRequestService.cs index 92294e2..7190825 100644 --- a/LightlessSync/Services/PairRequestService.cs +++ b/LightlessSync/Services/PairRequestService.cs @@ -19,7 +19,12 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(5); - public PairRequestService(ILogger logger, LightlessMediator mediator, DalamudUtilService dalamudUtil, PairManager pairManager, Lazy apiController) + public PairRequestService( + ILogger logger, + LightlessMediator mediator, + DalamudUtilService dalamudUtil, + PairManager pairManager, + Lazy apiController) : base(logger, mediator) { _dalamudUtil = dalamudUtil; @@ -215,9 +220,13 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase }); } - public void DeclinePairRequest(string hashedCid) + public void DeclinePairRequest(string hashedCid, string displayName) { RemoveRequest(hashedCid); + Mediator.Publish(new NotificationMessage("Pair request declined", + "Declined " + displayName + "'s pending pair request.", + NotificationType.Info, + TimeSpan.FromSeconds(3))); Logger.LogDebug("Declined pair request from {HashedCid}", hashedCid); } diff --git a/LightlessSync/Services/UiService.cs b/LightlessSync/Services/UiService.cs index 3740114..f08b1fc 100644 --- a/LightlessSync/Services/UiService.cs +++ b/LightlessSync/Services/UiService.cs @@ -23,8 +23,7 @@ public sealed class UiService : DisposableMediatorSubscriberBase LightlessConfigService lightlessConfigService, WindowSystem windowSystem, IEnumerable windows, UiFactory uiFactory, FileDialogManager fileDialogManager, - LightlessMediator lightlessMediator, - NotificationService notificationService) : base(logger, lightlessMediator) + LightlessMediator lightlessMediator) : base(logger, lightlessMediator) { _logger = logger; _logger.LogTrace("Creating {type}", GetType().Name); diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index c264681..0700de3 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -56,7 +56,6 @@ public class CompactUi : WindowMediatorSubscriberBase private readonly BroadcastService _broadcastService; private List _drawFolders; - private Dictionary>? _cachedAnalysis; private Pair? _lastAddedUser; private string _lastAddedUserComment = string.Empty; private Vector2 _lastPosition = Vector2.One; @@ -382,15 +381,26 @@ public class CompactUi : WindowMediatorSubscriberBase _uiSharedService.IconText(FontAwesomeIcon.Upload); ImGui.SameLine(35 * ImGuiHelpers.GlobalScale); - if (currentUploads.Any()) + if (currentUploads.Count > 0) { - var totalUploads = currentUploads.Count; + int totalUploads = currentUploads.Count; + int doneUploads = 0; + long totalUploaded = 0; + long totalToUpload = 0; - var doneUploads = currentUploads.Count(c => c.IsTransferred); - var activeUploads = currentUploads.Count(c => !c.IsTransferred); + foreach (var upload in currentUploads) + { + if (upload.IsTransferred) + { + doneUploads++; + } + + totalUploaded += upload.Transferred; + totalToUpload += upload.Total; + } + + int activeUploads = totalUploads - doneUploads; var uploadSlotLimit = Math.Clamp(_configService.Current.ParallelUploads, 1, 8); - var totalUploaded = currentUploads.Sum(c => c.Transferred); - var totalToUpload = currentUploads.Sum(c => c.Total); ImGui.TextUnformatted($"{doneUploads}/{totalUploads} (slots {activeUploads}/{uploadSlotLimit})"); var uploadText = $"({UiSharedService.ByteToString(totalUploaded)}/{UiSharedService.ByteToString(totalToUpload)})"; @@ -405,17 +415,17 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.TextUnformatted("No uploads in progress"); } - var currentDownloads = BuildCurrentDownloadSnapshot(); + var downloadSummary = GetDownloadSummary(); ImGui.AlignTextToFramePadding(); _uiSharedService.IconText(FontAwesomeIcon.Download); ImGui.SameLine(35 * ImGuiHelpers.GlobalScale); - if (currentDownloads.Any()) + if (downloadSummary.HasDownloads) { - var totalDownloads = currentDownloads.Sum(c => c.TotalFiles); - var doneDownloads = currentDownloads.Sum(c => c.TransferredFiles); - var totalDownloaded = currentDownloads.Sum(c => c.TransferredBytes); - var totalToDownload = currentDownloads.Sum(c => c.TotalBytes); + var totalDownloads = downloadSummary.TotalFiles; + var doneDownloads = downloadSummary.TransferredFiles; + var totalDownloaded = downloadSummary.TransferredBytes; + var totalToDownload = downloadSummary.TotalBytes; ImGui.TextUnformatted($"{doneDownloads}/{totalDownloads}"); var downloadText = @@ -433,27 +443,35 @@ public class CompactUi : WindowMediatorSubscriberBase } - private List BuildCurrentDownloadSnapshot() + private DownloadSummary GetDownloadSummary() { - List snapshot = new(); + long totalBytes = 0; + long transferredBytes = 0; + int totalFiles = 0; + int transferredFiles = 0; foreach (var kvp in _currentDownloads.ToArray()) { - var value = kvp.Value; - if (value == null || value.Count == 0) + if (kvp.Value is not { Count: > 0 } statuses) + { continue; - - try - { - snapshot.AddRange(value.Values.ToArray()); } - catch (System.ArgumentException) + + foreach (var status in statuses.Values) { - // skibidi + totalBytes += status.TotalBytes; + transferredBytes += status.TransferredBytes; + totalFiles += status.TotalFiles; + transferredFiles += status.TransferredFiles; } } - return snapshot; + return new DownloadSummary(totalFiles, transferredFiles, transferredBytes, totalBytes); + } + + private readonly record struct DownloadSummary(int TotalFiles, int TransferredFiles, long TransferredBytes, long TotalBytes) + { + public bool HasDownloads => TotalFiles > 0 || TotalBytes > 0; } private void DrawUIDHeader() @@ -480,7 +498,7 @@ public class CompactUi : WindowMediatorSubscriberBase } //Getting information of character and triangles threshold to show overlimit status in UID bar. - _cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone(); + var analysisSummary = _characterAnalyzer.LatestSummary; Vector2 uidTextSize, iconSize; using (_uiSharedService.UidFont.Push()) @@ -509,6 +527,7 @@ public class CompactUi : WindowMediatorSubscriberBase if (ImGui.IsItemHovered()) { ImGui.BeginTooltip(); + ImGui.PushTextWrapPos(ImGui.GetFontSize() * 32f); ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("PairBlue")); ImGui.Text("Lightfinder"); @@ -556,6 +575,7 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.PopStyleColor(); } + ImGui.PopTextWrapPos(); ImGui.EndTooltip(); } @@ -574,7 +594,7 @@ public class CompactUi : WindowMediatorSubscriberBase var seString = SeStringUtils.BuildFormattedPlayerName(uidText, vanityTextColor, vanityGlowColor); var cursorPos = ImGui.GetCursorScreenPos(); var fontPtr = ImGui.GetFont(); - SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr); + SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr, "uid-header"); } else { @@ -591,56 +611,40 @@ public class CompactUi : WindowMediatorSubscriberBase UiSharedService.AttachToolTip("Click to copy"); - if (_cachedAnalysis != null && _apiController.ServerState is ServerState.Connected) + if (_apiController.ServerState is ServerState.Connected && analysisSummary.HasData) { - var firstEntry = _cachedAnalysis.FirstOrDefault(); - var valueDict = firstEntry.Value; - if (valueDict != null && valueDict.Count > 0) + var objectSummary = analysisSummary.Objects.Values.FirstOrDefault(summary => summary.HasEntries); + if (objectSummary.HasEntries) { - var groupedfiles = valueDict - .Select(v => v.Value) - .Where(v => v != null) - .GroupBy(f => f.FileType, StringComparer.Ordinal) - .OrderBy(k => k.Key, StringComparer.Ordinal) - .ToList(); + var actualVramUsage = objectSummary.TexOriginalBytes; + var actualTriCount = objectSummary.TotalTriangles; - var actualTriCount = valueDict - .Select(v => v.Value) - .Where(v => v != null) - .Sum(f => f.Triangles); + var isOverVRAMUsage = _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024 < actualVramUsage; + var isOverTriHold = actualTriCount > (_playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000); - if (groupedfiles != null) + if ((isOverTriHold || isOverVRAMUsage) && _playerPerformanceConfig.Current.WarnOnExceedingThresholds) { - //Checking of VRAM threshhold - var texGroup = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal)); - var actualVramUsage = texGroup != null ? texGroup.Sum(f => f.OriginalSize) : 0L; - var isOverVRAMUsage = _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024 < actualVramUsage; - var isOverTriHold = actualTriCount > (_playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000); + ImGui.SameLine(); + ImGui.SetCursorPosY(cursorY + 15f); + _uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow")); - if ((isOverTriHold || isOverVRAMUsage) && _playerPerformanceConfig.Current.WarnOnExceedingThresholds) + string warningMessage = ""; + if (isOverTriHold) { - ImGui.SameLine(); - ImGui.SetCursorPosY(cursorY + 15f); - _uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow")); + warningMessage += $"You exceed your own triangles threshold by " + + $"{actualTriCount - _playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000} triangles."; + warningMessage += Environment.NewLine; - string warningMessage = ""; - if (isOverTriHold) - { - warningMessage += $"You exceed your own triangles threshold by " + - $"{actualTriCount - _playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000} triangles."; - warningMessage += Environment.NewLine; - - } - if (isOverVRAMUsage) - { - warningMessage += $"You exceed your own VRAM threshold by " + - $"{UiSharedService.ByteToString(actualVramUsage - (_playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024))}."; - } - UiSharedService.AttachToolTip(warningMessage); - if (ImGui.IsItemClicked()) - { - _lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi))); - } + } + if (isOverVRAMUsage) + { + warningMessage += $"You exceed your own VRAM threshold by " + + $"{UiSharedService.ByteToString(actualVramUsage - (_playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024))}."; + } + UiSharedService.AttachToolTip(warningMessage); + if (ImGui.IsItemClicked()) + { + _lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi))); } } } @@ -663,7 +667,7 @@ public class CompactUi : WindowMediatorSubscriberBase var seString = SeStringUtils.BuildFormattedPlayerName(_apiController.UID, vanityTextColor, vanityGlowColor); var cursorPos = ImGui.GetCursorScreenPos(); var fontPtr = ImGui.GetFont(); - SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr); + SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr, "uid-footer"); } else { @@ -921,4 +925,4 @@ public class CompactUi : WindowMediatorSubscriberBase _wasOpen = IsOpen; IsOpen = false; } -} \ No newline at end of file +} diff --git a/LightlessSync/UI/Components/DrawUserPair.cs b/LightlessSync/UI/Components/DrawUserPair.cs index fa5022e..4c4c1d4 100644 --- a/LightlessSync/UI/Components/DrawUserPair.cs +++ b/LightlessSync/UI/Components/DrawUserPair.cs @@ -2,6 +2,7 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; +using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.User; @@ -13,6 +14,9 @@ using LightlessSync.Services.ServerConfiguration; using LightlessSync.UI.Handlers; using LightlessSync.Utils; using LightlessSync.WebAPI; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text; namespace LightlessSync.UI.Components; @@ -32,6 +36,8 @@ public class DrawUserPair private readonly CharaDataManager _charaDataManager; private float _menuWidth = -1; private bool _wasHovered = false; + private TooltipSnapshot _tooltipSnapshot = TooltipSnapshot.Empty; + private string _cachedTooltip = string.Empty; public DrawUserPair(string id, Pair entry, List syncedGroups, GroupFullInfoDto? currentGroup, @@ -190,15 +196,12 @@ public class DrawUserPair private void DrawLeftSide() { - string userPairText = string.Empty; - ImGui.AlignTextToFramePadding(); if (_pair.IsPaused) { using var _ = ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow")); _uiSharedService.IconText(FontAwesomeIcon.PauseCircle); - userPairText = _pair.UserData.AliasOrUID + " is paused"; } else if (!_pair.IsOnline) { @@ -207,12 +210,10 @@ public class DrawUserPair ? FontAwesomeIcon.ArrowsLeftRight : (_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.Bidirectional ? FontAwesomeIcon.User : FontAwesomeIcon.Users)); - userPairText = _pair.UserData.AliasOrUID + " is offline"; } else if (_pair.IsVisible) { _uiSharedService.IconText(FontAwesomeIcon.Eye, UIColors.Get("LightlessBlue")); - userPairText = _pair.UserData.AliasOrUID + " is visible: " + _pair.PlayerName + Environment.NewLine + "Click to target this player"; if (ImGui.IsItemClicked()) { _mediator.Publish(new TargetPairMessage(_pair)); @@ -223,46 +224,9 @@ public class DrawUserPair using var _ = ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("PairBlue")); _uiSharedService.IconText(_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.Bidirectional ? FontAwesomeIcon.User : FontAwesomeIcon.Users); - userPairText = _pair.UserData.AliasOrUID + " is online"; } - if (_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.OneSided) - { - userPairText += UiSharedService.TooltipSeparator + "User has not added you back"; - } - else if (_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.Bidirectional) - { - userPairText += UiSharedService.TooltipSeparator + "You are directly Paired"; - } - - if (_pair.LastAppliedDataBytes >= 0) - { - userPairText += UiSharedService.TooltipSeparator; - userPairText += ((!_pair.IsPaired) ? "(Last) " : string.Empty) + "Mods Info" + Environment.NewLine; - userPairText += "Files Size: " + UiSharedService.ByteToString(_pair.LastAppliedDataBytes, true); - if (_pair.LastAppliedApproximateVRAMBytes >= 0) - { - userPairText += Environment.NewLine + "Approx. VRAM Usage: " + UiSharedService.ByteToString(_pair.LastAppliedApproximateVRAMBytes, true); - } - if (_pair.LastAppliedDataTris >= 0) - { - userPairText += Environment.NewLine + "Approx. Triangle Count (excl. Vanilla): " - + (_pair.LastAppliedDataTris > 1000 ? (_pair.LastAppliedDataTris / 1000d).ToString("0.0'k'") : _pair.LastAppliedDataTris); - } - } - - if (_syncedGroups.Any()) - { - userPairText += UiSharedService.TooltipSeparator + string.Join(Environment.NewLine, - _syncedGroups.Select(g => - { - var groupNote = _serverConfigurationManager.GetNoteForGid(g.GID); - var groupString = string.IsNullOrEmpty(groupNote) ? g.GroupAliasOrGID : $"{groupNote} ({g.GroupAliasOrGID})"; - return "Paired through " + groupString; - })); - } - - UiSharedService.AttachToolTip(userPairText); + UiSharedService.AttachToolTip(GetUserTooltip()); if (_performanceConfigService.Current.ShowPerformanceIndicator && !_performanceConfigService.Current.UIDsToIgnore @@ -327,6 +291,143 @@ public class DrawUserPair _displayHandler.DrawPairText(_id, _pair, leftSide, () => rightSide - leftSide); } + private string GetUserTooltip() + { + List? groupDisplays = null; + if (_syncedGroups.Count > 0) + { + groupDisplays = new List(_syncedGroups.Count); + foreach (var group in _syncedGroups) + { + var groupNote = _serverConfigurationManager.GetNoteForGid(group.GID); + groupDisplays.Add(string.IsNullOrEmpty(groupNote) ? group.GroupAliasOrGID : $"{groupNote} ({group.GroupAliasOrGID})"); + } + } + + var snapshot = new TooltipSnapshot( + _pair.IsPaused, + _pair.IsOnline, + _pair.IsVisible, + _pair.IndividualPairStatus, + _pair.UserData.AliasOrUID, + _pair.PlayerName ?? string.Empty, + _pair.LastAppliedDataBytes, + _pair.LastAppliedApproximateVRAMBytes, + _pair.LastAppliedDataTris, + _pair.IsPaired, + groupDisplays is null ? ImmutableArray.Empty : ImmutableArray.CreateRange(groupDisplays)); + + if (!_tooltipSnapshot.Equals(snapshot)) + { + _cachedTooltip = BuildTooltip(snapshot); + _tooltipSnapshot = snapshot; + } + + return _cachedTooltip; + } + + private static string BuildTooltip(in TooltipSnapshot snapshot) + { + var builder = new StringBuilder(256); + + if (snapshot.IsPaused) + { + builder.Append(snapshot.AliasOrUid); + builder.Append(" is paused"); + } + else if (!snapshot.IsOnline) + { + builder.Append(snapshot.AliasOrUid); + builder.Append(" is offline"); + } + else if (snapshot.IsVisible) + { + builder.Append(snapshot.AliasOrUid); + builder.Append(" is visible: "); + builder.Append(snapshot.PlayerName); + builder.Append(Environment.NewLine); + builder.Append("Click to target this player"); + } + else + { + builder.Append(snapshot.AliasOrUid); + builder.Append(" is online"); + } + + if (snapshot.PairStatus == IndividualPairStatus.OneSided) + { + builder.Append(UiSharedService.TooltipSeparator); + builder.Append("User has not added you back"); + } + else if (snapshot.PairStatus == IndividualPairStatus.Bidirectional) + { + builder.Append(UiSharedService.TooltipSeparator); + builder.Append("You are directly Paired"); + } + + if (snapshot.LastAppliedDataBytes >= 0) + { + builder.Append(UiSharedService.TooltipSeparator); + if (!snapshot.IsPaired) + { + builder.Append("(Last) "); + } + builder.Append("Mods Info"); + builder.Append(Environment.NewLine); + builder.Append("Files Size: "); + builder.Append(UiSharedService.ByteToString(snapshot.LastAppliedDataBytes, true)); + + if (snapshot.LastAppliedApproximateVRAMBytes >= 0) + { + builder.Append(Environment.NewLine); + builder.Append("Approx. VRAM Usage: "); + builder.Append(UiSharedService.ByteToString(snapshot.LastAppliedApproximateVRAMBytes, true)); + } + + if (snapshot.LastAppliedDataTris >= 0) + { + builder.Append(Environment.NewLine); + builder.Append("Approx. Triangle Count (excl. Vanilla): "); + builder.Append(snapshot.LastAppliedDataTris > 1000 + ? (snapshot.LastAppliedDataTris / 1000d).ToString("0.0'k'") + : snapshot.LastAppliedDataTris); + } + } + + if (!snapshot.GroupDisplays.IsEmpty) + { + builder.Append(UiSharedService.TooltipSeparator); + for (int i = 0; i < snapshot.GroupDisplays.Length; i++) + { + if (i > 0) + { + builder.Append(Environment.NewLine); + } + builder.Append("Paired through "); + builder.Append(snapshot.GroupDisplays[i]); + } + } + + return builder.ToString(); + } + + private readonly record struct TooltipSnapshot( + bool IsPaused, + bool IsOnline, + bool IsVisible, + IndividualPairStatus PairStatus, + string AliasOrUid, + string PlayerName, + long LastAppliedDataBytes, + long LastAppliedApproximateVRAMBytes, + long LastAppliedDataTris, + bool IsPaired, + ImmutableArray GroupDisplays) + { + public static TooltipSnapshot Empty { get; } = + new(false, false, false, IndividualPairStatus.None, string.Empty, string.Empty, -1, -1, -1, false, ImmutableArray.Empty); + } + private void DrawPairedClientMenu() { DrawIndividualMenu(); diff --git a/LightlessSync/UI/DownloadUi.cs b/LightlessSync/UI/DownloadUi.cs index 1b1ec16..a592e43 100644 --- a/LightlessSync/UI/DownloadUi.cs +++ b/LightlessSync/UI/DownloadUi.cs @@ -22,13 +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; private int _lastDownloadStateHash = 0; public DownloadUi(ILogger logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService, PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared, - PerformanceCollectorService performanceCollectorService, NotificationService notificationService) + PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService) { _dalamudUtilService = dalamudUtilService; @@ -36,7 +35,6 @@ public class DownloadUi : WindowMediatorSubscriberBase _pairProcessingLimiter = pairProcessingLimiter; _fileTransferManager = fileTransferManager; _uiShared = uiShared; - _notificationService = notificationService; SizeConstraints = new WindowSizeConstraints() { @@ -359,7 +357,7 @@ public class DownloadUi : WindowMediatorSubscriberBase _lastDownloadStateHash = currentHash; if (downloadStatus.Count > 0 || queueWaiting > 0) { - _notificationService.ShowPairDownloadNotification(downloadStatus, queueWaiting); + Mediator.Publish(new PairDownloadStatusMessage(downloadStatus, queueWaiting)); } } } diff --git a/LightlessSync/UI/EditProfileUi.cs b/LightlessSync/UI/EditProfileUi.cs index 696c2e8..5dedf81 100644 --- a/LightlessSync/UI/EditProfileUi.cs +++ b/LightlessSync/UI/EditProfileUi.cs @@ -281,7 +281,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase { _uiSharedService.MediumText("Supporter Vanity Settings", UIColors.Get("LightlessPurple")); ImGui.Dummy(new Vector2(4)); - _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "Must be a supporter through Patreon/Ko-fi to access these settings."); + _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "Must be a supporter through Patreon/Ko-fi to access these settings. If you have the vanity role, you must interact with the Discord bot first."); var hasVanity = _apiController.HasVanity; diff --git a/LightlessSync/UI/Handlers/IdDisplayHandler.cs b/LightlessSync/UI/Handlers/IdDisplayHandler.cs index 01f0df6..4d362a9 100644 --- a/LightlessSync/UI/Handlers/IdDisplayHandler.cs +++ b/LightlessSync/UI/Handlers/IdDisplayHandler.cs @@ -157,7 +157,7 @@ public class IdDisplayHandler Vector2 textSize; using (ImRaii.PushFont(font, textIsUid)) { - SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, font); + SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, font, pair.UserData.UID); itemMin = ImGui.GetItemRectMin(); itemMax = ImGui.GetItemRectMax(); //textSize = itemMax - itemMin; diff --git a/LightlessSync/UI/LightlessNotificationUI.cs b/LightlessSync/UI/LightlessNotificationUI.cs index 2ac26b7..3d2d748 100644 --- a/LightlessSync/UI/LightlessNotificationUI.cs +++ b/LightlessSync/UI/LightlessNotificationUI.cs @@ -15,17 +15,17 @@ using Dalamud.Bindings.ImGui; namespace LightlessSync.UI; -public class LightlessNotificationUI : WindowMediatorSubscriberBase +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 const float ContentPaddingX = 10f; - private const float ContentPaddingY = 6f; - private const float TitleMessageSpacing = 4f; - private const float ActionButtonSpacing = 8f; + 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 const float _contentPaddingX = 10f; + private const float _contentPaddingY = 6f; + private const float _titleMessageSpacing = 4f; + private const float _actionButtonSpacing = 8f; private readonly List _notifications = new(); private readonly object _notificationLock = new(); @@ -33,7 +33,7 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase private readonly Dictionary _notificationYOffsets = new(); private readonly Dictionary _notificationTargetYOffsets = new(); - public LightlessNotificationUI(ILogger logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService) + public LightlessNotificationUi(ILogger logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService) : base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector) { _configService = configService; @@ -155,8 +155,8 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase var width = _configService.Current.NotificationWidth; float posX = corner == NotificationCorner.Left - ? viewport.WorkPos.X + offsetX - WindowPaddingOffset - : viewport.WorkPos.X + viewport.WorkSize.X - width - offsetX - WindowPaddingOffset; + ? viewport.WorkPos.X + offsetX - _windowPaddingOffset + : viewport.WorkPos.X + viewport.WorkSize.X - width - offsetX - _windowPaddingOffset; return new Vector2(posX, viewport.WorkPos.Y); } @@ -274,7 +274,7 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase else if (notification.IsAnimatingOut && notification.AnimationProgress > 0f) { notification.AnimationProgress = Math.Max(0f, - notification.AnimationProgress - deltaTime * _configService.Current.NotificationAnimationSpeed * OutAnimationSpeedMultiplier); + notification.AnimationProgress - deltaTime * _configService.Current.NotificationAnimationSpeed * _outAnimationSpeedMultiplier); } else if (!notification.IsAnimatingOut && !notification.IsDismissed) { @@ -289,7 +289,7 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase private Vector2 CalculateSlideOffset(float alpha) { - var distance = (1f - alpha) * SlideAnimationDistance; + var distance = (1f - alpha) * _slideAnimationDistance; var corner = _configService.Current.NotificationCorner; return corner == NotificationCorner.Left ? new Vector2(-distance, 0) : new Vector2(distance, 0); } @@ -466,7 +466,7 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase private void DrawNotificationText(LightlessNotification notification, float alpha) { - var contentPos = new Vector2(ContentPaddingX, ContentPaddingY); + var contentPos = new Vector2(_contentPaddingX, _contentPaddingY); var windowSize = ImGui.GetWindowSize(); var contentWidth = CalculateContentWidth(windowSize.X); @@ -483,7 +483,7 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase } private float CalculateContentWidth(float windowWidth) => - windowWidth - (ContentPaddingX * 2); + windowWidth - (_contentPaddingX * 2); private bool HasActions(LightlessNotification notification) => notification.Actions.Count > 0; @@ -491,9 +491,9 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase private void PositionActionsAtBottom(float windowHeight) { var actionHeight = ImGui.GetFrameHeight(); - var bottomY = windowHeight - ContentPaddingY - actionHeight; + var bottomY = windowHeight - _contentPaddingY - actionHeight; ImGui.SetCursorPosY(bottomY); - ImGui.SetCursorPosX(ContentPaddingX); + ImGui.SetCursorPosX(_contentPaddingX); } private float DrawTitle(LightlessNotification notification, float contentWidth, float alpha) @@ -530,7 +530,7 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase { if (string.IsNullOrEmpty(notification.Message)) return; - var messagePos = contentPos + new Vector2(0f, titleHeight + TitleMessageSpacing); + var messagePos = contentPos + new Vector2(0f, titleHeight + _titleMessageSpacing); var messageColor = new Vector4(0.9f, 0.9f, 0.9f, alpha); ImGui.SetCursorPos(messagePos); @@ -563,13 +563,13 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase private float CalculateActionButtonWidth(int actionCount, float availableWidth) { - var totalSpacing = (actionCount - 1) * ActionButtonSpacing; + var totalSpacing = (actionCount - 1) * _actionButtonSpacing; return (availableWidth - totalSpacing) / actionCount; } private void PositionActionButton(int index, float startX, float buttonWidth) { - var xPosition = startX + index * (buttonWidth + ActionButtonSpacing); + var xPosition = startX + index * (buttonWidth + _actionButtonSpacing); ImGui.SetCursorPosX(xPosition); } @@ -687,7 +687,7 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase height += 12f; } - return Math.Clamp(height, NotificationMinHeight, NotificationMaxHeight); + return Math.Clamp(height, _notificationMinHeight, _notificationMaxHeight); } private float CalculateTitleHeight(LightlessNotification notification, float contentWidth) diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 3b87baa..febc142 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -63,7 +63,6 @@ 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; @@ -107,8 +106,7 @@ public class SettingsUi : WindowMediatorSubscriberBase IpcManager ipcManager, CacheMonitor cacheMonitor, DalamudUtilService dalamudUtilService, HttpClient httpClient, NameplateService nameplateService, - NameplateHandler nameplateHandler, - NotificationService lightlessNotificationService) : base(logger, mediator, "Lightless Sync Settings", + NameplateHandler nameplateHandler) : base(logger, mediator, "Lightless Sync Settings", performanceCollector) { _configService = configService; @@ -130,7 +128,6 @@ 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); @@ -3616,20 +3613,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_pair", new Vector2(availableWidth, 0))) { - _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)); - } - ); + Mediator.Publish(new PairRequestReceivedMessage("test-uid-123", "Test User wants to pair with you.")); } } UiSharedService.AttachToolTip("Test pair request notification"); @@ -3652,15 +3636,14 @@ public class SettingsUi : WindowMediatorSubscriberBase { if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_download", new Vector2(availableWidth, 0))) { - _lightlessNotificationService.ShowPairDownloadNotification( - new List<(string playerName, float progress, string status)> - { + Mediator.Publish(new PairDownloadStatusMessage( + [ ("Player One", 0.35f, "downloading"), ("Player Two", 0.75f, "downloading"), ("Player Three", 1.0f, "downloading") - }, - queueWaiting: 2 - ); + ], + 2 + )); } } UiSharedService.AttachToolTip("Test download progress notification"); diff --git a/LightlessSync/UI/UpdateNotesUi.cs b/LightlessSync/UI/UpdateNotesUi.cs index f7544e1..bc60ab5 100644 --- a/LightlessSync/UI/UpdateNotesUi.cs +++ b/LightlessSync/UI/UpdateNotesUi.cs @@ -52,12 +52,12 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase private float _particleSpawnTimer; private readonly Random _random = new(); - private const float HeaderHeight = 150f; - private const float ParticleSpawnInterval = 0.2f; - private const int MaxParticles = 50; - private const int MaxTrailLength = 50; - private const float EdgeFadeDistance = 30f; - private const float ExtendedParticleHeight = 40f; + private const float _headerHeight = 150f; + private const float _particleSpawnInterval = 0.2f; + private const int _maxParticles = 50; + private const int _maxTrailLength = 50; + private const float _edgeFadeDistance = 30f; + private const float _extendedParticleHeight = 40f; public UpdateNotesUi(ILogger logger, LightlessMediator mediator, @@ -111,16 +111,16 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase var headerWidth = (800f * ImGuiHelpers.GlobalScale) - (windowPadding.X * 2); var headerStart = windowPos + new Vector2(windowPadding.X, windowPadding.Y); - var headerEnd = headerStart + new Vector2(headerWidth, HeaderHeight); + var headerEnd = headerStart + new Vector2(headerWidth, _headerHeight); - var extendedParticleSize = new Vector2(headerWidth, HeaderHeight + ExtendedParticleHeight); + var extendedParticleSize = new Vector2(headerWidth, _headerHeight + _extendedParticleHeight); DrawGradientBackground(headerStart, headerEnd); DrawHeaderText(headerStart); DrawHeaderButtons(headerStart, headerWidth); DrawBottomGradient(headerStart, headerEnd, headerWidth); - ImGui.SetCursorPosY(windowPadding.Y + HeaderHeight + 5); + ImGui.SetCursorPosY(windowPadding.Y + _headerHeight + 5); ImGui.SetCursorPosX(20); using (ImRaii.PushFont(UiBuilder.IconFont)) { @@ -260,7 +260,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase var deltaTime = ImGui.GetIO().DeltaTime; _particleSpawnTimer += deltaTime; - if (_particleSpawnTimer > ParticleSpawnInterval && _particles.Count < MaxParticles) + if (_particleSpawnTimer > _particleSpawnInterval && _particles.Count < _maxParticles) { SpawnParticle(bannerSize); _particleSpawnTimer = 0f; @@ -282,7 +282,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase if (particle.Type == ParticleType.ShootingStar && particle.Trail != null) { particle.Trail.Insert(0, particle.Position); - if (particle.Trail.Count > MaxTrailLength) + if (particle.Trail.Count > _maxTrailLength) particle.Trail.RemoveAt(particle.Trail.Count - 1); } @@ -316,12 +316,12 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase var lifeFade = Math.Min(fadeIn, fadeOut); var edgeFadeX = Math.Min( - Math.Min(1f, (particle.Position.X + EdgeFadeDistance) / EdgeFadeDistance), - Math.Min(1f, (bannerSize.X - particle.Position.X + EdgeFadeDistance) / EdgeFadeDistance) + Math.Min(1f, (particle.Position.X + _edgeFadeDistance) / _edgeFadeDistance), + Math.Min(1f, (bannerSize.X - particle.Position.X + _edgeFadeDistance) / _edgeFadeDistance) ); var edgeFadeY = Math.Min( - Math.Min(1f, (particle.Position.Y + EdgeFadeDistance) / EdgeFadeDistance), - Math.Min(1f, (bannerSize.Y - particle.Position.Y + EdgeFadeDistance) / EdgeFadeDistance) + Math.Min(1f, (particle.Position.Y + _edgeFadeDistance) / _edgeFadeDistance), + Math.Min(1f, (bannerSize.Y - particle.Position.Y + _edgeFadeDistance) / _edgeFadeDistance) ); var edgeFade = Math.Min(edgeFadeX, edgeFadeY); diff --git a/LightlessSync/Utils/SeStringUtils.cs b/LightlessSync/Utils/SeStringUtils.cs index a19a343..7507515 100644 --- a/LightlessSync/Utils/SeStringUtils.cs +++ b/LightlessSync/Utils/SeStringUtils.cs @@ -7,6 +7,7 @@ using Dalamud.Interface.Utility; using Lumina.Text; using System; using System.Numerics; +using System.Threading; using DalamudSeString = Dalamud.Game.Text.SeStringHandling.SeString; using DalamudSeStringBuilder = Dalamud.Game.Text.SeStringHandling.SeStringBuilder; using LuminaSeStringBuilder = Lumina.Text.SeStringBuilder; @@ -15,6 +16,9 @@ namespace LightlessSync.Utils; public static class SeStringUtils { + private static int _seStringHitboxCounter; + private static int _iconHitboxCounter; + public static DalamudSeString BuildFormattedPlayerName(string text, Vector4? textColor, Vector4? glowColor) { var b = new DalamudSeStringBuilder(); @@ -119,7 +123,7 @@ public static class SeStringUtils ImGui.Dummy(new Vector2(0f, textSize.Y)); } - public static Vector2 RenderSeStringWithHitbox(DalamudSeString seString, Vector2 position, ImFontPtr? font = null) + public static Vector2 RenderSeStringWithHitbox(DalamudSeString seString, Vector2 position, ImFontPtr? font = null, string? id = null) { var drawList = ImGui.GetWindowDrawList(); @@ -137,12 +141,28 @@ public static class SeStringUtils var textSize = ImGui.CalcTextSize(seString.TextValue); ImGui.SetCursorScreenPos(position); - ImGui.InvisibleButton($"##hitbox_{Guid.NewGuid()}", textSize); + if (id is not null) + { + ImGui.PushID(id); + } + else + { + ImGui.PushID(Interlocked.Increment(ref _seStringHitboxCounter)); + } + + try + { + ImGui.InvisibleButton("##hitbox", textSize); + } + finally + { + ImGui.PopID(); + } return textSize; } - public static Vector2 RenderIconWithHitbox(int iconId, Vector2 position, ImFontPtr? font = null) + public static Vector2 RenderIconWithHitbox(int iconId, Vector2 position, ImFontPtr? font = null, string? id = null) { var drawList = ImGui.GetWindowDrawList(); @@ -158,7 +178,23 @@ public static class SeStringUtils var drawResult = ImGuiHelpers.CompileSeStringWrapped(iconMacro, drawParams); ImGui.SetCursorScreenPos(position); - ImGui.InvisibleButton($"##iconHitbox_{Guid.NewGuid()}", drawResult.Size); + if (id is not null) + { + ImGui.PushID(id); + } + else + { + ImGui.PushID(Interlocked.Increment(ref _iconHitboxCounter)); + } + + try + { + ImGui.InvisibleButton("##iconHitbox", drawResult.Size); + } + finally + { + ImGui.PopID(); + } return drawResult.Size; } diff --git a/LightlessSync/WebAPI/Files/FileDownloadManager.cs b/LightlessSync/WebAPI/Files/FileDownloadManager.cs index cc82d04..b8f81f2 100644 --- a/LightlessSync/WebAPI/Files/FileDownloadManager.cs +++ b/LightlessSync/WebAPI/Files/FileDownloadManager.cs @@ -215,6 +215,26 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase await Task.Delay(retryDelay, ct).ConfigureAwait(false); } + catch (TaskCanceledException ex) when (!ct.IsCancellationRequested) + { + response?.Dispose(); + retryCount++; + + Logger.LogWarning(ex, "Cancellation/timeout during download of {requestUrl}. Attempt {attempt} of {maxRetries}", requestUrl, retryCount, maxRetries); + + if (retryCount >= maxRetries) + { + Logger.LogError("Max retries reached for {requestUrl} after TaskCanceledException", requestUrl); + throw; + } + + await Task.Delay(retryDelay, ct).ConfigureAwait(false); + } + catch (OperationCanceledException) when (ct.IsCancellationRequested) + { + response?.Dispose(); + throw; + } catch (HttpRequestException ex) { response?.Dispose(); diff --git a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs index da07460..8323fc3 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs @@ -107,17 +107,17 @@ public partial class ApiController } public Task Client_ReceiveBroadcastPairRequest(UserPairNotificationDto dto) { - if (dto == null) + Logger.LogDebug("Client_ReceiveBroadcastPairRequest: {dto}", dto); + + if (dto is null) + { return Task.CompletedTask; + } - var request = _pairRequestService.RegisterIncomingRequest(dto.myHashedCid, dto.message ?? string.Empty); - var senderName = string.IsNullOrEmpty(request.DisplayName) ? "Unknown User" : request.DisplayName; - - _lightlessNotificationService.ShowPairRequestNotification( - senderName, - request.HashedCid, - onAccept: () => _pairRequestService.AcceptPairRequest(request.HashedCid, senderName), - onDecline: () => _pairRequestService.DeclinePairRequest(request.HashedCid)); + ExecuteSafely(() => + { + Mediator.Publish(new PairRequestReceivedMessage(dto.myHashedCid, dto.message ?? string.Empty)); + }); return Task.CompletedTask; } diff --git a/LightlessSync/WebAPI/SignalR/ApiController.cs b/LightlessSync/WebAPI/SignalR/ApiController.cs index 56ab36e..15aef29 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.cs @@ -32,7 +32,6 @@ 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; @@ -54,7 +53,6 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL _serverManager = serverManager; _tokenProvider = tokenProvider; _lightlessConfigService = lightlessConfigService; - _lightlessNotificationService = lightlessNotificationService; _connectionCancellationTokenSource = new CancellationTokenSource(); Mediator.Subscribe(this, (_) => DalamudUtilOnLogIn());