From 0ec423e65c0b49444b73dcb6666c3b6853636f76 Mon Sep 17 00:00:00 2001 From: choco Date: Sun, 21 Dec 2025 22:34:39 +0100 Subject: [PATCH 01/28] potential resolve disposal crashes and race conditions --- .../LightFinder/LightFinderScannerService.cs | 52 ++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/LightlessSync/Services/LightFinder/LightFinderScannerService.cs b/LightlessSync/Services/LightFinder/LightFinderScannerService.cs index 52ff1dc..35c7d0b 100644 --- a/LightlessSync/Services/LightFinder/LightFinderScannerService.cs +++ b/LightlessSync/Services/LightFinder/LightFinderScannerService.cs @@ -1,4 +1,4 @@ -using Dalamud.Plugin.Services; +using Dalamud.Plugin.Services; using LightlessSync.API.Dto.User; using LightlessSync.Services.ActorTracking; using LightlessSync.Services.Mediator; @@ -33,6 +33,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase private const int _maxQueueSize = 100; private volatile bool _batchRunning = false; + private volatile bool _disposed = false; public IReadOnlyDictionary BroadcastCache => _broadcastCache; public readonly record struct BroadcastEntry(bool IsBroadcasting, DateTime ExpiryTime, string? GID); @@ -63,6 +64,9 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase public void Update() { + if (_disposed) + return; + _frameCounter++; var lookupsThisFrame = 0; @@ -104,7 +108,14 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase private async Task BatchUpdateBroadcastCacheAsync(List cids) { + if (_disposed) + return; + var results = await _broadcastService.AreUsersBroadcastingAsync(cids).ConfigureAwait(false); + + if (_disposed) + return; + var now = DateTime.UtcNow; foreach (var (cid, info) in results) @@ -123,6 +134,9 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase (_, old) => new BroadcastEntry(info.IsBroadcasting, expiry, info.GID)); } + if (_disposed) + return; + var activeCids = _broadcastCache .Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now) .Select(e => e.Key) @@ -134,6 +148,9 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg) { + if (_disposed) + return; + if (!msg.Enabled) { _broadcastCache.Clear(); @@ -147,6 +164,9 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase private void UpdateSyncshellBroadcasts() { + if (_disposed) + return; + var now = DateTime.UtcNow; var newSet = _broadcastCache .Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID)) @@ -230,17 +250,35 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase protected override void Dispose(bool disposing) { + _disposed = true; base.Dispose(disposing); _framework.Update -= OnFrameworkUpdate; - if (_cleanupTask != null) + + try { - _cleanupTask?.Wait(100, _cleanupCts.Token); + _cleanupCts.Cancel(); + } + catch (ObjectDisposedException) + { + // Already disposed, can be ignored :) } - _cleanupCts.Cancel(); - _cleanupCts.Dispose(); + try + { + _cleanupTask?.Wait(100); + } + catch (Exception) + { + // Task may have already completed or been cancelled? + } - _cleanupTask?.Wait(100); - _cleanupCts.Dispose(); + try + { + _cleanupCts.Dispose(); + } + catch (ObjectDisposedException) + { + // Already disposed, ignore + } } } From 4e03b381dc2a9cab6e90297adfdc5b39d28aa7e9 Mon Sep 17 00:00:00 2001 From: choco Date: Tue, 23 Dec 2025 00:48:47 +0100 Subject: [PATCH 02/28] animated header main menu redesign test --- LightlessAPI | 2 +- LightlessSync/UI/CompactUI.cs | 9 +++++++++ LightlessSync/UI/Style/MainStyle.cs | 7 ++++--- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/LightlessAPI b/LightlessAPI index 8e4432a..5656600 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 8e4432af45c1955436afe309c93e019577ad10e5 +Subproject commit 56566003e0e93bba05dcef49fd3ce23c6a204d81 diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index b1195b4..2c9e112 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -68,6 +68,7 @@ public class CompactUi : WindowMediatorSubscriberBase private bool _wasOpen; private float _windowContentWidth; private readonly SeluneBrush _seluneBrush = new(); + private readonly AnimatedHeader _animatedHeader = new() { Height = 120f, EnableBottomGradient = false }; private const float _connectButtonHighlightThickness = 14f; private Pair? _focusedPair; private Pair? _pendingFocusPair; @@ -153,6 +154,7 @@ public class CompactUi : WindowMediatorSubscriberBase public override void OnClose() { ForceReleaseFocus(); + _animatedHeader.ClearParticles(); base.OnClose(); } @@ -164,6 +166,13 @@ public class CompactUi : WindowMediatorSubscriberBase using var selune = Selune.Begin(_seluneBrush, drawList, windowPos, windowSize); _windowContentWidth = UiSharedService.GetWindowContentRegionWidth(); + + // Draw animated header background (just the gradient/particles, content drawn by existing methods) + var startCursorY = ImGui.GetCursorPosY(); + _animatedHeader.Draw(_windowContentWidth, (_, _) => { }); + // Reset cursor to draw content on top of the header background + ImGui.SetCursorPosY(startCursorY); + if (!_apiController.IsCurrentVersion) { var ver = _apiController.CurrentClientVersion; diff --git a/LightlessSync/UI/Style/MainStyle.cs b/LightlessSync/UI/Style/MainStyle.cs index 3da7455..53dd682 100644 --- a/LightlessSync/UI/Style/MainStyle.cs +++ b/LightlessSync/UI/Style/MainStyle.cs @@ -40,9 +40,10 @@ internal static class MainStyle new("color.frameBg", "Frame Background", () => Rgba(40, 40, 40, 255), ImGuiCol.FrameBg), new("color.frameBgHovered", "Frame Background (Hover)", () => Rgba(50, 50, 50, 100), ImGuiCol.FrameBgHovered), new("color.frameBgActive", "Frame Background (Active)", () => Rgba(30, 30, 30, 255), ImGuiCol.FrameBgActive), - new("color.titleBg", "Title Background", () => Rgba(24, 24, 24, 232), ImGuiCol.TitleBg), - new("color.titleBgActive", "Title Background (Active)", () => Rgba(30, 30, 30, 255), ImGuiCol.TitleBgActive), - new("color.titleBgCollapsed", "Title Background (Collapsed)", () => Rgba(27, 27, 27, 255), ImGuiCol.TitleBgCollapsed), + new("color.titleBg", "Title Background", () => Rgba(22, 14, 41, 255), ImGuiCol.TitleBg), + new("color.titleBgActive", "Title Background (Active)", () => Rgba(22, 14, 41, 255), ImGuiCol.TitleBgActive), + new("color.titleBgCollapsed", "Title Background (Collapsed)", () => Rgba(22, 14, 41, 255), ImGuiCol.TitleBgCollapsed), + new("color.menuBarBg", "Menu Bar Background", () => Rgba(36, 36, 36, 255), ImGuiCol.MenuBarBg), new("color.scrollbarBg", "Scrollbar Background", () => Rgba(0, 0, 0, 0), ImGuiCol.ScrollbarBg), new("color.scrollbarGrab", "Scrollbar Grab", () => Rgba(62, 62, 62, 255), ImGuiCol.ScrollbarGrab), From 5b81caf5a84ebe52e2b9b2db70b1dac83ce442f5 Mon Sep 17 00:00:00 2001 From: choco Date: Tue, 23 Dec 2025 17:16:51 +0100 Subject: [PATCH 03/28] compact menu redesign with new animated particle header, enable particles toggle added in UI settings --- .../Configurations/LightlessConfig.cs | 1 + LightlessSync/UI/CompactUI.cs | 189 ++++++++---------- LightlessSync/UI/SettingsUi.cs | 9 +- LightlessSync/UI/Style/AnimatedHeader.cs | 4 +- LightlessSync/UI/UpdateNotesUi.cs | 4 +- 5 files changed, 102 insertions(+), 105 deletions(-) diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs index 9b4055b..4987cc1 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs @@ -154,4 +154,5 @@ public class LightlessConfig : ILightlessConfiguration public bool SyncshellFinderEnabled { get; set; } = false; public string? SelectedFinderSyncshell { get; set; } = null; public string LastSeenVersion { get; set; } = string.Empty; + public bool EnableParticleEffects { get; set; } = true; } diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index 2c9e112..ec2ff0d 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -68,7 +68,7 @@ public class CompactUi : WindowMediatorSubscriberBase private bool _wasOpen; private float _windowContentWidth; private readonly SeluneBrush _seluneBrush = new(); - private readonly AnimatedHeader _animatedHeader = new() { Height = 120f, EnableBottomGradient = false }; + private readonly AnimatedHeader _animatedHeader = new(); private const float _connectButtonHighlightThickness = 14f; private Pair? _focusedPair; private Pair? _pendingFocusPair; @@ -128,6 +128,11 @@ public class CompactUi : WindowMediatorSubscriberBase .Apply(); _drawFolders = [.. DrawFolders]; + + _animatedHeader.Height = 120f; + _animatedHeader.EnableBottomGradient = true; + _animatedHeader.GradientHeight = 250f; + _animatedHeader.EnableParticles = _configService.Current.EnableParticleEffects; #if DEBUG string dev = "Dev Build"; @@ -218,17 +223,11 @@ public class CompactUi : WindowMediatorSubscriberBase } using (ImRaii.PushId("header")) DrawUIDHeader(); - _uiSharedService.RoundedSeparator(UIColors.Get("LightlessPurple"), 2.5f, 1f, 12f); - using (ImRaii.PushId("serverstatus")) - { - DrawServerStatus(); - } selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime); var style = ImGui.GetStyle(); var contentMinY = windowPos.Y + ImGui.GetWindowContentRegionMin().Y; var gradientInset = 4f * ImGuiHelpers.GlobalScale; var gradientTop = MathF.Max(contentMinY, ImGui.GetCursorScreenPos().Y - style.ItemSpacing.Y + gradientInset); - ImGui.Separator(); if (_apiController.ServerState is ServerState.Connected) { @@ -236,7 +235,6 @@ public class CompactUi : WindowMediatorSubscriberBase using (ImRaii.PushId("global-topmenu")) _tabMenu.Draw(pairSnapshot); using (ImRaii.PushId("pairlist")) DrawPairs(); - ImGui.Separator(); var transfersTop = ImGui.GetCursorScreenPos().Y; var gradientBottom = MathF.Max(gradientTop, transfersTop - style.ItemSpacing.Y - gradientInset); selune.DrawGradient(gradientTop, gradientBottom, ImGui.GetIO().DeltaTime); @@ -317,95 +315,6 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.EndChild(); } - private void DrawServerStatus() - { - var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Link); - var userCount = _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture); - var userSize = ImGui.CalcTextSize(userCount); - var textSize = ImGui.CalcTextSize("Users Online"); -#if DEBUG - string shardConnection = $"Shard: {_apiController.ServerInfo.ShardName}"; -#else - string shardConnection = string.Equals(_apiController.ServerInfo.ShardName, "Main", StringComparison.OrdinalIgnoreCase) ? string.Empty : $"Shard: {_apiController.ServerInfo.ShardName}"; -#endif - var shardTextSize = ImGui.CalcTextSize(shardConnection); - var printShard = !string.IsNullOrEmpty(_apiController.ServerInfo.ShardName) && shardConnection != string.Empty; - - if (_apiController.ServerState is ServerState.Connected) - { - ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth()) / 2 - (userSize.X + textSize.X) / 2 - ImGui.GetStyle().ItemSpacing.X / 2); - if (!printShard) ImGui.AlignTextToFramePadding(); - ImGui.TextColored(UIColors.Get("LightlessPurple"), userCount); - ImGui.SameLine(); - if (!printShard) ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("Users Online"); - } - else - { - ImGui.AlignTextToFramePadding(); - ImGui.TextColored(UIColors.Get("DimRed"), "Not connected to any server"); - } - - if (printShard) - { - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetStyle().ItemSpacing.Y); - ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth()) / 2 - shardTextSize.X / 2); - ImGui.TextUnformatted(shardConnection); - } - - ImGui.SameLine(); - if (printShard) - { - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((userSize.Y + textSize.Y) / 2 + shardTextSize.Y) / 2 - ImGui.GetStyle().ItemSpacing.Y + buttonSize.Y / 2); - } - bool isConnectingOrConnected = _apiController.ServerState is ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting; - var color = UiSharedService.GetBoolColor(!isConnectingOrConnected); - var connectedIcon = isConnectingOrConnected ? FontAwesomeIcon.Unlink : FontAwesomeIcon.Link; - - ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X); - if (printShard) - { - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((userSize.Y + textSize.Y) / 2 + shardTextSize.Y) / 2 - ImGui.GetStyle().ItemSpacing.Y + buttonSize.Y / 2); - } - - if (_apiController.ServerState is not (ServerState.Reconnecting or ServerState.Disconnecting)) - { - using (ImRaii.PushColor(ImGuiCol.Text, color)) - { - if (_uiSharedService.IconButton(connectedIcon)) - { - if (isConnectingOrConnected && !_serverManager.CurrentServer.FullPause) - { - _serverManager.CurrentServer.FullPause = true; - _serverManager.Save(); - } - else if (!isConnectingOrConnected && _serverManager.CurrentServer.FullPause) - { - _serverManager.CurrentServer.FullPause = false; - _serverManager.Save(); - } - - _ = _apiController.CreateConnectionsAsync(); - } - } - - if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive()) - { - Selune.RegisterHighlight( - ImGui.GetItemRectMin(), - ImGui.GetItemRectMax(), - SeluneHighlightMode.Both, - borderOnly: true, - borderThicknessOverride: _connectButtonHighlightThickness, - exactSize: true, - clipToElement: true, - roundingOverride: ImGui.GetStyle().FrameRounding); - } - - UiSharedService.AttachToolTip(isConnectingOrConnected ? "Disconnect from " + _serverManager.CurrentServer.ServerName : "Connect to " + _serverManager.CurrentServer.ServerName); - } - } - private void DrawTransfers() { var currentUploads = _fileTransferManager.GetCurrentUploadsSnapshot(); @@ -541,8 +450,7 @@ public class CompactUi : WindowMediatorSubscriberBase using (_uiSharedService.IconFont.Push()) iconSize = ImGui.CalcTextSize(FontAwesomeIcon.PersonCirclePlus.ToIconString()); - float contentWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; - float uidStartX = (contentWidth - uidTextSize.X) / 2f; + float uidStartX = 25f; float cursorY = ImGui.GetCursorPosY(); if (_configService.Current.BroadcastEnabled && _apiController.IsConnected) @@ -671,6 +579,12 @@ public class CompactUi : WindowMediatorSubscriberBase UiSharedService.AttachToolTip("Click to copy"); + // Connect/Disconnect button next to big UID + DrawConnectButton(cursorY, uidTextSize.Y); + + // Add spacing below the big UID + ImGuiHelpers.ScaledDummy(5f); + if (_apiController.ServerState is ServerState.Connected && analysisSummary.HasData) { var objectSummary = analysisSummary.Objects.Values.FirstOrDefault(summary => summary.HasEntries); @@ -717,10 +631,12 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.SetClipboardText(_apiController.DisplayName); } - if (!string.Equals(_apiController.DisplayName, _apiController.UID, StringComparison.Ordinal)) + // Only show smaller UID line if DisplayName differs from UID (custom vanity name) + bool hasCustomName = !string.Equals(_apiController.DisplayName, _apiController.UID, StringComparison.OrdinalIgnoreCase); + + if (hasCustomName) { - var origTextSize = ImGui.CalcTextSize(_apiController.UID); - ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) / 2 - (origTextSize.X / 2)); + ImGui.SetCursorPosX(uidStartX); if (useVanityColors) { @@ -755,14 +671,83 @@ public class CompactUi : WindowMediatorSubscriberBase { ImGui.SetClipboardText(_apiController.UID); } + + // Users Online on same line as smaller UID (with separator) + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("|"); + ImGui.SameLine(); + ImGui.TextColored(UIColors.Get("LightlessGreen"), _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture)); + ImGui.SameLine(); + ImGui.TextUnformatted("Users Online"); + } + else + { + // No custom name - just show Users Online aligned to uidStartX + ImGui.SetCursorPosX(uidStartX); + ImGui.TextColored(UIColors.Get("LightlessGreen"), _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture)); + ImGui.SameLine(); + ImGui.TextUnformatted("Users Online"); } } else { + ImGui.SetCursorPosX(uidStartX); UiSharedService.ColorTextWrapped(_apiController.ServerState.GetServerError(_apiController.AuthFailureMessage), uidColor); } } + private void DrawConnectButton(float cursorY, float buttonHeight) + { + var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Link); + bool isConnectingOrConnected = _apiController.ServerState is ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting; + var color = UiSharedService.GetBoolColor(!isConnectingOrConnected); + var connectedIcon = isConnectingOrConnected ? FontAwesomeIcon.Unlink : FontAwesomeIcon.Link; + + // Position next to big UID on the right side + + if (_apiController.ServerState is not (ServerState.Reconnecting or ServerState.Disconnecting)) + { + ImGui.SetCursorPosX(UiSharedService.GetWindowContentRegionWidth() - buttonSize.X - 13f); + ImGui.SetCursorPosY(buttonHeight); + + using (ImRaii.PushColor(ImGuiCol.Text, color)) + using (ImRaii.PushColor(ImGuiCol.Button, ImGui.ColorConvertFloat4ToU32(new(0, 0, 0, 0)))) + { + if (_uiSharedService.IconButton(connectedIcon, buttonSize.Y)) + { + if (isConnectingOrConnected && !_serverManager.CurrentServer.FullPause) + { + _serverManager.CurrentServer.FullPause = true; + _serverManager.Save(); + } + else if (!isConnectingOrConnected && _serverManager.CurrentServer.FullPause) + { + _serverManager.CurrentServer.FullPause = false; + _serverManager.Save(); + } + + _ = _apiController.CreateConnectionsAsync(); + } + } + + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive()) + { + Selune.RegisterHighlight( + ImGui.GetItemRectMin(), + ImGui.GetItemRectMax(), + SeluneHighlightMode.Both, + borderOnly: true, + borderThicknessOverride: _connectButtonHighlightThickness, + exactSize: true, + clipToElement: true, + roundingOverride: ImGui.GetStyle().FrameRounding); + } + + UiSharedService.AttachToolTip(isConnectingOrConnected ? "Disconnect from " + _serverManager.CurrentServer.ServerName : "Connect to " + _serverManager.CurrentServer.ServerName); + } + } + private IEnumerable DrawFolders { get diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 548fc75..580849e 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -2082,7 +2082,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Separator(); var openPopupOnAddition = _configService.Current.OpenPopupOnAdd; - + using (var popupTree = BeginGeneralTree("Popup & Auto Fill", UIColors.Get("LightlessPurple"))) { if (popupTree.Visible) @@ -2139,11 +2139,18 @@ public class SettingsUi : WindowMediatorSubscriberBase var groupInVisible = _configService.Current.ShowSyncshellUsersInVisible; var syncshellOfflineSeparate = _configService.Current.ShowSyncshellOfflineUsersSeparately; var greenVisiblePair = _configService.Current.ShowVisiblePairsGreenEye; + var enableParticleEffects = _configService.Current.EnableParticleEffects; using (var behaviorTree = BeginGeneralTree("Behavior", UIColors.Get("LightlessPurple"))) { if (behaviorTree.Visible) { + if (ImGui.Checkbox("Enable Particle Effects", ref enableParticleEffects)) + { + _configService.Current.EnableParticleEffects = enableParticleEffects; + _configService.Save(); + } + if (ImGui.Checkbox("Enable Game Right Click Menu Entries", ref enableRightClickMenu)) { _configService.Current.EnableRightClickMenus = enableRightClickMenu; diff --git a/LightlessSync/UI/Style/AnimatedHeader.cs b/LightlessSync/UI/Style/AnimatedHeader.cs index 15488ac..839c704 100644 --- a/LightlessSync/UI/Style/AnimatedHeader.cs +++ b/LightlessSync/UI/Style/AnimatedHeader.cs @@ -47,6 +47,8 @@ public class AnimatedHeader public Vector4 BottomColor { get; set; } = new(0.12f, 0.08f, 0.20f, 1.0f); public bool EnableParticles { get; set; } = true; public bool EnableBottomGradient { get; set; } = true; + + public float GradientHeight { get; set; } = 60f; /// /// Draws the animated header with some customizable content @@ -171,7 +173,7 @@ public class AnimatedHeader private void DrawBottomGradient(Vector2 headerStart, Vector2 headerEnd, float width) { var drawList = ImGui.GetWindowDrawList(); - var gradientHeight = 60f; + var gradientHeight = GradientHeight; for (int i = 0; i < gradientHeight; i++) { diff --git a/LightlessSync/UI/UpdateNotesUi.cs b/LightlessSync/UI/UpdateNotesUi.cs index e1b3ab3..d27e87c 100644 --- a/LightlessSync/UI/UpdateNotesUi.cs +++ b/LightlessSync/UI/UpdateNotesUi.cs @@ -40,6 +40,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase logger.LogInformation("UpdateNotesUi constructor called"); _uiShared = uiShared; _configService = configService; + _animatedHeader.EnableParticles = _configService.Current.EnableParticleEffects; RespectCloseHotkey = true; ShowCloseButton = true; @@ -48,7 +49,8 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove; PositionCondition = ImGuiCond.Always; - + + WindowBuilder.For(this) .AllowPinning(false) .AllowClickthrough(false) From 6c1cc77aaa5912e7c7996a08559b7c0095617dce Mon Sep 17 00:00:00 2001 From: choco Date: Tue, 23 Dec 2025 17:36:36 +0100 Subject: [PATCH 04/28] settings animated header --- LightlessSync/UI/SettingsUi.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 580849e..91f69d5 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -69,6 +69,8 @@ public class SettingsUi : WindowMediatorSubscriberBase private readonly UiSharedService _uiShared; private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress; private readonly NameplateService _nameplateService; + private readonly AnimatedHeader _animatedHeader = new(); + private (int, int, FileCacheEntity) _currentProgress; private bool _deleteAccountPopupModalShown = false; private bool _deleteFilesPopupModalShown = false; @@ -203,7 +205,10 @@ public class SettingsUi : WindowMediatorSubscriberBase _nameplateService = nameplateService; _actorObjectService = actorObjectService; _validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v); - + _animatedHeader.Height = 120f; + _animatedHeader.EnableBottomGradient = true; + _animatedHeader.GradientHeight = 250f; + _animatedHeader.EnableParticles = _configService.Current.EnableParticleEffects; WindowBuilder.For(this) .AllowPinning(true) .AllowClickthrough(false) @@ -239,6 +244,7 @@ public class SettingsUi : WindowMediatorSubscriberBase public override void OnClose() { + _animatedHeader.ClearParticles(); _uiShared.EditTrackerPosition = false; _uidToAddForIgnore = string.Empty; _secretKeysConversionCts = _secretKeysConversionCts.CancelRecreate(); @@ -253,8 +259,8 @@ public class SettingsUi : WindowMediatorSubscriberBase protected override void DrawInternal() { + _animatedHeader.Draw(ImGui.GetContentRegionAvail().X, (_, _) => { }); _ = _uiShared.DrawOtherPluginState(); - DrawSettingsContent(); } private static Vector3 PackedColorToVector3(uint color) @@ -2151,6 +2157,8 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Save(); } + _uiShared.DrawHelpText("This will enable particle effects in the UI."); + if (ImGui.Checkbox("Enable Game Right Click Menu Entries", ref enableRightClickMenu)) { _configService.Current.EnableRightClickMenus = enableRightClickMenu; From ced72ab9eb46b1980aacd26c97c3ea57d90f53c0 Mon Sep 17 00:00:00 2001 From: choco Date: Wed, 24 Dec 2025 16:59:46 +0100 Subject: [PATCH 05/28] icon centering changes --- LightlessSync/Plugin.cs | 3 +- LightlessSync/UI/CompactUI.cs | 168 +++++++++++++++++++--------------- 2 files changed, 94 insertions(+), 77 deletions(-) diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 8198bb3..425703a 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -556,7 +556,6 @@ public sealed class Plugin : IDalamudPlugin public void Dispose() { - _host.StopAsync().GetAwaiter().GetResult(); - _host.Dispose(); + _host.StopAsync().ContinueWith(_ => _host.Dispose()).Wait(TimeSpan.FromSeconds(5)); } } diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index ec2ff0d..e297e11 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -453,17 +453,49 @@ public class CompactUi : WindowMediatorSubscriberBase float uidStartX = 25f; float cursorY = ImGui.GetCursorPosY(); + ImGui.SetCursorPosY(cursorY); + ImGui.SetCursorPosX(uidStartX); + + bool headerItemClicked; + using (_uiSharedService.UidFont.Push()) + { + if (useVanityColors) + { + var seString = SeStringUtils.BuildFormattedPlayerName(uidText, vanityTextColor, vanityGlowColor); + var cursorPos = ImGui.GetCursorScreenPos(); + var targetFontSize = ImGui.GetFontSize(); + var font = ImGui.GetFont(); + SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, targetFontSize ,font , "uid-header"); + } + else + { + ImGui.TextColored(uidColor, uidText); + } + } + + // Get the actual rendered text rect for proper icon alignment + var uidTextRect = ImGui.GetItemRectMax() - ImGui.GetItemRectMin(); + var uidTextRectMin = ImGui.GetItemRectMin(); + var uidTextHovered = ImGui.IsItemHovered(); + headerItemClicked = ImGui.IsItemClicked(); + + // Track position for icons next to UID text + // Use uidTextSize.Y (actual font height) for vertical centering, not hitbox height + float nextIconX = uidTextRectMin.X + uidTextRect.X + 10f; + float iconYOffset = (uidTextSize.Y - iconSize.Y) * 0.5f; + float textVerticalOffset = (uidTextRect.Y - uidTextSize.Y) * 0.5f; + var buttonSize = new Vector2(iconSize.X, uidTextSize.Y); + if (_configService.Current.BroadcastEnabled && _apiController.IsConnected) { - float iconYOffset = (uidTextSize.Y - iconSize.Y) * 0.5f; - var buttonSize = new Vector2(iconSize.X, uidTextSize.Y); - - ImGui.SetCursorPos(new Vector2(ImGui.GetStyle().ItemSpacing.X + 5f, cursorY)); + ImGui.SetCursorScreenPos(new Vector2(nextIconX, uidTextRectMin.Y + textVerticalOffset)); + ImGui.InvisibleButton("BroadcastIcon", buttonSize); - var iconPos = ImGui.GetItemRectMin() + new Vector2(0f, iconYOffset); using (_uiSharedService.IconFont.Push()) ImGui.GetWindowDrawList().AddText(iconPos, ImGui.GetColorU32(UIColors.Get("LightlessGreen")), FontAwesomeIcon.PersonCirclePlus.ToIconString()); + + nextIconX = ImGui.GetItemRectMax().X + 6f; if (ImGui.IsItemHovered()) @@ -535,56 +567,8 @@ public class CompactUi : WindowMediatorSubscriberBase if (ImGui.IsItemClicked()) _lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI))); } - - ImGui.SetCursorPosY(cursorY); - ImGui.SetCursorPosX(uidStartX); - - bool headerItemClicked; - using (_uiSharedService.UidFont.Push()) - { - if (useVanityColors) - { - var seString = SeStringUtils.BuildFormattedPlayerName(uidText, vanityTextColor, vanityGlowColor); - var cursorPos = ImGui.GetCursorScreenPos(); - var targetFontSize = ImGui.GetFontSize(); - var font = ImGui.GetFont(); - SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, targetFontSize ,font , "uid-header"); - } - else - { - ImGui.TextColored(uidColor, uidText); - } - } - - if (ImGui.IsItemHovered()) - { - var padding = new Vector2(35f * ImGuiHelpers.GlobalScale); - Selune.RegisterHighlight( - ImGui.GetItemRectMin() - padding, - ImGui.GetItemRectMax() + padding, - SeluneHighlightMode.Point, - exactSize: true, - clipToElement: true, - clipPadding: padding, - highlightColorOverride: vanityGlowColor, - highlightAlphaOverride: 0.05f); - } - - headerItemClicked = ImGui.IsItemClicked(); - - if (headerItemClicked) - { - ImGui.SetClipboardText(uidText); - } - - UiSharedService.AttachToolTip("Click to copy"); - - // Connect/Disconnect button next to big UID - DrawConnectButton(cursorY, uidTextSize.Y); - - // Add spacing below the big UID - ImGuiHelpers.ScaledDummy(5f); - + + // Warning threshold icon (next to lightfinder or UID text) if (_apiController.ServerState is ServerState.Connected && analysisSummary.HasData) { var objectSummary = analysisSummary.Objects.Values.FirstOrDefault(summary => summary.HasEntries); @@ -598,24 +582,30 @@ public class CompactUi : WindowMediatorSubscriberBase if ((isOverTriHold || isOverVRAMUsage) && _playerPerformanceConfig.Current.WarnOnExceedingThresholds) { - ImGui.SameLine(); - ImGui.SetCursorPosY(cursorY + 15f); - _uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow")); + ImGui.SetCursorScreenPos(new Vector2(nextIconX, uidTextRectMin.Y + textVerticalOffset)); + + ImGui.InvisibleButton("WarningThresholdIcon", buttonSize); + var warningIconPos = ImGui.GetItemRectMin() + new Vector2(0f, iconYOffset); + using (_uiSharedService.IconFont.Push()) + ImGui.GetWindowDrawList().AddText(warningIconPos, ImGui.GetColorU32(UIColors.Get("LightlessYellow")), FontAwesomeIcon.ExclamationTriangle.ToIconString()); - string warningMessage = ""; - if (isOverTriHold) + if (ImGui.IsItemHovered()) { - 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 (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))); @@ -624,6 +614,33 @@ public class CompactUi : WindowMediatorSubscriberBase } } + if (uidTextHovered) + { + var padding = new Vector2(35f * ImGuiHelpers.GlobalScale); + Selune.RegisterHighlight( + uidTextRectMin - padding, + uidTextRectMin + uidTextRect + padding, + SeluneHighlightMode.Point, + exactSize: true, + clipToElement: true, + clipPadding: padding, + highlightColorOverride: vanityGlowColor, + highlightAlphaOverride: 0.05f); + } + + if (headerItemClicked) + { + ImGui.SetClipboardText(uidText); + } + + UiSharedService.AttachToolTip("Click to copy"); + + // Connect/Disconnect button next to big UID (use screen pos to avoid affecting layout) + DrawConnectButton(uidTextRectMin.Y + textVerticalOffset, uidTextSize.Y); + + // Add spacing below the big UID + ImGuiHelpers.ScaledDummy(5f); + if (_apiController.ServerState is ServerState.Connected) { if (headerItemClicked) @@ -697,19 +714,20 @@ public class CompactUi : WindowMediatorSubscriberBase } } - private void DrawConnectButton(float cursorY, float buttonHeight) + private void DrawConnectButton(float screenY, float textHeight) { var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Link); bool isConnectingOrConnected = _apiController.ServerState is ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting; var color = UiSharedService.GetBoolColor(!isConnectingOrConnected); var connectedIcon = isConnectingOrConnected ? FontAwesomeIcon.Unlink : FontAwesomeIcon.Link; - // Position next to big UID on the right side - + // Position on right side, vertically centered with text if (_apiController.ServerState is not (ServerState.Reconnecting or ServerState.Disconnecting)) { - ImGui.SetCursorPosX(UiSharedService.GetWindowContentRegionWidth() - buttonSize.X - 13f); - ImGui.SetCursorPosY(buttonHeight); + var windowPos = ImGui.GetWindowPos(); + var screenX = windowPos.X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X - 13f; + var yOffset = (textHeight - buttonSize.Y) * 0.5f; + ImGui.SetCursorScreenPos(new Vector2(screenX, screenY + yOffset)); using (ImRaii.PushColor(ImGuiCol.Text, color)) using (ImRaii.PushColor(ImGuiCol.Button, ImGui.ColorConvertFloat4ToU32(new(0, 0, 0, 0)))) From f792bc19546628848fa59003c05f5f223660f96d Mon Sep 17 00:00:00 2001 From: choco Date: Fri, 26 Dec 2025 00:00:13 +0100 Subject: [PATCH 06/28] compact ui design refactor with lightfinder redesign --- LightlessSync/Plugin.cs | 9 - .../Services/Mediator/LightlessMediator.cs | 34 +- LightlessSync/UI/CompactUI.cs | 119 +- LightlessSync/UI/LightFinderUI.cs | 1501 ++++++++++++----- LightlessSync/UI/SyncshellFinderUI.cs | 850 ---------- LightlessSync/UI/TopTabMenu.cs | 36 +- .../WebAPI/Files/FileDownloadManager.cs | 14 +- 7 files changed, 1248 insertions(+), 1315 deletions(-) delete mode 100644 LightlessSync/UI/SyncshellFinderUI.cs diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 425703a..606a23d 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -459,15 +459,6 @@ public sealed class Plugin : IDalamudPlugin sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), - sp.GetRequiredService())); - - services.AddScoped(sp => new SyncshellFinderUI( - sp.GetRequiredService>(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), diff --git a/LightlessSync/Services/Mediator/LightlessMediator.cs b/LightlessSync/Services/Mediator/LightlessMediator.cs index 52399e2..87e36ad 100644 --- a/LightlessSync/Services/Mediator/LightlessMediator.cs +++ b/LightlessSync/Services/Mediator/LightlessMediator.cs @@ -63,23 +63,31 @@ public sealed class LightlessMediator : IHostedService _ = Task.Run(async () => { - while (!_loopCts.Token.IsCancellationRequested) + try { - while (!_processQueue) + while (!_loopCts.Token.IsCancellationRequested) { + while (!_processQueue) + { + await Task.Delay(100, _loopCts.Token).ConfigureAwait(false); + } + await Task.Delay(100, _loopCts.Token).ConfigureAwait(false); + + HashSet processedMessages = []; + while (_messageQueue.TryDequeue(out var message)) + { + if (processedMessages.Contains(message)) { continue; } + + processedMessages.Add(message); + + ExecuteMessage(message); + } } - - await Task.Delay(100, _loopCts.Token).ConfigureAwait(false); - - HashSet processedMessages = []; - while (_messageQueue.TryDequeue(out var message)) - { - if (processedMessages.Contains(message)) { continue; } - processedMessages.Add(message); - - ExecuteMessage(message); - } + } + catch (OperationCanceledException) + { + _logger.LogInformation("LightlessMediator stopped"); } }); diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index e297e11..be56434 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -34,45 +34,65 @@ namespace LightlessSync.UI; public class CompactUi : WindowMediatorSubscriberBase { - private readonly CharacterAnalyzer _characterAnalyzer; + #region Constants + + private const float ConnectButtonHighlightThickness = 14f; + + #endregion + + #region Services + private readonly ApiController _apiController; + private readonly CharacterAnalyzer _characterAnalyzer; + private readonly DalamudUtilService _dalamudUtilService; + private readonly DrawEntityFactory _drawEntityFactory; + private readonly FileUploadManager _fileTransferManager; + private readonly IpcManager _ipcManager; + private readonly LightFinderService _broadcastService; private readonly LightlessConfigService _configService; private readonly LightlessMediator _lightlessMediator; private readonly PairLedger _pairLedger; - private readonly ConcurrentDictionary> _currentDownloads = new(); - private readonly DrawEntityFactory _drawEntityFactory; - private readonly FileUploadManager _fileTransferManager; - private readonly PlayerPerformanceConfigService _playerPerformanceConfig; private readonly PairUiService _pairUiService; - private readonly SelectTagForPairUi _selectTagForPairUi; - private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi; - private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi; - private readonly RenameSyncshellTagUi _renameSyncshellTagUi; - private readonly SelectPairForTagUi _selectPairsForGroupUi; - private readonly RenamePairTagUi _renamePairTagUi; - private readonly IpcManager _ipcManager; + private readonly PlayerPerformanceConfigService _playerPerformanceConfig; private readonly ServerConfigurationManager _serverManager; - private readonly TopTabMenu _tabMenu; private readonly TagHandler _tagHandler; private readonly UiSharedService _uiSharedService; - private readonly LightFinderService _broadcastService; - private readonly DalamudUtilService _dalamudUtilService; - + + #endregion + + #region UI Components + + private readonly AnimatedHeader _animatedHeader = new(); + private readonly RenamePairTagUi _renamePairTagUi; + private readonly RenameSyncshellTagUi _renameSyncshellTagUi; + private readonly SelectPairForTagUi _selectPairsForGroupUi; + private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi; + private readonly SelectTagForPairUi _selectTagForPairUi; + private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi; + private readonly SeluneBrush _seluneBrush = new(); + private readonly TopTabMenu _tabMenu; + + #endregion + + #region State + + private readonly ConcurrentDictionary> _currentDownloads = new(); private List _drawFolders; + private Pair? _focusedPair; private Pair? _lastAddedUser; private string _lastAddedUserComment = string.Empty; private Vector2 _lastPosition = Vector2.One; private Vector2 _lastSize = Vector2.One; + private int _pendingFocusFrame = -1; + private Pair? _pendingFocusPair; private bool _showModalForUserAddition; private float _transferPartHeight; private bool _wasOpen; private float _windowContentWidth; - private readonly SeluneBrush _seluneBrush = new(); - private readonly AnimatedHeader _animatedHeader = new(); - private const float _connectButtonHighlightThickness = 14f; - private Pair? _focusedPair; - private Pair? _pendingFocusPair; - private int _pendingFocusFrame = -1; + + #endregion + + #region Constructor public CompactUi( ILogger logger, @@ -156,6 +176,10 @@ public class CompactUi : WindowMediatorSubscriberBase _lightlessMediator = mediator; } + #endregion + + #region Lifecycle + public override void OnClose() { ForceReleaseFocus(); @@ -297,6 +321,10 @@ public class CompactUi : WindowMediatorSubscriberBase } } + #endregion + + #region Content Drawing + private void DrawPairs() { float ySize = Math.Abs(_transferPartHeight) < 0.0001f @@ -410,11 +438,9 @@ public class CompactUi : WindowMediatorSubscriberBase return new DownloadSummary(totalFiles, transferredFiles, transferredBytes, totalBytes); } - [StructLayout(LayoutKind.Auto)] - private readonly record struct DownloadSummary(int TotalFiles, int TransferredFiles, long TransferredBytes, long TotalBytes) - { - public bool HasDownloads => TotalFiles > 0 || TotalBytes > 0; - } + #endregion + + #region Header Drawing private void DrawUIDHeader() { @@ -626,6 +652,8 @@ public class CompactUi : WindowMediatorSubscriberBase clipPadding: padding, highlightColorOverride: vanityGlowColor, highlightAlphaOverride: 0.05f); + + ImGui.SetTooltip("Click to copy"); } if (headerItemClicked) @@ -633,8 +661,6 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.SetClipboardText(uidText); } - UiSharedService.AttachToolTip("Click to copy"); - // Connect/Disconnect button next to big UID (use screen pos to avoid affecting layout) DrawConnectButton(uidTextRectMin.Y + textVerticalOffset, uidTextSize.Y); @@ -756,7 +782,7 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.GetItemRectMax(), SeluneHighlightMode.Both, borderOnly: true, - borderThicknessOverride: _connectButtonHighlightThickness, + borderThicknessOverride: ConnectButtonHighlightThickness, exactSize: true, clipToElement: true, roundingOverride: ImGui.GetStyle().FrameRounding); @@ -766,6 +792,10 @@ public class CompactUi : WindowMediatorSubscriberBase } } + #endregion + + #region Folder Building + private IEnumerable DrawFolders { get @@ -901,6 +931,10 @@ public class CompactUi : WindowMediatorSubscriberBase } } + #endregion + + #region Filtering & Sorting + private static bool PassesFilter(PairUiEntry entry, string filter) { if (string.IsNullOrEmpty(filter)) return true; @@ -1044,10 +1078,11 @@ public class CompactUi : WindowMediatorSubscriberBase return SortGroupEntries(entries, group); } - private void UiSharedService_GposeEnd() - { - IsOpen = _wasOpen; - } + #endregion + + #region GPose Handlers + + private void UiSharedService_GposeEnd() => IsOpen = _wasOpen; private void UiSharedService_GposeStart() { @@ -1055,6 +1090,10 @@ public class CompactUi : WindowMediatorSubscriberBase IsOpen = false; } + #endregion + + #region Focus Tracking + private void RegisterFocusCharacter(Pair pair) { _pendingFocusPair = pair; @@ -1100,4 +1139,16 @@ public class CompactUi : WindowMediatorSubscriberBase _pendingFocusPair = null; _pendingFocusFrame = -1; } + + #endregion + + #region Helper Types + + [StructLayout(LayoutKind.Auto)] + private readonly record struct DownloadSummary(int TotalFiles, int TransferredFiles, long TransferredBytes, long TotalBytes) + { + public bool HasDownloads => TotalFiles > 0 || TotalBytes > 0; + } + + #endregion } diff --git a/LightlessSync/UI/LightFinderUI.cs b/LightlessSync/UI/LightFinderUI.cs index 22911cb..b6bfc73 100644 --- a/LightlessSync/UI/LightFinderUI.cs +++ b/LightlessSync/UI/LightFinderUI.cs @@ -1,445 +1,1188 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; +using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; +using LightlessSync.API.Dto; using LightlessSync.API.Dto.Group; using LightlessSync.LightlessConfiguration; using LightlessSync.Services; using LightlessSync.Services.LightFinder; using LightlessSync.Services.Mediator; +using LightlessSync.UI.Services; +using LightlessSync.UI.Style; +using LightlessSync.UI.Tags; using LightlessSync.Utils; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; using System.Numerics; -namespace LightlessSync.UI +namespace LightlessSync.UI; + +public class LightFinderUI : WindowMediatorSubscriberBase { - public class LightFinderUI : WindowMediatorSubscriberBase + #region Services + + private readonly ApiController _apiController; + private readonly DalamudUtilService _dalamudUtilService; + private readonly LightFinderScannerService _broadcastScannerService; + private readonly LightFinderService _broadcastService; + private readonly LightlessConfigService _configService; + private readonly LightlessProfileManager _lightlessProfileManager; + private readonly PairUiService _pairUiService; + private readonly UiSharedService _uiSharedService; + + #endregion + + #region UI Components + + private readonly AnimatedHeader _animatedHeader = new(); + private readonly List _seResolvedSegments = new(); + private readonly Vector4 _tagBackgroundColor = new(0.18f, 0.18f, 0.18f, 0.95f); + private readonly Vector4 _tagBorderColor = new(0.35f, 0.35f, 0.35f, 0.4f); + + #endregion + + #region State + + private IReadOnlyList _allSyncshells = Array.Empty(); + private bool _compactView; + private List _currentSyncshells = []; + private GroupJoinDto? _joinDto; + private GroupJoinInfoDto? _joinInfo; + private readonly List _nearbySyncshells = []; + private DefaultPermissionsDto _ownPermissions = null!; + private readonly HashSet _recentlyJoined = new(StringComparer.Ordinal); + private int _selectedNearbyIndex = -1; + private readonly List<(string Label, string? GID, bool IsAvailable)> _syncshellOptions = new(); + private LightfinderTab _selectedTab = LightfinderTab.NearbySyncshells; + private string _userUid = string.Empty; + + private enum LightfinderTab { NearbySyncshells, BroadcastSettings, Help } + +#if DEBUG + private enum LightfinderTabDebug { NearbySyncshells, BroadcastSettings, Help, Debug } + private LightfinderTabDebug _selectedTabDebug = LightfinderTabDebug.NearbySyncshells; +#endif + +#if DEBUG + private bool _useTestSyncshells; +#endif + + #endregion + + #region Constructor + + public LightFinderUI( + ILogger logger, + LightlessMediator mediator, + PerformanceCollectorService performanceCollectorService, + LightFinderService broadcastService, + LightlessConfigService configService, + UiSharedService uiShared, + ApiController apiController, + LightFinderScannerService broadcastScannerService, + PairUiService pairUiService, + DalamudUtilService dalamudUtilService, + LightlessProfileManager lightlessProfileManager + ) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService) { - private readonly ApiController _apiController; - private readonly LightlessConfigService _configService; - private readonly LightFinderService _broadcastService; - private readonly UiSharedService _uiSharedService; - private readonly LightFinderScannerService _broadcastScannerService; + _broadcastService = broadcastService; + _uiSharedService = uiShared; + _configService = configService; + _apiController = apiController; + _broadcastScannerService = broadcastScannerService; + _pairUiService = pairUiService; + _dalamudUtilService = dalamudUtilService; + _lightlessProfileManager = lightlessProfileManager; - private IReadOnlyList _allSyncshells = Array.Empty(); - private string _userUid = string.Empty; + _animatedHeader.Height = 100f; + _animatedHeader.EnableBottomGradient = true; + _animatedHeader.GradientHeight = 120f; + _animatedHeader.EnableParticles = _configService.Current.EnableParticleEffects; - private readonly List<(string Label, string? GID, bool IsAvailable)> _syncshellOptions = new(); + IsOpen = false; + WindowBuilder.For(this) + .SetSizeConstraints(new Vector2(620, 85), new Vector2(700, 600)) + .Apply(); - public LightFinderUI( - ILogger logger, - LightlessMediator mediator, - PerformanceCollectorService performanceCollectorService, - LightFinderService broadcastService, - LightlessConfigService configService, - UiSharedService uiShared, - ApiController apiController, - LightFinderScannerService broadcastScannerService - ) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService) + Mediator.Subscribe(this, async _ => await RefreshNearbySyncshellsAsync().ConfigureAwait(false)); + Mediator.Subscribe(this, async _ => await RefreshNearbySyncshellsAsync().ConfigureAwait(false)); + Mediator.Subscribe(this, async _ => await RefreshNearbySyncshellsAsync(_.gid).ConfigureAwait(false)); + Mediator.Subscribe(this, async _ => await RefreshNearbySyncshellsAsync(_.gid).ConfigureAwait(false)); + } + + #endregion + + #region Lifecycle + + public override void OnOpen() + { + _userUid = _apiController.UID; + _ownPermissions = _apiController.DefaultPermissions.DeepClone()!; + _ = RefreshSyncshellsAsync(); + _ = RefreshNearbySyncshellsAsync(); + } + + public override void OnClose() + { + _animatedHeader.ClearParticles(); + ClearSelection(); + base.OnClose(); + } + + #endregion + + #region Main Drawing + + protected override void DrawInternal() + { + var contentWidth = ImGui.GetContentRegionAvail().X; + _animatedHeader.Draw(contentWidth, (_, _) => { }); + + if (!_broadcastService.IsLightFinderAvailable) { - _broadcastService = broadcastService; - _uiSharedService = uiShared; - _configService = configService; - _apiController = apiController; - _broadcastScannerService = broadcastScannerService; - - IsOpen = false; - WindowBuilder.For(this) - .SetSizeConstraints(new Vector2(600, 465), new Vector2(750, 525)) - .Apply(); + ImGui.TextColored(UIColors.Get("LightlessYellow"), "This server doesn't support Lightfinder."); + ImGuiHelpers.ScaledDummy(2f); } - private void RebuildSyncshellDropdownOptions() + DrawStatusPanel(); + ImGuiHelpers.ScaledDummy(4f); + +#if DEBUG + var debugTabOptions = new List> { - var selectedGid = _configService.Current.SelectedFinderSyncshell; - var allSyncshells = _allSyncshells ?? []; - var filteredSyncshells = allSyncshells - .Where(g => string.Equals(g.OwnerUID, _userUid, StringComparison.Ordinal) || g.GroupUserInfo.IsModerator()) - .ToList(); + new("Nearby Syncshells", LightfinderTabDebug.NearbySyncshells), + new("Broadcast", LightfinderTabDebug.BroadcastSettings), + new("Help", LightfinderTabDebug.Help), + new("Debug", LightfinderTabDebug.Debug) + }; + UiSharedService.Tab("LightfinderTabs", debugTabOptions, ref _selectedTabDebug); + + ImGuiHelpers.ScaledDummy(4f); + + switch (_selectedTabDebug) + { + case LightfinderTabDebug.NearbySyncshells: + DrawNearbySyncshellsTab(); + break; + case LightfinderTabDebug.BroadcastSettings: + DrawBroadcastSettingsTab(); + break; + case LightfinderTabDebug.Help: + DrawHelpTab(); + break; + case LightfinderTabDebug.Debug: + DrawDebugTab(); + break; + } +#else + var tabOptions = new List> + { + new("Nearby Syncshells", LightfinderTab.NearbySyncshells), + new("Broadcast", LightfinderTab.BroadcastSettings), + new("Help", LightfinderTab.Help) + }; + UiSharedService.Tab("LightfinderTabs", tabOptions, ref _selectedTab); + + ImGuiHelpers.ScaledDummy(4f); + + switch (_selectedTab) + { + case LightfinderTab.NearbySyncshells: + DrawNearbySyncshellsTab(); + break; + case LightfinderTab.BroadcastSettings: + DrawBroadcastSettingsTab(); + break; + case LightfinderTab.Help: + DrawHelpTab(); + break; + } +#endif - _syncshellOptions.Clear(); - _syncshellOptions.Add(("None", null, true)); + if (_joinDto != null && _joinInfo != null && _joinInfo.Success) + DrawJoinConfirmation(); + } - var addedGids = new HashSet(StringComparer.Ordinal); + private void DrawStatusPanel() + { + var scale = ImGuiHelpers.GlobalScale; + var isBroadcasting = _broadcastService.IsBroadcasting; + var cooldown = _broadcastService.RemainingCooldown; + var isOnCooldown = cooldown.HasValue && cooldown.Value.TotalSeconds > 0; - foreach (var shell in filteredSyncshells) + var accent = isBroadcasting ? UIColors.Get("LightlessGreen") : UIColors.Get("LightlessPurple"); + var accentBg = new Vector4(accent.X, accent.Y, accent.Z, 0.16f); + var accentBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.32f); + var infoColor = ImGuiColors.DalamudGrey; + + var summaryHeight = MathF.Max(ImGui.GetTextLineHeightWithSpacing() * 2.4f, 46f * scale); + float buttonWidth = 130 * scale; + + using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f * scale)) + using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize))) + using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(12f * scale, 6f * scale))) + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(18f * scale, 4f * scale))) + using (ImRaii.PushColor(ImGuiCol.ChildBg, UiSharedService.Color(accentBg))) + using (ImRaii.PushColor(ImGuiCol.Border, UiSharedService.Color(accentBorder))) + using (var child = ImRaii.Child("StatusPanel", new Vector2(-1f, summaryHeight), true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)) + { + if (child) { - var label = shell.GroupAliasOrGID ?? shell.GID; - _syncshellOptions.Add((label, shell.GID, true)); - addedGids.Add(shell.GID); - } - - if (!string.IsNullOrEmpty(selectedGid) && !addedGids.Contains(selectedGid)) - { - var matching = allSyncshells.FirstOrDefault(g => string.Equals(g.GID, selectedGid, StringComparison.Ordinal)); - if (matching != null) + using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(8f * scale, 4f * scale))) { - var label = matching.GroupAliasOrGID ?? matching.GID; - _syncshellOptions.Add((label, matching.GID, true)); - addedGids.Add(matching.GID); + if (ImGui.BeginTable("StatusPanelTable", 6, ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.NoBordersInBody)) + { + ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.WidthStretch, 1f); + ImGui.TableSetupColumn("Time", ImGuiTableColumnFlags.WidthStretch, 1f); + ImGui.TableSetupColumn("NearbyPlayers", ImGuiTableColumnFlags.WidthStretch, 1f); + ImGui.TableSetupColumn("NearbySyncshells", ImGuiTableColumnFlags.WidthStretch, 1f); + ImGui.TableSetupColumn("Broadcasting", ImGuiTableColumnFlags.WidthStretch, 1f); + ImGui.TableSetupColumn("Button", ImGuiTableColumnFlags.WidthFixed, buttonWidth + 16f * scale); + + ImGui.TableNextRow(); + + // Status cell + var statusColor = isBroadcasting ? UIColors.Get("LightlessGreen") : UIColors.Get("DimRed"); + var statusText = isBroadcasting ? "Active" : "Inactive"; + var statusIcon = isBroadcasting ? FontAwesomeIcon.CheckCircle : FontAwesomeIcon.TimesCircle; + DrawStatusCell(statusIcon, statusColor, statusText, "Status", infoColor, scale); + + // Time remaining Cooldown cell + string timeValue; + string timeSub; + Vector4 timeColor; + if (isOnCooldown) + { + timeValue = $"{Math.Ceiling(cooldown!.Value.TotalSeconds)}s"; + timeSub = "Cooldown"; + timeColor = UIColors.Get("DimRed"); + } + else if (isBroadcasting && _broadcastService.RemainingTtl is { } remaining && remaining > TimeSpan.Zero) + { + timeValue = $"{remaining:hh\\:mm\\:ss}"; + timeSub = "Time left"; + timeColor = UIColors.Get("LightlessYellow"); + } + else + { + timeValue = "--:--:--"; + timeSub = "Time left"; + timeColor = infoColor; + } + DrawStatusCell(FontAwesomeIcon.Clock, timeColor, timeValue, timeSub, infoColor, scale); + + // Nearby players cell + var nearbyPlayerCount = _broadcastScannerService.CountActiveBroadcasts(); + var nearbyPlayerColor = nearbyPlayerCount > 0 ? UIColors.Get("LightlessBlue") : infoColor; + DrawStatusCell(FontAwesomeIcon.Users, nearbyPlayerColor, nearbyPlayerCount.ToString(), "Players", infoColor, scale); + + // Nearby syncshells cell + var nearbySyncshellCount = _nearbySyncshells.Count; + var nearbySyncshellColor = nearbySyncshellCount > 0 ? UIColors.Get("LightlessPurple") : infoColor; + DrawStatusCell(FontAwesomeIcon.Compass, nearbySyncshellColor, nearbySyncshellCount.ToString(), "Syncshells", infoColor, scale); + + // Broadcasting syncshell cell + var isBroadcastingSyncshell = _configService.Current.SyncshellFinderEnabled && isBroadcasting; + var broadcastSyncshellColor = isBroadcastingSyncshell ? UIColors.Get("LightlessGreen") : infoColor; + var broadcastSyncshellText = isBroadcastingSyncshell ? "Yes" : "No"; + var broadcastSyncshellIcon = FontAwesomeIcon.Wifi; + DrawStatusCell(broadcastSyncshellIcon, broadcastSyncshellColor, broadcastSyncshellText, "Broadcasting", infoColor, scale); + + // Enable/Disable button cell - right aligned + ImGui.TableNextColumn(); + + float cellWidth = ImGui.GetContentRegionAvail().X; + float offsetX = cellWidth - buttonWidth; + if (offsetX > 0) + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + offsetX); + + using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 6f * scale)) + { + Vector4 buttonColor; + if (isOnCooldown) + buttonColor = UIColors.Get("DimRed"); + else if (isBroadcasting) + buttonColor = UIColors.Get("LightlessGreen"); + else + buttonColor = UIColors.Get("LightlessPurple"); + + using (ImRaii.PushColor(ImGuiCol.Button, buttonColor)) + using (ImRaii.PushColor(ImGuiCol.ButtonHovered, buttonColor.WithAlpha(0.85f))) + using (ImRaii.PushColor(ImGuiCol.ButtonActive, buttonColor.WithAlpha(0.75f))) + using (ImRaii.Disabled(isOnCooldown || !_broadcastService.IsLightFinderAvailable)) + { + string buttonText = isBroadcasting ? "Disable" : "Enable"; + if (ImGui.Button(buttonText, new Vector2(buttonWidth, 0))) + _broadcastService.ToggleBroadcast(); + } + } + + ImGui.EndTable(); + } } } + } + } - if (!string.IsNullOrEmpty(selectedGid) && !addedGids.Contains(selectedGid)) + private void DrawStatusCell(FontAwesomeIcon icon, Vector4 iconColor, string mainText, string subText, Vector4 subColor, float scale) + { + ImGui.TableNextColumn(); + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(6f * scale, 2f * scale))) + using (ImRaii.Group()) + { + _uiSharedService.IconText(icon, iconColor); + ImGui.SameLine(0f, 6f * scale); + using (ImRaii.PushColor(ImGuiCol.Text, iconColor)) { - _syncshellOptions.Add(($"[Unavailable] {selectedGid}", selectedGid, false)); + ImGui.TextUnformatted(mainText); + } + using (ImRaii.PushColor(ImGuiCol.Text, subColor)) + { + ImGui.TextUnformatted(subText); + } + } + } + + #endregion + + #region Nearby Syncshells Tab + + private void DrawNearbySyncshellsTab() + { + ImGui.BeginGroup(); + +#if DEBUG + if (ImGui.SmallButton("Test Data")) + { + _useTestSyncshells = !_useTestSyncshells; + _ = Task.Run(async () => await RefreshNearbySyncshellsAsync().ConfigureAwait(false)); + } + ImGui.SameLine(); +#endif + + string checkboxLabel = "Compact"; + float checkboxWidth = ImGui.CalcTextSize(checkboxLabel).X + ImGui.GetFrameHeight() + 8f; + float availWidth = ImGui.GetContentRegionAvail().X; + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + availWidth - checkboxWidth); + ImGui.Checkbox(checkboxLabel, ref _compactView); + ImGui.EndGroup(); + + ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale)); + + if (_nearbySyncshells.Count == 0) + { + DrawNoSyncshellsMessage(); + return; + } + + var cardData = BuildSyncshellCardData(); + if (cardData.Count == 0) + { + ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells found."); + return; + } + + if (_compactView) + DrawSyncshellGrid(cardData); + else + DrawSyncshellList(cardData); + } + + private void DrawNoSyncshellsMessage() + { + ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted."); + + if (!_broadcastService.IsBroadcasting) + { + ImGuiHelpers.ScaledDummy(4f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow")); + ImGuiHelpers.ScaledDummy(2f); + + ImGui.TextColored(UIColors.Get("LightlessYellow"), "Lightfinder must be active to find nearby syncshells."); + } + } + + private List<(GroupJoinDto Shell, string BroadcasterName)> BuildSyncshellCardData() + { + string? myHashedCid = null; + try + { + var cid = _dalamudUtilService.GetCID(); + myHashedCid = cid.ToString().GetHash256(); + } + catch { } + + var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts() + .Where(b => !string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal)) + .ToList(); + + var cardData = new List<(GroupJoinDto Shell, string BroadcasterName)>(); + + foreach (var shell in _nearbySyncshells) + { + if (shell?.Group == null || string.IsNullOrEmpty(shell.Group.GID)) + continue; + +#if DEBUG + if (_useTestSyncshells) + { + var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID; + cardData.Add((shell, $"{displayName} (Test World)")); + continue; + } +#endif + + var broadcast = broadcasts.FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal)); + if (broadcast == null) + continue; + + var (name, address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID); + if (string.IsNullOrEmpty(name)) + continue; + + var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(address); + var broadcasterName = !string.IsNullOrEmpty(worldName) ? $"{name} ({worldName})" : name; + + cardData.Add((shell, broadcasterName)); + } + + return cardData; + } + + private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName)> listData) + { + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8f); + ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f); + + if (ImGui.BeginChild("SyncshellListScroll", new Vector2(-1, -1), border: false)) + { + foreach (var (shell, broadcasterName) in listData) + { + DrawSyncshellListItem(shell, broadcasterName); + ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale)); + } + } + ImGui.EndChild(); + + ImGui.PopStyleVar(2); + } + + private void DrawSyncshellListItem(GroupJoinDto shell, string broadcasterName) + { + ImGui.PushID(shell.Group.GID); + float rowHeight = 74f * ImGuiHelpers.GlobalScale; + + ImGui.BeginChild($"ShellRow##{shell.Group.GID}", new Vector2(-1, rowHeight), border: true); + + var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID; + var style = ImGui.GetStyle(); + float startX = ImGui.GetCursorPosX(); + float regionW = ImGui.GetContentRegionAvail().X; + float rightTxtW = ImGui.CalcTextSize(broadcasterName).X; + + _uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple")); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Click to open profile."); + if (ImGui.IsItemClicked()) + Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group)); + + float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X; + ImGui.SameLine(); + ImGui.SetCursorPosX(rightX); + ImGui.TextUnformatted(broadcasterName); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Broadcaster"); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault")); + + var groupProfile = _lightlessProfileManager.GetLightlessGroupProfile(shell.Group); + IReadOnlyList groupTags = groupProfile?.Tags.Count > 0 + ? ProfileTagService.ResolveTags(groupProfile.Tags) + : []; + + var limitedTags = groupTags.Count > 3 ? [.. groupTags.Take(3)] : groupTags; + float tagScale = ImGuiHelpers.GlobalScale * 0.9f; + + Vector2 rowStartLocal = ImGui.GetCursorPos(); + float tagsWidth = 0f; + + if (limitedTags.Count > 0) + (tagsWidth, _) = RenderProfileTagsSingleRow(limitedTags, tagScale); + else + { + ImGui.SetCursorPosX(startX); + ImGui.TextDisabled("No tags"); + ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale)); + } + + float joinX = rowStartLocal.X + (tagsWidth > 0 ? tagsWidth + style.ItemSpacing.X : 0f); + ImGui.SetCursorPos(new Vector2(joinX, rowStartLocal.Y)); + DrawJoinButton(shell, false); + + ImGui.EndChild(); + ImGui.PopID(); + } + + private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName)> cardData) + { + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8f); + ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f); + + foreach (var (shell, _) in cardData) + { + DrawSyncshellCompactItem(shell); + ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale)); + } + + ImGui.PopStyleVar(2); + } + + private void DrawSyncshellCompactItem(GroupJoinDto shell) + { + ImGui.PushID(shell.Group.GID); + float rowHeight = 36f * ImGuiHelpers.GlobalScale; + + ImGui.BeginChild($"ShellCompact##{shell.Group.GID}", new Vector2(-1, rowHeight), border: true); + + var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID; + var style = ImGui.GetStyle(); + float availW = ImGui.GetContentRegionAvail().X; + + ImGui.AlignTextToFramePadding(); + _uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple")); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Click to open profile."); + if (ImGui.IsItemClicked()) + Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group)); + + ImGui.SameLine(); + DrawJoinButton(shell, false); + + ImGui.EndChild(); + ImGui.PopID(); + } + + private void DrawJoinButton(GroupJoinDto shell, bool fullWidth) + { + const string visibleLabel = "Join"; + var label = $"{visibleLabel}##{shell.Group.GID}"; + + var isAlreadyMember = _currentSyncshells.Exists(g => string.Equals(g.GID, shell.Group.GID, StringComparison.Ordinal)); + var isRecentlyJoined = _recentlyJoined.Contains(shell.Group.GID); + var isOwnBroadcast = _configService.Current.SyncshellFinderEnabled + && _broadcastService.IsBroadcasting + && string.Equals(_configService.Current.SelectedFinderSyncshell, shell.Group.GID, StringComparison.Ordinal); + + Vector2 buttonSize; + if (fullWidth) + { + buttonSize = new Vector2(-1, 0); + } + else + { + var textSize = ImGui.CalcTextSize(visibleLabel); + var width = textSize.X + ImGui.GetStyle().FramePadding.X * 20f; + buttonSize = new Vector2(width, 30f); + + float availX = ImGui.GetContentRegionAvail().X; + float curX = ImGui.GetCursorPosX(); + ImGui.SetCursorPosX(curX + availX - buttonSize.X); + } + + if (isOwnBroadcast) + { + ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessPurple")); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple").WithAlpha(0.85f)); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurple").WithAlpha(0.75f)); + + using (ImRaii.Disabled()) + ImGui.Button(label, buttonSize); + + UiSharedService.AttachToolTip("You can't join your own Syncshell, silly! That's like trying to high-five yourself."); + } + else if (!isAlreadyMember && !isRecentlyJoined) + { + ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen")); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f)); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f)); + + if (ImGui.Button(label, buttonSize)) + { + _ = Task.Run(async () => + { + try + { + var info = await _apiController.GroupJoinHashed(new GroupJoinHashedDto( + shell.Group, shell.Password, shell.GroupUserPreferredPermissions + )).ConfigureAwait(false); + + if (info?.Success == true) + { + _joinDto = new GroupJoinDto(shell.Group, shell.Password, shell.GroupUserPreferredPermissions); + _joinInfo = info; + _ownPermissions = _apiController.DefaultPermissions.DeepClone()!; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Join failed for {GID}", shell.Group.GID); + } + }); + } + } + else + { + ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("DimRed")); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("DimRed").WithAlpha(0.85f)); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("DimRed").WithAlpha(0.75f)); + + using (ImRaii.Disabled()) + ImGui.Button(label, buttonSize); + + UiSharedService.AttachToolTip("Already a member of this Syncshell."); + } + + ImGui.PopStyleColor(3); + } + + + private void DrawJoinConfirmation() + { + if (_joinDto == null || _joinInfo == null) return; + + ImGui.Separator(); + ImGui.TextUnformatted($"Join Syncshell: {_joinDto.Group.AliasOrGID} by {_joinInfo.OwnerAliasOrUID}"); + ImGuiHelpers.ScaledDummy(2f); + ImGui.TextUnformatted("Suggested Permissions:"); + + DrawPermissionRow("Sounds", _joinInfo.GroupPermissions.IsPreferDisableSounds(), _ownPermissions.DisableGroupSounds, v => _ownPermissions.DisableGroupSounds = v); + DrawPermissionRow("Animations", _joinInfo.GroupPermissions.IsPreferDisableAnimations(), _ownPermissions.DisableGroupAnimations, v => _ownPermissions.DisableGroupAnimations = v); + DrawPermissionRow("VFX", _joinInfo.GroupPermissions.IsPreferDisableVFX(), _ownPermissions.DisableGroupVFX, v => _ownPermissions.DisableGroupVFX = v); + + ImGui.NewLine(); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, $"Finalize and join {_joinDto.Group.AliasOrGID}")) + { + var finalPermissions = GroupUserPreferredPermissions.NoneSet; + finalPermissions.SetDisableSounds(_ownPermissions.DisableGroupSounds); + finalPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations); + finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX); + + _ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions)); + _recentlyJoined.Add(_joinDto.Group.GID); + + _joinDto = null; + _joinInfo = null; + } + + ImGui.SameLine(); + if (ImGui.Button("Cancel")) + { + _joinDto = null; + _joinInfo = null; + } + } + + private void DrawPermissionRow(string label, bool suggested, bool current, Action apply) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted($"- {label}"); + + ImGui.SameLine(120 * ImGuiHelpers.GlobalScale); + ImGui.Text("Current:"); + ImGui.SameLine(); + _uiSharedService.BooleanToColoredIcon(!current); + + ImGui.SameLine(240 * ImGuiHelpers.GlobalScale); + ImGui.Text("Suggested:"); + ImGui.SameLine(); + _uiSharedService.BooleanToColoredIcon(!suggested); + + ImGui.SameLine(380 * ImGuiHelpers.GlobalScale); + using var id = ImRaii.PushId(label); + if (current != suggested) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Apply")) + apply(suggested); + } + + ImGui.NewLine(); + } + + #endregion + + #region Broadcast Settings Tab + + private void DrawBroadcastSettingsTab() + { + _uiSharedService.MediumText("Syncshell Broadcasting", UIColors.Get("PairBlue")); + ImGuiHelpers.ScaledDummy(2f); + + ImGui.PushTextWrapPos(); + ImGui.TextColored(ImGuiColors.DalamudGrey, "Broadcast your Syncshell to nearby Lightfinder users. They can then join directly from the Nearby Syncshells tab."); + ImGui.PopTextWrapPos(); + + ImGuiHelpers.ScaledDummy(4f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); + ImGuiHelpers.ScaledDummy(4f); + + bool isBroadcasting = _broadcastService.IsBroadcasting; + + if (isBroadcasting) + { + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), + new SeStringUtils.RichTextEntry("Settings can only be changed while Lightfinder is disabled.", UIColors.Get("LightlessYellow"))); + ImGuiHelpers.ScaledDummy(4f); + } + + if (isBroadcasting) + ImGui.BeginDisabled(); + + bool shellFinderEnabled = _configService.Current.SyncshellFinderEnabled; + if (ImGui.Checkbox("Enable Syncshell Broadcasting", ref shellFinderEnabled)) + { + _configService.Current.SyncshellFinderEnabled = shellFinderEnabled; + _configService.Save(); + } + UiSharedService.AttachToolTip("When enabled and Lightfinder is active, your selected Syncshell will be visible to nearby users."); + + ImGuiHelpers.ScaledDummy(4f); + + ImGui.Text("Select Syncshell to broadcast:"); + + var selectedGid = _configService.Current.SelectedFinderSyncshell; + var currentOption = _syncshellOptions.FirstOrDefault(o => string.Equals(o.GID, selectedGid, StringComparison.Ordinal)); + var preview = currentOption.Label ?? "Select a Syncshell..."; + + ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); + if (ImGui.BeginCombo("##SyncshellDropdown", preview)) + { + foreach (var (label, gid, available) in _syncshellOptions) + { + bool isSelected = string.Equals(gid, selectedGid, StringComparison.Ordinal); + + if (!available) + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); + + if (ImGui.Selectable(label, isSelected)) + { + _configService.Current.SelectedFinderSyncshell = gid; + _configService.Save(); + _ = RefreshSyncshellsAsync(); + } + + if (!available && ImGui.IsItemHovered()) + ImGui.SetTooltip("This Syncshell is not available on the current service."); + + if (!available) + ImGui.PopStyleColor(); + + if (isSelected) + ImGui.SetItemDefaultFocus(); + } + + ImGui.EndCombo(); + } + + if (isBroadcasting) + ImGui.EndDisabled(); + + ImGui.SameLine(); + if (_uiSharedService.IconButton(FontAwesomeIcon.Sync)) + _ = RefreshSyncshellsAsync(); + UiSharedService.AttachToolTip("Refresh Syncshell list"); + + ImGuiHelpers.ScaledDummy(8f); + + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8f); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Cog, "Advanced Settings")) + Mediator.Publish(new OpenLightfinderSettingsMessage()); + ImGui.PopStyleVar(); + UiSharedService.AttachToolTip("Open Lightfinder settings in the Settings window."); + } + + #endregion + + #region Help Tab + + private void DrawHelpTab() + { + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(4, 4)); + + _uiSharedService.MediumText("What is Lightfinder?", UIColors.Get("PairBlue")); + ImGui.PushTextWrapPos(); + ImGui.TextColored(ImGuiColors.DalamudGrey, "Lightfinder lets other Lightless users know you use Lightless. While enabled, you and others can see each other via a nameplate label."); + ImGui.PopTextWrapPos(); + + ImGuiHelpers.ScaledDummy(6f); + + _uiSharedService.MediumText("Pairing", UIColors.Get("PairBlue")); + ImGui.PushTextWrapPos(); + ImGui.TextColored(ImGuiColors.DalamudGrey, "Pairing can be initiated via the right-click context menu on another player. The process requires mutual confirmation from both users."); + ImGui.PopTextWrapPos(); + + ImGuiHelpers.ScaledDummy(2f); + + _uiSharedService.DrawNoteLine("", UIColors.Get("LightlessGreen"), + new SeStringUtils.RichTextEntry("If Lightfinder is "), + new SeStringUtils.RichTextEntry("ENABLED", UIColors.Get("LightlessGreen"), true), + new SeStringUtils.RichTextEntry(", the receiving user will get notified about pair requests.")); + + _uiSharedService.DrawNoteLine("", UIColors.Get("DimRed"), + new SeStringUtils.RichTextEntry("If Lightfinder is "), + new SeStringUtils.RichTextEntry("DISABLED", UIColors.Get("DimRed"), true), + new SeStringUtils.RichTextEntry(", pair requests will NOT be visible to the recipient.")); + + ImGuiHelpers.ScaledDummy(6f); + + _uiSharedService.MediumText("Privacy", UIColors.Get("PairBlue")); + + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), + new SeStringUtils.RichTextEntry("Lightfinder is entirely "), + new SeStringUtils.RichTextEntry("opt-in", UIColors.Get("LightlessYellow"), true), + new SeStringUtils.RichTextEntry(" and does not share personal data with other users.")); + + ImGui.PushTextWrapPos(); + ImGui.TextColored(ImGuiColors.DalamudGrey, "All identifying information remains private to the server. Use Lightfinder when you're okay with being visible to other users."); + ImGui.PopTextWrapPos(); + + ImGuiHelpers.ScaledDummy(6f); + + _uiSharedService.MediumText("Syncshell Broadcasting", UIColors.Get("PairBlue")); + ImGui.PushTextWrapPos(); + ImGui.TextColored(ImGuiColors.DalamudGrey, "You can broadcast a Syncshell you own or moderate to nearby Lightfinder users. Configure this in the Broadcast Settings tab."); + ImGui.PopTextWrapPos(); + + ImGui.PopStyleVar(); + } + + #endregion + +#if DEBUG + #region Debug Tab + + private void DrawDebugTab() + { + ImGui.Text("Broadcast Cache"); + + if (ImGui.BeginTable("##BroadcastCacheTable", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, new Vector2(-1, 200f))) + { + ImGui.TableSetupColumn("CID", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Broadcasting", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Expires In", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Syncshell GID", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableHeadersRow(); + + var now = DateTime.UtcNow; + + foreach (var (cid, entry) in _broadcastScannerService.BroadcastCache) + { + ImGui.TableNextRow(); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(cid.Truncate(12)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(cid); + + ImGui.TableNextColumn(); + var colorBroadcast = entry.IsBroadcasting ? UIColors.Get("LightlessGreen") : UIColors.Get("DimRed"); + ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, ImGui.GetColorU32(colorBroadcast)); + ImGui.TextUnformatted(entry.IsBroadcasting.ToString()); + + ImGui.TableNextColumn(); + var remaining = entry.ExpiryTime - now; + var colorTtl = remaining <= TimeSpan.Zero ? UIColors.Get("DimRed") + : remaining < TimeSpan.FromSeconds(10) ? UIColors.Get("LightlessYellow") + : (Vector4?)null; + + if (colorTtl != null) + ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, ImGui.GetColorU32(colorTtl.Value)); + + ImGui.TextUnformatted(remaining > TimeSpan.Zero ? remaining.ToString("hh\\:mm\\:ss") : "Expired"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(entry.GID ?? "-"); + } + + ImGui.EndTable(); + } + } + + #endregion +#endif + + #region Data Refresh + + private async Task RefreshSyncshellsAsync() + { + if (!_apiController.IsConnected) + { + _allSyncshells = []; + RebuildSyncshellDropdownOptions(); + return; + } + + try + { + _allSyncshells = await _apiController.GroupsGetAll().ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to fetch Syncshells."); + _allSyncshells = []; + } + + RebuildSyncshellDropdownOptions(); + } + + private void RebuildSyncshellDropdownOptions() + { + var selectedGid = _configService.Current.SelectedFinderSyncshell; + var allSyncshells = _allSyncshells ?? []; + var filteredSyncshells = allSyncshells + .Where(g => string.Equals(g.OwnerUID, _userUid, StringComparison.Ordinal) || g.GroupUserInfo.IsModerator()) + .ToList(); + + _syncshellOptions.Clear(); + _syncshellOptions.Add(("None", null, true)); + + var addedGids = new HashSet(StringComparer.Ordinal); + + foreach (var shell in filteredSyncshells) + { + var label = shell.GroupAliasOrGID ?? shell.GID; + _syncshellOptions.Add((label, shell.GID, true)); + addedGids.Add(shell.GID); + } + + if (!string.IsNullOrEmpty(selectedGid) && !addedGids.Contains(selectedGid)) + { + var matching = allSyncshells.FirstOrDefault(g => string.Equals(g.GID, selectedGid, StringComparison.Ordinal)); + if (matching != null) + { + var label = matching.GroupAliasOrGID ?? matching.GID; + _syncshellOptions.Add((label, matching.GID, true)); + addedGids.Add(matching.GID); } } - public Task RefreshSyncshells() - { - return RefreshSyncshellsInternal(); - } + if (!string.IsNullOrEmpty(selectedGid) && !addedGids.Contains(selectedGid)) + _syncshellOptions.Add(($"[Unavailable] {selectedGid}", selectedGid, false)); + } - private async Task RefreshSyncshellsInternal() + private async Task RefreshNearbySyncshellsAsync(string? gid = null) + { + var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts(); + var snapshot = _pairUiService.GetSnapshot(); + _currentSyncshells = [.. snapshot.GroupPairs.Keys]; + + _recentlyJoined.RemoveWhere(g => _currentSyncshells.Exists(s => string.Equals(s.GID, g, StringComparison.Ordinal))); + + List? updatedList = []; + +#if DEBUG + if (_useTestSyncshells) { - if (!_apiController.IsConnected) + updatedList = BuildTestSyncshells(); + } + else +#endif + { + if (syncshellBroadcasts.Count == 0) { - _allSyncshells = []; - RebuildSyncshellDropdownOptions(); + ClearSyncshells(); return; } try { - _allSyncshells = await _apiController.GroupsGetAll().ConfigureAwait(false); + var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts).ConfigureAwait(false); + updatedList = groups?.DistinctBy(g => g.Group.GID).ToList(); } catch (Exception ex) { - _logger.LogError(ex, "Failed to fetch Syncshells."); - _allSyncshells = []; + _logger.LogError(ex, "Failed to refresh broadcasted syncshells."); + return; } - - RebuildSyncshellDropdownOptions(); } - - public override void OnOpen() + if (updatedList == null || updatedList.Count == 0) { - _userUid = _apiController.UID; - _ = RefreshSyncshells(); + ClearSyncshells(); + return; } - protected override void DrawInternal() + if (gid != null && _recentlyJoined.Contains(gid)) + _recentlyJoined.Clear(); + + var previousGid = GetSelectedGid(); + + _nearbySyncshells.Clear(); + _nearbySyncshells.AddRange(updatedList); + + if (previousGid != null) { - if (!_broadcastService.IsLightFinderAvailable) + var newIndex = _nearbySyncshells.FindIndex(s => string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal)); + if (newIndex >= 0) { - _uiSharedService.MediumText("This server doesn't support Lightfinder.", UIColors.Get("LightlessYellow")); - - ImGuiHelpers.ScaledDummy(0.25f); + _selectedNearbyIndex = newIndex; + return; } + } - if (ImGui.BeginTabBar("##BroadcastTabs")) - { - if (ImGui.BeginTabItem("Lightfinder")) - { - _uiSharedService.MediumText("Lightfinder", UIColors.Get("PairBlue")); - - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, -2)); - - _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"),"This lets other Lightless users know you use Lightless. While enabled, you and others using Lightfinder can see each other identified as Lightless users."); - - ImGui.Indent(15f); - ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey); - ImGui.Text("- This is done using a 'Lightless' label above player nameplates."); - ImGui.PopStyleColor(); - ImGui.Unindent(15f); - - ImGuiHelpers.ScaledDummy(3f); - - _uiSharedService.MediumText("Pairing", UIColors.Get("PairBlue")); - _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "Pairing may be initiated via the right-click context menu on another player." + - " The process requires mutual confirmation: the sender initiates the request, and the recipient completes it by responding with a request in return."); - - _uiSharedService.DrawNoteLine( - "! ", - UIColors.Get("LightlessYellow"), - new SeStringUtils.RichTextEntry("If Lightfinder is "), - new SeStringUtils.RichTextEntry("ENABLED", UIColors.Get("LightlessGreen"), true), - new SeStringUtils.RichTextEntry(" when a pair request is made, the receiving user will get notified about it.")); - - _uiSharedService.DrawNoteLine( - "! ", - UIColors.Get("LightlessYellow"), - new SeStringUtils.RichTextEntry("If Lightfinder is "), - new SeStringUtils.RichTextEntry("DISABLED", UIColors.Get("DimRed"), true), - new SeStringUtils.RichTextEntry(" when a pair request is made, the receiving user will "), - new SeStringUtils.RichTextEntry("NOT", UIColors.Get("DimRed"), true), - new SeStringUtils.RichTextEntry(" get a notification, and the request will not be visible to them in any way.")); - - ImGuiHelpers.ScaledDummy(3f); - - _uiSharedService.MediumText("Privacy", UIColors.Get("PairBlue")); - - _uiSharedService.DrawNoteLine( - "! ", - UIColors.Get("DimRed"), - new SeStringUtils.RichTextEntry("Lightfinder is entirely "), - new SeStringUtils.RichTextEntry("opt-in", UIColors.Get("LightlessYellow"), true), - new SeStringUtils.RichTextEntry(" and does not share any data with other users. All identifying information remains private to the server.")); - - _uiSharedService.DrawNoteLine("! ", UIColors.Get("DimRed"), "Pairing is intended as a mutual agreement between both parties. A pair request will not be visible to the recipient unless Lightfinder is enabled."); - - ImGuiHelpers.ScaledDummy(5f); - - ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); - ImGui.TextWrapped("Use Lightfinder when you're okay with being visible to other users and understand that you are responsible for your own experience."); - ImGui.PopStyleColor(); - - ImGui.PopStyleVar(); - - ImGuiHelpers.ScaledDummy(3f); - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); - - if (_configService.Current.BroadcastEnabled) - { - ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessGreen")); - ImGui.Text("The Lightfinder calls, and somewhere, a soul may answer."); // cringe.. - ImGui.PopStyleColor(); - - var ttl = _broadcastService.RemainingTtl; - if (ttl is { } remaining && remaining > TimeSpan.Zero) - { - ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow")); - ImGui.Text($"Still shining, for {remaining:hh\\:mm\\:ss}"); - ImGui.PopStyleColor(); - } - else - { - ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); - ImGui.Text("The Lightfinder’s light wanes, but not in vain."); // cringe.. - ImGui.PopStyleColor(); - } - } - else - { - ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); - ImGui.Text("The Lightfinder rests, waiting to shine again."); // cringe.. - ImGui.PopStyleColor(); - } - - var cooldown = _broadcastService.RemainingCooldown; - if (cooldown is { } cd) - { - ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); - ImGui.Text($"The Lightfinder gathers its strength... ({Math.Ceiling(cd.TotalSeconds)}s)"); - ImGui.PopStyleColor(); - } - - ImGuiHelpers.ScaledDummy(0.5f); - - bool isBroadcasting = _broadcastService.IsBroadcasting; - bool isOnCooldown = cooldown.HasValue && cooldown.Value.TotalSeconds > 0; - - ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10.0f); - - if (isOnCooldown) - ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("DimRed")); - else if (isBroadcasting) - ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen")); - else - ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("PairBlue")); - - if (isOnCooldown || !_broadcastService.IsLightFinderAvailable) - ImGui.BeginDisabled(); - - string buttonText = isBroadcasting ? "Disable Lightfinder" : "Enable Lightfinder"; - - if (ImGui.Button(buttonText, new Vector2(200 * ImGuiHelpers.GlobalScale, 0))) - { - _broadcastService.ToggleBroadcast(); - } - - var toggleButtonHeight = ImGui.GetItemRectSize().Y; - - if (isOnCooldown || !_broadcastService.IsLightFinderAvailable) - ImGui.EndDisabled(); - - ImGui.PopStyleColor(); - ImGui.PopStyleVar(); - - ImGui.SameLine(); - if (_uiSharedService.IconButton(FontAwesomeIcon.Cog, toggleButtonHeight)) - { - Mediator.Publish(new OpenLightfinderSettingsMessage()); - } - - if (ImGui.IsItemHovered()) - { - ImGui.BeginTooltip(); - ImGui.TextUnformatted("Open Lightfinder settings."); - ImGui.EndTooltip(); - } - - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Syncshell Finder")) - { - if (_allSyncshells == null) - { - ImGui.Text("Loading Syncshells..."); - _ = RefreshSyncshells(); - return; - } - - _uiSharedService.MediumText("Syncshell Finder", UIColors.Get("PairBlue")); - - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); - - ImGui.PushTextWrapPos(); - ImGui.Text("Allow your owned Syncshell to be indexed by the Nearby Syncshell Finder."); - ImGui.Text("To enable this, select one of your owned Syncshells from the dropdown menu below and ensure that \"Toggle Syncshell Finder\" is enabled. Your Syncshell will be visible in the Nearby Syncshell Finder as long as Lightfinder is active."); - ImGui.PopTextWrapPos(); - - ImGuiHelpers.ScaledDummy(0.2f); - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); - - bool ShellFinderEnabled = _configService.Current.SyncshellFinderEnabled; - bool isBroadcasting = _broadcastService.IsBroadcasting; - - if (isBroadcasting) - { - var warningColor = UIColors.Get("LightlessYellow"); - _uiSharedService.DrawNoteLine("! ", warningColor, - new SeStringUtils.RichTextEntry("Syncshell Finder can only be changed while Lightfinder is disabled.", warningColor)); - ImGuiHelpers.ScaledDummy(0.2f); - } - - if (isBroadcasting) - ImGui.BeginDisabled(); - - if (ImGui.Checkbox("Toggle Syncshell Finder", ref ShellFinderEnabled)) - { - _configService.Current.SyncshellFinderEnabled = ShellFinderEnabled; - _configService.Save(); - } - - if (ImGui.IsItemHovered()) - { - ImGui.BeginTooltip(); - ImGui.Text("Toggle to broadcast specified Syncshell."); - ImGui.EndTooltip(); - } - - var selectedGid = _configService.Current.SelectedFinderSyncshell; - var currentOption = _syncshellOptions.FirstOrDefault(o => string.Equals(o.GID, selectedGid, StringComparison.Ordinal)); - var preview = currentOption.Label ?? "Select a Syncshell..."; - - if (ImGui.BeginCombo("##SyncshellDropdown", preview)) - { - foreach (var (label, gid, available) in _syncshellOptions) - { - bool isSelected = string.Equals(gid, selectedGid, StringComparison.Ordinal); - - if (!available) - ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); - - if (ImGui.Selectable(label, isSelected)) - { - _configService.Current.SelectedFinderSyncshell = gid; - _configService.Save(); - _ = RefreshSyncshells(); - } - - if (!available && ImGui.IsItemHovered()) - { - ImGui.BeginTooltip(); - ImGui.Text("This Syncshell is not available on the current service."); - ImGui.EndTooltip(); - } - - if (!available) - ImGui.PopStyleColor(); - - if (isSelected) - ImGui.SetItemDefaultFocus(); - } - - ImGui.EndCombo(); - } - - - if (ImGui.IsItemHovered()) - { - ImGui.BeginTooltip(); - ImGui.Text("Choose one of the available options."); - ImGui.EndTooltip(); - } - - - if (isBroadcasting) - ImGui.EndDisabled(); - - ImGui.EndTabItem(); - } + ClearSelection(); + } #if DEBUG - if (ImGui.BeginTabItem("Debug")) - { - ImGui.Text("Broadcast Cache"); - - if (ImGui.BeginTable("##BroadcastCacheTable", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, new Vector2(-1, 225f))) - { - ImGui.TableSetupColumn("CID", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("IsBroadcasting", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Expires In", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Syncshell GID", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableHeadersRow(); - - var now = DateTime.UtcNow; - - foreach (var (cid, entry) in _broadcastScannerService.BroadcastCache) - { - ImGui.TableNextRow(); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted(cid.Truncate(12)); - if (ImGui.IsItemHovered()) - { - ImGui.BeginTooltip(); - ImGui.TextUnformatted(cid); - ImGui.EndTooltip(); - } - - ImGui.TableNextColumn(); - var colorBroadcast = entry.IsBroadcasting - ? UIColors.Get("LightlessGreen") - : UIColors.Get("DimRed"); - - ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, ImGui.GetColorU32(colorBroadcast)); - ImGui.TextUnformatted(entry.IsBroadcasting.ToString()); - - ImGui.TableNextColumn(); - var remaining = entry.ExpiryTime - now; - var colorTtl = - remaining <= TimeSpan.Zero ? UIColors.Get("DimRed") : - remaining < TimeSpan.FromSeconds(10) ? UIColors.Get("LightlessYellow") : - (Vector4?)null; - - if (colorTtl != null) - ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, ImGui.GetColorU32(colorTtl.Value)); - - ImGui.TextUnformatted(remaining > TimeSpan.Zero - ? remaining.ToString("hh\\:mm\\:ss") - : "Expired"); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted(entry.GID ?? "-"); - } - - ImGui.EndTable(); - } - - ImGui.EndTabItem(); - } + private static List BuildTestSyncshells() + { + return + [ + new(new GroupData("TEST-ALPHA", "Alpha Shell"), "", GroupUserPreferredPermissions.NoneSet), + new(new GroupData("TEST-BETA", "Beta Shell"), "", GroupUserPreferredPermissions.NoneSet), + new(new GroupData("TEST-GAMMA", "Gamma Shell"), "", GroupUserPreferredPermissions.NoneSet), + new(new GroupData("TEST-DELTA", "Delta Shell"), "", GroupUserPreferredPermissions.NoneSet), + new(new GroupData("TEST-EPSILON", "Epsilon Shell"), "", GroupUserPreferredPermissions.NoneSet), + new(new GroupData("TEST-ZETA", "Zeta Shell"), "", GroupUserPreferredPermissions.NoneSet), + new(new GroupData("TEST-ETA", "Eta Shell"), "", GroupUserPreferredPermissions.NoneSet), + new(new GroupData("TEST-THETA", "Theta Shell"), "", GroupUserPreferredPermissions.NoneSet), + new(new GroupData("TEST-IOTA", "Iota Shell"), "", GroupUserPreferredPermissions.NoneSet), + new(new GroupData("TEST-KAPPA", "Kappa Shell"), "", GroupUserPreferredPermissions.NoneSet), + ]; + } #endif - ImGui.EndTabBar(); + private void ClearSyncshells() + { + if (_nearbySyncshells.Count == 0) return; + _nearbySyncshells.Clear(); + ClearSelection(); + } + + private void ClearSelection() + { + _selectedNearbyIndex = -1; + _joinDto = null; + _joinInfo = null; + } + + private string? GetSelectedGid() + { + if (_selectedNearbyIndex < 0 || _selectedNearbyIndex >= _nearbySyncshells.Count) + return null; + return _nearbySyncshells[_selectedNearbyIndex].Group.GID; + } + + #endregion + + #region Helpers + + private (float widthUsed, float rowHeight) RenderProfileTagsSingleRow(IReadOnlyList tags, float scale) + { + if (tags == null || tags.Count == 0) + return (0f, 0f); + + var drawList = ImGui.GetWindowDrawList(); + var style = ImGui.GetStyle(); + var defaultTextColorU32 = ImGui.GetColorU32(ImGuiCol.Text); + + var baseLocal = ImGui.GetCursorPos(); + var baseScreen = ImGui.GetCursorScreenPos(); + float availableWidth = ImGui.GetContentRegionAvail().X; + if (availableWidth <= 0f) + availableWidth = 1f; + + float cursorLocalX = baseLocal.X; + float cursorScreenX = baseScreen.X; + float rowHeight = 0f; + + for (int i = 0; i < tags.Count; i++) + { + var tag = tags[i]; + if (!tag.HasContent) + continue; + + var tagSize = ProfileTagRenderer.MeasureTag(tag, scale, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _seResolvedSegments, GetIconWrap, _logger); + + float tagWidth = tagSize.X; + float tagHeight = tagSize.Y; + + if (cursorLocalX > baseLocal.X && cursorLocalX + tagWidth > baseLocal.X + availableWidth) + break; + + var tagScreenPos = new Vector2(cursorScreenX, baseScreen.Y); + ImGui.SetCursorScreenPos(tagScreenPos); + ImGui.InvisibleButton($"##profileTagInline_{i}", tagSize); + + ProfileTagRenderer.RenderTag(tag, tagScreenPos, scale, drawList, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _seResolvedSegments, GetIconWrap, _logger); + + cursorLocalX += tagWidth + style.ItemSpacing.X; + cursorScreenX += tagWidth + style.ItemSpacing.X; + rowHeight = MathF.Max(rowHeight, tagHeight); + } + + ImGui.SetCursorPos(new Vector2(baseLocal.X, baseLocal.Y + rowHeight)); + + float widthUsed = cursorLocalX - baseLocal.X; + return (widthUsed, rowHeight); + } + + private static string TruncateTextToWidth(string text, float maxWidth) + { + if (string.IsNullOrEmpty(text)) + return text; + + const string ellipsis = "..."; + float ellipsisWidth = ImGui.CalcTextSize(ellipsis).X; + + if (maxWidth <= ellipsisWidth) + return ellipsis; + + int low = 0; + int high = text.Length; + string best = ellipsis; + + while (low <= high) + { + int mid = (low + high) / 2; + string candidate = string.Concat(text.AsSpan(0, mid), ellipsis); + float width = ImGui.CalcTextSize(candidate).X; + + if (width <= maxWidth) + { + best = candidate; + low = mid + 1; + } + else + { + high = mid - 1; } } + + return best; } + + private IDalamudTextureWrap? GetIconWrap(uint iconId) + { + try + { + if (_uiSharedService.TryGetIcon(iconId, out var wrap) && wrap != null) + return wrap; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to resolve icon {IconId}", iconId); + } + + return null; + } + + #endregion } diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs deleted file mode 100644 index 0586c06..0000000 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ /dev/null @@ -1,850 +0,0 @@ -using Dalamud.Bindings.ImGui; -using Dalamud.Interface; -using Dalamud.Interface.Colors; -using Dalamud.Interface.Textures.TextureWraps; -using Dalamud.Interface.Utility; -using Dalamud.Interface.Utility.Raii; -using Dalamud.Plugin.Services; -using LightlessSync.API.Data; -using LightlessSync.API.Data.Enum; -using LightlessSync.API.Data.Extensions; -using LightlessSync.API.Dto; -using LightlessSync.API.Dto.Group; -using LightlessSync.Services; -using LightlessSync.Services.LightFinder; -using LightlessSync.Services.Mediator; -using LightlessSync.UI.Services; -using LightlessSync.UI.Tags; -using LightlessSync.Utils; -using LightlessSync.WebAPI; -using Microsoft.Extensions.Logging; -using System.Numerics; - -namespace LightlessSync.UI; - -public class SyncshellFinderUI : WindowMediatorSubscriberBase -{ - private readonly ApiController _apiController; - private readonly LightFinderService _broadcastService; - private readonly UiSharedService _uiSharedService; - private readonly LightFinderScannerService _broadcastScannerService; - private readonly PairUiService _pairUiService; - private readonly DalamudUtilService _dalamudUtilService; - - private Vector4 _tagBackgroundColor = new(0.18f, 0.18f, 0.18f, 0.95f); - private Vector4 _tagBorderColor = new(0.35f, 0.35f, 0.35f, 0.4f); - - private readonly List _seResolvedSegments = new(); - private readonly List _nearbySyncshells = []; - private List _currentSyncshells = []; - private int _selectedNearbyIndex = -1; - private int _syncshellPageIndex = 0; - private readonly HashSet _recentlyJoined = new(StringComparer.Ordinal); - - private GroupJoinDto? _joinDto; - private GroupJoinInfoDto? _joinInfo; - private DefaultPermissionsDto _ownPermissions = null!; - private bool _useTestSyncshells = false; - - private bool _compactView = false; - private readonly LightlessProfileManager _lightlessProfileManager; - - public SyncshellFinderUI( - ILogger logger, - LightlessMediator mediator, - PerformanceCollectorService performanceCollectorService, - LightFinderService broadcastService, - UiSharedService uiShared, - ApiController apiController, - LightFinderScannerService broadcastScannerService, - PairUiService pairUiService, - DalamudUtilService dalamudUtilService, - LightlessProfileManager lightlessProfileManager) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService) - { - _broadcastService = broadcastService; - _uiSharedService = uiShared; - _apiController = apiController; - _broadcastScannerService = broadcastScannerService; - _pairUiService = pairUiService; - _dalamudUtilService = dalamudUtilService; - _lightlessProfileManager = lightlessProfileManager; - - IsOpen = false; - WindowBuilder.For(this) - .SetSizeConstraints(new Vector2(600, 400), new Vector2(600, 550)) - .Apply(); - - Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false)); - Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false)); - Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync(_.gid).ConfigureAwait(false)); - Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync(_.gid).ConfigureAwait(false)); - } - - public override async void OnOpen() - { - _ownPermissions = _apiController.DefaultPermissions.DeepClone()!; - await RefreshSyncshellsAsync().ConfigureAwait(false); - } - - protected override void DrawInternal() - { - ImGui.BeginGroup(); - _uiSharedService.MediumText("Nearby Syncshells", UIColors.Get("LightlessPurple")); - -#if DEBUG - if (ImGui.SmallButton("Show test syncshells")) - { - _useTestSyncshells = !_useTestSyncshells; - _ = Task.Run(async () => await RefreshSyncshellsAsync().ConfigureAwait(false)); - } - ImGui.SameLine(); -#endif - - string checkboxLabel = "Compact view"; - float availWidth = ImGui.GetContentRegionAvail().X; - float checkboxWidth = ImGui.CalcTextSize(checkboxLabel).X + ImGui.GetFrameHeight(); - - float rightX = ImGui.GetCursorPosX() + availWidth - checkboxWidth - 4.0f; - ImGui.SetCursorPosX(rightX); - ImGui.Checkbox(checkboxLabel, ref _compactView); - ImGui.EndGroup(); - - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault")); - ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale)); - if (_nearbySyncshells.Count == 0) - { - ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted."); - - if (!_broadcastService.IsBroadcasting) - { - - UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow")); - - ImGui.TextColored(UIColors.Get("LightlessYellow"), "Lightfinder is currently disabled, to locate nearby syncshells, Lightfinder must be active."); - ImGuiHelpers.ScaledDummy(0.5f); - - ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10.0f); - ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessPurple")); - - if (ImGui.Button("Open Lightfinder", new Vector2(200 * ImGuiHelpers.GlobalScale, 0))) - { - Mediator.Publish(new UiToggleMessage(typeof(LightFinderUI))); - } - - ImGui.PopStyleColor(); - ImGui.PopStyleVar(); - - return; - } - - return; - } - - string? myHashedCid = null; - try - { - var cid = _dalamudUtilService.GetCID(); - myHashedCid = cid.ToString().GetHash256(); - } - catch (Exception ex) - { - _logger.LogDebug(ex, "Failed to get CID, not excluding own broadcast."); - } - var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().Where(b => !string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal)).ToList() ?? []; - - var cardData = new List<(GroupJoinDto Shell, string BroadcasterName)>(); - - foreach (var shell in _nearbySyncshells) - { - string broadcasterName; - - if (shell?.Group == null || string.IsNullOrEmpty(shell.Group.GID)) - continue; - - if (_useTestSyncshells) - { - var displayName = !string.IsNullOrEmpty(shell.Group.Alias) - ? shell.Group.Alias - : shell.Group.GID; - - broadcasterName = $"{displayName} (Tester of TestWorld)"; - } - else - { - var broadcast = broadcasts - .FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal)); - - if (broadcast == null) - continue; - - var (name, address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID); - if (string.IsNullOrEmpty(name)) - continue; - - var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(address); - broadcasterName = !string.IsNullOrEmpty(worldName) - ? $"{name} ({worldName})" - : name; - } - - cardData.Add((shell, broadcasterName)); - } - - if (cardData.Count == 0) - { - ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted."); - return; - } - - if (_compactView) - { - DrawSyncshellGrid(cardData); - } - else - { - DrawSyncshellList(cardData); - } - - - if (_joinDto != null && _joinInfo != null && _joinInfo.Success) - DrawConfirmation(); - } - - private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName)> listData) - { - const int shellsPerPage = 3; - var totalPages = (int)Math.Ceiling(listData.Count / (float)shellsPerPage); - if (totalPages <= 0) - totalPages = 1; - - _syncshellPageIndex = Math.Clamp(_syncshellPageIndex, 0, totalPages - 1); - - var firstIndex = _syncshellPageIndex * shellsPerPage; - var lastExclusive = Math.Min(firstIndex + shellsPerPage, listData.Count); - - ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8.0f); - ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1.0f); - - for (int index = firstIndex; index < lastExclusive; index++) - { - var (shell, broadcasterName) = listData[index]; - - ImGui.PushID(shell.Group.GID); - float rowHeight = 74f * ImGuiHelpers.GlobalScale; - - ImGui.BeginChild($"ShellRow##{shell.Group.GID}", new Vector2(-1, rowHeight), border: true); - - var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID; - - var style = ImGui.GetStyle(); - float startX = ImGui.GetCursorPosX(); - float regionW = ImGui.GetContentRegionAvail().X; - float rightTxtW = ImGui.CalcTextSize(broadcasterName).X; - - _uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple")); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Click to open profile."); - if (ImGui.IsItemClicked()) - { - Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group)); - } - - float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X; - ImGui.SameLine(); - ImGui.SetCursorPosX(rightX); - ImGui.TextUnformatted(broadcasterName); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Broadcaster of the syncshell."); - - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault")); - - var groupProfile = _lightlessProfileManager.GetLightlessGroupProfile(shell.Group); - - IReadOnlyList groupTags = - groupProfile != null && groupProfile.Tags.Count > 0 - ? ProfileTagService.ResolveTags(groupProfile.Tags) - : []; - - var limitedTags = groupTags.Count > 3 - ? [.. groupTags.Take(3)] - : groupTags; - - float tagScale = ImGuiHelpers.GlobalScale * 0.9f; - - Vector2 rowStartLocal = ImGui.GetCursorPos(); - - float tagsWidth = 0f; - float tagsHeight = 0f; - - if (limitedTags.Count > 0) - { - (tagsWidth, tagsHeight) = RenderProfileTagsSingleRow(limitedTags, tagScale); - } - else - { - ImGui.SetCursorPosX(startX); - ImGui.TextDisabled("-- No tags set --"); - ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale)); - } - - float btnBaselineY = rowStartLocal.Y; - float joinX = rowStartLocal.X + (tagsWidth > 0 ? tagsWidth + style.ItemSpacing.X : 0f); - - ImGui.SetCursorPos(new Vector2(joinX, btnBaselineY)); - DrawJoinButton(shell); - - float btnHeight = ImGui.GetFrameHeightWithSpacing(); - float rowHeightUsed = MathF.Max(tagsHeight, btnHeight); - - ImGui.SetCursorPos(new Vector2( - rowStartLocal.X, - rowStartLocal.Y + rowHeightUsed)); - - ImGui.EndChild(); - ImGui.PopID(); - - ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale)); - } - - ImGui.PopStyleVar(2); - - DrawPagination(totalPages); - } - - private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName)> cardData) - { - const int shellsPerPage = 4; - var totalPages = (int)Math.Ceiling(cardData.Count / (float)shellsPerPage); - if (totalPages <= 0) - totalPages = 1; - - _syncshellPageIndex = Math.Clamp(_syncshellPageIndex, 0, totalPages - 1); - - var firstIndex = _syncshellPageIndex * shellsPerPage; - var lastExclusive = Math.Min(firstIndex + shellsPerPage, cardData.Count); - - var avail = ImGui.GetContentRegionAvail(); - var spacing = ImGui.GetStyle().ItemSpacing; - - var cardWidth = (avail.X - spacing.X) / 2.0f; - var cardHeight = (avail.Y - spacing.Y - (ImGui.GetFrameHeightWithSpacing() * 2.0f)) / 2.0f; - cardHeight = MathF.Max(110f * ImGuiHelpers.GlobalScale, cardHeight); - - ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8.0f); - ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1.0f); - - for (int index = firstIndex; index < lastExclusive; index++) - { - var localIndex = index - firstIndex; - var (shell, broadcasterName) = cardData[index]; - - if (localIndex % 2 != 0) - ImGui.SameLine(); - - ImGui.PushID(shell.Group.GID); - - ImGui.BeginGroup(); - _ = ImGui.BeginChild("ShellCard##" + shell.Group.GID, new Vector2(cardWidth, cardHeight), border: true); - - var displayName = !string.IsNullOrEmpty(shell.Group.Alias) - ? shell.Group.Alias - : shell.Group.GID; - - var style = ImGui.GetStyle(); - float startX = ImGui.GetCursorPosX(); - float availW = ImGui.GetContentRegionAvail().X; - - ImGui.BeginGroup(); - - _uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple")); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Click to open profile."); - if (ImGui.IsItemClicked()) - { - Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group)); - } - - float nameRightX = ImGui.GetItemRectMax().X; - - var regionMinScreen = ImGui.GetCursorScreenPos(); - float regionRightX = regionMinScreen.X + availW; - - float minBroadcasterX = nameRightX + style.ItemSpacing.X; - - float maxBroadcasterWidth = regionRightX - minBroadcasterX; - - string broadcasterToShow = broadcasterName; - - if (!string.IsNullOrEmpty(broadcasterName) && maxBroadcasterWidth > 0f) - { - float bcFullWidth = ImGui.CalcTextSize(broadcasterName).X; - string toolTip; - - if (bcFullWidth > maxBroadcasterWidth) - { - broadcasterToShow = TruncateTextToWidth(broadcasterName, maxBroadcasterWidth); - toolTip = broadcasterName + Environment.NewLine + Environment.NewLine + "Broadcaster of the syncshell."; - } - else - { - toolTip = "Broadcaster of the syncshell."; - } - - float bcWidth = ImGui.CalcTextSize(broadcasterToShow).X; - - float broadX = regionRightX - bcWidth; - - broadX = MathF.Max(broadX, minBroadcasterX); - - ImGui.SameLine(); - var curPos = ImGui.GetCursorPos(); - ImGui.SetCursorPos(new Vector2(broadX - regionMinScreen.X + startX, curPos.Y + 3f * ImGuiHelpers.GlobalScale)); - ImGui.TextUnformatted(broadcasterToShow); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip(toolTip); - } - - ImGui.EndGroup(); - - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault")); - - ImGui.Dummy(new Vector2(0, 6 * ImGuiHelpers.GlobalScale)); - - var groupProfile = _lightlessProfileManager.GetLightlessGroupProfile(shell.Group); - - IReadOnlyList groupTags = - groupProfile != null && groupProfile.Tags.Count > 0 - ? ProfileTagService.ResolveTags(groupProfile.Tags) - : []; - - float tagScale = ImGuiHelpers.GlobalScale * 0.9f; - - if (groupTags.Count > 0) - { - var limitedTags = groupTags.Count > 2 - ? [.. groupTags.Take(2)] - : groupTags; - - ImGui.SetCursorPosX(startX); - - var (_, tagsHeight) = RenderProfileTagsSingleRow(limitedTags, tagScale); - - ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale)); - } - else - { - ImGui.SetCursorPosX(startX); - ImGui.TextDisabled("-- No tags set --"); - ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale)); - } - - var buttonHeight = ImGui.GetFrameHeightWithSpacing(); - var remainingY = ImGui.GetContentRegionAvail().Y - buttonHeight; - if (remainingY > 0) - ImGui.Dummy(new Vector2(0, remainingY)); - - DrawJoinButton(shell); - - ImGui.EndChild(); - ImGui.EndGroup(); - - ImGui.PopID(); - } - - ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale)); - ImGui.PopStyleVar(2); - - DrawPagination(totalPages); - } - - private void DrawPagination(int totalPages) - { - if (totalPages > 1) - { - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault")); - - var style = ImGui.GetStyle(); - string pageLabel = $"Page {_syncshellPageIndex + 1}/{totalPages}"; - - float prevWidth = ImGui.CalcTextSize("<").X + style.FramePadding.X * 2; - float nextWidth = ImGui.CalcTextSize(">").X + style.FramePadding.X * 2; - float textWidth = ImGui.CalcTextSize(pageLabel).X; - - float totalWidth = prevWidth + textWidth + nextWidth + style.ItemSpacing.X * 2; - - float availWidth = ImGui.GetContentRegionAvail().X; - float offsetX = (availWidth - totalWidth) * 0.5f; - - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + offsetX); - - if (ImGui.Button("<##PrevSyncshellPage") && _syncshellPageIndex > 0) - _syncshellPageIndex--; - - ImGui.SameLine(); - ImGui.Text(pageLabel); - - ImGui.SameLine(); - if (ImGui.Button(">##NextSyncshellPage") && _syncshellPageIndex < totalPages - 1) - _syncshellPageIndex++; - } - } - - private void DrawJoinButton(dynamic shell) - { - const string visibleLabel = "Join"; - var label = $"{visibleLabel}##{shell.Group.GID}"; - - var isAlreadyMember = _currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal)); - var isRecentlyJoined = _recentlyJoined.Contains(shell.GID); - - Vector2 buttonSize; - - if (!_compactView) - { - var style = ImGui.GetStyle(); - var textSize = ImGui.CalcTextSize(visibleLabel); - - var width = textSize.X + style.FramePadding.X * 20f; - buttonSize = new Vector2(width, 30f); - - float availX = ImGui.GetContentRegionAvail().X; - float curX = ImGui.GetCursorPosX(); - float newX = curX + (availX - buttonSize.X); - ImGui.SetCursorPosX(newX); - } - else - { - buttonSize = new Vector2(-1, 0); - } - - if (!isAlreadyMember && !isRecentlyJoined) - { - ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen")); - ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f)); - ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f)); - if (ImGui.Button(label, buttonSize)) - { - _logger.LogInformation($"Join requested for Syncshell {shell.Group.GID} ({shell.Group.Alias})"); - - _ = Task.Run(async () => - { - try - { - var info = await _apiController.GroupJoinHashed(new GroupJoinHashedDto( - shell.Group, - shell.Password, - shell.GroupUserPreferredPermissions - )).ConfigureAwait(false); - - if (info != null && info.Success) - { - _joinDto = new GroupJoinDto(shell.Group, shell.Password, shell.GroupUserPreferredPermissions); - _joinInfo = info; - _ownPermissions = _apiController.DefaultPermissions.DeepClone()!; - - _logger.LogInformation($"Fetched join info for {shell.Group.GID}"); - } - else - { - _logger.LogWarning($"Failed to join {shell.Group.GID}: info was null or unsuccessful"); - } - } - catch (Exception ex) - { - _logger.LogError(ex, $"Join failed for {shell.Group.GID}"); - } - }); - } - } - else - { - ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("DimRed")); - ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("DimRed").WithAlpha(0.85f)); - ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("DimRed").WithAlpha(0.75f)); - - using (ImRaii.Disabled()) - { - ImGui.Button(label, buttonSize); - } - - UiSharedService.AttachToolTip("Already a member or owner of this Syncshell."); - } - - ImGui.PopStyleColor(3); - } - - private (float widthUsed, float rowHeight) RenderProfileTagsSingleRow(IReadOnlyList tags, float scale) - { - if (tags == null || tags.Count == 0) - return (0f, 0f); - - var drawList = ImGui.GetWindowDrawList(); - var style = ImGui.GetStyle(); - var defaultTextColorU32 = ImGui.GetColorU32(ImGuiCol.Text); - - var baseLocal = ImGui.GetCursorPos(); - var baseScreen = ImGui.GetCursorScreenPos(); - float availableWidth = ImGui.GetContentRegionAvail().X; - if (availableWidth <= 0f) - availableWidth = 1f; - - float cursorLocalX = baseLocal.X; - float cursorScreenX = baseScreen.X; - float rowHeight = 0f; - - for (int i = 0; i < tags.Count; i++) - { - var tag = tags[i]; - if (!tag.HasContent) - continue; - - var tagSize = ProfileTagRenderer.MeasureTag(tag, scale, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _seResolvedSegments, GetIconWrap, _logger); - - float tagWidth = tagSize.X; - float tagHeight = tagSize.Y; - - if (cursorLocalX > baseLocal.X && cursorLocalX + tagWidth > baseLocal.X + availableWidth) - break; - - var tagScreenPos = new Vector2(cursorScreenX, baseScreen.Y); - ImGui.SetCursorScreenPos(tagScreenPos); - ImGui.InvisibleButton($"##profileTagInline_{i}", tagSize); - - ProfileTagRenderer.RenderTag(tag, tagScreenPos, scale, drawList, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _seResolvedSegments, GetIconWrap, _logger); - - cursorLocalX += tagWidth + style.ItemSpacing.X; - cursorScreenX += tagWidth + style.ItemSpacing.X; - rowHeight = MathF.Max(rowHeight, tagHeight); - } - - ImGui.SetCursorPos(new Vector2(baseLocal.X, baseLocal.Y + rowHeight)); - - float widthUsed = cursorLocalX - baseLocal.X; - return (widthUsed, rowHeight); - } - private static string TruncateTextToWidth(string text, float maxWidth) - { - if (string.IsNullOrEmpty(text)) - return text; - - const string ellipsis = "..."; - float ellipsisWidth = ImGui.CalcTextSize(ellipsis).X; - - if (maxWidth <= ellipsisWidth) - return ellipsis; - - int low = 0; - int high = text.Length; - string best = ellipsis; - - while (low <= high) - { - int mid = (low + high) / 2; - string candidate = string.Concat(text.AsSpan(0, mid), ellipsis); - float width = ImGui.CalcTextSize(candidate).X; - - if (width <= maxWidth) - { - best = candidate; - low = mid + 1; - } - else - { - high = mid - 1; - } - } - - return best; - } - - private IDalamudTextureWrap? GetIconWrap(uint iconId) - { - try - { - if (_uiSharedService.TryGetIcon(iconId, out var wrap) && wrap != null) - return wrap; - } - catch (Exception ex) - { - _logger.LogDebug(ex, "Failed to resolve icon {IconId} for profile tags", iconId); - } - - return null; - } - - private void DrawConfirmation() - { - if (_joinDto != null && _joinInfo != null) - { - ImGui.Separator(); - ImGui.TextUnformatted($"Join Syncshell: {_joinDto.Group.AliasOrGID} by {_joinInfo.OwnerAliasOrUID}"); - ImGuiHelpers.ScaledDummy(2f); - ImGui.TextUnformatted("Suggested Syncshell Permissions:"); - - DrawPermissionRow("Sounds", _joinInfo.GroupPermissions.IsPreferDisableSounds(), _ownPermissions.DisableGroupSounds, v => _ownPermissions.DisableGroupSounds = v); - DrawPermissionRow("Animations", _joinInfo.GroupPermissions.IsPreferDisableAnimations(), _ownPermissions.DisableGroupAnimations, v => _ownPermissions.DisableGroupAnimations = v); - DrawPermissionRow("VFX", _joinInfo.GroupPermissions.IsPreferDisableVFX(), _ownPermissions.DisableGroupVFX, v => _ownPermissions.DisableGroupVFX = v); - - ImGui.NewLine(); - ImGui.NewLine(); - - if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Plus, $"Finalize and join {_joinDto.Group.AliasOrGID}")) - { - var finalPermissions = GroupUserPreferredPermissions.NoneSet; - finalPermissions.SetDisableSounds(_ownPermissions.DisableGroupSounds); - finalPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations); - finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX); - - _ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions)); - - _recentlyJoined.Add(_joinDto.Group.GID); - - _joinDto = null; - _joinInfo = null; - } - } - } - - private void DrawPermissionRow(string label, bool suggested, bool current, Action apply) - { - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted($"- {label}"); - - ImGui.SameLine(150 * ImGuiHelpers.GlobalScale); - ImGui.TextUnformatted("Current:"); - ImGui.SameLine(); - _uiSharedService.BooleanToColoredIcon(!current); - - ImGui.SameLine(300 * ImGuiHelpers.GlobalScale); - ImGui.TextUnformatted("Suggested:"); - ImGui.SameLine(); - _uiSharedService.BooleanToColoredIcon(!suggested); - - ImGui.SameLine(450 * ImGuiHelpers.GlobalScale); - using var id = ImRaii.PushId(label); - if (current != suggested) - { - if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Apply")) - apply(suggested); - } - - ImGui.NewLine(); - } - - private async Task RefreshSyncshellsAsync(string? gid = null) - { - var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts(); - var snapshot = _pairUiService.GetSnapshot(); - _currentSyncshells = [.. snapshot.GroupPairs.Keys]; - - _recentlyJoined.RemoveWhere(gid => - _currentSyncshells.Exists(s => string.Equals(s.GID, gid, StringComparison.Ordinal))); - - List? updatedList = []; - - if (_useTestSyncshells) - { - updatedList = BuildTestSyncshells(); - } - else - { - if (syncshellBroadcasts.Count == 0) - { - ClearSyncshells(); - return; - } - - try - { - var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts) - .ConfigureAwait(false); - updatedList = groups?.DistinctBy(g => g.Group.GID).ToList(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to refresh broadcasted syncshells."); - return; - } - } - - if (updatedList == null || updatedList.Count == 0) - { - ClearSyncshells(); - return; - } - - if (gid != null && _recentlyJoined.Contains(gid)) - { - _recentlyJoined.Clear(); - } - - var previousGid = GetSelectedGid(); - - _nearbySyncshells.Clear(); - _nearbySyncshells.AddRange(updatedList); - - if (previousGid != null) - { - var newIndex = _nearbySyncshells.FindIndex(s => - string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal)); - - if (newIndex >= 0) - { - _selectedNearbyIndex = newIndex; - return; - } - } - - ClearSelection(); - } - - private static List BuildTestSyncshells() - { - var testGroup1 = new GroupData("TEST-ALPHA", "Alpha Shell"); - var testGroup2 = new GroupData("TEST-BETA", "Beta Shell"); - var testGroup3 = new GroupData("TEST-GAMMA", "Gamma Shell"); - var testGroup4 = new GroupData("TEST-DELTA", "Delta Shell"); - var testGroup5 = new GroupData("TEST-CHARLIE", "Charlie Shell"); - var testGroup6 = new GroupData("TEST-OMEGA", "Omega Shell"); - var testGroup7 = new GroupData("TEST-POINT", "Point Shell"); - var testGroup8 = new GroupData("TEST-HOTEL", "Hotel Shell"); - - return - [ - new(testGroup1, "", GroupUserPreferredPermissions.NoneSet), - new(testGroup2, "", GroupUserPreferredPermissions.NoneSet), - new(testGroup3, "", GroupUserPreferredPermissions.NoneSet), - new(testGroup4, "", GroupUserPreferredPermissions.NoneSet), - new(testGroup5, "", GroupUserPreferredPermissions.NoneSet), - new(testGroup6, "", GroupUserPreferredPermissions.NoneSet), - new(testGroup7, "", GroupUserPreferredPermissions.NoneSet), - new(testGroup8, "", GroupUserPreferredPermissions.NoneSet), - ]; - } - - private void ClearSyncshells() - { - if (_nearbySyncshells.Count == 0) - return; - - _nearbySyncshells.Clear(); - ClearSelection(); - } - - private void ClearSelection() - { - _selectedNearbyIndex = -1; - _syncshellPageIndex = 0; - _joinDto = null; - _joinInfo = null; - } - - private string? GetSelectedGid() - { - if (_selectedNearbyIndex < 0 || _selectedNearbyIndex >= _nearbySyncshells.Count) - return null; - - return _nearbySyncshells[_selectedNearbyIndex].Group.GID; - } - -} diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index cc69a5d..b63b631 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -162,24 +162,18 @@ public class TopTabMenu ImGui.SameLine(); using (ImRaii.PushFont(UiBuilder.IconFont)) { - var x = ImGui.GetCursorScreenPos(); if (ImGui.Button(FontAwesomeIcon.Compass.ToIconString(), buttonSize)) { - TabSelection = TabSelection == SelectedTab.Lightfinder ? SelectedTab.None : SelectedTab.Lightfinder; + _lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI))); } if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive()) { Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding); } - ImGui.SameLine(); - var xAfter = ImGui.GetCursorScreenPos(); - if (TabSelection == SelectedTab.Lightfinder) - drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, - xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X }, - underlineColor, 2); + } - UiSharedService.AttachToolTip("Lightfinder"); + UiSharedService.AttachToolTip(GetLightfinderTooltip()); ImGui.SameLine(); using (ImRaii.PushFont(UiBuilder.IconFont)) @@ -234,10 +228,7 @@ public class TopTabMenu DrawSyncshellMenu(availableWidth, spacing.X); DrawGlobalSyncshellButtons(availableWidth, spacing.X); } - else if (TabSelection == SelectedTab.Lightfinder) - { - DrawLightfinderMenu(availableWidth, spacing.X); - } + else if (TabSelection == SelectedTab.UserConfig) { DrawUserConfig(availableWidth, spacing.X); @@ -779,26 +770,17 @@ public class TopTabMenu private void DrawLightfinderMenu(float availableWidth, float spacingX) { - var buttonX = (availableWidth - (spacingX)) / 2f; - - if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCirclePlus, "Lightfinder", buttonX, center: true)) + var lightfinderLabel = GetLightfinderTooltip(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCirclePlus, lightfinderLabel, availableWidth, center: true)) { _lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI))); } - - ImGui.SameLine(); - - var syncshellFinderLabel = GetSyncshellFinderLabel(); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Globe, syncshellFinderLabel, buttonX, center: true)) - { - _lightlessMediator.Publish(new UiToggleMessage(typeof(SyncshellFinderUI))); - } } - private string GetSyncshellFinderLabel() + private string GetLightfinderTooltip() { if (!_lightFinderService.IsBroadcasting) - return "Syncshell Finder"; + return "Open Lightfinder"; string? myHashedCid = null; try @@ -820,7 +802,7 @@ public class TopTabMenu .Distinct(StringComparer.Ordinal) .Count(); - return nearbyCount > 0 ? $"Syncshell Finder ({nearbyCount})" : "Syncshell Finder"; + return nearbyCount > 0 ? $"Lightfinder ({nearbyCount} nearby)" : "Open Lightfinder"; } private void DrawUserConfig(float availableWidth, float spacingX) diff --git a/LightlessSync/WebAPI/Files/FileDownloadManager.cs b/LightlessSync/WebAPI/Files/FileDownloadManager.cs index 49dd868..de9bca7 100644 --- a/LightlessSync/WebAPI/Files/FileDownloadManager.cs +++ b/LightlessSync/WebAPI/Files/FileDownloadManager.cs @@ -861,9 +861,17 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase { if (downloadCt.IsCancellationRequested) throw; - var req = await _orchestrator.SendRequestAsync(HttpMethod.Get, LightlessFiles.RequestCheckQueueFullPath(downloadFileTransfer[0].DownloadUri, requestId), - downloadFileTransfer.Select(c => c.Hash).ToList(), downloadCt).ConfigureAwait(false); - req.EnsureSuccessStatusCode(); + try + { + var req = await _orchestrator.SendRequestAsync(HttpMethod.Get, LightlessFiles.RequestCheckQueueFullPath(downloadFileTransfer[0].DownloadUri, requestId), + downloadFileTransfer.Select(c => c.Hash).ToList(), downloadCt).ConfigureAwait(false); + req.EnsureSuccessStatusCode(); + } + catch (Exception ex) when (!downloadCt.IsCancellationRequested) + { + Logger.LogDebug(ex, "Transient error checking queue status for {requestId}, will retry", requestId); + } + localTimeoutCts.Dispose(); composite.Dispose(); localTimeoutCts = new(); From 1ab4e2f94be84649d8a4737da52a8f7311a31afa Mon Sep 17 00:00:00 2001 From: cake Date: Fri, 26 Dec 2025 22:26:29 +0100 Subject: [PATCH 07/28] Added color options for header --- LightlessSync/UI/SettingsUi.cs | 25 ++++++----- LightlessSync/UI/Style/AnimatedHeader.cs | 53 +++++++++++++++++++----- LightlessSync/UI/UIColors.cs | 16 ++++--- 3 files changed, 68 insertions(+), 26 deletions(-) diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 91f69d5..a6b9c33 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -2795,16 +2795,21 @@ public class SettingsUi : WindowMediatorSubscriberBase var colorNames = new[] { - ("LightlessPurple", "Primary Purple", "Section titles and dividers"), - ("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"), - ("LightlessOrange", "Performance Orange", "Performance notifications and warnings"), - ("PairBlue", "Syncshell Blue", "Syncshell headers, toggle highlights, and moderator actions"), - ("DimRed", "Error Red", "Error and offline colors") - }; + ("LightlessPurple", "Primary Purple", "Section titles and dividers"), + ("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"), + ("LightlessOrange", "Performance Orange", "Performance notifications and warnings"), + ("PairBlue", "Syncshell Blue", "Syncshell headers, toggle highlights, and moderator actions"), + ("DimRed", "Error Red", "Error and offline colors"), + ("HeaderGradientTop", "Header Gradient (Top)", "Top color of the animated header background"), + ("HeaderGradientBottom", "Header Gradient (Bottom)", "Bottom color of the animated header background"), + ("HeaderStaticStar", "Header Stars", "Tint color for the static background stars in the header"), + ("HeaderShootingStar", "Header Shooting Star", "Tint color for the shooting star effect"), + }; + if (ImGui.BeginTable("##ColorTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) { diff --git a/LightlessSync/UI/Style/AnimatedHeader.cs b/LightlessSync/UI/Style/AnimatedHeader.cs index 839c704..9df0847 100644 --- a/LightlessSync/UI/Style/AnimatedHeader.cs +++ b/LightlessSync/UI/Style/AnimatedHeader.cs @@ -43,8 +43,19 @@ public class AnimatedHeader private const float _extendedParticleHeight = 40f; public float Height { get; set; } = 150f; + + // Color keys for theming + public string? TopColorKey { get; set; } = "HeaderGradientTop"; + public string? BottomColorKey { get; set; } = "HeaderGradientBottom"; + public string? StaticStarColorKey { get; set; } = "HeaderStaticStar"; + public string? ShootingStarColorKey { get; set; } = "HeaderShootingStar"; + + // Fallbacks if the color keys are not found public Vector4 TopColor { get; set; } = new(0.08f, 0.05f, 0.15f, 1.0f); public Vector4 BottomColor { get; set; } = new(0.12f, 0.08f, 0.20f, 1.0f); + public Vector4 StaticStarColor { get; set; } = new(1f, 1f, 1f, 1f); + public Vector4 ShootingStarColor { get; set; } = new(0.4f, 0.8f, 1.0f, 1.0f); + public bool EnableParticles { get; set; } = true; public bool EnableBottomGradient { get; set; } = true; @@ -148,16 +159,21 @@ public class AnimatedHeader { var drawList = ImGui.GetWindowDrawList(); + var top = ResolveColor(TopColorKey, TopColor); + var bottom = ResolveColor(BottomColorKey, BottomColor); + drawList.AddRectFilledMultiColor( headerStart, headerEnd, - ImGui.GetColorU32(TopColor), - ImGui.GetColorU32(TopColor), - ImGui.GetColorU32(BottomColor), - ImGui.GetColorU32(BottomColor) + ImGui.GetColorU32(top), + ImGui.GetColorU32(top), + ImGui.GetColorU32(bottom), + ImGui.GetColorU32(bottom) ); // Draw static background stars + var starBase = ResolveColor(StaticStarColorKey, StaticStarColor); + var random = new Random(42); for (int i = 0; i < 50; i++) { @@ -166,7 +182,9 @@ public class AnimatedHeader (float)random.NextDouble() * (headerEnd.Y - headerStart.Y) ); var brightness = 0.3f + (float)random.NextDouble() * 0.4f; - drawList.AddCircleFilled(starPos, 1f, ImGui.GetColorU32(new Vector4(1f, 1f, 1f, brightness))); + var starColor = starBase with { W = starBase.W * brightness }; + + drawList.AddCircleFilled(starPos, 1f, ImGui.GetColorU32(starColor)); } } @@ -174,15 +192,18 @@ public class AnimatedHeader { var drawList = ImGui.GetWindowDrawList(); var gradientHeight = GradientHeight; + var bottom = ResolveColor(BottomColorKey, BottomColor); for (int i = 0; i < gradientHeight; i++) { var progress = i / gradientHeight; var smoothProgress = progress * progress; - var r = BottomColor.X + (0.0f - BottomColor.X) * smoothProgress; - var g = BottomColor.Y + (0.0f - BottomColor.Y) * smoothProgress; - var b = BottomColor.Z + (0.0f - BottomColor.Z) * smoothProgress; + + var r = bottom.X + (0.0f - bottom.X) * smoothProgress; + var g = bottom.Y + (0.0f - bottom.Y) * smoothProgress; + var b = bottom.Z + (0.0f - bottom.Z) * smoothProgress; var alpha = 1f - smoothProgress; + var gradientColor = new Vector4(r, g, b, alpha); drawList.AddLine( new Vector2(headerStart.X, headerEnd.Y + i), @@ -310,9 +331,11 @@ public class AnimatedHeader ? baseAlpha * (0.6f + 0.4f * MathF.Sin(particle.Twinkle)) : baseAlpha; + var shootingBase = ResolveColor(ShootingStarColorKey, ShootingStarColor); + if (particle.Type == ParticleType.ShootingStar && particle.Trail != null && particle.Trail.Count > 1) { - var cyanColor = new Vector4(0.4f, 0.8f, 1.0f, 1.0f); + var baseColor = shootingBase; for (int t = 1; t < particle.Trail.Count; t++) { @@ -321,17 +344,18 @@ public class AnimatedHeader var trailWidth = (1f - trailProgress) * 3f + 1f; var glowAlpha = trailAlpha * 0.4f; + drawList.AddLine( bannerStart + particle.Trail[t - 1], bannerStart + particle.Trail[t], - ImGui.GetColorU32(cyanColor with { W = glowAlpha }), + ImGui.GetColorU32(baseColor with { W = glowAlpha }), trailWidth + 4f ); drawList.AddLine( bannerStart + particle.Trail[t - 1], bannerStart + particle.Trail[t], - ImGui.GetColorU32(cyanColor with { W = trailAlpha }), + ImGui.GetColorU32(baseColor with { W = trailAlpha }), trailWidth ); } @@ -450,6 +474,13 @@ public class AnimatedHeader Hue = 270f }); } + private static Vector4 ResolveColor(string? key, Vector4 fallback) + { + if (string.IsNullOrWhiteSpace(key)) + return fallback; + + return UIColors.Get(key); + } /// /// Clears all active particles. Useful when closing or hiding a window with an animated header. diff --git a/LightlessSync/UI/UIColors.cs b/LightlessSync/UI/UIColors.cs index 9d7f770..1decb77 100644 --- a/LightlessSync/UI/UIColors.cs +++ b/LightlessSync/UI/UIColors.cs @@ -6,7 +6,7 @@ namespace LightlessSync.UI { internal static class UIColors { - private static readonly Dictionary DefaultHexColors = new(StringComparer.OrdinalIgnoreCase) + private static readonly Dictionary _defaultHexColors = new(StringComparer.OrdinalIgnoreCase) { { "LightlessPurple", "#ad8af5" }, { "LightlessPurpleActive", "#be9eff" }, @@ -31,6 +31,12 @@ namespace LightlessSync.UI { "ProfileBodyGradientTop", "#2f283fff" }, { "ProfileBodyGradientBottom", "#372d4d00" }, + + { "HeaderGradientTop", "#140D26FF" }, + { "HeaderGradientBottom", "#1F1433FF" }, + + { "HeaderStaticStar", "#FFFFFFFF" }, + { "HeaderShootingStar", "#66CCFFFF" }, }; private static LightlessConfigService? _configService; @@ -45,7 +51,7 @@ namespace LightlessSync.UI if (_configService?.Current.CustomUIColors.TryGetValue(name, out var customColorHex) == true) return HexToRgba(customColorHex); - if (!DefaultHexColors.TryGetValue(name, out var hex)) + if (!_defaultHexColors.TryGetValue(name, out var hex)) throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name)); return HexToRgba(hex); @@ -53,7 +59,7 @@ namespace LightlessSync.UI public static void Set(string name, Vector4 color) { - if (!DefaultHexColors.ContainsKey(name)) + if (!_defaultHexColors.ContainsKey(name)) throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name)); if (_configService != null) @@ -83,7 +89,7 @@ namespace LightlessSync.UI public static Vector4 GetDefault(string name) { - if (!DefaultHexColors.TryGetValue(name, out var hex)) + if (!_defaultHexColors.TryGetValue(name, out var hex)) throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name)); return HexToRgba(hex); @@ -96,7 +102,7 @@ namespace LightlessSync.UI public static IEnumerable GetColorNames() { - return DefaultHexColors.Keys; + return _defaultHexColors.Keys; } public static Vector4 HexToRgba(string hexColor) From 5eed65149ae3cf35c684efd746ccee492e783f93 Mon Sep 17 00:00:00 2001 From: choco Date: Sat, 27 Dec 2025 02:38:56 +0100 Subject: [PATCH 08/28] nearby lightfinder users window, wiht pair func --- LightlessSync/Plugin.cs | 3 +- LightlessSync/Services/DalamudUtilService.cs | 22 ++ LightlessSync/UI/CompactUI.cs | 3 +- LightlessSync/UI/LightFinderUI.cs | 374 +++++++++++++++++-- 4 files changed, 366 insertions(+), 36 deletions(-) diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 606a23d..d4e13b3 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -462,7 +462,8 @@ public sealed class Plugin : IDalamudPlugin sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), - sp.GetRequiredService())); + sp.GetRequiredService(), + sp.GetRequiredService())); services.AddScoped(); services.AddScoped(); diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index 106adf2..5908624 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -776,6 +776,28 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber return WorldData.Value.TryGetValue(worldId, out var worldName) ? worldName : null; } + public void TargetPlayerByAddress(nint address) + { + if (address == nint.Zero) return; + if (_clientState.IsPvP) return; + + _ = RunOnFrameworkThread(() => + { + var gameObject = CreateGameObject(address); + if (gameObject is null) return; + + var useFocusTarget = _configService.Current.UseFocusTarget; + if (useFocusTarget) + { + _targetManager.FocusTarget = gameObject; + } + else + { + _targetManager.Target = gameObject; + } + }); + } + private unsafe void CheckCharacterForDrawing(nint address, string characterName) { var gameObj = (GameObject*)address; diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index be56434..79fbc88 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -519,7 +519,7 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.InvisibleButton("BroadcastIcon", buttonSize); var iconPos = ImGui.GetItemRectMin() + new Vector2(0f, iconYOffset); using (_uiSharedService.IconFont.Push()) - ImGui.GetWindowDrawList().AddText(iconPos, ImGui.GetColorU32(UIColors.Get("LightlessGreen")), FontAwesomeIcon.PersonCirclePlus.ToIconString()); + ImGui.GetWindowDrawList().AddText(iconPos, ImGui.GetColorU32(UIColors.Get("LightlessGreen")), FontAwesomeIcon.Wifi.ToIconString()); nextIconX = ImGui.GetItemRectMax().X + 6f; @@ -640,6 +640,7 @@ public class CompactUi : WindowMediatorSubscriberBase } } + if (uidTextHovered) { var padding = new Vector2(35f * ImGuiHelpers.GlobalScale); diff --git a/LightlessSync/UI/LightFinderUI.cs b/LightlessSync/UI/LightFinderUI.cs index b6bfc73..ed55d4f 100644 --- a/LightlessSync/UI/LightFinderUI.cs +++ b/LightlessSync/UI/LightFinderUI.cs @@ -11,7 +11,9 @@ using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto; using LightlessSync.API.Dto.Group; using LightlessSync.LightlessConfiguration; +using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; +using LightlessSync.Services.ActorTracking; using LightlessSync.Services.LightFinder; using LightlessSync.Services.Mediator; using LightlessSync.UI.Services; @@ -28,6 +30,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase { #region Services + private readonly ActorObjectService _actorObjectService; private readonly ApiController _apiController; private readonly DalamudUtilService _dalamudUtilService; private readonly LightFinderScannerService _broadcastScannerService; @@ -63,10 +66,10 @@ public class LightFinderUI : WindowMediatorSubscriberBase private LightfinderTab _selectedTab = LightfinderTab.NearbySyncshells; private string _userUid = string.Empty; - private enum LightfinderTab { NearbySyncshells, BroadcastSettings, Help } + private enum LightfinderTab { NearbySyncshells, NearbyPlayers, BroadcastSettings, Help } #if DEBUG - private enum LightfinderTabDebug { NearbySyncshells, BroadcastSettings, Help, Debug } + private enum LightfinderTabDebug { NearbySyncshells, NearbyPlayers, BroadcastSettings, Help, Debug } private LightfinderTabDebug _selectedTabDebug = LightfinderTabDebug.NearbySyncshells; #endif @@ -89,7 +92,8 @@ public class LightFinderUI : WindowMediatorSubscriberBase LightFinderScannerService broadcastScannerService, PairUiService pairUiService, DalamudUtilService dalamudUtilService, - LightlessProfileManager lightlessProfileManager + LightlessProfileManager lightlessProfileManager, + ActorObjectService actorObjectService ) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService) { _broadcastService = broadcastService; @@ -100,6 +104,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase _pairUiService = pairUiService; _dalamudUtilService = dalamudUtilService; _lightlessProfileManager = lightlessProfileManager; + _actorObjectService = actorObjectService; _animatedHeader.Height = 100f; _animatedHeader.EnableBottomGradient = true; @@ -158,6 +163,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase var debugTabOptions = new List> { new("Nearby Syncshells", LightfinderTabDebug.NearbySyncshells), + new("Nearby Players", LightfinderTabDebug.NearbyPlayers), new("Broadcast", LightfinderTabDebug.BroadcastSettings), new("Help", LightfinderTabDebug.Help), new("Debug", LightfinderTabDebug.Debug) @@ -171,6 +177,9 @@ public class LightFinderUI : WindowMediatorSubscriberBase case LightfinderTabDebug.NearbySyncshells: DrawNearbySyncshellsTab(); break; + case LightfinderTabDebug.NearbyPlayers: + DrawNearbyPlayersTab(); + break; case LightfinderTabDebug.BroadcastSettings: DrawBroadcastSettingsTab(); break; @@ -185,6 +194,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase var tabOptions = new List> { new("Nearby Syncshells", LightfinderTab.NearbySyncshells), + new("Nearby Players", LightfinderTab.NearbyPlayers), new("Broadcast", LightfinderTab.BroadcastSettings), new("Help", LightfinderTab.Help) }; @@ -197,6 +207,9 @@ public class LightFinderUI : WindowMediatorSubscriberBase case LightfinderTab.NearbySyncshells: DrawNearbySyncshellsTab(); break; + case LightfinderTab.NearbyPlayers: + DrawNearbyPlayersTab(); + break; case LightfinderTab.BroadcastSettings: DrawBroadcastSettingsTab(); break; @@ -241,8 +254,8 @@ public class LightFinderUI : WindowMediatorSubscriberBase { ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.WidthStretch, 1f); ImGui.TableSetupColumn("Time", ImGuiTableColumnFlags.WidthStretch, 1f); - ImGui.TableSetupColumn("NearbyPlayers", ImGuiTableColumnFlags.WidthStretch, 1f); ImGui.TableSetupColumn("NearbySyncshells", ImGuiTableColumnFlags.WidthStretch, 1f); + ImGui.TableSetupColumn("NearbyPlayers", ImGuiTableColumnFlags.WidthStretch, 1f); ImGui.TableSetupColumn("Broadcasting", ImGuiTableColumnFlags.WidthStretch, 1f); ImGui.TableSetupColumn("Button", ImGuiTableColumnFlags.WidthFixed, buttonWidth + 16f * scale); @@ -278,16 +291,18 @@ public class LightFinderUI : WindowMediatorSubscriberBase } DrawStatusCell(FontAwesomeIcon.Clock, timeColor, timeValue, timeSub, infoColor, scale); - // Nearby players cell - var nearbyPlayerCount = _broadcastScannerService.CountActiveBroadcasts(); - var nearbyPlayerColor = nearbyPlayerCount > 0 ? UIColors.Get("LightlessBlue") : infoColor; - DrawStatusCell(FontAwesomeIcon.Users, nearbyPlayerColor, nearbyPlayerCount.ToString(), "Players", infoColor, scale); - // Nearby syncshells cell var nearbySyncshellCount = _nearbySyncshells.Count; var nearbySyncshellColor = nearbySyncshellCount > 0 ? UIColors.Get("LightlessPurple") : infoColor; DrawStatusCell(FontAwesomeIcon.Compass, nearbySyncshellColor, nearbySyncshellCount.ToString(), "Syncshells", infoColor, scale); - + + // Nearby players cell (exclude self) + string? myHashedCidForCount = null; + try { myHashedCidForCount = _dalamudUtilService.GetCID().ToString().GetHash256(); } catch { } + var nearbyPlayerCount = _broadcastScannerService.CountActiveBroadcasts(myHashedCidForCount); + var nearbyPlayerColor = nearbyPlayerCount > 0 ? UIColors.Get("LightlessBlue") : infoColor; + DrawStatusCell(FontAwesomeIcon.Users, nearbyPlayerColor, nearbyPlayerCount.ToString(), "Players", infoColor, scale); + // Broadcasting syncshell cell var isBroadcastingSyncshell = _configService.Current.SyncshellFinderEnabled && isBroadcasting; var broadcastSyncshellColor = isBroadcastingSyncshell ? UIColors.Get("LightlessGreen") : infoColor; @@ -409,7 +424,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase } } - private List<(GroupJoinDto Shell, string BroadcasterName)> BuildSyncshellCardData() + private List<(GroupJoinDto Shell, string BroadcasterName, bool IsOwnBroadcast)> BuildSyncshellCardData() { string? myHashedCid = null; try @@ -419,11 +434,9 @@ public class LightFinderUI : WindowMediatorSubscriberBase } catch { } - var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts() - .Where(b => !string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal)) - .ToList(); + var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().ToList(); - var cardData = new List<(GroupJoinDto Shell, string BroadcasterName)>(); + var cardData = new List<(GroupJoinDto Shell, string BroadcasterName, bool IsOwnBroadcast)>(); foreach (var shell in _nearbySyncshells) { @@ -434,7 +447,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase if (_useTestSyncshells) { var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID; - cardData.Add((shell, $"{displayName} (Test World)")); + cardData.Add((shell, $"{displayName} (Test World)", false)); continue; } #endif @@ -443,29 +456,39 @@ public class LightFinderUI : WindowMediatorSubscriberBase if (broadcast == null) continue; - var (name, address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID); - if (string.IsNullOrEmpty(name)) - continue; + var isOwnBroadcast = !string.IsNullOrEmpty(myHashedCid) && string.Equals(broadcast.HashedCID, myHashedCid, StringComparison.Ordinal); - var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(address); - var broadcasterName = !string.IsNullOrEmpty(worldName) ? $"{name} ({worldName})" : name; + string broadcasterName; + if (isOwnBroadcast) + { + broadcasterName = "You"; + } + else + { + var (name, address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID); + if (string.IsNullOrEmpty(name)) + continue; - cardData.Add((shell, broadcasterName)); + var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(address); + broadcasterName = !string.IsNullOrEmpty(worldName) ? $"{name} ({worldName})" : name; + } + + cardData.Add((shell, broadcasterName, isOwnBroadcast)); } return cardData; } - private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName)> listData) + private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName, bool IsOwnBroadcast)> listData) { ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8f); ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f); if (ImGui.BeginChild("SyncshellListScroll", new Vector2(-1, -1), border: false)) { - foreach (var (shell, broadcasterName) in listData) + foreach (var (shell, broadcasterName, isOwnBroadcast) in listData) { - DrawSyncshellListItem(shell, broadcasterName); + DrawSyncshellListItem(shell, broadcasterName, isOwnBroadcast); ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale)); } } @@ -474,7 +497,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase ImGui.PopStyleVar(2); } - private void DrawSyncshellListItem(GroupJoinDto shell, string broadcasterName) + private void DrawSyncshellListItem(GroupJoinDto shell, string broadcasterName, bool isOwnBroadcast) { ImGui.PushID(shell.Group.GID); float rowHeight = 74f * ImGuiHelpers.GlobalScale; @@ -496,9 +519,12 @@ public class LightFinderUI : WindowMediatorSubscriberBase float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X; ImGui.SameLine(); ImGui.SetCursorPosX(rightX); - ImGui.TextUnformatted(broadcasterName); + if (isOwnBroadcast) + ImGui.TextColored(UIColors.Get("LightlessGreen"), broadcasterName); + else + ImGui.TextUnformatted(broadcasterName); if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Broadcaster"); + ImGui.SetTooltip(isOwnBroadcast ? "Your broadcast" : "Broadcaster"); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault")); @@ -530,21 +556,21 @@ public class LightFinderUI : WindowMediatorSubscriberBase ImGui.PopID(); } - private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName)> cardData) + private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName, bool IsOwnBroadcast)> cardData) { ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8f); ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f); - foreach (var (shell, _) in cardData) + foreach (var (shell, _, isOwnBroadcast) in cardData) { - DrawSyncshellCompactItem(shell); + DrawSyncshellCompactItem(shell, isOwnBroadcast); ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale)); } ImGui.PopStyleVar(2); } - private void DrawSyncshellCompactItem(GroupJoinDto shell) + private void DrawSyncshellCompactItem(GroupJoinDto shell, bool isOwnBroadcast) { ImGui.PushID(shell.Group.GID); float rowHeight = 36f * ImGuiHelpers.GlobalScale; @@ -552,11 +578,13 @@ public class LightFinderUI : WindowMediatorSubscriberBase ImGui.BeginChild($"ShellCompact##{shell.Group.GID}", new Vector2(-1, rowHeight), border: true); var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID; + if (isOwnBroadcast) + displayName += " (You)"; var style = ImGui.GetStyle(); float availW = ImGui.GetContentRegionAvail().X; ImGui.AlignTextToFramePadding(); - _uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple")); + _uiSharedService.MediumText(displayName, isOwnBroadcast ? UIColors.Get("LightlessGreen") : UIColors.Get("LightlessPurple")); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Click to open profile."); if (ImGui.IsItemClicked()) @@ -605,7 +633,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase using (ImRaii.Disabled()) ImGui.Button(label, buttonSize); - UiSharedService.AttachToolTip("You can't join your own Syncshell, silly! That's like trying to high-five yourself."); + UiSharedService.AttachToolTip("You can't join your own Syncshell..."); } else if (!isAlreadyMember && !isRecentlyJoined) { @@ -718,6 +746,284 @@ public class LightFinderUI : WindowMediatorSubscriberBase #endregion + #region Nearby Players Tab + + private void DrawNearbyPlayersTab() + { + ImGui.BeginGroup(); + string checkboxLabel = "Compact"; + float checkboxWidth = ImGui.CalcTextSize(checkboxLabel).X + ImGui.GetFrameHeight() + 8f; + float availWidth = ImGui.GetContentRegionAvail().X; + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + availWidth - checkboxWidth); + ImGui.Checkbox(checkboxLabel, ref _compactView); + ImGui.EndGroup(); + + ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale)); + + if (!_broadcastService.IsBroadcasting) + { + ImGui.TextColored(UIColors.Get("LightlessYellow"), "Lightfinder must be active to see nearby players."); + return; + } + + string? myHashedCid = null; + try + { + var cid = _dalamudUtilService.GetCID(); + myHashedCid = cid.ToString().GetHash256(); + } + catch { } + + var activeBroadcasts = _broadcastScannerService.GetActiveBroadcasts(myHashedCid); + + if (activeBroadcasts.Count == 0) + { + ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby Lightfinder players found."); + return; + } + + var playerData = BuildNearbyPlayerData(activeBroadcasts); + if (playerData.Count == 0) + { + ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby Lightfinder players found."); + return; + } + + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8f); + ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f); + + if (ImGui.BeginChild("NearbyPlayersScroll", new Vector2(-1, -1), border: false)) + { + foreach (var data in playerData) + { + if (_compactView) + DrawNearbyPlayerCompactRow(data); + else + DrawNearbyPlayerRow(data); + ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale)); + } + } + ImGui.EndChild(); + + ImGui.PopStyleVar(2); + } + + private List BuildNearbyPlayerData(List> activeBroadcasts) + { + var snapshot = _pairUiService.GetSnapshot(); + var playerData = new List(); + + foreach (var broadcast in activeBroadcasts) + { + var (name, address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.Key); + if (string.IsNullOrEmpty(name) || address == nint.Zero) + continue; + + var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(address); + var pair = snapshot.PairsByUid.Values.FirstOrDefault(p => + p.IsVisible && + !string.IsNullOrEmpty(p.GetPlayerNameHash()) && + string.Equals(p.GetPlayerNameHash(), broadcast.Key, StringComparison.Ordinal)); + + var isDirectlyPaired = pair?.IsDirectlyPaired ?? false; + var sharedGroups = pair?.UserPair?.Groups ?? []; + var sharedGroupNames = sharedGroups + .Select(gid => snapshot.GroupsByGid.TryGetValue(gid, out var g) ? g.GroupAliasOrGID : gid) + .ToList(); + + playerData.Add(new NearbyPlayerData(broadcast.Key, name, worldName, address, pair, isDirectlyPaired, sharedGroupNames)); + } + + return playerData; + } + + private readonly record struct NearbyPlayerData( + string HashedCid, + string Name, + string? World, + nint Address, + Pair? Pair, + bool IsDirectlyPaired, + List SharedSyncshells); + + private void DrawNearbyPlayerRow(NearbyPlayerData data) + { + ImGui.PushID(data.HashedCid); + float rowHeight = 74f * ImGuiHelpers.GlobalScale; + + ImGui.BeginChild($"PlayerRow##{data.HashedCid}", new Vector2(-1, rowHeight), border: true); + + var serverName = !string.IsNullOrEmpty(data.World) ? data.World : "Unknown"; + var style = ImGui.GetStyle(); + float startX = ImGui.GetCursorPosX(); + float regionW = ImGui.GetContentRegionAvail().X; + float rightTxtW = ImGui.CalcTextSize(serverName).X; + + _uiSharedService.MediumText(data.Name, UIColors.Get("LightlessPurple")); + if (data.Pair != null) + { + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Click to open profile."); + if (ImGui.IsItemClicked()) + Mediator.Publish(new ProfileOpenStandaloneMessage(data.Pair)); + } + + float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X; + ImGui.SameLine(); + ImGui.SetCursorPosX(rightX); + ImGui.TextUnformatted(serverName); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Home World"); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault")); + + Vector2 rowStartLocal = ImGui.GetCursorPos(); + + if (data.IsDirectlyPaired) + { + ImGui.SetCursorPosX(startX); + _uiSharedService.IconText(FontAwesomeIcon.UserCheck, UIColors.Get("LightlessGreen")); + ImGui.SameLine(0f, 4f * ImGuiHelpers.GlobalScale); + ImGui.TextColored(UIColors.Get("LightlessGreen"), "Direct Pair"); + } + else if (data.SharedSyncshells.Count > 0) + { + ImGui.SetCursorPosX(startX); + _uiSharedService.IconText(FontAwesomeIcon.Users, UIColors.Get("LightlessPurple")); + ImGui.SameLine(0f, 4f * ImGuiHelpers.GlobalScale); + var shellText = data.SharedSyncshells.Count == 1 + ? data.SharedSyncshells[0] + : $"{data.SharedSyncshells.Count} shared shells"; + ImGui.TextColored(UIColors.Get("LightlessPurple"), shellText); + if (data.SharedSyncshells.Count > 1 && ImGui.IsItemHovered()) + ImGui.SetTooltip(string.Join("\n", data.SharedSyncshells)); + } + else + { + ImGui.SetCursorPosX(startX); + _uiSharedService.IconText(FontAwesomeIcon.Wifi, UIColors.Get("LightlessBlue")); + ImGui.SameLine(0f, 4f * ImGuiHelpers.GlobalScale); + ImGui.TextColored(ImGuiColors.DalamudGrey, "Lightfinder User"); + } + + ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale)); + + DrawPlayerActionButtons(data, startX, regionW, rowStartLocal.Y, style); + + ImGui.EndChild(); + ImGui.PopID(); + } + + private void DrawNearbyPlayerCompactRow(NearbyPlayerData data) + { + ImGui.PushID(data.HashedCid); + float rowHeight = 36f * ImGuiHelpers.GlobalScale; + + ImGui.BeginChild($"PlayerCompact##{data.HashedCid}", new Vector2(-1, rowHeight), border: true); + + ImGui.AlignTextToFramePadding(); + + if (data.IsDirectlyPaired) + { + _uiSharedService.IconText(FontAwesomeIcon.UserCheck, UIColors.Get("LightlessGreen")); + ImGui.SameLine(0f, 4f * ImGuiHelpers.GlobalScale); + } + else if (data.SharedSyncshells.Count > 0) + { + _uiSharedService.IconText(FontAwesomeIcon.Users, UIColors.Get("LightlessPurple")); + ImGui.SameLine(0f, 4f * ImGuiHelpers.GlobalScale); + } + else + { + _uiSharedService.IconText(FontAwesomeIcon.Wifi, UIColors.Get("LightlessBlue")); + ImGui.SameLine(0f, 4f * ImGuiHelpers.GlobalScale); + } + + var displayText = !string.IsNullOrEmpty(data.World) ? $"{data.Name} ({data.World})" : data.Name; + _uiSharedService.MediumText(displayText, UIColors.Get("LightlessPurple")); + if (data.Pair != null) + { + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Click to open profile."); + if (ImGui.IsItemClicked()) + Mediator.Publish(new ProfileOpenStandaloneMessage(data.Pair)); + } + + ImGui.SameLine(); + DrawPlayerActionButtons(data, 0, ImGui.GetContentRegionAvail().X, ImGui.GetCursorPosY(), ImGui.GetStyle(), compact: true); + + ImGui.EndChild(); + ImGui.PopID(); + } + + private void DrawPlayerActionButtons(NearbyPlayerData data, float startX, float regionW, float rowY, ImGuiStylePtr style, bool compact = false) + { + float buttonWidth = compact ? 60f * ImGuiHelpers.GlobalScale : 80f * ImGuiHelpers.GlobalScale; + float buttonHeight = compact ? 0 : 30f; + float totalButtonsWidth = buttonWidth * 2 + style.ItemSpacing.X; + + if (compact) + { + float availX = ImGui.GetContentRegionAvail().X; + float curX = ImGui.GetCursorPosX(); + ImGui.SetCursorPosX(curX + availX - totalButtonsWidth); + } + else + { + ImGui.SetCursorPos(new Vector2(startX + regionW - totalButtonsWidth - style.ItemSpacing.X, rowY)); + } + + using (ImRaii.PushColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"))) + using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f))) + using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f))) + using (ImRaii.Disabled(data.IsDirectlyPaired)) + { + if (ImGui.Button($"Pair##{data.HashedCid}", new Vector2(buttonWidth, buttonHeight))) + { + _ = SendPairRequestAsync(data.HashedCid); + } + } + if (data.IsDirectlyPaired) + UiSharedService.AttachToolTip("Already directly paired with this player."); + + ImGui.SameLine(); + + using (ImRaii.PushColor(ImGuiCol.Button, UIColors.Get("LightlessPurple"))) + using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple").WithAlpha(0.85f))) + using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurple").WithAlpha(0.75f))) + { + if (ImGui.Button($"Target##{data.HashedCid}", new Vector2(buttonWidth, buttonHeight))) + { + TargetPlayerByAddress(data.Address); + } + } + } + + private async Task SendPairRequestAsync(string hashedCid) + { + if (string.IsNullOrWhiteSpace(hashedCid)) + return; + + try + { + await _apiController.TryPairWithContentId(hashedCid).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send pair request to {HashedCid}", hashedCid); + } + } + + private void TargetPlayerByAddress(nint address) + { + if (address == nint.Zero) + return; + + _dalamudUtilService.TargetPlayerByAddress(address); + } + + #endregion + #region Broadcast Settings Tab private void DrawBroadcastSettingsTab() From 5c8e239a7b77191f9b1f125b5666a0f8a56f2930 Mon Sep 17 00:00:00 2001 From: Tsubasahane Date: Sat, 27 Dec 2025 17:04:39 +0800 Subject: [PATCH 09/28] implement playerState - use IPlayerState for DalamudUtilService and make things less asynced - make LocationInfo work with ContentFinderData --- .gitignore | 3 + .../FileCache/TransientResourceManager.cs | 2 +- LightlessSync/Plugin.cs | 3 +- .../CharaDataGposeTogetherManager.cs | 2 +- .../Services/CharaData/CharaDataManager.cs | 2 +- .../CharaData/CharaDataNearbyManager.cs | 4 +- .../Services/Chat/ZoneChatService.cs | 6 +- LightlessSync/Services/ContextMenuService.cs | 2 +- LightlessSync/Services/DalamudUtilService.cs | 174 ++++++++++-------- .../LightFinder/LightFinderService.cs | 2 +- .../ServerConfigurationManager.cs | 22 +-- LightlessSync/UI/DtrEntry.cs | 2 +- LightlessSync/UI/TopTabMenu.cs | 2 +- .../SignalR/ApIController.Functions.Users.cs | 2 +- LightlessSync/WebAPI/SignalR/ApiController.cs | 6 +- LightlessSync/WebAPI/SignalR/TokenProvider.cs | 4 +- 16 files changed, 132 insertions(+), 106 deletions(-) diff --git a/.gitignore b/.gitignore index dfcfd56..fecf7ea 100644 --- a/.gitignore +++ b/.gitignore @@ -348,3 +348,6 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ + +# idea +/.idea diff --git a/LightlessSync/FileCache/TransientResourceManager.cs b/LightlessSync/FileCache/TransientResourceManager.cs index 115c616..ed2cca6 100644 --- a/LightlessSync/FileCache/TransientResourceManager.cs +++ b/LightlessSync/FileCache/TransientResourceManager.cs @@ -78,7 +78,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase } } - private string PlayerPersistentDataKey => _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult() + "_" + _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult(); + private string PlayerPersistentDataKey => _dalamudUtil.GetPlayerName() + "_" + _dalamudUtil.GetHomeWorldId(); private ConcurrentDictionary> SemiTransientResources { get diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index a9be72b..cd57c34 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -52,7 +52,7 @@ public sealed class Plugin : IDalamudPlugin IFramework framework, IObjectTable objectTable, IClientState clientState, ICondition condition, IChatGui chatGui, IGameGui gameGui, IDtrBar dtrBar, IPluginLog pluginLog, ITargetManager targetManager, INotificationManager notificationManager, ITextureProvider textureProvider, IContextMenu contextMenu, IGameInteropProvider gameInteropProvider, IGameConfig gameConfig, - ISigScanner sigScanner, INamePlateGui namePlateGui, IAddonLifecycle addonLifecycle) + ISigScanner sigScanner, INamePlateGui namePlateGui, IAddonLifecycle addonLifecycle, IPlayerState playerState) { NativeDll.Initialize(pluginInterface.AssemblyLocation.DirectoryName); if (!Directory.Exists(pluginInterface.ConfigDirectory.FullName)) @@ -216,6 +216,7 @@ public sealed class Plugin : IDalamudPlugin gameData, targetManager, gameConfig, + playerState, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), diff --git a/LightlessSync/Services/CharaData/CharaDataGposeTogetherManager.cs b/LightlessSync/Services/CharaData/CharaDataGposeTogetherManager.cs index d7463b2..cb5b0aa 100644 --- a/LightlessSync/Services/CharaData/CharaDataGposeTogetherManager.cs +++ b/LightlessSync/Services/CharaData/CharaDataGposeTogetherManager.cs @@ -450,7 +450,7 @@ public class CharaDataGposeTogetherManager : DisposableMediatorSubscriberBase }; } - var loc = await _dalamudUtil.GetMapDataAsync().ConfigureAwait(false); + var loc = _dalamudUtil.GetMapData(); worldData.LocationInfo = loc; if (_forceResendWorldData || worldData != _lastWorldData) diff --git a/LightlessSync/Services/CharaData/CharaDataManager.cs b/LightlessSync/Services/CharaData/CharaDataManager.cs index d8b2387..4a30639 100644 --- a/LightlessSync/Services/CharaData/CharaDataManager.cs +++ b/LightlessSync/Services/CharaData/CharaDataManager.cs @@ -254,7 +254,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase Logger.LogTrace("Attaching World data {data}", worldData); - worldData.LocationInfo = await _dalamudUtilService.GetMapDataAsync().ConfigureAwait(false); + worldData.LocationInfo = _dalamudUtilService.GetMapData(); Logger.LogTrace("World data serialized: {data}", worldData); diff --git a/LightlessSync/Services/CharaData/CharaDataNearbyManager.cs b/LightlessSync/Services/CharaData/CharaDataNearbyManager.cs index b3d4800..c66588d 100644 --- a/LightlessSync/Services/CharaData/CharaDataNearbyManager.cs +++ b/LightlessSync/Services/CharaData/CharaDataNearbyManager.cs @@ -186,8 +186,8 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase var previousPoses = _nearbyData.Keys.ToList(); _nearbyData.Clear(); - var ownLocation = await _dalamudUtilService.RunOnFrameworkThread(() => _dalamudUtilService.GetMapData()).ConfigureAwait(false); - var player = await _dalamudUtilService.RunOnFrameworkThread(() => _dalamudUtilService.GetPlayerCharacter()).ConfigureAwait(false); + var ownLocation = _dalamudUtilService.GetMapData(); + var player = await _dalamudUtilService.GetPlayerCharacterAsync().ConfigureAwait(false); var currentServer = player.CurrentWorld; var playerPos = player.Position; diff --git a/LightlessSync/Services/Chat/ZoneChatService.cs b/LightlessSync/Services/Chat/ZoneChatService.cs index 38e76d9..5d37f2b 100644 --- a/LightlessSync/Services/Chat/ZoneChatService.cs +++ b/LightlessSync/Services/Chat/ZoneChatService.cs @@ -571,7 +571,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS try { - var location = await _dalamudUtilService.GetMapDataAsync().ConfigureAwait(false); + var location = _dalamudUtilService.GetMapData(); var territoryId = (ushort)location.TerritoryId; var worldId = (ushort)location.ServerId; @@ -697,7 +697,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS { try { - var worldId = (ushort)await _dalamudUtilService.GetWorldIdAsync().ConfigureAwait(false); + var worldId = (ushort)_dalamudUtilService.GetWorldId(); return definition.Descriptor with { WorldId = worldId }; } catch (Exception ex) @@ -1149,7 +1149,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS { try { - return _dalamudUtilService.GetPlayerNameAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + return _dalamudUtilService.GetPlayerName(); } catch (Exception ex) { diff --git a/LightlessSync/Services/ContextMenuService.cs b/LightlessSync/Services/ContextMenuService.cs index 7d35529..3fe893c 100644 --- a/LightlessSync/Services/ContextMenuService.cs +++ b/LightlessSync/Services/ContextMenuService.cs @@ -241,7 +241,7 @@ internal class ContextMenuService : IHostedService return; } - var senderCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); + var senderCid = _dalamudUtil.GetCID().ToString().GetHash256(); var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address); _logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid); diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index 91d0037..ef6fe7a 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -1,5 +1,4 @@ using Dalamud.Game.ClientState.Conditions; -using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; @@ -37,6 +36,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber private readonly ICondition _condition; private readonly IDataManager _gameData; private readonly IGameConfig _gameConfig; + private readonly IPlayerState _playerState; private readonly BlockedCharacterHandler _blockedCharacterHandler; private readonly IFramework _framework; private readonly IGameGui _gameGui; @@ -60,7 +60,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber private Lazy _cid; public DalamudUtilService(ILogger logger, IClientState clientState, IObjectTable objectTable, IFramework framework, - IGameGui gameGui, ICondition condition, IDataManager gameData, ITargetManager targetManager, IGameConfig gameConfig, + IGameGui gameGui, ICondition condition, IDataManager gameData, ITargetManager targetManager, IGameConfig gameConfig, IPlayerState playerState, ActorObjectService actorObjectService, BlockedCharacterHandler blockedCharacterHandler, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService, PlayerPerformanceConfigService playerPerformanceConfigService, Lazy pairFactory) { @@ -72,6 +72,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber _condition = condition; _gameData = gameData; _gameConfig = gameConfig; + _playerState = playerState; _actorObjectService = actorObjectService; _targetManager = targetManager; _blockedCharacterHandler = blockedCharacterHandler; @@ -95,6 +96,12 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber TerritoryData = new(() => BuildTerritoryData(clientLanguage)); TerritoryDataEnglish = new(() => BuildTerritoryData(Dalamud.Game.ClientLanguage.English)); MapData = new(() => BuildMapData(clientLanguage)); + ContentFinderData = new Lazy>(() => + { + return _gameData.GetExcelSheet()! + .Where(w => w.RowId != 0 && !string.IsNullOrEmpty(w.ContentFinderCondition.ValueNullable?.Name.ToString())) + .ToDictionary(w => w.RowId, w => w.ContentFinderCondition.Value.Name.ToString()); + }); mediator.Subscribe(this, (msg) => { if (clientState.IsPvP) return; @@ -279,6 +286,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public Lazy> TerritoryData { get; private set; } public Lazy> TerritoryDataEnglish { get; private set; } public Lazy> MapData { get; private set; } + public Lazy> ContentFinderData { get; private set; } public bool IsLodEnabled { get; private set; } public LightlessMediator Mediator { get; } @@ -372,7 +380,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public bool GetIsPlayerPresent() { EnsureIsOnFramework(); - return _objectTable.LocalPlayer != null && _objectTable.LocalPlayer.IsValid(); + return _objectTable.LocalPlayer != null && _objectTable.LocalPlayer.IsValid() && _playerState.IsLoaded; } public async Task GetIsPlayerPresentAsync() @@ -517,34 +525,17 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public string GetPlayerName() { - EnsureIsOnFramework(); - return _objectTable.LocalPlayer?.Name.ToString() ?? "--"; - } - - public async Task GetPlayerNameAsync() - { - return await RunOnFrameworkThread(GetPlayerName).ConfigureAwait(false); - } - - public async Task GetCIDAsync() - { - return await RunOnFrameworkThread(GetCID).ConfigureAwait(false); + return _playerState.CharacterName; } public unsafe ulong GetCID() { - EnsureIsOnFramework(); - var playerChar = GetPlayerCharacter(); - - if (playerChar == null || playerChar.Address == IntPtr.Zero) - return 0; - - return ((BattleChara*)playerChar.Address)->Character.ContentId; + return _playerState.ContentId; } - public async Task GetPlayerNameHashedAsync() + public string GetPlayerNameHashed() { - return await RunOnFrameworkThread(() => _cid.Value.ToString().GetHash256()).ConfigureAwait(false); + return _cid.Value.ToString().GetHash256(); } public static unsafe bool TryGetHashedCID(IPlayerCharacter? playerCharacter, out string hashedCid) @@ -583,54 +574,100 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public uint GetHomeWorldId() { - EnsureIsOnFramework(); - return _objectTable.LocalPlayer?.HomeWorld.RowId ?? 0; + return _playerState.HomeWorld.RowId; } public uint GetWorldId() { - EnsureIsOnFramework(); - return _objectTable.LocalPlayer!.CurrentWorld.RowId; + return _playerState.CurrentWorld.RowId; } public unsafe LocationInfo GetMapData() { - EnsureIsOnFramework(); - var agentMap = AgentMap.Instance(); var houseMan = HousingManager.Instance(); - uint serverId = 0; - if (_objectTable.LocalPlayer == null) serverId = 0; - else serverId = _objectTable.LocalPlayer.CurrentWorld.RowId; - uint mapId = agentMap == null ? 0 : agentMap->CurrentMapId; - uint territoryId = agentMap == null ? 0 : agentMap->CurrentTerritoryId; - uint divisionId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentDivision()); - uint wardId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentWard() + 1); - uint houseId = 0; - var tempHouseId = houseMan == null ? 0 : (houseMan->GetCurrentPlot()); - if (!houseMan->IsInside()) tempHouseId = 0; - if (tempHouseId < -1) - { - divisionId = tempHouseId == -127 ? 2 : (uint)1; - tempHouseId = 100; - } - if (tempHouseId == -1) tempHouseId = 0; - houseId = (uint)tempHouseId; - if (houseId != 0) - { - territoryId = HousingManager.GetOriginalHouseTerritoryTypeId(); - } - uint roomId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentRoom()); - return new LocationInfo() + var location = new LocationInfo(); + location.ServerId = _playerState.CurrentWorld.RowId; + //location.InstanceId = UIState.Instance()->PublicInstance.InstanceId; //TODO:Need API update first + location.TerritoryId = _clientState.TerritoryType; + location.MapId = _clientState.MapId; + if (houseMan != null) { - ServerId = serverId, - MapId = mapId, - TerritoryId = territoryId, - DivisionId = divisionId, - WardId = wardId, - HouseId = houseId, - RoomId = roomId - }; + if (houseMan->IsInside()) + { + location.TerritoryId = HousingManager.GetOriginalHouseTerritoryTypeId(); + var house = houseMan->GetCurrentIndoorHouseId(); + location.WardId = house.WardIndex + 1u; + location.HouseId = house.IsApartment ? 100 : house.PlotIndex + 1u; + location.RoomId = (uint)house.RoomNumber; + location.DivisionId = house.IsApartment ? house.ApartmentDivision + 1u : houseMan->GetCurrentDivision(); + } + else if (houseMan->IsInWorkshop()) + { + var workShop = houseMan->WorkshopTerritory; + var house = workShop->HouseId; + location.WardId = house.WardIndex + 1u; + location.HouseId = house.PlotIndex + 1u; + } + else if (houseMan->IsOutside()) + { + var outside = houseMan->OutdoorTerritory; + var house = outside->HouseId; + location.WardId = house.WardIndex + 1u; + location.HouseId = (uint)houseMan->GetCurrentPlot() + 1; + location.DivisionId = houseMan->GetCurrentDivision(); + } + //_logger.LogWarning(LocationToString(location)); + } + return location; + } + + public string LocationToString(LocationInfo location) + { + if (location.ServerId is 0 || location.TerritoryId is 0) return String.Empty; + var str = WorldData.Value[(ushort)location.ServerId]; + + if (ContentFinderData.Value.TryGetValue(location.TerritoryId , out var dutyName)) + { + str += $" - [In Duty]{dutyName}"; + } + else + { + if (location.HouseId is not 0 || location.MapId is 0) // Dont show mapName when in house/no map available + { + str += $" - {TerritoryData.Value[(ushort)location.TerritoryId]}"; + } + else + { + str += $" - {MapData.Value[(ushort)location.MapId].MapName}"; + } + + // if (location.InstanceId is not 0) + // { + // str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString(); + // } + + if (location.WardId is not 0) + { + str += $" Ward #{location.WardId}"; + } + + if (location.HouseId is not 0 and not 100) + { + str += $" House #{location.HouseId}"; + } + else if (location.HouseId is 100) + { + str += $" {(location.DivisionId == 2 ? "[Subdivision]" : "")} Apartment"; + } + + if (location.RoomId is not 0) + { + str += $" Room #{location.RoomId}"; + } + } + + return str; } public unsafe void SetMarkerAndOpenMap(Vector3 position, Map map) @@ -642,21 +679,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber agentMap->SetFlagMapMarker(map.TerritoryType.RowId, map.RowId, position); } - public async Task GetMapDataAsync() - { - return await RunOnFrameworkThread(GetMapData).ConfigureAwait(false); - } - - public async Task GetWorldIdAsync() - { - return await RunOnFrameworkThread(GetWorldId).ConfigureAwait(false); - } - - public async Task GetHomeWorldIdAsync() - { - return await RunOnFrameworkThread(GetHomeWorldId).ConfigureAwait(false); - } - public unsafe bool IsGameObjectPresent(IntPtr key) { return _objectTable.Any(f => f.Address == key); diff --git a/LightlessSync/Services/LightFinder/LightFinderService.cs b/LightlessSync/Services/LightFinder/LightFinderService.cs index 82a51c7..f07064c 100644 --- a/LightlessSync/Services/LightFinder/LightFinderService.cs +++ b/LightlessSync/Services/LightFinder/LightFinderService.cs @@ -67,7 +67,7 @@ public class LightFinderService : IHostedService, IMediatorSubscriber { try { - var cid = await _dalamudUtil.GetCIDAsync().ConfigureAwait(false); + var cid = _dalamudUtil.GetCID(); return cid.ToString().GetHash256(); } catch (Exception ex) diff --git a/LightlessSync/Services/ServerConfiguration/ServerConfigurationManager.cs b/LightlessSync/Services/ServerConfiguration/ServerConfigurationManager.cs index 5cb3e15..339f462 100644 --- a/LightlessSync/Services/ServerConfiguration/ServerConfigurationManager.cs +++ b/LightlessSync/Services/ServerConfiguration/ServerConfigurationManager.cs @@ -101,9 +101,9 @@ public class ServerConfigurationManager } hasMulti = false; - var charaName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(); - var worldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult(); - var cid = _dalamudUtil.GetCIDAsync().GetAwaiter().GetResult(); + var charaName = _dalamudUtil.GetPlayerName(); + var worldId = _dalamudUtil.GetHomeWorldId(); + var cid = _dalamudUtil.GetCID(); var auth = currentServer.Authentications.FindAll(f => string.Equals(f.CharacterName, charaName) && f.WorldId == worldId); if (auth.Count >= 2) @@ -148,9 +148,9 @@ public class ServerConfigurationManager } hasMulti = false; - var charaName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(); - var worldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult(); - var cid = _dalamudUtil.GetCIDAsync().GetAwaiter().GetResult(); + var charaName = _dalamudUtil.GetPlayerName(); + var worldId = _dalamudUtil.GetHomeWorldId(); + var cid = _dalamudUtil.GetCID(); if (!currentServer.Authentications.Any() && currentServer.SecretKeys.Any()) { currentServer.Authentications.Add(new Authentication() @@ -268,16 +268,16 @@ public class ServerConfigurationManager { if (serverSelectionIndex == -1) serverSelectionIndex = CurrentServerIndex; var server = GetServerByIndex(serverSelectionIndex); - if (server.Authentications.Exists(c => string.Equals(c.CharacterName, _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(), StringComparison.Ordinal) - && c.WorldId == _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult())) + if (server.Authentications.Exists(c => string.Equals(c.CharacterName, _dalamudUtil.GetPlayerName(), StringComparison.Ordinal) + && c.WorldId == _dalamudUtil.GetHomeWorldId())) return; server.Authentications.Add(new Authentication() { - CharacterName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(), - WorldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult(), + CharacterName = _dalamudUtil.GetPlayerName(), + WorldId = _dalamudUtil.GetHomeWorldId(), SecretKeyIdx = !server.UseOAuth2 ? server.SecretKeys.Last().Key : -1, - LastSeenCID = _dalamudUtil.GetCIDAsync().GetAwaiter().GetResult() + LastSeenCID = _dalamudUtil.GetCID() }); Save(); } diff --git a/LightlessSync/UI/DtrEntry.cs b/LightlessSync/UI/DtrEntry.cs index 9cadb4c..71eddfb 100644 --- a/LightlessSync/UI/DtrEntry.cs +++ b/LightlessSync/UI/DtrEntry.cs @@ -344,7 +344,7 @@ public sealed class DtrEntry : IDisposable, IHostedService try { - var cid = _dalamudUtilService.GetCIDAsync().GetAwaiter().GetResult(); + var cid = _dalamudUtilService.GetCID(); var hashedCid = cid.ToString().GetHash256(); _localHashedCid = hashedCid; _localHashedCidFetchedAt = now; diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index 92215fe..1297fa6 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -440,7 +440,7 @@ public class TopTabMenu try { - var myCidHash = (await _dalamudUtilService.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); + var myCidHash = _dalamudUtilService.GetCID().ToString().GetHash256(); await _apiController.TryPairWithContentId(request.HashedCid).ConfigureAwait(false); _pairRequestService.RemoveRequest(request.HashedCid); diff --git a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs index 2f317b9..fdc4719 100644 --- a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs +++ b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs @@ -193,7 +193,7 @@ public partial class ApiController CensusDataDto? censusDto = null; if (_serverManager.SendCensusData && _lastCensus != null) { - var world = await _dalamudUtil.GetWorldIdAsync().ConfigureAwait(false); + var world = _dalamudUtil.GetWorldId(); censusDto = new((ushort)world, _lastCensus.RaceId, _lastCensus.TribeId, _lastCensus.Gender); Logger.LogDebug("Attaching Census Data: {data}", censusDto); } diff --git a/LightlessSync/WebAPI/SignalR/ApiController.cs b/LightlessSync/WebAPI/SignalR/ApiController.cs index 14c90ca..9639f6f 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.cs @@ -544,8 +544,8 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL private void DalamudUtilOnLogIn() { - var charaName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(); - var worldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult(); + var charaName = _dalamudUtil.GetPlayerName(); + var worldId = _dalamudUtil.GetHomeWorldId(); var auth = _serverManager.CurrentServer.Authentications.Find(f => string.Equals(f.CharacterName, charaName, StringComparison.Ordinal) && f.WorldId == worldId); if (auth?.AutoLogin ?? false) { @@ -653,7 +653,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL CensusDataDto? dto = null; if (_serverManager.SendCensusData && _lastCensus != null) { - var world = await _dalamudUtil.GetWorldIdAsync().ConfigureAwait(false); + var world = _dalamudUtil.GetWorldId(); dto = new((ushort)world, _lastCensus.RaceId, _lastCensus.TribeId, _lastCensus.Gender); Logger.LogDebug("Attaching Census Data: {data}", dto); } diff --git a/LightlessSync/WebAPI/SignalR/TokenProvider.cs b/LightlessSync/WebAPI/SignalR/TokenProvider.cs index fa23363..2446986 100644 --- a/LightlessSync/WebAPI/SignalR/TokenProvider.cs +++ b/LightlessSync/WebAPI/SignalR/TokenProvider.cs @@ -72,7 +72,7 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber result = await _httpClient.PostAsync(tokenUri, new FormUrlEncodedContent( [ new KeyValuePair("auth", auth), - new KeyValuePair("charaIdent", await _dalamudUtil.GetPlayerNameHashedAsync().ConfigureAwait(false)), + new KeyValuePair("charaIdent", _dalamudUtil.GetPlayerNameHashed()), ]), ct).ConfigureAwait(false); } else @@ -152,7 +152,7 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber JwtIdentifier jwtIdentifier; try { - var playerIdentifier = await _dalamudUtil.GetPlayerNameHashedAsync().ConfigureAwait(false); + var playerIdentifier = _dalamudUtil.GetPlayerNameHashed(); if (string.IsNullOrEmpty(playerIdentifier)) { From 70745613e13cb27dd7e90d30a811bd8b1f28d965 Mon Sep 17 00:00:00 2001 From: Tsubasahane Date: Sat, 27 Dec 2025 19:57:21 +0800 Subject: [PATCH 10/28] Location Sharing --- LightlessAPI | 2 +- LightlessSync/Plugin.cs | 1 + LightlessSync/Services/DalamudUtilService.cs | 13 ++-- .../Services/LocationShareService.cs | 78 +++++++++++++++++++ LightlessSync/Services/Mediator/Messages.cs | 2 + .../UI/Components/DrawFolderGroup.cs | 8 ++ LightlessSync/UI/Components/DrawUserPair.cs | 75 ++++++++++++++++++ LightlessSync/UI/DrawEntityFactory.cs | 4 + LightlessSync/UI/PermissionWindowUI.cs | 20 +++++ .../SignalR/ApIController.Functions.Users.cs | 12 +++ .../ApiController.Functions.Callbacks.cs | 13 ++++ LightlessSync/WebAPI/SignalR/ApiController.cs | 2 + 12 files changed, 224 insertions(+), 6 deletions(-) create mode 100644 LightlessSync/Services/LocationShareService.cs diff --git a/LightlessAPI b/LightlessAPI index 5656600..fdd492a 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 56566003e0e93bba05dcef49fd3ce23c6a204d81 +Subproject commit fdd492a8f478949d910ed0efd3e4a3ca3312ed9c diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index cd57c34..4131e40 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -137,6 +137,7 @@ public sealed class Plugin : IDalamudPlugin services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(sp => new TextureMetadataHelper(sp.GetRequiredService>(), gameData)); diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index ef6fe7a..36ce98b 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -1,11 +1,13 @@ using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.Text; using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Control; +using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using LightlessSync.API.Dto.CharaData; @@ -25,6 +27,7 @@ using System.Runtime.CompilerServices; using System.Text; using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; +using Map = Lumina.Excel.Sheets.Map; using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags; namespace LightlessSync.Services; @@ -588,7 +591,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber var location = new LocationInfo(); location.ServerId = _playerState.CurrentWorld.RowId; - //location.InstanceId = UIState.Instance()->PublicInstance.InstanceId; //TODO:Need API update first + location.InstanceId = UIState.Instance()->PublicInstance.InstanceId; location.TerritoryId = _clientState.TerritoryType; location.MapId = _clientState.MapId; if (houseMan != null) @@ -642,10 +645,10 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber str += $" - {MapData.Value[(ushort)location.MapId].MapName}"; } - // if (location.InstanceId is not 0) - // { - // str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString(); - // } + if (location.InstanceId is not 0) + { + str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString(); + } if (location.WardId is not 0) { diff --git a/LightlessSync/Services/LocationShareService.cs b/LightlessSync/Services/LocationShareService.cs new file mode 100644 index 0000000..0d3f5dc --- /dev/null +++ b/LightlessSync/Services/LocationShareService.cs @@ -0,0 +1,78 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Dto.CharaData; +using LightlessSync.API.Dto.User; +using LightlessSync.Services.Mediator; +using LightlessSync.WebAPI; +using Microsoft.Extensions.Logging; + +namespace LightlessSync.Services +{ + public class LocationShareService : DisposableMediatorSubscriberBase + { + private readonly DalamudUtilService _dalamudUtilService; + private readonly ApiController _apiController; + private Dictionary _locations = []; + + public LocationShareService(ILogger logger, LightlessMediator mediator, DalamudUtilService dalamudUtilService, ApiController apiController) : base(logger, mediator) + { + _dalamudUtilService = dalamudUtilService; + _apiController = apiController; + + + Mediator.Subscribe(this, (msg) => _locations.Clear()); + Mediator.Subscribe(this, (msg) => + { + _ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, apiController.DisplayName), _dalamudUtilService.GetMapData())); + _ = RequestAllLocation(); + } ); + Mediator.Subscribe(this, UpdateLocationList); + Mediator.Subscribe(this, + msg => _ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, _apiController.DisplayName), _dalamudUtilService.GetMapData()))); + } + + private void UpdateLocationList(LocationMessage msg) + { + if (_locations.ContainsKey(msg.Uid) && msg.LocationInfo.ServerId is 0) + { + _locations.Remove(msg.Uid); + return; + } + + if ( msg.LocationInfo.ServerId is not 0 && !_locations.TryAdd(msg.Uid, msg.LocationInfo)) + { + _locations[msg.Uid] = msg.LocationInfo; + } + } + + private async Task RequestAllLocation() + { + try + { + var data = await _apiController.RequestAllLocationInfo().ConfigureAwait(false); + _locations = data.ToDictionary(x => x.user.UID, x => x.location, StringComparer.Ordinal); + } + catch (Exception e) + { + Logger.LogError(e,"RequestAllLocation error : "); + throw; + } + } + + public string GetUserLocation(string uid) + { + try + { + if (_locations.TryGetValue(uid, out var location)) + { + return _dalamudUtilService.LocationToString(location); + } + return String.Empty; + } + catch (Exception e) + { + Logger.LogError(e,"GetUserLocation error : "); + throw; + } + } + } +} \ No newline at end of file diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index 758b9f5..be0c06b 100644 --- a/LightlessSync/Services/Mediator/Messages.cs +++ b/LightlessSync/Services/Mediator/Messages.cs @@ -135,5 +135,7 @@ public record ChatChannelsUpdated : MessageBase; public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Message) : MessageBase; public record GroupCollectionChangedMessage : MessageBase; public record OpenUserProfileMessage(UserData User) : MessageBase; +public record LocationMessage(string Uid, LocationInfo LocationInfo) : 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/UI/Components/DrawFolderGroup.cs b/LightlessSync/UI/Components/DrawFolderGroup.cs index c39326c..277115a 100644 --- a/LightlessSync/UI/Components/DrawFolderGroup.cs +++ b/LightlessSync/UI/Components/DrawFolderGroup.cs @@ -131,6 +131,7 @@ public class DrawFolderGroup : DrawFolderBase bool disableSounds = perm.IsDisableSounds(); bool disableAnims = perm.IsDisableAnimations(); bool disableVfx = perm.IsDisableVFX(); + bool shareLocation = perm.IsSharingLocation(); if ((_groupFullInfoDto.GroupPermissions.IsPreferDisableAnimations() != disableAnims || _groupFullInfoDto.GroupPermissions.IsPreferDisableSounds() != disableSounds @@ -164,6 +165,13 @@ public class DrawFolderGroup : DrawFolderBase _ = _apiController.GroupChangeIndividualPermissionState(new(_groupFullInfoDto.Group, new(_apiController.UID), perm)); ImGui.CloseCurrentPopup(); } + + if (_uiSharedService.IconTextButton(!shareLocation ? FontAwesomeIcon.Globe : FontAwesomeIcon.StopCircle, !shareLocation ? "Share your location to all users in Syncshell" : "STOP Share your location to all users in Syncshell", menuWidth, true)) + { + perm.SetShareLocation(!shareLocation); + _ = _apiController.GroupChangeIndividualPermissionState(new(_groupFullInfoDto.Group, new(_apiController.UID), perm)); + ImGui.CloseCurrentPopup(); + } if (IsModerator || IsOwner) { diff --git a/LightlessSync/UI/Components/DrawUserPair.cs b/LightlessSync/UI/Components/DrawUserPair.cs index 8e03ae4..5f3d300 100644 --- a/LightlessSync/UI/Components/DrawUserPair.cs +++ b/LightlessSync/UI/Components/DrawUserPair.cs @@ -37,6 +37,7 @@ public class DrawUserPair private readonly UiSharedService _uiSharedService; private readonly PlayerPerformanceConfigService _performanceConfigService; private readonly LightlessConfigService _configService; + private readonly LocationShareService _locationShareService; private readonly CharaDataManager _charaDataManager; private readonly PairLedger _pairLedger; private float _menuWidth = -1; @@ -57,6 +58,7 @@ public class DrawUserPair UiSharedService uiSharedService, PlayerPerformanceConfigService performanceConfigService, LightlessConfigService configService, + LocationShareService locationShareService, CharaDataManager charaDataManager, PairLedger pairLedger) { @@ -74,6 +76,7 @@ public class DrawUserPair _uiSharedService = uiSharedService; _performanceConfigService = performanceConfigService; _configService = configService; + _locationShareService = locationShareService; _charaDataManager = charaDataManager; _pairLedger = pairLedger; } @@ -216,6 +219,17 @@ public class DrawUserPair _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions)); } UiSharedService.AttachToolTip("Changes VFX sync permissions with this user." + (individual ? individualText : string.Empty)); + + var isShareingLocation = _pair.UserPair!.OwnPermissions.IsSharingLocation(); + string isShareingLocationText = isShareingLocation ? "Disable location sharing" : "Enable location sharing"; + var isShareingLocationIcon = isShareingLocation ? FontAwesomeIcon.StopCircle : FontAwesomeIcon.Globe; + if (_uiSharedService.IconTextButton(isShareingLocationIcon, isShareingLocationText, _menuWidth, true)) + { + var permissions = _pair.UserPair.OwnPermissions; + permissions.SetShareLocation(!isShareingLocation); + _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions)); + } + UiSharedService.AttachToolTip("Changes location sharing permissions with this user." + (individual ? individualText : string.Empty)); } private void DrawIndividualMenu() @@ -567,6 +581,7 @@ public class DrawUserPair : UiSharedService.TooltipSeparator + "Hold CTRL to enable preferred permissions while pausing." + Environment.NewLine + "This will leave this pair paused even if unpausing syncshells including this pair.")) : "Resume pairing with " + _pair.UserData.AliasOrUID); + //Location sharing if (_pair.IsPaired) { var individualSoundsDisabled = (_pair.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableSounds() ?? false); @@ -574,6 +589,66 @@ public class DrawUserPair var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false); var individualIsSticky = _pair.UserPair!.OwnPermissions.IsSticky(); var individualIcon = individualIsSticky ? FontAwesomeIcon.ArrowCircleUp : FontAwesomeIcon.InfoCircle; + + + var shareLocationIcon = FontAwesomeIcon.Globe; + var shareLocation = _pair.UserPair?.OwnPermissions.IsSharingLocation() ?? false; + var shareLocationOther = _pair.UserPair?.OtherPermissions.IsSharingLocation() ?? false; + var shareColor = shareLocation switch + { + true when shareLocationOther => UIColors.Get("LightlessGreen"), + false when shareLocationOther => UIColors.Get("LightlessBlue"), + _ => UIColors.Get("LightlessYellow"), + }; + + if (shareLocation || shareLocationOther) + { + currentRightSide -= (_uiSharedService.GetIconSize(shareLocationIcon).X + spacingX); + ImGui.SameLine(currentRightSide); + using (ImRaii.PushColor(ImGuiCol.Text, shareColor, shareLocation || shareLocationOther)) + _uiSharedService.IconText(shareLocationIcon); + + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + if (shareLocationOther) + { + var location = _locationShareService.GetUserLocation(_pair.UserPair!.User.UID); + if (_pair.IsOnline) + { + if (!string.IsNullOrEmpty(location)) + { + _uiSharedService.IconText(FontAwesomeIcon.LocationArrow); + ImGui.SameLine(); + ImGui.TextUnformatted(location); + } + else + { + ImGui.TextUnformatted("Location info not updated, reconnect or waiting for zone-changing."); + } + } + else + { + ImGui.TextUnformatted("User not onlineㄟ( ▔, ▔ )ㄏ"); + } + } + else + { + ImGui.TextUnformatted("NOT Sharing location with you.(⊙x⊙;)"); + } + ImGui.Separator(); + + if (shareLocation) + { + ImGui.TextUnformatted("Sharing your location.ヾ(•ω•`)o"); + } + else + { + ImGui.TextUnformatted("NOT sharing your location.(´。_。`)"); + } + ImGui.EndTooltip(); + } + } if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky) { diff --git a/LightlessSync/UI/DrawEntityFactory.cs b/LightlessSync/UI/DrawEntityFactory.cs index 3c71f5c..e810a29 100644 --- a/LightlessSync/UI/DrawEntityFactory.cs +++ b/LightlessSync/UI/DrawEntityFactory.cs @@ -28,6 +28,7 @@ public class DrawEntityFactory private readonly ServerConfigurationManager _serverConfigurationManager; private readonly LightlessConfigService _configService; private readonly UiSharedService _uiSharedService; + private readonly LocationShareService _locationShareService; private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; private readonly CharaDataManager _charaDataManager; private readonly SelectTagForPairUi _selectTagForPairUi; @@ -52,6 +53,7 @@ public class DrawEntityFactory ServerConfigurationManager serverConfigurationManager, LightlessConfigService configService, UiSharedService uiSharedService, + LocationShareService locationShareService, PlayerPerformanceConfigService playerPerformanceConfigService, CharaDataManager charaDataManager, SelectTagForSyncshellUi selectTagForSyncshellUi, @@ -71,6 +73,7 @@ public class DrawEntityFactory _serverConfigurationManager = serverConfigurationManager; _configService = configService; _uiSharedService = uiSharedService; + _locationShareService = locationShareService; _playerPerformanceConfigService = playerPerformanceConfigService; _charaDataManager = charaDataManager; _selectTagForSyncshellUi = selectTagForSyncshellUi; @@ -162,6 +165,7 @@ public class DrawEntityFactory _uiSharedService, _playerPerformanceConfigService, _configService, + _locationShareService, _charaDataManager, _pairLedger); } diff --git a/LightlessSync/UI/PermissionWindowUI.cs b/LightlessSync/UI/PermissionWindowUI.cs index 5dee098..9d88547 100644 --- a/LightlessSync/UI/PermissionWindowUI.cs +++ b/LightlessSync/UI/PermissionWindowUI.cs @@ -43,6 +43,7 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase var disableSounds = _ownPermissions.IsDisableSounds(); var disableAnimations = _ownPermissions.IsDisableAnimations(); var disableVfx = _ownPermissions.IsDisableVFX(); + var shareLocation = _ownPermissions.IsSharingLocation(); var style = ImGui.GetStyle(); var indentSize = ImGui.GetFrameHeight() + style.ItemSpacing.X; @@ -70,6 +71,7 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase var otherDisableSounds = otherPerms.IsDisableSounds(); var otherDisableAnimations = otherPerms.IsDisableAnimations(); var otherDisableVFX = otherPerms.IsDisableVFX(); + var otherShareLocation = otherPerms.IsSharingLocation(); using (ImRaii.PushIndent(indentSize, false)) { @@ -124,6 +126,24 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase ImGui.AlignTextToFramePadding(); ImGui.Text(Pair.UserData.AliasOrUID + " has " + (!otherDisableVFX ? "not " : string.Empty) + "disabled VFX sync with you"); } + + if (ImGui.Checkbox("Enable location Sharing", ref shareLocation)) + { + _ownPermissions.SetShareLocation(shareLocation); + } + _uiSharedService.DrawHelpText("Enable location sharing will only effect your side." + UiSharedService.TooltipSeparator + + "Note: this is NOT bidirectional, you can choose to share even others dont share with you."); + using (ImRaii.PushIndent(indentSize, false)) + { + _uiSharedService.BooleanToColoredIcon(shareLocation, false); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.Text((!shareLocation ? "Not" : string.Empty) + "sharing location with " + Pair.UserData.AliasOrUID + " ."); + +#if DEBUG + _uiSharedService.BooleanToColoredIcon(otherShareLocation, true); +#endif + } ImGuiHelpers.ScaledDummy(0.5f); ImGui.Separator(); diff --git a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs index fdc4719..6cd704f 100644 --- a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs +++ b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs @@ -200,5 +200,17 @@ public partial class ApiController await UserPushData(new(visibleCharacters, character, censusDto)).ConfigureAwait(false); } + + public async Task UpdateLocation(LocationDto locationDto, bool offline = false) + { + if (!IsConnected) return; + await _lightlessHub!.SendAsync(nameof(UpdateLocation), locationDto, offline).ConfigureAwait(false); + } + + public async Task> RequestAllLocationInfo() + { + if (!IsConnected) return []; + return await _lightlessHub!.InvokeAsync>(nameof(RequestAllLocationInfo)).ConfigureAwait(false); + } } #pragma warning restore MA0040 \ 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 490800f..01abc88 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs @@ -259,6 +259,13 @@ public partial class ApiController ExecuteSafely(() => Mediator.Publish(new GPoseLobbyReceiveWorldData(userData, worldData))); return Task.CompletedTask; } + + public Task Client_SendLocationToClient(LocationDto locationDto) + { + Logger.LogDebug($"{nameof(Client_SendLocationToClient)}: {locationDto.user}"); + ExecuteSafely(() => Mediator.Publish(new LocationMessage(locationDto.user.UID, locationDto.location))); + return Task.CompletedTask; + } public void OnDownloadReady(Action act) { @@ -440,6 +447,12 @@ public partial class ApiController if (_initialized) return; _lightlessHub!.On(nameof(Client_GposeLobbyPushWorldData), act); } + + public void OnReciveLocation(Action act) + { + if (_initialized) return; + _lightlessHub!.On(nameof(Client_SendLocationToClient), act); + } private void ExecuteSafely(Action act) { diff --git a/LightlessSync/WebAPI/SignalR/ApiController.cs b/LightlessSync/WebAPI/SignalR/ApiController.cs index 9639f6f..a814758 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.cs @@ -606,6 +606,8 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL OnGposeLobbyPushCharacterData((dto) => _ = Client_GposeLobbyPushCharacterData(dto)); OnGposeLobbyPushPoseData((dto, data) => _ = Client_GposeLobbyPushPoseData(dto, data)); OnGposeLobbyPushWorldData((dto, data) => _ = Client_GposeLobbyPushWorldData(dto, data)); + OnReciveLocation(dto => _ = Client_SendLocationToClient(dto)); + _healthCheckTokenSource?.Cancel(); _healthCheckTokenSource?.Dispose(); From 24fca31606a7ec16f4941ecfa6e83ccc53ea8c58 Mon Sep 17 00:00:00 2001 From: choco Date: Sat, 27 Dec 2025 23:09:29 +0100 Subject: [PATCH 11/28] join syncshell draw modal --- LightlessSync/UI/LightFinderUI.cs | 308 +++++++++++++++++++++++++----- 1 file changed, 258 insertions(+), 50 deletions(-) diff --git a/LightlessSync/UI/LightFinderUI.cs b/LightlessSync/UI/LightFinderUI.cs index ed55d4f..9cb7891 100644 --- a/LightlessSync/UI/LightFinderUI.cs +++ b/LightlessSync/UI/LightFinderUI.cs @@ -58,6 +58,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase private List _currentSyncshells = []; private GroupJoinDto? _joinDto; private GroupJoinInfoDto? _joinInfo; + private bool _joinModalOpen = true; private readonly List _nearbySyncshells = []; private DefaultPermissionsDto _ownPermissions = null!; private readonly HashSet _recentlyJoined = new(StringComparer.Ordinal); @@ -66,6 +67,11 @@ public class LightFinderUI : WindowMediatorSubscriberBase private LightfinderTab _selectedTab = LightfinderTab.NearbySyncshells; private string _userUid = string.Empty; + private const float AnimationSpeed = 6f; + private readonly Dictionary _itemAlpha = new(StringComparer.Ordinal); + private readonly HashSet _currentVisibleItems = new(StringComparer.Ordinal); + private readonly HashSet _previousVisibleItems = new(StringComparer.Ordinal); + private enum LightfinderTab { NearbySyncshells, NearbyPlayers, BroadcastSettings, Help } #if DEBUG @@ -106,14 +112,14 @@ public class LightFinderUI : WindowMediatorSubscriberBase _lightlessProfileManager = lightlessProfileManager; _actorObjectService = actorObjectService; - _animatedHeader.Height = 100f; + _animatedHeader.Height = 85f; _animatedHeader.EnableBottomGradient = true; - _animatedHeader.GradientHeight = 120f; + _animatedHeader.GradientHeight = 90f; _animatedHeader.EnableParticles = _configService.Current.EnableParticleEffects; IsOpen = false; WindowBuilder.For(this) - .SetSizeConstraints(new Vector2(620, 85), new Vector2(700, 600)) + .SetSizeConstraints(new Vector2(620, 85), new Vector2(700, 2000)) .Apply(); Mediator.Subscribe(this, async _ => await RefreshNearbySyncshellsAsync().ConfigureAwait(false)); @@ -367,6 +373,52 @@ public class LightFinderUI : WindowMediatorSubscriberBase #endregion + #region Animation Helpers + + private void UpdateItemAnimations(IEnumerable visibleItemIds) + { + var deltaTime = ImGui.GetIO().DeltaTime; + + _previousVisibleItems.Clear(); + foreach (var id in _currentVisibleItems) + _previousVisibleItems.Add(id); + + _currentVisibleItems.Clear(); + foreach (var id in visibleItemIds) + _currentVisibleItems.Add(id); + + // Fade in new items + foreach (var id in _currentVisibleItems) + { + if (!_itemAlpha.ContainsKey(id)) + _itemAlpha[id] = 0f; + + _itemAlpha[id] = Math.Min(1f, _itemAlpha[id] + deltaTime * AnimationSpeed); + } + + // Fade out removed items + var toRemove = new List(); + foreach (var (id, alpha) in _itemAlpha) + { + if (!_currentVisibleItems.Contains(id)) + { + _itemAlpha[id] = Math.Max(0f, alpha - deltaTime * AnimationSpeed); + if (_itemAlpha[id] <= 0.01f) + toRemove.Add(id); + } + } + + foreach (var id in toRemove) + _itemAlpha.Remove(id); + } + + private float GetItemAlpha(string itemId) + { + return _itemAlpha.TryGetValue(itemId, out var alpha) ? alpha : 1f; + } + + #endregion + #region Nearby Syncshells Tab private void DrawNearbySyncshellsTab() @@ -400,10 +452,14 @@ public class LightFinderUI : WindowMediatorSubscriberBase var cardData = BuildSyncshellCardData(); if (cardData.Count == 0) { + UpdateItemAnimations([]); ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells found."); return; } + // Update animations for syncshell items + UpdateItemAnimations(cardData.Select(c => $"shell_{c.Shell.Group.GID}")); + if (_compactView) DrawSyncshellGrid(cardData); else @@ -499,7 +555,13 @@ public class LightFinderUI : WindowMediatorSubscriberBase private void DrawSyncshellListItem(GroupJoinDto shell, string broadcasterName, bool isOwnBroadcast) { + var itemId = $"shell_{shell.Group.GID}"; + var alpha = GetItemAlpha(itemId); + if (alpha <= 0.01f) + return; + ImGui.PushID(shell.Group.GID); + using var alphaStyle = ImRaii.PushStyle(ImGuiStyleVar.Alpha, alpha); float rowHeight = 74f * ImGuiHelpers.GlobalScale; ImGui.BeginChild($"ShellRow##{shell.Group.GID}", new Vector2(-1, rowHeight), border: true); @@ -572,7 +634,13 @@ public class LightFinderUI : WindowMediatorSubscriberBase private void DrawSyncshellCompactItem(GroupJoinDto shell, bool isOwnBroadcast) { + var itemId = $"shell_{shell.Group.GID}"; + var alpha = GetItemAlpha(itemId); + if (alpha <= 0.01f) + return; + ImGui.PushID(shell.Group.GID); + using var alphaStyle = ImRaii.PushStyle(ImGuiStyleVar.Alpha, alpha); float rowHeight = 36f * ImGuiHelpers.GlobalScale; ImGui.BeginChild($"ShellCompact##{shell.Group.GID}", new Vector2(-1, rowHeight), border: true); @@ -655,6 +723,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase { _joinDto = new GroupJoinDto(shell.Group, shell.Password, shell.GroupUserPreferredPermissions); _joinInfo = info; + _joinModalOpen = true; _ownPermissions = _apiController.DefaultPermissions.DeepClone()!; } } @@ -685,65 +754,188 @@ public class LightFinderUI : WindowMediatorSubscriberBase { if (_joinDto == null || _joinInfo == null) return; - ImGui.Separator(); - ImGui.TextUnformatted($"Join Syncshell: {_joinDto.Group.AliasOrGID} by {_joinInfo.OwnerAliasOrUID}"); - ImGuiHelpers.ScaledDummy(2f); - ImGui.TextUnformatted("Suggested Permissions:"); + var scale = ImGuiHelpers.GlobalScale; + + // if not already open + if (!ImGui.IsPopupOpen("JoinSyncshellModal")) + ImGui.OpenPopup("JoinSyncshellModal"); - DrawPermissionRow("Sounds", _joinInfo.GroupPermissions.IsPreferDisableSounds(), _ownPermissions.DisableGroupSounds, v => _ownPermissions.DisableGroupSounds = v); - DrawPermissionRow("Animations", _joinInfo.GroupPermissions.IsPreferDisableAnimations(), _ownPermissions.DisableGroupAnimations, v => _ownPermissions.DisableGroupAnimations = v); - DrawPermissionRow("VFX", _joinInfo.GroupPermissions.IsPreferDisableVFX(), _ownPermissions.DisableGroupVFX, v => _ownPermissions.DisableGroupVFX = v); - - ImGui.NewLine(); - - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, $"Finalize and join {_joinDto.Group.AliasOrGID}")) + Vector2 windowPos = ImGui.GetWindowPos(); + Vector2 windowSize = ImGui.GetWindowSize(); + float modalWidth = Math.Min(420f * scale, windowSize.X - 40f * scale); + float modalHeight = 295f * scale; + ImGui.SetNextWindowPos(new Vector2( + windowPos.X + (windowSize.X - modalWidth) * 0.5f, + windowPos.Y + (windowSize.Y - modalHeight) * 0.5f + ), ImGuiCond.Always); + ImGui.SetNextWindowSize(new Vector2(modalWidth, modalHeight)); + ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 6f * ImGuiHelpers.GlobalScale); + ImGui.PushStyleColor(ImGuiCol.Border, Vector4.Zero); + + using ImRaii.Color modalBorder = ImRaii.PushColor(ImGuiCol.Border, UIColors.Get("LightlessPurple").WithAlpha(0.5f)); + using ImRaii.Style rounding = ImRaii.PushStyle(ImGuiStyleVar.WindowRounding, 8f * scale); + using ImRaii.Style borderSize = ImRaii.PushStyle(ImGuiStyleVar.WindowBorderSize, 2f * scale); + using ImRaii.Style padding = ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(16f * scale, 16f * scale)); + + ImGuiWindowFlags flags = ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoScrollbar; + if (ImGui.BeginPopupModal("JoinSyncshellModal", ref _joinModalOpen, flags)) { - var finalPermissions = GroupUserPreferredPermissions.NoneSet; - finalPermissions.SetDisableSounds(_ownPermissions.DisableGroupSounds); - finalPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations); - finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX); + float contentWidth = ImGui.GetContentRegionAvail().X; - _ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions)); - _recentlyJoined.Add(_joinDto.Group.GID); + // Header + _uiSharedService.MediumText("Join Syncshell", UIColors.Get("LightlessPurple")); + ImGuiHelpers.ScaledDummy(2f); + ImGui.TextColored(ImGuiColors.DalamudGrey, $"{_joinDto.Group.AliasOrGID} by {_joinInfo.OwnerAliasOrUID}"); - _joinDto = null; - _joinInfo = null; - } + ImGuiHelpers.ScaledDummy(8f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault").WithAlpha(0.4f)); + ImGuiHelpers.ScaledDummy(8f); - ImGui.SameLine(); - if (ImGui.Button("Cancel")) - { - _joinDto = null; - _joinInfo = null; + // Permissions section + ImGui.TextColored(ImGuiColors.DalamudWhite, "Permissions"); + ImGuiHelpers.ScaledDummy(6f); + + DrawPermissionToggleRow("Sounds", FontAwesomeIcon.VolumeUp, + _joinInfo.GroupPermissions.IsPreferDisableSounds(), + _ownPermissions.DisableGroupSounds, + v => _ownPermissions.DisableGroupSounds = v, + contentWidth); + + DrawPermissionToggleRow("Animations", FontAwesomeIcon.Running, + _joinInfo.GroupPermissions.IsPreferDisableAnimations(), + _ownPermissions.DisableGroupAnimations, + v => _ownPermissions.DisableGroupAnimations = v, + contentWidth); + + DrawPermissionToggleRow("VFX", FontAwesomeIcon.Magic, + _joinInfo.GroupPermissions.IsPreferDisableVFX(), + _ownPermissions.DisableGroupVFX, + v => _ownPermissions.DisableGroupVFX = v, + contentWidth); + + ImGuiHelpers.ScaledDummy(12f); + + // Buttons + float buttonHeight = 32f * scale; + float buttonSpacing = 8f * scale; + float joinButtonWidth = (contentWidth - buttonSpacing) * 0.65f; + float cancelButtonWidth = (contentWidth - buttonSpacing) * 0.35f; + + using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 6f * scale)) + { + // Join button + using (ImRaii.PushColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"))) + using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f))) + using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f))) + { + if (ImGui.Button($"Join Syncshell##{_joinDto.Group.GID}", new Vector2(joinButtonWidth, buttonHeight))) + { + var finalPermissions = GroupUserPreferredPermissions.NoneSet; + finalPermissions.SetDisableSounds(_ownPermissions.DisableGroupSounds); + finalPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations); + finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX); + + _ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions)); + _recentlyJoined.Add(_joinDto.Group.GID); + + _joinDto = null; + _joinInfo = null; + ImGui.CloseCurrentPopup(); + } + } + + ImGui.SameLine(0f, buttonSpacing); + + // Cancel button + using (ImRaii.PushColor(ImGuiCol.Button, new Vector4(0.3f, 0.3f, 0.3f, 1f))) + using (ImRaii.PushColor(ImGuiCol.ButtonHovered, new Vector4(0.4f, 0.4f, 0.4f, 1f))) + using (ImRaii.PushColor(ImGuiCol.ButtonActive, new Vector4(0.25f, 0.25f, 0.25f, 1f))) + { + if (ImGui.Button("Cancel", new Vector2(cancelButtonWidth, buttonHeight))) + { + _joinDto = null; + _joinInfo = null; + ImGui.CloseCurrentPopup(); + } + } + } + + // Handle modal close via the bool ref + if (!_joinModalOpen) + { + _joinDto = null; + _joinInfo = null; + } + + ImGui.EndPopup(); } } - private void DrawPermissionRow(string label, bool suggested, bool current, Action apply) + private void DrawPermissionToggleRow(string label, FontAwesomeIcon icon, bool suggested, bool current, Action apply, float contentWidth) { - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted($"- {label}"); - - ImGui.SameLine(120 * ImGuiHelpers.GlobalScale); - ImGui.Text("Current:"); - ImGui.SameLine(); - _uiSharedService.BooleanToColoredIcon(!current); - - ImGui.SameLine(240 * ImGuiHelpers.GlobalScale); - ImGui.Text("Suggested:"); - ImGui.SameLine(); - _uiSharedService.BooleanToColoredIcon(!suggested); - - ImGui.SameLine(380 * ImGuiHelpers.GlobalScale); - using var id = ImRaii.PushId(label); - if (current != suggested) + var scale = ImGuiHelpers.GlobalScale; + float rowHeight = 28f * scale; + bool isDifferent = current != suggested; + + using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 4f * scale)) + using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 4f * scale)) + using (ImRaii.PushColor(ImGuiCol.ChildBg, new Vector4(0.18f, 0.15f, 0.22f, 0.6f))) { - if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Apply")) - apply(suggested); - } + ImGui.BeginChild($"PermRow_{label}", new Vector2(contentWidth, rowHeight), false, ImGuiWindowFlags.NoScrollbar); + + float innerPadding = 8f * scale; + ImGui.SetCursorPos(new Vector2(innerPadding, (rowHeight - ImGui.GetTextLineHeight()) * 0.5f)); - ImGui.NewLine(); + // Icon and label + var enabledColor = UIColors.Get("LightlessGreen"); + var disabledColor = UIColors.Get("DimRed"); + var currentColor = !current ? enabledColor : disabledColor; + + _uiSharedService.IconText(icon, currentColor); + ImGui.SameLine(0f, 6f * scale); + ImGui.TextUnformatted(label); + + // Current status + ImGui.SameLine(); + float statusX = contentWidth * 0.38f; + ImGui.SetCursorPosX(statusX); + ImGui.TextColored(ImGuiColors.DalamudGrey, "Current:"); + ImGui.SameLine(0f, 4f * scale); + _uiSharedService.BooleanToColoredIcon(!current, false); + + // Suggested status + ImGui.SameLine(); + float suggestedX = contentWidth * 0.60f; + ImGui.SetCursorPosX(suggestedX); + ImGui.TextColored(ImGuiColors.DalamudGrey, "Suggested:"); + ImGui.SameLine(0f, 4f * scale); + _uiSharedService.BooleanToColoredIcon(!suggested, false); + + // Apply checkmark button if different + if (isDifferent) + { + ImGui.SameLine(); + float applyX = contentWidth - 26f * scale; + ImGui.SetCursorPosX(applyX); + ImGui.SetCursorPosY((rowHeight - ImGui.GetFrameHeight()) * 0.5f); + + using (ImRaii.PushColor(ImGuiCol.Button, UIColors.Get("LightlessGreen").WithAlpha(0.6f))) + using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen"))) + using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreenDefault"))) + { + if (_uiSharedService.IconButton(FontAwesomeIcon.Check)) + apply(suggested); + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Apply suggested"); + } + + ImGui.EndChild(); + } + ImGui.Dummy(new Vector2(0, 2f * scale)); } + #endregion #region Nearby Players Tab @@ -785,10 +977,14 @@ public class LightFinderUI : WindowMediatorSubscriberBase var playerData = BuildNearbyPlayerData(activeBroadcasts); if (playerData.Count == 0) { + UpdateItemAnimations([]); ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby Lightfinder players found."); return; } + // Update animations for player items + UpdateItemAnimations(playerData.Select(p => $"player_{p.HashedCid}")); + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8f); ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f); @@ -848,7 +1044,13 @@ public class LightFinderUI : WindowMediatorSubscriberBase private void DrawNearbyPlayerRow(NearbyPlayerData data) { + var itemId = $"player_{data.HashedCid}"; + var alpha = GetItemAlpha(itemId); + if (alpha <= 0.01f) + return; + ImGui.PushID(data.HashedCid); + using var alphaStyle = ImRaii.PushStyle(ImGuiStyleVar.Alpha, alpha); float rowHeight = 74f * ImGuiHelpers.GlobalScale; ImGui.BeginChild($"PlayerRow##{data.HashedCid}", new Vector2(-1, rowHeight), border: true); @@ -903,7 +1105,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase ImGui.SetCursorPosX(startX); _uiSharedService.IconText(FontAwesomeIcon.Wifi, UIColors.Get("LightlessBlue")); ImGui.SameLine(0f, 4f * ImGuiHelpers.GlobalScale); - ImGui.TextColored(ImGuiColors.DalamudGrey, "Lightfinder User"); + ImGui.TextColored(ImGuiColors.DalamudGrey, "Lightfinder sir ma'am or whatever"); } ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale)); @@ -916,7 +1118,13 @@ public class LightFinderUI : WindowMediatorSubscriberBase private void DrawNearbyPlayerCompactRow(NearbyPlayerData data) { + var itemId = $"player_{data.HashedCid}"; + var alpha = GetItemAlpha(itemId); + if (alpha <= 0.01f) + return; + ImGui.PushID(data.HashedCid); + using var alphaStyle = ImRaii.PushStyle(ImGuiStyleVar.Alpha, alpha); float rowHeight = 36f * ImGuiHelpers.GlobalScale; ImGui.BeginChild($"PlayerCompact##{data.HashedCid}", new Vector2(-1, rowHeight), border: true); From eeda5aeb66943d3075c04882dd35ec5e4bb3992b Mon Sep 17 00:00:00 2001 From: Tsubasahane Date: Sun, 28 Dec 2025 10:54:01 +0800 Subject: [PATCH 12/28] Revert "Location Sharing" This reverts commit 70745613e13cb27dd7e90d30a811bd8b1f28d965. --- LightlessAPI | 2 +- LightlessSync/Plugin.cs | 1 - LightlessSync/Services/DalamudUtilService.cs | 13 ++-- .../Services/LocationShareService.cs | 78 ------------------- LightlessSync/Services/Mediator/Messages.cs | 2 - .../UI/Components/DrawFolderGroup.cs | 8 -- LightlessSync/UI/Components/DrawUserPair.cs | 75 ------------------ LightlessSync/UI/DrawEntityFactory.cs | 4 - LightlessSync/UI/PermissionWindowUI.cs | 20 ----- .../SignalR/ApIController.Functions.Users.cs | 12 --- .../ApiController.Functions.Callbacks.cs | 13 ---- LightlessSync/WebAPI/SignalR/ApiController.cs | 2 - 12 files changed, 6 insertions(+), 224 deletions(-) delete mode 100644 LightlessSync/Services/LocationShareService.cs diff --git a/LightlessAPI b/LightlessAPI index fdd492a..5656600 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit fdd492a8f478949d910ed0efd3e4a3ca3312ed9c +Subproject commit 56566003e0e93bba05dcef49fd3ce23c6a204d81 diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 4131e40..cd57c34 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -137,7 +137,6 @@ public sealed class Plugin : IDalamudPlugin services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(sp => new TextureMetadataHelper(sp.GetRequiredService>(), gameData)); diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index 36ce98b..ef6fe7a 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -1,13 +1,11 @@ using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Game.Text; using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Control; -using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using LightlessSync.API.Dto.CharaData; @@ -27,7 +25,6 @@ using System.Runtime.CompilerServices; using System.Text; using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; -using Map = Lumina.Excel.Sheets.Map; using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags; namespace LightlessSync.Services; @@ -591,7 +588,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber var location = new LocationInfo(); location.ServerId = _playerState.CurrentWorld.RowId; - location.InstanceId = UIState.Instance()->PublicInstance.InstanceId; + //location.InstanceId = UIState.Instance()->PublicInstance.InstanceId; //TODO:Need API update first location.TerritoryId = _clientState.TerritoryType; location.MapId = _clientState.MapId; if (houseMan != null) @@ -645,10 +642,10 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber str += $" - {MapData.Value[(ushort)location.MapId].MapName}"; } - if (location.InstanceId is not 0) - { - str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString(); - } + // if (location.InstanceId is not 0) + // { + // str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString(); + // } if (location.WardId is not 0) { diff --git a/LightlessSync/Services/LocationShareService.cs b/LightlessSync/Services/LocationShareService.cs deleted file mode 100644 index 0d3f5dc..0000000 --- a/LightlessSync/Services/LocationShareService.cs +++ /dev/null @@ -1,78 +0,0 @@ -using LightlessSync.API.Data; -using LightlessSync.API.Dto.CharaData; -using LightlessSync.API.Dto.User; -using LightlessSync.Services.Mediator; -using LightlessSync.WebAPI; -using Microsoft.Extensions.Logging; - -namespace LightlessSync.Services -{ - public class LocationShareService : DisposableMediatorSubscriberBase - { - private readonly DalamudUtilService _dalamudUtilService; - private readonly ApiController _apiController; - private Dictionary _locations = []; - - public LocationShareService(ILogger logger, LightlessMediator mediator, DalamudUtilService dalamudUtilService, ApiController apiController) : base(logger, mediator) - { - _dalamudUtilService = dalamudUtilService; - _apiController = apiController; - - - Mediator.Subscribe(this, (msg) => _locations.Clear()); - Mediator.Subscribe(this, (msg) => - { - _ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, apiController.DisplayName), _dalamudUtilService.GetMapData())); - _ = RequestAllLocation(); - } ); - Mediator.Subscribe(this, UpdateLocationList); - Mediator.Subscribe(this, - msg => _ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, _apiController.DisplayName), _dalamudUtilService.GetMapData()))); - } - - private void UpdateLocationList(LocationMessage msg) - { - if (_locations.ContainsKey(msg.Uid) && msg.LocationInfo.ServerId is 0) - { - _locations.Remove(msg.Uid); - return; - } - - if ( msg.LocationInfo.ServerId is not 0 && !_locations.TryAdd(msg.Uid, msg.LocationInfo)) - { - _locations[msg.Uid] = msg.LocationInfo; - } - } - - private async Task RequestAllLocation() - { - try - { - var data = await _apiController.RequestAllLocationInfo().ConfigureAwait(false); - _locations = data.ToDictionary(x => x.user.UID, x => x.location, StringComparer.Ordinal); - } - catch (Exception e) - { - Logger.LogError(e,"RequestAllLocation error : "); - throw; - } - } - - public string GetUserLocation(string uid) - { - try - { - if (_locations.TryGetValue(uid, out var location)) - { - return _dalamudUtilService.LocationToString(location); - } - return String.Empty; - } - catch (Exception e) - { - Logger.LogError(e,"GetUserLocation error : "); - throw; - } - } - } -} \ No newline at end of file diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index be0c06b..758b9f5 100644 --- a/LightlessSync/Services/Mediator/Messages.cs +++ b/LightlessSync/Services/Mediator/Messages.cs @@ -135,7 +135,5 @@ public record ChatChannelsUpdated : MessageBase; public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Message) : MessageBase; public record GroupCollectionChangedMessage : MessageBase; public record OpenUserProfileMessage(UserData User) : MessageBase; -public record LocationMessage(string Uid, LocationInfo LocationInfo) : 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/UI/Components/DrawFolderGroup.cs b/LightlessSync/UI/Components/DrawFolderGroup.cs index 277115a..c39326c 100644 --- a/LightlessSync/UI/Components/DrawFolderGroup.cs +++ b/LightlessSync/UI/Components/DrawFolderGroup.cs @@ -131,7 +131,6 @@ public class DrawFolderGroup : DrawFolderBase bool disableSounds = perm.IsDisableSounds(); bool disableAnims = perm.IsDisableAnimations(); bool disableVfx = perm.IsDisableVFX(); - bool shareLocation = perm.IsSharingLocation(); if ((_groupFullInfoDto.GroupPermissions.IsPreferDisableAnimations() != disableAnims || _groupFullInfoDto.GroupPermissions.IsPreferDisableSounds() != disableSounds @@ -165,13 +164,6 @@ public class DrawFolderGroup : DrawFolderBase _ = _apiController.GroupChangeIndividualPermissionState(new(_groupFullInfoDto.Group, new(_apiController.UID), perm)); ImGui.CloseCurrentPopup(); } - - if (_uiSharedService.IconTextButton(!shareLocation ? FontAwesomeIcon.Globe : FontAwesomeIcon.StopCircle, !shareLocation ? "Share your location to all users in Syncshell" : "STOP Share your location to all users in Syncshell", menuWidth, true)) - { - perm.SetShareLocation(!shareLocation); - _ = _apiController.GroupChangeIndividualPermissionState(new(_groupFullInfoDto.Group, new(_apiController.UID), perm)); - ImGui.CloseCurrentPopup(); - } if (IsModerator || IsOwner) { diff --git a/LightlessSync/UI/Components/DrawUserPair.cs b/LightlessSync/UI/Components/DrawUserPair.cs index 5f3d300..8e03ae4 100644 --- a/LightlessSync/UI/Components/DrawUserPair.cs +++ b/LightlessSync/UI/Components/DrawUserPair.cs @@ -37,7 +37,6 @@ public class DrawUserPair private readonly UiSharedService _uiSharedService; private readonly PlayerPerformanceConfigService _performanceConfigService; private readonly LightlessConfigService _configService; - private readonly LocationShareService _locationShareService; private readonly CharaDataManager _charaDataManager; private readonly PairLedger _pairLedger; private float _menuWidth = -1; @@ -58,7 +57,6 @@ public class DrawUserPair UiSharedService uiSharedService, PlayerPerformanceConfigService performanceConfigService, LightlessConfigService configService, - LocationShareService locationShareService, CharaDataManager charaDataManager, PairLedger pairLedger) { @@ -76,7 +74,6 @@ public class DrawUserPair _uiSharedService = uiSharedService; _performanceConfigService = performanceConfigService; _configService = configService; - _locationShareService = locationShareService; _charaDataManager = charaDataManager; _pairLedger = pairLedger; } @@ -219,17 +216,6 @@ public class DrawUserPair _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions)); } UiSharedService.AttachToolTip("Changes VFX sync permissions with this user." + (individual ? individualText : string.Empty)); - - var isShareingLocation = _pair.UserPair!.OwnPermissions.IsSharingLocation(); - string isShareingLocationText = isShareingLocation ? "Disable location sharing" : "Enable location sharing"; - var isShareingLocationIcon = isShareingLocation ? FontAwesomeIcon.StopCircle : FontAwesomeIcon.Globe; - if (_uiSharedService.IconTextButton(isShareingLocationIcon, isShareingLocationText, _menuWidth, true)) - { - var permissions = _pair.UserPair.OwnPermissions; - permissions.SetShareLocation(!isShareingLocation); - _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions)); - } - UiSharedService.AttachToolTip("Changes location sharing permissions with this user." + (individual ? individualText : string.Empty)); } private void DrawIndividualMenu() @@ -581,7 +567,6 @@ public class DrawUserPair : UiSharedService.TooltipSeparator + "Hold CTRL to enable preferred permissions while pausing." + Environment.NewLine + "This will leave this pair paused even if unpausing syncshells including this pair.")) : "Resume pairing with " + _pair.UserData.AliasOrUID); - //Location sharing if (_pair.IsPaired) { var individualSoundsDisabled = (_pair.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableSounds() ?? false); @@ -589,66 +574,6 @@ public class DrawUserPair var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false); var individualIsSticky = _pair.UserPair!.OwnPermissions.IsSticky(); var individualIcon = individualIsSticky ? FontAwesomeIcon.ArrowCircleUp : FontAwesomeIcon.InfoCircle; - - - var shareLocationIcon = FontAwesomeIcon.Globe; - var shareLocation = _pair.UserPair?.OwnPermissions.IsSharingLocation() ?? false; - var shareLocationOther = _pair.UserPair?.OtherPermissions.IsSharingLocation() ?? false; - var shareColor = shareLocation switch - { - true when shareLocationOther => UIColors.Get("LightlessGreen"), - false when shareLocationOther => UIColors.Get("LightlessBlue"), - _ => UIColors.Get("LightlessYellow"), - }; - - if (shareLocation || shareLocationOther) - { - currentRightSide -= (_uiSharedService.GetIconSize(shareLocationIcon).X + spacingX); - ImGui.SameLine(currentRightSide); - using (ImRaii.PushColor(ImGuiCol.Text, shareColor, shareLocation || shareLocationOther)) - _uiSharedService.IconText(shareLocationIcon); - - if (ImGui.IsItemHovered()) - { - ImGui.BeginTooltip(); - if (shareLocationOther) - { - var location = _locationShareService.GetUserLocation(_pair.UserPair!.User.UID); - if (_pair.IsOnline) - { - if (!string.IsNullOrEmpty(location)) - { - _uiSharedService.IconText(FontAwesomeIcon.LocationArrow); - ImGui.SameLine(); - ImGui.TextUnformatted(location); - } - else - { - ImGui.TextUnformatted("Location info not updated, reconnect or waiting for zone-changing."); - } - } - else - { - ImGui.TextUnformatted("User not onlineㄟ( ▔, ▔ )ㄏ"); - } - } - else - { - ImGui.TextUnformatted("NOT Sharing location with you.(⊙x⊙;)"); - } - ImGui.Separator(); - - if (shareLocation) - { - ImGui.TextUnformatted("Sharing your location.ヾ(•ω•`)o"); - } - else - { - ImGui.TextUnformatted("NOT sharing your location.(´。_。`)"); - } - ImGui.EndTooltip(); - } - } if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky) { diff --git a/LightlessSync/UI/DrawEntityFactory.cs b/LightlessSync/UI/DrawEntityFactory.cs index e810a29..3c71f5c 100644 --- a/LightlessSync/UI/DrawEntityFactory.cs +++ b/LightlessSync/UI/DrawEntityFactory.cs @@ -28,7 +28,6 @@ public class DrawEntityFactory private readonly ServerConfigurationManager _serverConfigurationManager; private readonly LightlessConfigService _configService; private readonly UiSharedService _uiSharedService; - private readonly LocationShareService _locationShareService; private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; private readonly CharaDataManager _charaDataManager; private readonly SelectTagForPairUi _selectTagForPairUi; @@ -53,7 +52,6 @@ public class DrawEntityFactory ServerConfigurationManager serverConfigurationManager, LightlessConfigService configService, UiSharedService uiSharedService, - LocationShareService locationShareService, PlayerPerformanceConfigService playerPerformanceConfigService, CharaDataManager charaDataManager, SelectTagForSyncshellUi selectTagForSyncshellUi, @@ -73,7 +71,6 @@ public class DrawEntityFactory _serverConfigurationManager = serverConfigurationManager; _configService = configService; _uiSharedService = uiSharedService; - _locationShareService = locationShareService; _playerPerformanceConfigService = playerPerformanceConfigService; _charaDataManager = charaDataManager; _selectTagForSyncshellUi = selectTagForSyncshellUi; @@ -165,7 +162,6 @@ public class DrawEntityFactory _uiSharedService, _playerPerformanceConfigService, _configService, - _locationShareService, _charaDataManager, _pairLedger); } diff --git a/LightlessSync/UI/PermissionWindowUI.cs b/LightlessSync/UI/PermissionWindowUI.cs index 9d88547..5dee098 100644 --- a/LightlessSync/UI/PermissionWindowUI.cs +++ b/LightlessSync/UI/PermissionWindowUI.cs @@ -43,7 +43,6 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase var disableSounds = _ownPermissions.IsDisableSounds(); var disableAnimations = _ownPermissions.IsDisableAnimations(); var disableVfx = _ownPermissions.IsDisableVFX(); - var shareLocation = _ownPermissions.IsSharingLocation(); var style = ImGui.GetStyle(); var indentSize = ImGui.GetFrameHeight() + style.ItemSpacing.X; @@ -71,7 +70,6 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase var otherDisableSounds = otherPerms.IsDisableSounds(); var otherDisableAnimations = otherPerms.IsDisableAnimations(); var otherDisableVFX = otherPerms.IsDisableVFX(); - var otherShareLocation = otherPerms.IsSharingLocation(); using (ImRaii.PushIndent(indentSize, false)) { @@ -126,24 +124,6 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase ImGui.AlignTextToFramePadding(); ImGui.Text(Pair.UserData.AliasOrUID + " has " + (!otherDisableVFX ? "not " : string.Empty) + "disabled VFX sync with you"); } - - if (ImGui.Checkbox("Enable location Sharing", ref shareLocation)) - { - _ownPermissions.SetShareLocation(shareLocation); - } - _uiSharedService.DrawHelpText("Enable location sharing will only effect your side." + UiSharedService.TooltipSeparator - + "Note: this is NOT bidirectional, you can choose to share even others dont share with you."); - using (ImRaii.PushIndent(indentSize, false)) - { - _uiSharedService.BooleanToColoredIcon(shareLocation, false); - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGui.Text((!shareLocation ? "Not" : string.Empty) + "sharing location with " + Pair.UserData.AliasOrUID + " ."); - -#if DEBUG - _uiSharedService.BooleanToColoredIcon(otherShareLocation, true); -#endif - } ImGuiHelpers.ScaledDummy(0.5f); ImGui.Separator(); diff --git a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs index 6cd704f..fdc4719 100644 --- a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs +++ b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs @@ -200,17 +200,5 @@ public partial class ApiController await UserPushData(new(visibleCharacters, character, censusDto)).ConfigureAwait(false); } - - public async Task UpdateLocation(LocationDto locationDto, bool offline = false) - { - if (!IsConnected) return; - await _lightlessHub!.SendAsync(nameof(UpdateLocation), locationDto, offline).ConfigureAwait(false); - } - - public async Task> RequestAllLocationInfo() - { - if (!IsConnected) return []; - return await _lightlessHub!.InvokeAsync>(nameof(RequestAllLocationInfo)).ConfigureAwait(false); - } } #pragma warning restore MA0040 \ 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 01abc88..490800f 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs @@ -259,13 +259,6 @@ public partial class ApiController ExecuteSafely(() => Mediator.Publish(new GPoseLobbyReceiveWorldData(userData, worldData))); return Task.CompletedTask; } - - public Task Client_SendLocationToClient(LocationDto locationDto) - { - Logger.LogDebug($"{nameof(Client_SendLocationToClient)}: {locationDto.user}"); - ExecuteSafely(() => Mediator.Publish(new LocationMessage(locationDto.user.UID, locationDto.location))); - return Task.CompletedTask; - } public void OnDownloadReady(Action act) { @@ -447,12 +440,6 @@ public partial class ApiController if (_initialized) return; _lightlessHub!.On(nameof(Client_GposeLobbyPushWorldData), act); } - - public void OnReciveLocation(Action act) - { - if (_initialized) return; - _lightlessHub!.On(nameof(Client_SendLocationToClient), act); - } private void ExecuteSafely(Action act) { diff --git a/LightlessSync/WebAPI/SignalR/ApiController.cs b/LightlessSync/WebAPI/SignalR/ApiController.cs index a814758..9639f6f 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.cs @@ -606,8 +606,6 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL OnGposeLobbyPushCharacterData((dto) => _ = Client_GposeLobbyPushCharacterData(dto)); OnGposeLobbyPushPoseData((dto, data) => _ = Client_GposeLobbyPushPoseData(dto, data)); OnGposeLobbyPushWorldData((dto, data) => _ = Client_GposeLobbyPushWorldData(dto, data)); - OnReciveLocation(dto => _ = Client_SendLocationToClient(dto)); - _healthCheckTokenSource?.Cancel(); _healthCheckTokenSource?.Dispose(); From a933330418c96b242c2b2d4693522c3bfd322ed0 Mon Sep 17 00:00:00 2001 From: Tsubasahane Date: Sun, 28 Dec 2025 23:07:45 +0800 Subject: [PATCH 13/28] Share location --- LightlessAPI | 2 +- LightlessSync/LightlessSync.csproj | 1 + LightlessSync/Plugin.cs | 1 + LightlessSync/Services/DalamudUtilService.cs | 16 ++- .../Services/LocationShareService.cs | 131 ++++++++++++++++++ LightlessSync/Services/Mediator/Messages.cs | 1 + LightlessSync/UI/Components/DrawUserPair.cs | 102 ++++++++++++++ LightlessSync/UI/DrawEntityFactory.cs | 4 + .../SignalR/ApIController.Functions.Users.cs | 16 +++ .../ApiController.Functions.Callbacks.cs | 13 ++ LightlessSync/WebAPI/SignalR/ApiController.cs | 2 + 11 files changed, 282 insertions(+), 7 deletions(-) create mode 100644 LightlessSync/Services/LocationShareService.cs diff --git a/LightlessAPI b/LightlessAPI index 5656600..852e2a0 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 56566003e0e93bba05dcef49fd3ce23c6a204d81 +Subproject commit 852e2a005f5bfdf3844e057c6ba71de6f5f84ed8 diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index 707d2a3..96efb14 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -37,6 +37,7 @@ + diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 2d46b43..d070831 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -140,6 +140,7 @@ public sealed class Plugin : IDalamudPlugin services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(sp => new TextureMetadataHelper(sp.GetRequiredService>(), gameData)); diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index 0b93997..96c78f3 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -1,11 +1,13 @@ using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.Text; using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Control; +using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using LightlessSync.API.Dto.CharaData; @@ -26,6 +28,7 @@ using System.Text; using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind; using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; +using Map = Lumina.Excel.Sheets.Map; using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags; namespace LightlessSync.Services; @@ -86,7 +89,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber WorldData = new(() => { return gameData.GetExcelSheet(clientLanguage)! - .Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0]))) + .Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0]) + || w is { RowId: > 1000, Region: 101 })) .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString()); }); JobData = new(() => @@ -659,7 +663,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber var location = new LocationInfo(); location.ServerId = _playerState.CurrentWorld.RowId; - //location.InstanceId = UIState.Instance()->PublicInstance.InstanceId; //TODO:Need API update first + location.InstanceId = UIState.Instance()->PublicInstance.InstanceId; location.TerritoryId = _clientState.TerritoryType; location.MapId = _clientState.MapId; if (houseMan != null) @@ -713,10 +717,10 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber str += $" - {MapData.Value[(ushort)location.MapId].MapName}"; } - // if (location.InstanceId is not 0) - // { - // str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString(); - // } + if (location.InstanceId is not 0) + { + str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString(); + } if (location.WardId is not 0) { diff --git a/LightlessSync/Services/LocationShareService.cs b/LightlessSync/Services/LocationShareService.cs new file mode 100644 index 0000000..71989e5 --- /dev/null +++ b/LightlessSync/Services/LocationShareService.cs @@ -0,0 +1,131 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Dto.CharaData; +using LightlessSync.API.Dto.User; +using LightlessSync.Services.Mediator; +using LightlessSync.WebAPI; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; + +namespace LightlessSync.Services +{ + public class LocationShareService : DisposableMediatorSubscriberBase + { + private readonly DalamudUtilService _dalamudUtilService; + private readonly ApiController _apiController; + private IMemoryCache _locations = new MemoryCache(new MemoryCacheOptions()); + private IMemoryCache _sharingStatus = new MemoryCache(new MemoryCacheOptions()); + private CancellationTokenSource _resetToken = new CancellationTokenSource(); + + + + public LocationShareService(ILogger logger, LightlessMediator mediator, DalamudUtilService dalamudUtilService, ApiController apiController) : base(logger, mediator) + { + _dalamudUtilService = dalamudUtilService; + _apiController = apiController; + + + Mediator.Subscribe(this, (msg) => + { + _resetToken.Cancel(); + _resetToken.Dispose(); + _resetToken = new CancellationTokenSource(); + }); + Mediator.Subscribe(this, (msg) => + { + _ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, apiController.DisplayName), _dalamudUtilService.GetMapData())); + _ = RequestAllLocation(); + } ); + Mediator.Subscribe(this, UpdateLocationList); + Mediator.Subscribe(this, + msg => _ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, _apiController.DisplayName), _dalamudUtilService.GetMapData()))); + } + + private void UpdateLocationList(LocationSharingMessage msg) + { + if (_locations.TryGetValue(msg.User.UID, out _) && msg.LocationInfo.ServerId is 0) + { + _locations.Remove(msg.User.UID); + return; + } + + if ( msg.LocationInfo.ServerId is not 0 && msg.ExpireAt > DateTime.UtcNow) + { + AddLocationInfo(msg.User.UID, msg.LocationInfo, msg.ExpireAt); + } + } + + private void AddLocationInfo(string uid, LocationInfo location, DateTimeOffset expireAt) + { + var options = new MemoryCacheEntryOptions() + .SetAbsoluteExpiration(expireAt) + .AddExpirationToken(new CancellationChangeToken(_resetToken.Token)); + _locations.Set(uid, location, options); + } + + private async Task RequestAllLocation() + { + try + { + var (data, status) = await _apiController.RequestAllLocationInfo().ConfigureAwait(false); + foreach (var dto in data) + { + AddLocationInfo(dto.LocationDto.User.UID, dto.LocationDto.Location, dto.ExpireAt); + } + + foreach (var dto in status) + { + AddStatus(dto.User.UID, dto.ExpireAt); + } + } + catch (Exception e) + { + Logger.LogError(e,"RequestAllLocation error : "); + throw; + } + } + + private void AddStatus(string uid, DateTimeOffset expireAt) + { + var options = new MemoryCacheEntryOptions() + .SetAbsoluteExpiration(expireAt) + .AddExpirationToken(new CancellationChangeToken(_resetToken.Token)); + _sharingStatus.Set(uid, expireAt, options); + } + + public string GetUserLocation(string uid) + { + try + { + if (_locations.TryGetValue(uid, out var location)) + { + return _dalamudUtilService.LocationToString(location); + } + return String.Empty; + } + catch (Exception e) + { + Logger.LogError(e,"GetUserLocation error : "); + throw; + } + } + + public DateTimeOffset GetSharingStatus(string uid) + { + try + { + if (_sharingStatus.TryGetValue(uid, out var expireAt)) + { + return expireAt; + } + return DateTimeOffset.MinValue; + } + catch (Exception e) + { + Logger.LogError(e,"GetSharingStatus error : "); + throw; + } + } + + } +} \ No newline at end of file diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index 758b9f5..00f8de7 100644 --- a/LightlessSync/Services/Mediator/Messages.cs +++ b/LightlessSync/Services/Mediator/Messages.cs @@ -135,5 +135,6 @@ public record ChatChannelsUpdated : MessageBase; public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Message) : MessageBase; public record GroupCollectionChangedMessage : MessageBase; public record OpenUserProfileMessage(UserData User) : MessageBase; +public record LocationSharingMessage(UserData User, LocationInfo LocationInfo, DateTimeOffset ExpireAt) : 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/UI/Components/DrawUserPair.cs b/LightlessSync/UI/Components/DrawUserPair.cs index a5fa953..b8e58c4 100644 --- a/LightlessSync/UI/Components/DrawUserPair.cs +++ b/LightlessSync/UI/Components/DrawUserPair.cs @@ -37,6 +37,7 @@ public class DrawUserPair private readonly UiSharedService _uiSharedService; private readonly PlayerPerformanceConfigService _performanceConfigService; private readonly LightlessConfigService _configService; + private readonly LocationShareService _locationShareService; private readonly CharaDataManager _charaDataManager; private readonly PairLedger _pairLedger; private float _menuWidth = -1; @@ -57,6 +58,7 @@ public class DrawUserPair UiSharedService uiSharedService, PlayerPerformanceConfigService performanceConfigService, LightlessConfigService configService, + LocationShareService locationShareService, CharaDataManager charaDataManager, PairLedger pairLedger) { @@ -74,6 +76,7 @@ public class DrawUserPair _uiSharedService = uiSharedService; _performanceConfigService = performanceConfigService; _configService = configService; + _locationShareService = locationShareService; _charaDataManager = charaDataManager; _pairLedger = pairLedger; } @@ -216,6 +219,41 @@ public class DrawUserPair _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions)); } UiSharedService.AttachToolTip("Changes VFX sync permissions with this user." + (individual ? individualText : string.Empty)); + + if (ImGui.BeginMenu(FontAwesomeIcon.Globe.ToIconString() + " Toggle Location sharing")) + { + if (ImGui.MenuItem("Share for 30 Mins")) + { + _ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddMinutes(30)); + } + + if (ImGui.MenuItem("Share for 1 Hour")) + { + _ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddHours(1)); + } + + if (ImGui.MenuItem("Share for 3 Hours")) + { + _ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddHours(3)); + } + + if (ImGui.MenuItem("Share until manually stop")) + { + _ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.MaxValue); + } + + ImGui.Separator(); + if (ImGui.MenuItem("Stop Sharing")) + { + _ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.MinValue); + } + ImGui.EndMenu(); + } + } + + private Task ToggleLocationSharing(List users, DateTimeOffset expireAt) + { + return _apiController.ToggleLocationSharing(new LocationSharingToggleDto(users, expireAt)); } private void DrawIndividualMenu() @@ -574,6 +612,70 @@ public class DrawUserPair var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false); var individualIsSticky = _pair.UserPair!.OwnPermissions.IsSticky(); var individualIcon = individualIsSticky ? FontAwesomeIcon.ArrowCircleUp : FontAwesomeIcon.InfoCircle; + + var shareLocationIcon = FontAwesomeIcon.Globe; + var location = _locationShareService.GetUserLocation(_pair.UserPair!.User.UID); + var shareLocation = !string.IsNullOrEmpty(location); + var expireAt = _locationShareService.GetSharingStatus(_pair.UserPair!.User.UID); + var shareLocationOther = expireAt > DateTimeOffset.UtcNow; + var shareColor = shareLocation switch + { + true when shareLocationOther => UIColors.Get("LightlessGreen"), + false when shareLocationOther => UIColors.Get("LightlessBlue"), + _ => UIColors.Get("LightlessYellow"), + }; + + if (shareLocation || shareLocationOther) + { + currentRightSide -= (_uiSharedService.GetIconSize(shareLocationIcon).X + spacingX); + ImGui.SameLine(currentRightSide); + using (ImRaii.PushColor(ImGuiCol.Text, shareColor, shareLocation || shareLocationOther)) + _uiSharedService.IconText(shareLocationIcon); + + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + if (shareLocationOther) + { + if (_pair.IsOnline) + { + if (!string.IsNullOrEmpty(location)) + { + _uiSharedService.IconText(FontAwesomeIcon.LocationArrow); + ImGui.SameLine(); + ImGui.TextUnformatted(location); + } + else + { + ImGui.TextUnformatted("Location info not updated, reconnect or wait for update."); + } + } + else + { + ImGui.TextUnformatted("User not onlineㄟ( ▔, ▔ )ㄏ"); + } + } + else + { + ImGui.TextUnformatted("NOT Sharing location with you.(⊙x⊙;)"); + } + ImGui.Separator(); + + if (shareLocation) + { + ImGui.TextUnformatted("Sharing your location.ヾ(•ω•`)o"); + if (expireAt != DateTimeOffset.MaxValue) + { + ImGui.TextUnformatted("Expired at " + expireAt.ToLocalTime().ToString("g")); + } + } + else + { + ImGui.TextUnformatted("NOT sharing your location.(´。_。`)"); + } + ImGui.EndTooltip(); + } + } if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky) { diff --git a/LightlessSync/UI/DrawEntityFactory.cs b/LightlessSync/UI/DrawEntityFactory.cs index 3c71f5c..e7bcc87 100644 --- a/LightlessSync/UI/DrawEntityFactory.cs +++ b/LightlessSync/UI/DrawEntityFactory.cs @@ -29,6 +29,7 @@ public class DrawEntityFactory private readonly LightlessConfigService _configService; private readonly UiSharedService _uiSharedService; private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; + private readonly LocationShareService _locationShareService; private readonly CharaDataManager _charaDataManager; private readonly SelectTagForPairUi _selectTagForPairUi; private readonly RenamePairTagUi _renamePairTagUi; @@ -53,6 +54,7 @@ public class DrawEntityFactory LightlessConfigService configService, UiSharedService uiSharedService, PlayerPerformanceConfigService playerPerformanceConfigService, + LocationShareService locationShareService, CharaDataManager charaDataManager, SelectTagForSyncshellUi selectTagForSyncshellUi, RenameSyncshellTagUi renameSyncshellTagUi, @@ -72,6 +74,7 @@ public class DrawEntityFactory _configService = configService; _uiSharedService = uiSharedService; _playerPerformanceConfigService = playerPerformanceConfigService; + _locationShareService = locationShareService; _charaDataManager = charaDataManager; _selectTagForSyncshellUi = selectTagForSyncshellUi; _renameSyncshellTagUi = renameSyncshellTagUi; @@ -162,6 +165,7 @@ public class DrawEntityFactory _uiSharedService, _playerPerformanceConfigService, _configService, + _locationShareService, _charaDataManager, _pairLedger); } diff --git a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs index fdc4719..fca42f3 100644 --- a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs +++ b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs @@ -200,5 +200,21 @@ public partial class ApiController await UserPushData(new(visibleCharacters, character, censusDto)).ConfigureAwait(false); } + + public async Task UpdateLocation(LocationDto locationDto, bool offline = false) + { + if (!IsConnected) return; + await _lightlessHub!.SendAsync(nameof(UpdateLocation), locationDto, offline).ConfigureAwait(false); + } + public async Task<(List, List)> RequestAllLocationInfo() + { + if (!IsConnected) return ([],[]); + return await _lightlessHub!.InvokeAsync<(List, List)>(nameof(RequestAllLocationInfo)).ConfigureAwait(false); + } + public async Task ToggleLocationSharing(LocationSharingToggleDto dto) + { + if (!IsConnected) return; + await _lightlessHub!.SendAsync(nameof(ToggleLocationSharing), dto).ConfigureAwait(false); + } } #pragma warning restore MA0040 \ 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 490800f..d0a05f7 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs @@ -259,6 +259,13 @@ public partial class ApiController ExecuteSafely(() => Mediator.Publish(new GPoseLobbyReceiveWorldData(userData, worldData))); return Task.CompletedTask; } + + public Task Client_SendLocationToClient(LocationDto locationDto, DateTimeOffset expireAt) + { + Logger.LogDebug($"{nameof(Client_SendLocationToClient)}: {locationDto.User} {expireAt}"); + ExecuteSafely(() => Mediator.Publish(new LocationSharingMessage(locationDto.User, locationDto.Location, expireAt))); + return Task.CompletedTask; + } public void OnDownloadReady(Action act) { @@ -441,6 +448,12 @@ public partial class ApiController _lightlessHub!.On(nameof(Client_GposeLobbyPushWorldData), act); } + public void OnReceiveLocation(Action act) + { + if (_initialized) return; + _lightlessHub!.On(nameof(Client_SendLocationToClient), act); + } + private void ExecuteSafely(Action act) { try diff --git a/LightlessSync/WebAPI/SignalR/ApiController.cs b/LightlessSync/WebAPI/SignalR/ApiController.cs index 9639f6f..45705bf 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.cs @@ -606,6 +606,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL OnGposeLobbyPushCharacterData((dto) => _ = Client_GposeLobbyPushCharacterData(dto)); OnGposeLobbyPushPoseData((dto, data) => _ = Client_GposeLobbyPushPoseData(dto, data)); OnGposeLobbyPushWorldData((dto, data) => _ = Client_GposeLobbyPushWorldData(dto, data)); + OnReceiveLocation((dto, time) => _ = Client_SendLocationToClient(dto, time)); _healthCheckTokenSource?.Cancel(); _healthCheckTokenSource?.Dispose(); @@ -774,5 +775,6 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL ServerState = state; } + } #pragma warning restore MA0040 From 9e600bfae06f048ca84da3b5b06957652debef7b Mon Sep 17 00:00:00 2001 From: cake Date: Sun, 28 Dec 2025 16:48:51 +0100 Subject: [PATCH 14/28] Fixed merge conflicts. --- LightlessSync/Plugin.cs | 13 +-- LightlessSync/UI/LightFinderUI.cs | 157 +++++++++++++++--------------- 2 files changed, 81 insertions(+), 89 deletions(-) diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 69a426d..1a4a7bd 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -480,20 +480,11 @@ public sealed class Plugin : IDalamudPlugin sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), - sp.GetRequiredService())); - - services.AddScoped(sp => new SyncshellFinderUI( - sp.GetRequiredService>(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), - sp.GetRequiredService())); + sp.GetRequiredService(), + sp.GetRequiredService())); services.AddScoped(); services.AddScoped(); diff --git a/LightlessSync/UI/LightFinderUI.cs b/LightlessSync/UI/LightFinderUI.cs index 439bc4c..e9aec9c 100644 --- a/LightlessSync/UI/LightFinderUI.cs +++ b/LightlessSync/UI/LightFinderUI.cs @@ -37,6 +37,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase private readonly LightFinderService _broadcastService; private readonly LightlessConfigService _configService; private readonly LightlessProfileManager _lightlessProfileManager; + private readonly LightFinderPlateHandler _lightFinderPlateHandler; private readonly PairUiService _pairUiService; private readonly UiSharedService _uiSharedService; @@ -100,7 +101,8 @@ public class LightFinderUI : WindowMediatorSubscriberBase DalamudUtilService dalamudUtilService, LightlessProfileManager lightlessProfileManager, ActorObjectService actorObjectService - ) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService) +, + LightFinderPlateHandler lightFinderPlateHandler) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService) { _broadcastService = broadcastService; _uiSharedService = uiShared; @@ -126,6 +128,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase Mediator.Subscribe(this, async _ => await RefreshNearbySyncshellsAsync().ConfigureAwait(false)); Mediator.Subscribe(this, async _ => await RefreshNearbySyncshellsAsync(_.gid).ConfigureAwait(false)); Mediator.Subscribe(this, async _ => await RefreshNearbySyncshellsAsync(_.gid).ConfigureAwait(false)); + _lightFinderPlateHandler = lightFinderPlateHandler; } #endregion @@ -1377,96 +1380,94 @@ public class LightFinderUI : WindowMediatorSubscriberBase ImGui.PopStyleVar(); } - #endregion - + private void DrawDebugTab() + { #if DEBUG - if (ImGui.BeginTabItem("Debug")) + if (ImGui.CollapsingHeader("LightFinder Plates", ImGuiTreeNodeFlags.DefaultOpen)) + { + var h = _lightFinderPlateHandler; + + var enabled = h.DebugEnabled; + if (ImGui.Checkbox("Enable LightFinder debug", ref enabled)) + h.DebugEnabled = enabled; + + if (h.DebugEnabled) { - if (ImGui.CollapsingHeader("LightFinder Plates", ImGuiTreeNodeFlags.DefaultOpen)) - { - var h = _lightFinderPlateHandler; + ImGui.Indent(); - var enabled = h.DebugEnabled; - if (ImGui.Checkbox("Enable LightFinder debug", ref enabled)) - h.DebugEnabled = enabled; + var disableOcc = h.DebugDisableOcclusion; + if (ImGui.Checkbox("Disable occlusion (force draw)", ref disableOcc)) + h.DebugDisableOcclusion = disableOcc; - if (h.DebugEnabled) - { - ImGui.Indent(); + var drawUiRects = h.DebugDrawUiRects; + if (ImGui.Checkbox("Draw UI rects", ref drawUiRects)) + h.DebugDrawUiRects = drawUiRects; - var disableOcc = h.DebugDisableOcclusion; - if (ImGui.Checkbox("Disable occlusion (force draw)", ref disableOcc)) - h.DebugDisableOcclusion = disableOcc; - - var drawUiRects = h.DebugDrawUiRects; - if (ImGui.Checkbox("Draw UI rects", ref drawUiRects)) - h.DebugDrawUiRects = drawUiRects; - - var drawLabelRects = h.DebugDrawLabelRects; - if (ImGui.Checkbox("Draw label rects", ref drawLabelRects)) - h.DebugDrawLabelRects = drawLabelRects; - - ImGui.Separator(); - ImGui.TextUnformatted($"Labels last frame: {h.DebugLabelCountLastFrame}"); - ImGui.TextUnformatted($"UI rects last frame: {h.DebugUiRectCountLastFrame}"); - ImGui.TextUnformatted($"Occluded last frame: {h.DebugOccludedCountLastFrame}"); - ImGui.TextUnformatted($"Last NamePlate frame: {h.DebugLastNameplateFrame}"); - - ImGui.Unindent(); - } - } + var drawLabelRects = h.DebugDrawLabelRects; + if (ImGui.Checkbox("Draw label rects", ref drawLabelRects)) + h.DebugDrawLabelRects = drawLabelRects; ImGui.Separator(); + ImGui.TextUnformatted($"Labels last frame: {h.DebugLabelCountLastFrame}"); + ImGui.TextUnformatted($"UI rects last frame: {h.DebugUiRectCountLastFrame}"); + ImGui.TextUnformatted($"Occluded last frame: {h.DebugOccludedCountLastFrame}"); + ImGui.TextUnformatted($"Last NamePlate frame: {h.DebugLastNameplateFrame}"); - ImGui.Text("Broadcast Cache"); - - if (ImGui.BeginTable("##BroadcastCacheTable", 4, - ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, - new Vector2(-1, 225f))) - { - ImGui.TableSetupColumn("CID", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("IsBroadcasting", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Expires In", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Syncshell GID", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableHeadersRow(); - - var now = DateTime.UtcNow; - - foreach (var (cid, entry) in _broadcastScannerService.BroadcastCache) - { - ImGui.TableNextRow(); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted(cid.Truncate(12)); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip(cid); - - ImGui.TableNextColumn(); - var colorBroadcast = entry.IsBroadcasting ? UIColors.Get("LightlessGreen") : UIColors.Get("DimRed"); - ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, ImGui.GetColorU32(colorBroadcast)); - ImGui.TextUnformatted(entry.IsBroadcasting.ToString()); - - ImGui.TableNextColumn(); - var remaining = entry.ExpiryTime - now; - var colorTtl = remaining <= TimeSpan.Zero ? UIColors.Get("DimRed") - : remaining < TimeSpan.FromSeconds(10) ? UIColors.Get("LightlessYellow") - : (Vector4?)null; - - if (colorTtl != null) - ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, ImGui.GetColorU32(colorTtl.Value)); - - ImGui.TextUnformatted(remaining > TimeSpan.Zero ? remaining.ToString("hh\\:mm\\:ss") : "Expired"); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted(entry.GID ?? "-"); + ImGui.Unindent(); + } } - ImGui.EndTable(); - } + ImGui.Separator(); + + ImGui.Text("Broadcast Cache"); + + if (ImGui.BeginTable("##BroadcastCacheTable", 4, + ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, + new Vector2(-1, 225f))) + { + ImGui.TableSetupColumn("CID", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("IsBroadcasting", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Expires In", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Syncshell GID", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableHeadersRow(); + + var now = DateTime.UtcNow; + + foreach (var (cid, entry) in _broadcastScannerService.BroadcastCache) + { + ImGui.TableNextRow(); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(cid.Truncate(12)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(cid); + + ImGui.TableNextColumn(); + var colorBroadcast = entry.IsBroadcasting ? UIColors.Get("LightlessGreen") : UIColors.Get("DimRed"); + ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, ImGui.GetColorU32(colorBroadcast)); + ImGui.TextUnformatted(entry.IsBroadcasting.ToString()); + + ImGui.TableNextColumn(); + var remaining = entry.ExpiryTime - now; + var colorTtl = remaining <= TimeSpan.Zero ? UIColors.Get("DimRed") + : remaining < TimeSpan.FromSeconds(10) ? UIColors.Get("LightlessYellow") + : (Vector4?)null; + + if (colorTtl != null) + ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, ImGui.GetColorU32(colorTtl.Value)); + + ImGui.TextUnformatted(remaining > TimeSpan.Zero ? remaining.ToString("hh\\:mm\\:ss") : "Expired"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(entry.GID ?? "-"); + } + + ImGui.EndTable(); + } +#endif } #endregion -#endif #region Data Refresh From d6b31ed5b958c5e2239cfea027f3bb13a634688e Mon Sep 17 00:00:00 2001 From: cake Date: Sun, 28 Dec 2025 16:55:01 +0100 Subject: [PATCH 15/28] Fixed finder again. --- LightlessSync/UI/LightFinderUI.cs | 82 +++++++++++++++---------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/LightlessSync/UI/LightFinderUI.cs b/LightlessSync/UI/LightFinderUI.cs index e9aec9c..0c0cad4 100644 --- a/LightlessSync/UI/LightFinderUI.cs +++ b/LightlessSync/UI/LightFinderUI.cs @@ -178,9 +178,9 @@ public class LightFinderUI : WindowMediatorSubscriberBase new("Debug", LightfinderTabDebug.Debug) }; UiSharedService.Tab("LightfinderTabs", debugTabOptions, ref _selectedTabDebug); - + ImGuiHelpers.ScaledDummy(4f); - + switch (_selectedTabDebug) { case LightfinderTabDebug.NearbySyncshells: @@ -304,14 +304,14 @@ public class LightFinderUI : WindowMediatorSubscriberBase var nearbySyncshellCount = _nearbySyncshells.Count; var nearbySyncshellColor = nearbySyncshellCount > 0 ? UIColors.Get("LightlessPurple") : infoColor; DrawStatusCell(FontAwesomeIcon.Compass, nearbySyncshellColor, nearbySyncshellCount.ToString(), "Syncshells", infoColor, scale); - + // Nearby players cell (exclude self) string? myHashedCidForCount = null; try { myHashedCidForCount = _dalamudUtilService.GetCID().ToString().GetHash256(); } catch { } var nearbyPlayerCount = _broadcastScannerService.CountActiveBroadcasts(myHashedCidForCount); var nearbyPlayerColor = nearbyPlayerCount > 0 ? UIColors.Get("LightlessBlue") : infoColor; DrawStatusCell(FontAwesomeIcon.Users, nearbyPlayerColor, nearbyPlayerCount.ToString(), "Players", infoColor, scale); - + // Broadcasting syncshell cell var isBroadcastingSyncshell = _configService.Current.SyncshellFinderEnabled && isBroadcasting; var broadcastSyncshellColor = isBroadcastingSyncshell ? UIColors.Get("LightlessGreen") : infoColor; @@ -321,7 +321,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase // Enable/Disable button cell - right aligned ImGui.TableNextColumn(); - + float cellWidth = ImGui.GetContentRegionAvail().X; float offsetX = cellWidth - buttonWidth; if (offsetX > 0) @@ -381,11 +381,11 @@ public class LightFinderUI : WindowMediatorSubscriberBase private void UpdateItemAnimations(IEnumerable visibleItemIds) { var deltaTime = ImGui.GetIO().DeltaTime; - + _previousVisibleItems.Clear(); foreach (var id in _currentVisibleItems) _previousVisibleItems.Add(id); - + _currentVisibleItems.Clear(); foreach (var id in visibleItemIds) _currentVisibleItems.Add(id); @@ -395,7 +395,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase { if (!_itemAlpha.ContainsKey(id)) _itemAlpha[id] = 0f; - + _itemAlpha[id] = Math.Min(1f, _itemAlpha[id] + deltaTime * AnimationSpeed); } @@ -410,7 +410,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase toRemove.Add(id); } } - + foreach (var id in toRemove) _itemAlpha.Remove(id); } @@ -668,14 +668,14 @@ public class LightFinderUI : WindowMediatorSubscriberBase ImGui.PopID(); } - private void DrawJoinButton(GroupJoinDto shell, bool fullWidth) + private void DrawJoinButton(GroupJoinDto shell, bool fullWidth) { const string visibleLabel = "Join"; var label = $"{visibleLabel}##{shell.Group.GID}"; var isAlreadyMember = _currentSyncshells.Exists(g => string.Equals(g.GID, shell.Group.GID, StringComparison.Ordinal)); var isRecentlyJoined = _recentlyJoined.Contains(shell.Group.GID); - var isOwnBroadcast = _configService.Current.SyncshellFinderEnabled + var isOwnBroadcast = _configService.Current.SyncshellFinderEnabled && _broadcastService.IsBroadcasting && string.Equals(_configService.Current.SelectedFinderSyncshell, shell.Group.GID, StringComparison.Ordinal); @@ -758,7 +758,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase if (_joinDto == null || _joinInfo == null) return; var scale = ImGuiHelpers.GlobalScale; - + // if not already open if (!ImGui.IsPopupOpen("JoinSyncshellModal")) ImGui.OpenPopup("JoinSyncshellModal"); @@ -774,12 +774,12 @@ public class LightFinderUI : WindowMediatorSubscriberBase ImGui.SetNextWindowSize(new Vector2(modalWidth, modalHeight)); ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 6f * ImGuiHelpers.GlobalScale); ImGui.PushStyleColor(ImGuiCol.Border, Vector4.Zero); - + using ImRaii.Color modalBorder = ImRaii.PushColor(ImGuiCol.Border, UIColors.Get("LightlessPurple").WithAlpha(0.5f)); using ImRaii.Style rounding = ImRaii.PushStyle(ImGuiStyleVar.WindowRounding, 8f * scale); using ImRaii.Style borderSize = ImRaii.PushStyle(ImGuiStyleVar.WindowBorderSize, 2f * scale); using ImRaii.Style padding = ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(16f * scale, 16f * scale)); - + ImGuiWindowFlags flags = ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoScrollbar; if (ImGui.BeginPopupModal("JoinSyncshellModal", ref _joinModalOpen, flags)) { @@ -798,22 +798,22 @@ public class LightFinderUI : WindowMediatorSubscriberBase ImGui.TextColored(ImGuiColors.DalamudWhite, "Permissions"); ImGuiHelpers.ScaledDummy(6f); - DrawPermissionToggleRow("Sounds", FontAwesomeIcon.VolumeUp, - _joinInfo.GroupPermissions.IsPreferDisableSounds(), - _ownPermissions.DisableGroupSounds, - v => _ownPermissions.DisableGroupSounds = v, + DrawPermissionToggleRow("Sounds", FontAwesomeIcon.VolumeUp, + _joinInfo.GroupPermissions.IsPreferDisableSounds(), + _ownPermissions.DisableGroupSounds, + v => _ownPermissions.DisableGroupSounds = v, contentWidth); - DrawPermissionToggleRow("Animations", FontAwesomeIcon.Running, - _joinInfo.GroupPermissions.IsPreferDisableAnimations(), - _ownPermissions.DisableGroupAnimations, - v => _ownPermissions.DisableGroupAnimations = v, + DrawPermissionToggleRow("Animations", FontAwesomeIcon.Running, + _joinInfo.GroupPermissions.IsPreferDisableAnimations(), + _ownPermissions.DisableGroupAnimations, + v => _ownPermissions.DisableGroupAnimations = v, contentWidth); - DrawPermissionToggleRow("VFX", FontAwesomeIcon.Magic, - _joinInfo.GroupPermissions.IsPreferDisableVFX(), - _ownPermissions.DisableGroupVFX, - v => _ownPermissions.DisableGroupVFX = v, + DrawPermissionToggleRow("VFX", FontAwesomeIcon.Magic, + _joinInfo.GroupPermissions.IsPreferDisableVFX(), + _ownPermissions.DisableGroupVFX, + v => _ownPermissions.DisableGroupVFX = v, contentWidth); ImGuiHelpers.ScaledDummy(12f); @@ -879,13 +879,13 @@ public class LightFinderUI : WindowMediatorSubscriberBase var scale = ImGuiHelpers.GlobalScale; float rowHeight = 28f * scale; bool isDifferent = current != suggested; - + using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 4f * scale)) using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 4f * scale)) using (ImRaii.PushColor(ImGuiCol.ChildBg, new Vector4(0.18f, 0.15f, 0.22f, 0.6f))) { ImGui.BeginChild($"PermRow_{label}", new Vector2(contentWidth, rowHeight), false, ImGuiWindowFlags.NoScrollbar); - + float innerPadding = 8f * scale; ImGui.SetCursorPos(new Vector2(innerPadding, (rowHeight - ImGui.GetTextLineHeight()) * 0.5f)); @@ -893,7 +893,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase var enabledColor = UIColors.Get("LightlessGreen"); var disabledColor = UIColors.Get("DimRed"); var currentColor = !current ? enabledColor : disabledColor; - + _uiSharedService.IconText(icon, currentColor); ImGui.SameLine(0f, 6f * scale); ImGui.TextUnformatted(label); @@ -921,7 +921,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase float applyX = contentWidth - 26f * scale; ImGui.SetCursorPosX(applyX); ImGui.SetCursorPosY((rowHeight - ImGui.GetFrameHeight()) * 0.5f); - + using (ImRaii.PushColor(ImGuiCol.Button, UIColors.Get("LightlessGreen").WithAlpha(0.6f))) using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen"))) using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreenDefault"))) @@ -1037,12 +1037,12 @@ public class LightFinderUI : WindowMediatorSubscriberBase } private readonly record struct NearbyPlayerData( - string HashedCid, - string Name, - string? World, - nint Address, - Pair? Pair, - bool IsDirectlyPaired, + string HashedCid, + string Name, + string? World, + nint Address, + Pair? Pair, + bool IsDirectlyPaired, List SharedSyncshells); private void DrawNearbyPlayerRow(NearbyPlayerData data) @@ -1096,8 +1096,8 @@ public class LightFinderUI : WindowMediatorSubscriberBase ImGui.SetCursorPosX(startX); _uiSharedService.IconText(FontAwesomeIcon.Users, UIColors.Get("LightlessPurple")); ImGui.SameLine(0f, 4f * ImGuiHelpers.GlobalScale); - var shellText = data.SharedSyncshells.Count == 1 - ? data.SharedSyncshells[0] + var shellText = data.SharedSyncshells.Count == 1 + ? data.SharedSyncshells[0] : $"{data.SharedSyncshells.Count} shared shells"; ImGui.TextColored(UIColors.Get("LightlessPurple"), shellText); if (data.SharedSyncshells.Count > 1 && ImGui.IsItemHovered()) @@ -1380,6 +1380,8 @@ public class LightFinderUI : WindowMediatorSubscriberBase ImGui.PopStyleVar(); } + #endregion + private void DrawDebugTab() { #if DEBUG @@ -1467,8 +1469,6 @@ public class LightFinderUI : WindowMediatorSubscriberBase #endif } - #endregion - #region Data Refresh private async Task RefreshSyncshellsAsync() @@ -1736,4 +1736,4 @@ public class LightFinderUI : WindowMediatorSubscriberBase } #endregion -} +} \ No newline at end of file From 18fa0a47b1fff1493976fdf3007279a7af858eff Mon Sep 17 00:00:00 2001 From: Tsubasahane Date: Mon, 29 Dec 2025 15:42:55 +0800 Subject: [PATCH 16/28] Locationshare fix --- LightlessAPI | 2 +- .../Services/LocationShareService.cs | 9 ++++++- LightlessSync/UI/Components/DrawUserPair.cs | 24 +++++++++++-------- .../SignalR/ApIController.Functions.Users.cs | 6 ++--- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/LightlessAPI b/LightlessAPI index 852e2a0..c3caa7e 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 852e2a005f5bfdf3844e057c6ba71de6f5f84ed8 +Subproject commit c3caa7e25cf17fd52c4765bf051ec37c8fd92082 diff --git a/LightlessSync/Services/LocationShareService.cs b/LightlessSync/Services/LocationShareService.cs index 71989e5..77b5c90 100644 --- a/LightlessSync/Services/LocationShareService.cs +++ b/LightlessSync/Services/LocationShareService.cs @@ -126,6 +126,13 @@ namespace LightlessSync.Services throw; } } - + + public void UpdateSharingStatus(List users, DateTimeOffset expireAt) + { + foreach (var user in users) + { + AddStatus(user, expireAt); + } + } } } \ No newline at end of file diff --git a/LightlessSync/UI/Components/DrawUserPair.cs b/LightlessSync/UI/Components/DrawUserPair.cs index b8e58c4..3bd9fc0 100644 --- a/LightlessSync/UI/Components/DrawUserPair.cs +++ b/LightlessSync/UI/Components/DrawUserPair.cs @@ -251,9 +251,13 @@ public class DrawUserPair } } - private Task ToggleLocationSharing(List users, DateTimeOffset expireAt) + private async Task ToggleLocationSharing(List users, DateTimeOffset expireAt) { - return _apiController.ToggleLocationSharing(new LocationSharingToggleDto(users, expireAt)); + var updated = await _apiController.ToggleLocationSharing(new LocationSharingToggleDto(users, expireAt)).ConfigureAwait(false); + if (updated) + { + _locationShareService.UpdateSharingStatus(users, expireAt); + } } private void DrawIndividualMenu() @@ -617,25 +621,25 @@ public class DrawUserPair var location = _locationShareService.GetUserLocation(_pair.UserPair!.User.UID); var shareLocation = !string.IsNullOrEmpty(location); var expireAt = _locationShareService.GetSharingStatus(_pair.UserPair!.User.UID); - var shareLocationOther = expireAt > DateTimeOffset.UtcNow; + var shareLocationToOther = expireAt > DateTimeOffset.UtcNow; var shareColor = shareLocation switch { - true when shareLocationOther => UIColors.Get("LightlessGreen"), - false when shareLocationOther => UIColors.Get("LightlessBlue"), + true when shareLocationToOther => UIColors.Get("LightlessGreen"), + true when !shareLocationToOther => UIColors.Get("LightlessBlue"), _ => UIColors.Get("LightlessYellow"), }; - if (shareLocation || shareLocationOther) + if (shareLocation || shareLocationToOther) { currentRightSide -= (_uiSharedService.GetIconSize(shareLocationIcon).X + spacingX); ImGui.SameLine(currentRightSide); - using (ImRaii.PushColor(ImGuiCol.Text, shareColor, shareLocation || shareLocationOther)) + using (ImRaii.PushColor(ImGuiCol.Text, shareColor, shareLocation || shareLocationToOther)) _uiSharedService.IconText(shareLocationIcon); if (ImGui.IsItemHovered()) { ImGui.BeginTooltip(); - if (shareLocationOther) + if (shareLocation) { if (_pair.IsOnline) { @@ -661,12 +665,12 @@ public class DrawUserPair } ImGui.Separator(); - if (shareLocation) + if (shareLocationToOther) { ImGui.TextUnformatted("Sharing your location.ヾ(•ω•`)o"); if (expireAt != DateTimeOffset.MaxValue) { - ImGui.TextUnformatted("Expired at " + expireAt.ToLocalTime().ToString("g")); + ImGui.TextUnformatted("Expires at " + expireAt.ToLocalTime().ToString("g")); } } else diff --git a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs index fca42f3..37b91f9 100644 --- a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs +++ b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs @@ -211,10 +211,10 @@ public partial class ApiController if (!IsConnected) return ([],[]); return await _lightlessHub!.InvokeAsync<(List, List)>(nameof(RequestAllLocationInfo)).ConfigureAwait(false); } - public async Task ToggleLocationSharing(LocationSharingToggleDto dto) + public async Task ToggleLocationSharing(LocationSharingToggleDto dto) { - if (!IsConnected) return; - await _lightlessHub!.SendAsync(nameof(ToggleLocationSharing), dto).ConfigureAwait(false); + if (!IsConnected) return false; + return await _lightlessHub!.InvokeAsync(nameof(ToggleLocationSharing), dto).ConfigureAwait(false); } } #pragma warning restore MA0040 \ No newline at end of file From f37fdefddd8318055b773d09333171ac5bab31a5 Mon Sep 17 00:00:00 2001 From: Tsubasahane Date: Mon, 29 Dec 2025 16:43:12 +0800 Subject: [PATCH 17/28] show icon correctly --- LightlessSync/UI/Components/DrawUserPair.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/LightlessSync/UI/Components/DrawUserPair.cs b/LightlessSync/UI/Components/DrawUserPair.cs index 3bd9fc0..021ad1a 100644 --- a/LightlessSync/UI/Components/DrawUserPair.cs +++ b/LightlessSync/UI/Components/DrawUserPair.cs @@ -219,8 +219,11 @@ public class DrawUserPair _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions)); } UiSharedService.AttachToolTip("Changes VFX sync permissions with this user." + (individual ? individualText : string.Empty)); - - if (ImGui.BeginMenu(FontAwesomeIcon.Globe.ToIconString() + " Toggle Location sharing")) + + ImGui.SetCursorPosX(10f); + _uiSharedService.IconText(FontAwesomeIcon.Globe); + ImGui.SameLine(); + if (ImGui.BeginMenu("Toggle Location sharing")) { if (ImGui.MenuItem("Share for 30 Mins")) { From 91e60694ad4de7261dbbf645e08b09495acf4d53 Mon Sep 17 00:00:00 2001 From: Tsubasahane Date: Tue, 30 Dec 2025 11:20:12 +0800 Subject: [PATCH 18/28] triggers update when map changes --- LightlessSync/Services/DalamudUtilService.cs | 15 ++++++++++++++- LightlessSync/Services/LocationShareService.cs | 5 ++--- LightlessSync/Services/Mediator/Messages.cs | 1 + 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index 96c78f3..03ed35d 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -60,6 +60,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber private string _lastGlobalBlockReason = string.Empty; private ushort _lastZone = 0; private ushort _lastWorldId = 0; + private uint _lastMapId = 0; private bool _sentBetweenAreas = false; private Lazy _cid; @@ -689,7 +690,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber var outside = houseMan->OutdoorTerritory; var house = outside->HouseId; location.WardId = house.WardIndex + 1u; - location.HouseId = (uint)houseMan->GetCurrentPlot() + 1; + //location.HouseId = (uint)houseMan->GetCurrentPlot() + 1; location.DivisionId = houseMan->GetCurrentDivision(); } //_logger.LogWarning(LocationToString(location)); @@ -1139,6 +1140,18 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber Mediator.Publish(new ZoneSwitchEndMessage()); Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas))); } + + //Map + if (!_sentBetweenAreas) + { + var mapid = _clientState.MapId; + if (mapid != _lastMapId) + { + _lastMapId = mapid; + Mediator.Publish(new MapChangedMessage(mapid)); + } + } + var localPlayer = _objectTable.LocalPlayer; if (localPlayer != null) diff --git a/LightlessSync/Services/LocationShareService.cs b/LightlessSync/Services/LocationShareService.cs index 77b5c90..38b2834 100644 --- a/LightlessSync/Services/LocationShareService.cs +++ b/LightlessSync/Services/LocationShareService.cs @@ -17,8 +17,6 @@ namespace LightlessSync.Services private IMemoryCache _sharingStatus = new MemoryCache(new MemoryCacheOptions()); private CancellationTokenSource _resetToken = new CancellationTokenSource(); - - public LocationShareService(ILogger logger, LightlessMediator mediator, DalamudUtilService dalamudUtilService, ApiController apiController) : base(logger, mediator) { _dalamudUtilService = dalamudUtilService; @@ -37,7 +35,7 @@ namespace LightlessSync.Services _ = RequestAllLocation(); } ); Mediator.Subscribe(this, UpdateLocationList); - Mediator.Subscribe(this, + Mediator.Subscribe(this, msg => _ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, _apiController.DisplayName), _dalamudUtilService.GetMapData()))); } @@ -134,5 +132,6 @@ namespace LightlessSync.Services AddStatus(user, expireAt); } } + } } \ No newline at end of file diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index 00f8de7..efe3341 100644 --- a/LightlessSync/Services/Mediator/Messages.cs +++ b/LightlessSync/Services/Mediator/Messages.cs @@ -136,5 +136,6 @@ public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Messag public record GroupCollectionChangedMessage : MessageBase; public record OpenUserProfileMessage(UserData User) : MessageBase; public record LocationSharingMessage(UserData User, LocationInfo LocationInfo, DateTimeOffset ExpireAt) : MessageBase; +public record MapChangedMessage(uint MapId) : MessageBase; #pragma warning restore S2094 #pragma warning restore MA0048 // File name must match type name \ No newline at end of file From d1c955c74f28bd47bc3c277cd2a67ef83a0eb4d7 Mon Sep 17 00:00:00 2001 From: Tsubasahane Date: Tue, 30 Dec 2025 14:23:37 +0800 Subject: [PATCH 19/28] Reuse WorldData and make context menu work for non-Global uses --- LightlessSync/Services/ContextMenuService.cs | 48 ++++---------------- LightlessSync/Services/DalamudUtilService.cs | 2 +- 2 files changed, 9 insertions(+), 41 deletions(-) diff --git a/LightlessSync/Services/ContextMenuService.cs b/LightlessSync/Services/ContextMenuService.cs index 3fe893c..024e17b 100644 --- a/LightlessSync/Services/ContextMenuService.cs +++ b/LightlessSync/Services/ContextMenuService.cs @@ -10,7 +10,6 @@ using LightlessSync.UI; using LightlessSync.UI.Services; using LightlessSync.Utils; using LightlessSync.WebAPI; -using Lumina.Excel.Sheets; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -172,9 +171,8 @@ internal class ContextMenuService : IHostedService _logger.LogTrace("Cannot send pair request to {TargetName}@{World} while in PvP or GPose.", target.TargetName, target.TargetHomeWorld.RowId); return; } - - var world = GetWorld(target.TargetHomeWorld.RowId); - if (!IsWorldValid(world)) + + if (!IsWorldValid(target.TargetHomeWorld.RowId)) { _logger.LogTrace("Target player {TargetName}@{World} is on an invalid world.", target.TargetName, target.TargetHomeWorld.RowId); return; @@ -226,9 +224,8 @@ internal class ContextMenuService : IHostedService { if (args.Target is not MenuTargetDefault target) return; - - var world = GetWorld(target.TargetHomeWorld.RowId); - if (!IsWorldValid(world)) + + if (!target.TargetHomeWorld.IsValid || !IsWorldValid(target.TargetHomeWorld.RowId)) return; try @@ -237,7 +234,7 @@ internal class ContextMenuService : IHostedService if (targetData == null || targetData.Address == nint.Zero) { - _logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, world.Name); + _logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, target.TargetHomeWorld.Value.Name); return; } @@ -252,7 +249,7 @@ internal class ContextMenuService : IHostedService } // Notify in chat when NotificationService is disabled - NotifyInChat($"Pair request sent to {target.TargetName}@{world.Name}.", NotificationType.Info); + NotifyInChat($"Pair request sent to {target.TargetName}@{target.TargetHomeWorld.Value.Name}.", NotificationType.Info); } catch (Exception ex) { @@ -312,37 +309,8 @@ internal class ContextMenuService : IHostedService p.HomeWorld.RowId == target.TargetHomeWorld.RowId); } - private World GetWorld(uint worldId) + private bool IsWorldValid(uint worldId) { - var sheet = _gameData.GetExcelSheet()!; - var luminaWorlds = sheet.Where(x => - { - var dc = x.DataCenter.ValueNullable; - var name = x.Name.ExtractText(); - var internalName = x.InternalName.ExtractText(); - - if (dc == null || dc.Value.Region == 0 || string.IsNullOrWhiteSpace(dc.Value.Name.ExtractText())) - return false; - - if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(internalName)) - return false; - - if (name.Contains('-', StringComparison.Ordinal) || name.Contains('_', StringComparison.Ordinal)) - return false; - - return x.DataCenter.Value.Region != 5 || x.RowId > 3001 && x.RowId != 1200 && IsChineseJapaneseKoreanString(name); - }); - - return luminaWorlds.FirstOrDefault(x => x.RowId == worldId); - } - - private static bool IsChineseJapaneseKoreanString(string text) => text.All(IsChineseJapaneseKoreanCharacter); - - private static bool IsChineseJapaneseKoreanCharacter(char c) => c >= 0x4E00 && c <= 0x9FFF; - - public static bool IsWorldValid(World world) - { - var name = world.Name.ToString(); - return !string.IsNullOrWhiteSpace(name) && char.IsUpper(name[0]); + return _dalamudUtil.WorldData.Value.ContainsKey((ushort)worldId); } } diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index 03ed35d..b278667 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -91,7 +91,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber { return gameData.GetExcelSheet(clientLanguage)! .Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0]) - || w is { RowId: > 1000, Region: 101 })) + || w is { RowId: > 1000, Region: 101 or 201 })) .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString()); }); JobData = new(() => From f8752fcb4d1a87a737db702e2e251a5e1aefbdb0 Mon Sep 17 00:00:00 2001 From: Tsubasahane Date: Tue, 30 Dec 2025 14:37:13 +0800 Subject: [PATCH 20/28] changed kanmoji to show correctly --- LightlessSync/UI/Components/DrawUserPair.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/LightlessSync/UI/Components/DrawUserPair.cs b/LightlessSync/UI/Components/DrawUserPair.cs index 021ad1a..ac5c01a 100644 --- a/LightlessSync/UI/Components/DrawUserPair.cs +++ b/LightlessSync/UI/Components/DrawUserPair.cs @@ -659,18 +659,18 @@ public class DrawUserPair } else { - ImGui.TextUnformatted("User not onlineㄟ( ▔, ▔ )ㄏ"); + ImGui.TextUnformatted("User not online. (´・ω・`)?"); } } else { - ImGui.TextUnformatted("NOT Sharing location with you.(⊙x⊙;)"); + ImGui.TextUnformatted("NOT Sharing location with you. o(TヘTo)"); } ImGui.Separator(); if (shareLocationToOther) { - ImGui.TextUnformatted("Sharing your location.ヾ(•ω•`)o"); + ImGui.TextUnformatted("Sharing your location. ヾ(•ω•`)o"); if (expireAt != DateTimeOffset.MaxValue) { ImGui.TextUnformatted("Expires at " + expireAt.ToLocalTime().ToString("g")); @@ -678,7 +678,7 @@ public class DrawUserPair } else { - ImGui.TextUnformatted("NOT sharing your location.(´。_。`)"); + ImGui.TextUnformatted("NOT sharing your location.  ̄へ ̄"); } ImGui.EndTooltip(); } From ca7375b9c3f82eaea2089dcf636b5acffd01bf77 Mon Sep 17 00:00:00 2001 From: Tsubasahane Date: Tue, 30 Dec 2025 14:42:02 +0800 Subject: [PATCH 21/28] dont check location when target is offline --- LightlessSync/UI/Components/DrawUserPair.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/LightlessSync/UI/Components/DrawUserPair.cs b/LightlessSync/UI/Components/DrawUserPair.cs index ac5c01a..c8725e2 100644 --- a/LightlessSync/UI/Components/DrawUserPair.cs +++ b/LightlessSync/UI/Components/DrawUserPair.cs @@ -642,9 +642,10 @@ public class DrawUserPair if (ImGui.IsItemHovered()) { ImGui.BeginTooltip(); - if (shareLocation) + + if (_pair.IsOnline) { - if (_pair.IsOnline) + if (shareLocation) { if (!string.IsNullOrEmpty(location)) { @@ -659,12 +660,12 @@ public class DrawUserPair } else { - ImGui.TextUnformatted("User not online. (´・ω・`)?"); + ImGui.TextUnformatted("NOT Sharing location with you. o(TヘTo)"); } } else { - ImGui.TextUnformatted("NOT Sharing location with you. o(TヘTo)"); + ImGui.TextUnformatted("User not online. (´・ω・`)?"); } ImGui.Separator(); From e25979e08991030fbfca121a9341b8c15d84d3f9 Mon Sep 17 00:00:00 2001 From: Tsubasahane Date: Tue, 30 Dec 2025 18:04:54 +0800 Subject: [PATCH 22/28] fix Icon direction --- LightlessSync/UI/DataAnalysisUi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessSync/UI/DataAnalysisUi.cs b/LightlessSync/UI/DataAnalysisUi.cs index 32245d2..a3061a7 100644 --- a/LightlessSync/UI/DataAnalysisUi.cs +++ b/LightlessSync/UI/DataAnalysisUi.cs @@ -2183,7 +2183,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase bool toggleClicked = false; if (showToggle) { - var icon = isCollapsed ? FontAwesomeIcon.ChevronRight : FontAwesomeIcon.ChevronLeft; + var icon = !isCollapsed ? FontAwesomeIcon.ChevronRight : FontAwesomeIcon.ChevronLeft; Vector2 iconSize; using (_uiSharedService.IconFont.Push()) { From f812b6d09e864c8d2244b846ce443b097a1c0bbf Mon Sep 17 00:00:00 2001 From: choco Date: Thu, 1 Jan 2026 22:32:34 +0100 Subject: [PATCH 23/28] own syncshell sometimes not showing in list bug --- .../Services/LightFinder/LightFinderScannerService.cs | 8 ++++++-- LightlessSync/Services/LightFinder/LightFinderService.cs | 7 +++++-- LightlessSync/Services/Mediator/Messages.cs | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/LightlessSync/Services/LightFinder/LightFinderScannerService.cs b/LightlessSync/Services/LightFinder/LightFinderScannerService.cs index 38417a2..16de3c4 100644 --- a/LightlessSync/Services/LightFinder/LightFinderScannerService.cs +++ b/LightlessSync/Services/LightFinder/LightFinderScannerService.cs @@ -23,6 +23,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase private readonly HashSet _syncshellCids = []; private volatile bool _pendingLocalBroadcast; private TimeSpan? _pendingLocalTtl; + private string? _pendingLocalGid; private static readonly TimeSpan _maxAllowedTtl = TimeSpan.FromMinutes(4); private static readonly TimeSpan _retryDelay = TimeSpan.FromMinutes(1); @@ -175,6 +176,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase _pendingLocalBroadcast = true; _pendingLocalTtl = msg.Ttl; + _pendingLocalGid = msg.Gid; TryPrimeLocalBroadcastCache(); } @@ -190,11 +192,12 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase var expiry = DateTime.UtcNow + ttl; _broadcastCache.AddOrUpdate(localCid, - new BroadcastEntry(true, expiry, null), - (_, old) => new BroadcastEntry(true, expiry, old.GID)); + new BroadcastEntry(true, expiry, _pendingLocalGid), + (_, old) => new BroadcastEntry(true, expiry, _pendingLocalGid ?? old.GID)); _pendingLocalBroadcast = false; _pendingLocalTtl = null; + _pendingLocalGid = null; var now = DateTime.UtcNow; var activeCids = _broadcastCache @@ -204,6 +207,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase _lightFinderPlateHandler.UpdateBroadcastingCids(activeCids); _lightFinderNativePlateHandler.UpdateBroadcastingCids(activeCids); + UpdateSyncshellBroadcasts(); } private void UpdateSyncshellBroadcasts() diff --git a/LightlessSync/Services/LightFinder/LightFinderService.cs b/LightlessSync/Services/LightFinder/LightFinderService.cs index f07064c..b73dbed 100644 --- a/LightlessSync/Services/LightFinder/LightFinderService.cs +++ b/LightlessSync/Services/LightFinder/LightFinderService.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface; +using Dalamud.Interface; using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.User; using LightlessSync.LightlessConfiguration; @@ -121,7 +121,10 @@ public class LightFinderService : IHostedService, IMediatorSubscriber _waitingForTtlFetch = false; if (!wasEnabled || previousRemaining != validTtl) - _mediator.Publish(new BroadcastStatusChangedMessage(true, validTtl)); + { + var gid = _config.Current.SyncshellFinderEnabled ? _config.Current.SelectedFinderSyncshell : null; + _mediator.Publish(new BroadcastStatusChangedMessage(true, validTtl, gid)); + } _logger.LogInformation("Lightfinder broadcast enabled ({Context}), TTL: {TTL}", context, validTtl); return true; diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index efe3341..ae74b78 100644 --- a/LightlessSync/Services/Mediator/Messages.cs +++ b/LightlessSync/Services/Mediator/Messages.cs @@ -123,7 +123,7 @@ public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) : public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase; public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase; public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase; -public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase; +public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl, string? Gid = null) : MessageBase; public record UserLeftSyncshell(string gid) : MessageBase; public record UserJoinedSyncshell(string gid) : MessageBase; public record SyncshellBroadcastsUpdatedMessage : MessageBase; From 906dda3885731135e1be7bea8e518e8b45c78fd1 Mon Sep 17 00:00:00 2001 From: choco Date: Thu, 1 Jan 2026 22:32:45 +0100 Subject: [PATCH 24/28] lightfinder nearby badge icon --- LightlessSync/UI/TopTabMenu.cs | 41 +++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index 110b6c3..b8f08e7 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -170,8 +170,22 @@ public class TopTabMenu { Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding); } + } - + var nearbyCount = GetNearbySyncshellCount(); + if (nearbyCount > 0) + { + var buttonMax = ImGui.GetItemRectMax(); + var badgeRadius = 8f * ImGuiHelpers.GlobalScale; + var badgeCenter = new Vector2(buttonMax.X - badgeRadius * 0.5f, buttonMax.Y - buttonSize.Y + badgeRadius * 0.5f); + var badgeText = nearbyCount > 99 ? "99+" : nearbyCount.ToString(); + var textSize = ImGui.CalcTextSize(badgeText); + + drawList.AddCircleFilled(badgeCenter, badgeRadius + 1f, ImGui.GetColorU32(new Vector4(0, 0, 0, 0.6f))); + drawList.AddCircleFilled(badgeCenter, badgeRadius, ImGui.GetColorU32(UIColors.Get("LightlessPurple"))); + + var textPos = new Vector2(badgeCenter.X - textSize.X * 0.5f, badgeCenter.Y - textSize.Y * 0.5f); + drawList.AddText(textPos, ImGui.GetColorU32(new Vector4(1, 1, 1, 1)), badgeText); } UiSharedService.AttachToolTip(GetLightfinderTooltip()); @@ -805,6 +819,31 @@ public class TopTabMenu return nearbyCount > 0 ? $"Lightfinder ({nearbyCount} nearby)" : "Open Lightfinder"; } + private int GetNearbySyncshellCount() + { + if (!_lightFinderService.IsBroadcasting) + return 0; + + string? myHashedCid = null; + try + { + var cid = _dalamudUtilService.GetCID(); + myHashedCid = cid.ToString().GetHash256(); + } + catch (Exception) + { + } + + return _lightFinderScannerService + .GetActiveSyncshellBroadcasts() + .Where(b => + !string.IsNullOrEmpty(b.GID) && + !string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal)) + .Select(b => b.GID!) + .Distinct(StringComparer.Ordinal) + .Count(); + } + private void DrawUserConfig(float availableWidth, float spacingX) { var buttonX = (availableWidth - spacingX) / 2f; From 5fba3c01e7d408eeea1b25d057cc8f782940674d Mon Sep 17 00:00:00 2001 From: choco Date: Fri, 2 Jan 2026 09:19:39 +0100 Subject: [PATCH 25/28] lightfinder nearby badge alignment --- LightlessSync/UI/TopTabMenu.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index b8f08e7..ae10dae 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -177,14 +177,14 @@ public class TopTabMenu { var buttonMax = ImGui.GetItemRectMax(); var badgeRadius = 8f * ImGuiHelpers.GlobalScale; - var badgeCenter = new Vector2(buttonMax.X - badgeRadius * 0.5f, buttonMax.Y - buttonSize.Y + badgeRadius * 0.5f); + var badgeCenter = new Vector2(buttonMax.X - badgeRadius * 1.3f, buttonMax.Y - buttonSize.Y + badgeRadius * 0.5f); var badgeText = nearbyCount > 99 ? "99+" : nearbyCount.ToString(); var textSize = ImGui.CalcTextSize(badgeText); drawList.AddCircleFilled(badgeCenter, badgeRadius + 1f, ImGui.GetColorU32(new Vector4(0, 0, 0, 0.6f))); drawList.AddCircleFilled(badgeCenter, badgeRadius, ImGui.GetColorU32(UIColors.Get("LightlessPurple"))); - var textPos = new Vector2(badgeCenter.X - textSize.X * 0.5f, badgeCenter.Y - textSize.Y * 0.5f); + var textPos = new Vector2(badgeCenter.X - textSize.X * 0.45f, badgeCenter.Y - textSize.Y * 0.55f); drawList.AddText(textPos, ImGui.GetColorU32(new Vector4(1, 1, 1, 1)), badgeText); } UiSharedService.AttachToolTip(GetLightfinderTooltip()); From a4d62af73d9b6cb21d07dc0cae4bd593181e6ea0 Mon Sep 17 00:00:00 2001 From: choco Date: Fri, 2 Jan 2026 09:23:23 +0100 Subject: [PATCH 26/28] lightfinder user text --- LightlessSync/UI/LightFinderUI.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessSync/UI/LightFinderUI.cs b/LightlessSync/UI/LightFinderUI.cs index 0c0cad4..cad3d0b 100644 --- a/LightlessSync/UI/LightFinderUI.cs +++ b/LightlessSync/UI/LightFinderUI.cs @@ -1108,7 +1108,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase ImGui.SetCursorPosX(startX); _uiSharedService.IconText(FontAwesomeIcon.Wifi, UIColors.Get("LightlessBlue")); ImGui.SameLine(0f, 4f * ImGuiHelpers.GlobalScale); - ImGui.TextColored(ImGuiColors.DalamudGrey, "Lightfinder sir ma'am or whatever"); + ImGui.TextColored(ImGuiColors.DalamudGrey, "Lightfinder user"); } ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale)); From f7bb73bcd1a03112118164d6a4af0afbb3747466 Mon Sep 17 00:00:00 2001 From: cake Date: Fri, 2 Jan 2026 18:34:07 +0100 Subject: [PATCH 27/28] Updated api --- LightlessAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessAPI b/LightlessAPI index c3caa7e..4ecd537 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit c3caa7e25cf17fd52c4765bf051ec37c8fd92082 +Subproject commit 4ecd5375e63082f44b841bcba38d5dd3f4a2a79b From 9ba45670c522131c05ec9a29bfc8e4a6b3b7303d Mon Sep 17 00:00:00 2001 From: choco Date: Sat, 3 Jan 2026 02:08:28 +0100 Subject: [PATCH 28/28] top menu cleanup, removed duplicate old code --- LightlessSync/UI/TopTabMenu.cs | 51 ++-------------------------------- 1 file changed, 3 insertions(+), 48 deletions(-) diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index ae10dae..4760713 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -187,7 +187,7 @@ public class TopTabMenu var textPos = new Vector2(badgeCenter.X - textSize.X * 0.45f, badgeCenter.Y - textSize.Y * 0.55f); drawList.AddText(textPos, ImGui.GetColorU32(new Vector4(1, 1, 1, 1)), badgeText); } - UiSharedService.AttachToolTip(GetLightfinderTooltip()); + UiSharedService.AttachToolTip(nearbyCount > 0 ? $"Lightfinder ({nearbyCount} nearby)" : "Open Lightfinder"); ImGui.SameLine(); using (ImRaii.PushFont(UiBuilder.IconFont)) @@ -781,58 +781,13 @@ public class TopTabMenu } } } - - private void DrawLightfinderMenu(float availableWidth, float spacingX) - { - var lightfinderLabel = GetLightfinderTooltip(); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCirclePlus, lightfinderLabel, availableWidth, center: true)) - { - _lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI))); - } - } - - private string GetLightfinderTooltip() - { - if (!_lightFinderService.IsBroadcasting) - return "Open Lightfinder"; - - string? myHashedCid = null; - try - { - var cid = _dalamudUtilService.GetCID(); - myHashedCid = cid.ToString().GetHash256(); - } - catch (Exception) - { - // Couldnt get own CID, log and return default table - } - - var nearbyCount = _lightFinderScannerService - .GetActiveSyncshellBroadcasts() - .Where(b => - !string.IsNullOrEmpty(b.GID) && - !string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal)) - .Select(b => b.GID!) - .Distinct(StringComparer.Ordinal) - .Count(); - - return nearbyCount > 0 ? $"Lightfinder ({nearbyCount} nearby)" : "Open Lightfinder"; - } - + private int GetNearbySyncshellCount() { if (!_lightFinderService.IsBroadcasting) return 0; - string? myHashedCid = null; - try - { - var cid = _dalamudUtilService.GetCID(); - myHashedCid = cid.ToString().GetHash256(); - } - catch (Exception) - { - } + var myHashedCid = _dalamudUtilService.GetCID().ToString().GetHash256(); return _lightFinderScannerService .GetActiveSyncshellBroadcasts()