From f01229a97f3f31b8ec219eac4260c580c3b2b8a4 Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Thu, 9 Oct 2025 07:33:49 +0900 Subject: [PATCH 1/5] rework lightfinder for new api --- LightlessAPI | 2 +- .../Configurations/LightlessConfig.cs | 7 + LightlessSync/Plugin.cs | 13 +- LightlessSync/Services/BroadcastService.cs | 275 +++++++++++----- LightlessSync/Services/ContextMenuService.cs | 2 +- LightlessSync/UI/BroadcastUI.cs | 2 +- LightlessSync/UI/DtrEntry.cs | 297 +++++++++++++----- LightlessSync/UI/SettingsUi.cs | 81 ++++- LightlessSync/UI/TopTabMenu.cs | 2 +- .../SignalR/ApIController.Functions.Users.cs | 12 +- PenumbraAPI | 2 +- 11 files changed, 531 insertions(+), 164 deletions(-) diff --git a/LightlessAPI b/LightlessAPI index 6c542c0..f3c6064 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 6c542c0ccca0327896ef895f9de02a76869ea311 +Subproject commit f3c60648921abab03c3a6cc6142543f06ba02c45 diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs index c20d631..c5e5b2f 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs @@ -22,6 +22,12 @@ public class LightlessConfig : ILightlessConfiguration public DtrEntry.Colors DtrColorsDefault { get; set; } = default; public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu); public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0xFFBA47u); + public bool ShowLightfinderInDtr { get; set; } = false; + public bool UseLightfinderColorsInDtr { get; set; } = true; + public DtrEntry.Colors DtrColorsLightfinderEnabled { get; set; } = new(Foreground: 0xB590FFu, Glow: 0x4F406Eu); + public DtrEntry.Colors DtrColorsLightfinderDisabled { get; set; } = new(Foreground: 0xD44444u, Glow: 0x642222u); + public DtrEntry.Colors DtrColorsLightfinderCooldown { get; set; } = new(Foreground: 0xFFE97Au, Glow: 0x766C3Au); + public DtrEntry.Colors DtrColorsLightfinderUnavailable { get; set; } = new(Foreground: 0x000000u, Glow: 0x000000u); public bool UseLightlessRedesign { get; set; } = true; public bool EnableRightClickMenus { get; set; } = true; public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both; @@ -75,6 +81,7 @@ public class LightlessConfig : ILightlessConfiguration public bool overrideFcTagColor { get; set; } = false; public bool useColoredUIDs { get; set; } = true; public bool BroadcastEnabled { get; set; } = false; + public bool LightfinderAutoEnableOnConnect { get; set; } = false; public short LightfinderLabelOffsetX { get; set; } = 0; public short LightfinderLabelOffsetY { get; set; } = 0; public bool LightfinderLabelUseIcon { get; set; } = false; diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index c1648ca..ff9b3d5 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -142,8 +142,15 @@ public sealed class Plugin : IDalamudPlugin clientState, objectTable, framework, gameGui, condition, gameData, targetManager, gameConfig, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); - collection.AddSingleton((s) => new DtrEntry(s.GetRequiredService>(), dtrBar, s.GetRequiredService(), - s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); + collection.AddSingleton((s) => new DtrEntry( + s.GetRequiredService>(), + dtrBar, + s.GetRequiredService(), + s.GetRequiredService(), + s.GetRequiredService(), + s.GetRequiredService(), + s.GetRequiredService(), + s.GetRequiredService())); collection.AddSingleton(s => new PairManager(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), contextMenu, s.GetRequiredService())); collection.AddSingleton(); @@ -279,4 +286,4 @@ public sealed class Plugin : IDalamudPlugin _host.StopAsync().GetAwaiter().GetResult(); _host.Dispose(); } -} \ No newline at end of file +} diff --git a/LightlessSync/Services/BroadcastService.cs b/LightlessSync/Services/BroadcastService.cs index bf9eb05..0cdd3c0 100644 --- a/LightlessSync/Services/BroadcastService.cs +++ b/LightlessSync/Services/BroadcastService.cs @@ -7,6 +7,7 @@ using LightlessSync.WebAPI; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using System.Threading; namespace LightlessSync.Services; public class BroadcastService : IHostedService, IMediatorSubscriber @@ -16,9 +17,11 @@ public class BroadcastService : IHostedService, IMediatorSubscriber private readonly LightlessMediator _mediator; private readonly LightlessConfigService _config; private readonly DalamudUtilService _dalamudUtil; + private CancellationTokenSource? _lightfinderCancelTokens; + private Action? _connectedHandler; public LightlessMediator Mediator => _mediator; - public bool IsLightFinderAvailable { get; private set; } = true; + public bool IsLightFinderAvailable { get; private set; } = false; public bool IsBroadcasting => _config.Current.BroadcastEnabled; private bool _syncedOnStartup = false; @@ -57,24 +60,125 @@ public class BroadcastService : IHostedService, IMediatorSubscriber await action().ConfigureAwait(false); } - public async Task StartAsync(CancellationToken cancellationToken) + private async Task GetLocalHashedCidAsync(string context) + { + try + { + var cid = await _dalamudUtil.GetCIDAsync().ConfigureAwait(false); + return cid.ToString().GetHash256(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to resolve CID for {Context}", context); + return null; + } + } + + private void ApplyBroadcastDisabled(bool forcePublish = false) + { + bool wasEnabled = _config.Current.BroadcastEnabled; + bool hadExpiry = _config.Current.BroadcastTtl != DateTime.MinValue; + bool hadRemaining = _remainingTtl.HasValue; + + _config.Current.BroadcastEnabled = false; + _config.Current.BroadcastTtl = DateTime.MinValue; + + if (wasEnabled || hadExpiry) + _config.Save(); + + _remainingTtl = null; + _waitingForTtlFetch = false; + _syncedOnStartup = false; + + if (forcePublish || wasEnabled || hadRemaining) + _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); + } + + private bool TryApplyBroadcastEnabled(TimeSpan? ttl, string context) + { + if (ttl is not { } validTtl || validTtl <= TimeSpan.Zero) + { + _logger.LogWarning("Lightfinder enable skipped ({Context}): invalid TTL ({TTL})", context, ttl); + return false; + } + + bool wasEnabled = _config.Current.BroadcastEnabled; + TimeSpan? previousRemaining = _remainingTtl; + DateTime previousExpiry = _config.Current.BroadcastTtl; + + var newExpiry = DateTime.UtcNow + validTtl; + + _config.Current.BroadcastEnabled = true; + _config.Current.BroadcastTtl = newExpiry; + + if (!wasEnabled || previousExpiry != newExpiry) + _config.Save(); + + _remainingTtl = validTtl; + _waitingForTtlFetch = false; + + if (!wasEnabled || previousRemaining != validTtl) + _mediator.Publish(new BroadcastStatusChangedMessage(true, validTtl)); + + _logger.LogInformation("Lightfinder broadcast enabled ({Context}), TTL: {TTL}", context, validTtl); + return true; + } + + private void HandleLightfinderUnavailable(string message, Exception? ex = null) + { + if (ex != null) + _logger.LogWarning(ex, message); + else + _logger.LogWarning(message); + + IsLightFinderAvailable = false; + ApplyBroadcastDisabled(forcePublish: true); + } + + private void OnDisconnected() + { + IsLightFinderAvailable = false; + ApplyBroadcastDisabled(forcePublish: true); + _logger.LogDebug("Cleared Lightfinder state due to disconnect."); + } + + public Task StartAsync(CancellationToken cancellationToken) { _mediator.Subscribe(this, OnEnableBroadcast); _mediator.Subscribe(this, OnBroadcastStatusChanged); _mediator.Subscribe(this, OnTick); + _mediator.Subscribe(this, _ => OnDisconnected()); - _apiController.OnConnected += () => _ = CheckLightfinderSupportAsync(cancellationToken); - //_ = CheckLightfinderSupportAsync(cancellationToken); + IsLightFinderAvailable = false; + + _lightfinderCancelTokens?.Cancel(); + _lightfinderCancelTokens?.Dispose(); + _lightfinderCancelTokens = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _connectedHandler = () => _ = CheckLightfinderSupportAsync(_lightfinderCancelTokens.Token); + _apiController.OnConnected += _connectedHandler; + + if (_apiController.IsConnected) + _ = CheckLightfinderSupportAsync(_lightfinderCancelTokens.Token); + + return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { + _lightfinderCancelTokens?.Cancel(); + _lightfinderCancelTokens?.Dispose(); + _lightfinderCancelTokens = null; + + if (_connectedHandler is not null) + { + _apiController.OnConnected -= _connectedHandler; + _connectedHandler = null; + } + _mediator.UnsubscribeAll(this); - _apiController.OnConnected -= () => _ = CheckLightfinderSupportAsync(cancellationToken); return Task.CompletedTask; } - // need to rework this, this is cooked private async Task CheckLightfinderSupportAsync(CancellationToken cancellationToken) { try @@ -85,25 +189,54 @@ public class BroadcastService : IHostedService, IMediatorSubscriber if (cancellationToken.IsCancellationRequested) return; - var dummy = "0".PadLeft(64, '0'); + var hashedCid = await GetLocalHashedCidAsync("Lightfinder state check").ConfigureAwait(false); + if (string.IsNullOrEmpty(hashedCid)) + return; - await _apiController.IsUserBroadcasting(dummy).ConfigureAwait(false); - await _apiController.SetBroadcastStatus(dummy, true, null).ConfigureAwait(false); - await _apiController.GetBroadcastTtl(dummy).ConfigureAwait(false); - await _apiController.AreUsersBroadcasting([dummy]).ConfigureAwait(false); + BroadcastStatusInfoDto? status = null; + try + { + status = await _apiController.IsUserBroadcasting(hashedCid).ConfigureAwait(false); + } + catch (HubException ex) when (ex.Message.Contains("Method does not exist", StringComparison.OrdinalIgnoreCase)) + { + HandleLightfinderUnavailable("Lightfinder unavailable on server (required method missing).", ex); + } + + if (!IsLightFinderAvailable) + _logger.LogInformation("Lightfinder is available."); IsLightFinderAvailable = true; - _logger.LogInformation("Lightfinder is available."); - } - catch (HubException ex) when (ex.Message.Contains("Method does not exist")) - { - _logger.LogWarning("Lightfinder unavailable: required method missing."); - IsLightFinderAvailable = false; - _config.Current.BroadcastEnabled = false; - _config.Current.BroadcastTtl = DateTime.MinValue; - _config.Save(); - _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); + bool isBroadcasting = status?.IsBroadcasting == true; + TimeSpan? ttl = status?.TTL; + + if (isBroadcasting) + { + if (ttl is not { } remaining || remaining <= TimeSpan.Zero) + ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false); + + if (TryApplyBroadcastEnabled(ttl, "server handshake")) + { + _syncedOnStartup = true; + } + else + { + isBroadcasting = false; + } + } + + if (!isBroadcasting) + { + ApplyBroadcastDisabled(forcePublish: true); + _logger.LogInformation("Lightfinder is available but no active broadcast was found."); + } + + if (_config.Current.LightfinderAutoEnableOnConnect && !isBroadcasting) + { + _logger.LogInformation("Auto-enabling Lightfinder broadcast after reconnect."); + _mediator.Publish(new EnableBroadcastMessage(hashedCid, true)); + } } catch (OperationCanceledException) { @@ -111,14 +244,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber } catch (Exception ex) { - _logger.LogWarning(ex, "Lightfinder check failed."); - IsLightFinderAvailable = false; - - _config.Current.BroadcastEnabled = false; - _config.Current.BroadcastTtl = DateTime.MinValue; - _config.Save(); - - _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); + HandleLightfinderUnavailable("Lightfinder check failed.", ex); } } @@ -139,46 +265,38 @@ public class BroadcastService : IHostedService, IMediatorSubscriber }; } - await _apiController.SetBroadcastStatus(msg.HashedCid, msg.Enabled, groupDto).ConfigureAwait(false); + await _apiController.SetBroadcastStatus(msg.Enabled, groupDto).ConfigureAwait(false); _logger.LogDebug("Broadcast {Status} for {Cid}", msg.Enabled ? "enabled" : "disabled", msg.HashedCid); if (!msg.Enabled) { - _config.Current.BroadcastEnabled = false; - _config.Current.BroadcastTtl = DateTime.MinValue; - _config.Save(); - - _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); + ApplyBroadcastDisabled(forcePublish: true); Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Disabled Lightfinder for Player: {msg.HashedCid}"))); return; } _waitingForTtlFetch = true; - TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false); - - if (ttl is { } remaining && remaining > TimeSpan.Zero) + try { - _config.Current.BroadcastTtl = DateTime.UtcNow + remaining; - _config.Current.BroadcastEnabled = true; - _config.Save(); + TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false); - _logger.LogDebug("Fetched TTL from server: {TTL}", remaining); - _mediator.Publish(new BroadcastStatusChangedMessage(true, remaining)); - Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}"))); + if (TryApplyBroadcastEnabled(ttl, "client request")) + { + _logger.LogDebug("Fetched TTL from server: {TTL}", ttl); + Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}"))); + } + else + { + ApplyBroadcastDisabled(forcePublish: true); + _logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling."); + } } - else + finally { - _logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling."); - _config.Current.BroadcastEnabled = false; - _config.Current.BroadcastTtl = DateTime.MinValue; - _config.Save(); - - _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); + _waitingForTtlFetch = false; } - - _waitingForTtlFetch = false; } catch (Exception ex) { @@ -219,17 +337,24 @@ public class BroadcastService : IHostedService, IMediatorSubscriber return result; } - public async Task GetBroadcastTtlAsync(string cid) + public async Task GetBroadcastTtlAsync(string? cidForLog = null) { TimeSpan? ttl = null; await RequireConnectionAsync(nameof(GetBroadcastTtlAsync), async () => { try { - ttl = await _apiController.GetBroadcastTtl(cid).ConfigureAwait(false); + ttl = await _apiController.GetBroadcastTtl().ConfigureAwait(false); } catch (Exception ex) { - _logger.LogWarning(ex, "Failed to fetch broadcast TTL for {cid}", cid); + if (cidForLog is { Length: > 0 }) + { + _logger.LogWarning(ex, "Failed to fetch broadcast TTL for {Cid}", cidForLog); + } + else + { + _logger.LogWarning(ex, "Failed to fetch broadcast TTL"); + } } }).ConfigureAwait(false); return ttl; @@ -281,7 +406,12 @@ public class BroadcastService : IHostedService, IMediatorSubscriber return; } - var hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); + var hashedCid = await GetLocalHashedCidAsync(nameof(ToggleBroadcast)).ConfigureAwait(false); + if (string.IsNullOrEmpty(hashedCid)) + { + _logger.LogWarning("ToggleBroadcast - unable to resolve CID."); + return; + } try { @@ -321,31 +451,31 @@ public class BroadcastService : IHostedService, IMediatorSubscriber await RequireConnectionAsync(nameof(OnTick), async () => { if (!_syncedOnStartup && _config.Current.BroadcastEnabled) { - _syncedOnStartup = true; try { - string hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); - TimeSpan? ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false); - if (ttl is { } - remaining && remaining > TimeSpan.Zero) + var hashedCid = await GetLocalHashedCidAsync("startup TTL refresh").ConfigureAwait(false); + if (string.IsNullOrEmpty(hashedCid)) { - _config.Current.BroadcastTtl = DateTime.UtcNow + remaining; - _config.Current.BroadcastEnabled = true; - _config.Save(); - _logger.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", remaining); + _logger.LogDebug("Skipping TTL refresh; hashed CID unavailable."); + return; + } + + TimeSpan? ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false); + if (TryApplyBroadcastEnabled(ttl, "startup TTL refresh")) + { + _syncedOnStartup = true; + _logger.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", ttl); } else { _logger.LogWarning("No valid TTL found on OnTick. Disabling broadcast state."); - _config.Current.BroadcastEnabled = false; - _config.Current.BroadcastTtl = DateTime.MinValue; - _config.Save(); - _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); + ApplyBroadcastDisabled(forcePublish: true); } } catch (Exception ex) { _logger.LogError(ex, "Failed to refresh TTL in OnTick"); + _syncedOnStartup = false; } } if (_config.Current.BroadcastEnabled) @@ -362,10 +492,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber if (_remainingTtl == null) { _logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally."); - _config.Current.BroadcastEnabled = false; - _config.Current.BroadcastTtl = DateTime.MinValue; - _config.Save(); - _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); + ApplyBroadcastDisabled(forcePublish: true); } } else diff --git a/LightlessSync/Services/ContextMenuService.cs b/LightlessSync/Services/ContextMenuService.cs index ad00514..97bfc17 100644 --- a/LightlessSync/Services/ContextMenuService.cs +++ b/LightlessSync/Services/ContextMenuService.cs @@ -147,7 +147,7 @@ internal class ContextMenuService : IHostedService var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address); _logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid); - await _apiController.TryPairWithContentId(receiverCid, senderCid).ConfigureAwait(false); + await _apiController.TryPairWithContentId(receiverCid).ConfigureAwait(false); if (!string.IsNullOrWhiteSpace(receiverCid)) { _pairRequestService.RemoveRequest(receiverCid); diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index 5749a9c..2e2334a 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -209,7 +209,7 @@ namespace LightlessSync.UI else { ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); - ImGui.Text("The Lightfinder’s light wanes, but not in vain."); // cringe.. + ImGui.Text("The Lightfinder’s light wanes, but not in vain."); // cringe.. ImGui.PopStyleColor(); } } diff --git a/LightlessSync/UI/DtrEntry.cs b/LightlessSync/UI/DtrEntry.cs index 0335136..1b3ab61 100644 --- a/LightlessSync/UI/DtrEntry.cs +++ b/LightlessSync/UI/DtrEntry.cs @@ -1,10 +1,11 @@ -using Dalamud.Game.Gui.Dtr; +using Dalamud.Game.Gui.Dtr; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Plugin.Services; using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration.Configurations; using LightlessSync.PlayerData.Pairs; +using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; using LightlessSync.WebAPI; @@ -12,6 +13,7 @@ using LightlessSync.WebAPI.SignalR.Utils; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Runtime.InteropServices; +using System.Text; namespace LightlessSync.UI; @@ -22,35 +24,52 @@ public sealed class DtrEntry : IDisposable, IHostedService private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly ConfigurationServiceBase _configService; private readonly IDtrBar _dtrBar; - private readonly Lazy _entry; + private readonly Lazy _statusEntry; + private readonly Lazy _lightfinderEntry; private readonly ILogger _logger; + private readonly BroadcastService _broadcastService; private readonly LightlessMediator _lightlessMediator; private readonly PairManager _pairManager; private Task? _runTask; - private string? _text; - private string? _tooltip; - private Colors _colors; + private string? _statusText; + private string? _statusTooltip; + private Colors _statusColors; + private string? _lightfinderText; + private string? _lightfinderTooltip; + private Colors _lightfinderColors; - public DtrEntry(ILogger logger, IDtrBar dtrBar, ConfigurationServiceBase configService, LightlessMediator lightlessMediator, PairManager pairManager, ApiController apiController, ServerConfigurationManager serverManager) + public DtrEntry( + ILogger logger, + IDtrBar dtrBar, + ConfigurationServiceBase configService, + LightlessMediator lightlessMediator, + PairManager pairManager, + ApiController apiController, + ServerConfigurationManager serverManager, + BroadcastService broadcastService) { _logger = logger; _dtrBar = dtrBar; - _entry = new(CreateEntry); + _statusEntry = new(CreateStatusEntry); + _lightfinderEntry = new(CreateLightfinderEntry); _configService = configService; _lightlessMediator = lightlessMediator; _pairManager = pairManager; _apiController = apiController; _serverManager = serverManager; + _broadcastService = broadcastService; } public void Dispose() { - if (_entry.IsValueCreated) + if (_statusEntry.IsValueCreated) { _logger.LogDebug("Disposing DtrEntry"); Clear(); - _entry.Value.Remove(); + _statusEntry.Value.Remove(); } + if (_lightfinderEntry.IsValueCreated) + _lightfinderEntry.Value.Remove(); } public Task StartAsync(CancellationToken cancellationToken) @@ -70,7 +89,7 @@ public sealed class DtrEntry : IDisposable, IHostedService } catch (OperationCanceledException) { - // ignore cancelled + } finally { @@ -80,33 +99,66 @@ public sealed class DtrEntry : IDisposable, IHostedService private void Clear() { - if (!_entry.IsValueCreated) return; - _logger.LogInformation("Clearing entry"); - _text = null; - _tooltip = null; - _colors = default; - - _entry.Value.Shown = false; + HideStatusEntry(); + HideLightfinderEntry(); } - private IDtrBarEntry CreateEntry() + private void HideStatusEntry() { - _logger.LogTrace("Creating new DtrBar entry"); + if (_statusEntry.IsValueCreated && _statusEntry.Value.Shown) + { + _logger.LogInformation("Hiding status entry"); + _statusEntry.Value.Shown = false; + } + + _statusText = null; + _statusTooltip = null; + _statusColors = default; + } + + private void HideLightfinderEntry() + { + if (_lightfinderEntry.IsValueCreated && _lightfinderEntry.Value.Shown) + { + _logger.LogInformation("Hiding Lightfinder entry"); + _lightfinderEntry.Value.Shown = false; + } + + _lightfinderText = null; + _lightfinderTooltip = null; + _lightfinderColors = default; + } + + private IDtrBarEntry CreateStatusEntry() + { + _logger.LogTrace("Creating status DtrBar entry"); var entry = _dtrBar.Get("Lightless Sync"); - entry.OnClick = interactionEvent => OnClickEvent(interactionEvent); + entry.OnClick = interactionEvent => OnStatusEntryClick(interactionEvent); return entry; } - - private void OnClickEvent(DtrInteractionEvent interactionEvent) + + private IDtrBarEntry CreateLightfinderEntry() { - if (interactionEvent.ClickType.Equals(MouseClickType.Left) && !interactionEvent.ModifierKeys.Equals(ClickModifierKeys.Shift)) + _logger.LogTrace("Creating Lightfinder DtrBar entry"); + var entry = _dtrBar.Get("Lightfinder"); + entry.OnClick = interactionEvent => OnLightfinderEntryClick(interactionEvent); + return entry; + } + + private void OnStatusEntryClick(DtrInteractionEvent interactionEvent) + { + if (interactionEvent.ClickType.Equals(MouseClickType.Left)) { - _lightlessMediator.Publish(new UiToggleMessage(typeof(CompactUi))); - } - else if (interactionEvent.ClickType.Equals(MouseClickType.Left) && interactionEvent.ModifierKeys.Equals(ClickModifierKeys.Shift)) - { - _lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi))); + if (interactionEvent.ModifierKeys.HasFlag(ClickModifierKeys.Shift)) + { + _lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi))); + } + else + { + _lightlessMediator.Publish(new UiToggleMessage(typeof(CompactUi))); + } + return; } if (interactionEvent.ClickType.Equals(MouseClickType.Right)) @@ -131,6 +183,17 @@ public sealed class DtrEntry : IDisposable, IHostedService } } + private void OnLightfinderEntryClick(DtrInteractionEvent interactionEvent) + { + if (!_configService.Current.ShowLightfinderInDtr) + return; + + if (interactionEvent.ClickType.Equals(MouseClickType.Left)) + { + _broadcastService.ToggleBroadcast(); + } + } + private async Task RunAsync() { while (!_cancellationTokenSource.IsCancellationRequested) @@ -143,73 +206,171 @@ public sealed class DtrEntry : IDisposable, IHostedService private void Update() { - if (!_configService.Current.EnableDtrEntry || !_configService.Current.HasValidSetup()) - { - if (_entry.IsValueCreated && _entry.Value.Shown) - { - _logger.LogInformation("Disabling entry"); + var config = _configService.Current; - Clear(); - } + if (!config.HasValidSetup()) + { + HideStatusEntry(); + HideLightfinderEntry(); return; } - if (!_entry.Value.Shown) - { - _logger.LogInformation("Showing entry"); - _entry.Value.Shown = true; - } + if (config.EnableDtrEntry) + UpdateStatusEntry(config); + else + HideStatusEntry(); + if (config.ShowLightfinderInDtr) + UpdateLightfinderEntry(config); + else + HideLightfinderEntry(); + } + + private void UpdateStatusEntry(LightlessConfig config) + { string text; string tooltip; Colors colors; + if (_apiController.IsConnected) { var pairCount = _pairManager.GetVisibleUserCount(); text = $"\uE044 {pairCount}"; if (pairCount > 0) { - IEnumerable visiblePairs; - if (_configService.Current.ShowUidInDtrTooltip) - { - visiblePairs = _pairManager.GetOnlineUserPairs() - .Where(x => x.IsVisible) - .Select(x => string.Format("{0} ({1})", _configService.Current.PreferNoteInDtrTooltip ? x.GetNote() ?? x.PlayerName : x.PlayerName, x.UserData.AliasOrUID)); - } - else - { - visiblePairs = _pairManager.GetOnlineUserPairs() - .Where(x => x.IsVisible) - .Select(x => string.Format("{0}", _configService.Current.PreferNoteInDtrTooltip ? x.GetNote() ?? x.PlayerName : x.PlayerName)); - } + var preferNote = config.PreferNoteInDtrTooltip; + var showUid = config.ShowUidInDtrTooltip; + + var visiblePairsQuery = _pairManager.GetOnlineUserPairs() + .Where(x => x.IsVisible); + + IEnumerable visiblePairs = showUid + ? visiblePairsQuery.Select(x => string.Format("{0} ({1})", preferNote ? x.GetNote() ?? x.PlayerName : x.PlayerName, x.UserData.AliasOrUID)) + : visiblePairsQuery.Select(x => string.Format("{0}", preferNote ? x.GetNote() ?? x.PlayerName : x.PlayerName)); tooltip = $"Lightless Sync: Connected{Environment.NewLine}----------{Environment.NewLine}{string.Join(Environment.NewLine, visiblePairs)}"; - colors = _configService.Current.DtrColorsPairsInRange; + colors = config.DtrColorsPairsInRange; } else { tooltip = "Lightless Sync: Connected"; - colors = _configService.Current.DtrColorsDefault; + colors = config.DtrColorsDefault; } } else { text = "\uE044 \uE04C"; tooltip = "Lightless Sync: Not Connected"; - colors = _configService.Current.DtrColorsNotConnected; + colors = config.DtrColorsNotConnected; } - if (!_configService.Current.UseColorsInDtr) + if (!config.UseColorsInDtr) colors = default; - if (!string.Equals(text, _text, StringComparison.Ordinal) || !string.Equals(tooltip, _tooltip, StringComparison.Ordinal) || colors != _colors) + var statusEntry = _statusEntry.Value; + if (!statusEntry.Shown) { - _text = text; - _tooltip = tooltip; - _colors = colors; - _entry.Value.Text = BuildColoredSeString(text, colors); - _entry.Value.Tooltip = tooltip; + _logger.LogInformation("Showing status entry"); + statusEntry.Shown = true; } + + bool statusNeedsUpdate = + !string.Equals(text, _statusText, StringComparison.Ordinal) || + !string.Equals(tooltip, _statusTooltip, StringComparison.Ordinal) || + colors != _statusColors; + + if (statusNeedsUpdate) + { + statusEntry.Text = BuildColoredSeString(text, colors); + statusEntry.Tooltip = tooltip; + _statusText = text; + _statusTooltip = tooltip; + _statusColors = colors; + } + } + + private void UpdateLightfinderEntry(LightlessConfig config) + { + var lightfinderEntry = _lightfinderEntry.Value; + if (!lightfinderEntry.Shown) + { + _logger.LogInformation("Showing Lightfinder entry"); + lightfinderEntry.Shown = true; + } + + var indicator = BuildLightfinderIndicator(); + var lightfinderText = indicator.Text ?? string.Empty; + var lightfinderColors = config.UseLightfinderColorsInDtr ? indicator.Colors : default; + var lightfinderTooltip = BuildLightfinderTooltip(indicator.Tooltip); + + bool lightfinderNeedsUpdate = + !string.Equals(lightfinderText, _lightfinderText, StringComparison.Ordinal) || + !string.Equals(lightfinderTooltip, _lightfinderTooltip, StringComparison.Ordinal) || + lightfinderColors != _lightfinderColors; + + if (lightfinderNeedsUpdate) + { + lightfinderEntry.Text = BuildColoredSeString(lightfinderText, lightfinderColors); + lightfinderEntry.Tooltip = lightfinderTooltip; + _lightfinderText = lightfinderText; + _lightfinderTooltip = lightfinderTooltip; + _lightfinderColors = lightfinderColors; + } + } + + private (string Text, Colors Colors, string Tooltip) BuildLightfinderIndicator() + { + var config = _configService.Current; + const string icon = "\uE048"; + if (!_broadcastService.IsLightFinderAvailable) + { + return ($"{icon} --", config.DtrColorsLightfinderUnavailable, "Lightfinder - Unavailable on this server."); + } + + if (_broadcastService.IsBroadcasting) + { + return ($"{icon} ON", config.DtrColorsLightfinderEnabled, "Lightfinder - Enabled"); + } + + var tooltip = new StringBuilder("Lightfinder - Disabled"); + var colors = config.DtrColorsLightfinderDisabled; + if (_broadcastService.RemainingCooldown is { } cooldown && cooldown > TimeSpan.Zero) + { + tooltip.AppendLine(); + tooltip.Append("Cooldown: ").Append(Math.Ceiling(cooldown.TotalSeconds)).Append("s"); + colors = config.DtrColorsLightfinderCooldown; + } + + return ($"{icon} OFF", colors, tooltip.ToString()); + } + + private static string BuildLightfinderTooltip(string baseTooltip) + { + var builder = new StringBuilder(); + if (!string.IsNullOrWhiteSpace(baseTooltip)) + builder.Append(baseTooltip.TrimEnd()); + else + builder.Append("Lightfinder status unavailable."); + + return builder.ToString().TrimEnd(); + } + + private static void AppendColoredSegment(SeStringBuilder builder, string? text, Colors colors) + { + if (string.IsNullOrEmpty(text)) + return; + + if (colors.Foreground != default) + builder.Add(BuildColorStartPayload(_colorTypeForeground, colors.Foreground)); + if (colors.Glow != default) + builder.Add(BuildColorStartPayload(_colorTypeGlow, colors.Glow)); + + builder.AddText(text); + + if (colors.Glow != default) + builder.Add(BuildColorEndPayload(_colorTypeGlow)); + if (colors.Foreground != default) + builder.Add(BuildColorEndPayload(_colorTypeForeground)); } #region Colored SeString @@ -219,15 +380,7 @@ public sealed class DtrEntry : IDisposable, IHostedService private static SeString BuildColoredSeString(string text, Colors colors) { var ssb = new SeStringBuilder(); - if (colors.Foreground != default) - ssb.Add(BuildColorStartPayload(_colorTypeForeground, colors.Foreground)); - if (colors.Glow != default) - ssb.Add(BuildColorStartPayload(_colorTypeGlow, colors.Glow)); - ssb.AddText(text); - if (colors.Glow != default) - ssb.Add(BuildColorEndPayload(_colorTypeGlow)); - if (colors.Foreground != default) - ssb.Add(BuildColorEndPayload(_colorTypeForeground)); + AppendColoredSegment(ssb, text, colors); return ssb.Build(); } diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 84e4e94..52de306 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -181,12 +181,13 @@ public class SettingsUi : WindowMediatorSubscriberBase var foregroundColor = ConvertColor(colors.Foreground); var glowColor = ConvertColor(colors.Glow); - var ret = ImGui.ColorEdit3("###foreground", ref foregroundColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel | ImGuiColorEditFlags.Uint8); + const ImGuiColorEditFlags colorFlags = ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel; + var ret = ImGui.ColorEdit3("###foreground", ref foregroundColor, colorFlags); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Foreground Color - Set to pure black (#000000) to use the default color"); ImGui.SameLine(0.0f, innerSpacing); - ret |= ImGui.ColorEdit3("###glow", ref glowColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel | ImGuiColorEditFlags.Uint8); + ret |= ImGui.ColorEdit3("###glow", ref glowColor, colorFlags); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Glow Color - Set to pure black (#000000) to use the default color"); @@ -199,10 +200,26 @@ public class SettingsUi : WindowMediatorSubscriberBase return ret; static Vector3 ConvertColor(uint color) - => unchecked(new((byte)color / 255.0f, (byte)(color >> 8) / 255.0f, (byte)(color >> 16) / 255.0f)); + { + var r = (color & 0xFF) / 255.0f; + var g = ((color >> 8) & 0xFF) / 255.0f; + var b = ((color >> 16) & 0xFF) / 255.0f; + return new(r, g, b); + } static uint ConvertBackColor(Vector3 color) - => byte.CreateSaturating(color.X * 255.0f) | ((uint)byte.CreateSaturating(color.Y * 255.0f) << 8) | ((uint)byte.CreateSaturating(color.Z * 255.0f) << 16); + { + static byte ToByte(float channel) + { + var scaled = MathF.Round(Math.Clamp(channel, 0f, 1f) * 255.0f); + return (byte)Math.Clamp((int)scaled, 0, 255); + } + + var r = ToByte(color.X); + var g = ToByte(color.Y); + var b = ToByte(color.Z); + return (uint)(r | (g << 8) | (b << 16)); + } } private void DrawBlockedTransfers() @@ -1066,10 +1083,66 @@ public class SettingsUi : WindowMediatorSubscriberBase if (_uiShared.MediumTreeNode("Lightfinder", UIColors.Get("LightlessPurple"))) { + bool autoEnable = _configService.Current.LightfinderAutoEnableOnConnect; var autoAlign = _configService.Current.LightfinderAutoAlign; var offsetX = (int)_configService.Current.LightfinderLabelOffsetX; var offsetY = (int)_configService.Current.LightfinderLabelOffsetY; var labelScale = _configService.Current.LightfinderLabelScale; + bool showLightfinderInDtr = _configService.Current.ShowLightfinderInDtr; + var dtrLightfinderEnabled = _configService.Current.DtrColorsLightfinderEnabled; + var dtrLightfinderDisabled = _configService.Current.DtrColorsLightfinderDisabled; + var dtrLightfinderCooldown = _configService.Current.DtrColorsLightfinderCooldown; + var dtrLightfinderUnavailable = _configService.Current.DtrColorsLightfinderUnavailable; + + ImGui.TextUnformatted("Connection"); + if (ImGui.Checkbox("Auto-enable Lightfinder on server connection", ref autoEnable)) + { + _configService.Current.LightfinderAutoEnableOnConnect = autoEnable; + _configService.Save(); + } + _uiShared.DrawHelpText("When enabled, Lightfinder will automatically turn on after reconnecting to the Lightless server."); + + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + + ImGui.TextUnformatted("Lightfinder Info Bar"); + if (ImGui.Checkbox("Show Lightfinder status in Server info bar", ref showLightfinderInDtr)) + { + _configService.Current.ShowLightfinderInDtr = showLightfinderInDtr; + _configService.Save(); + } + _uiShared.DrawHelpText("Adds a Lightfinder status to the Server info bar. Left click toggles Lightfinder when visible."); + + bool useLightfinderColors = _configService.Current.UseLightfinderColorsInDtr; + if (ImGui.Checkbox("Color-code the Lightfinder info bar according to status", ref useLightfinderColors)) + { + _configService.Current.UseLightfinderColorsInDtr = useLightfinderColors; + _configService.Save(); + } + + ImGui.BeginDisabled(!showLightfinderInDtr || !useLightfinderColors); + if (InputDtrColors("Enables", ref dtrLightfinderEnabled)) + { + _configService.Current.DtrColorsLightfinderEnabled = dtrLightfinderEnabled; + _configService.Save(); + } + if (InputDtrColors("Disabled", ref dtrLightfinderDisabled)) + { + _configService.Current.DtrColorsLightfinderDisabled = dtrLightfinderDisabled; + _configService.Save(); + } + if (InputDtrColors("Cooldown", ref dtrLightfinderCooldown)) + { + _configService.Current.DtrColorsLightfinderCooldown = dtrLightfinderCooldown; + _configService.Save(); + } + if (InputDtrColors("Unavailable", ref dtrLightfinderUnavailable)) + { + _configService.Current.DtrColorsLightfinderUnavailable = dtrLightfinderUnavailable; + _configService.Save(); + } + ImGui.EndDisabled(); + + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); ImGui.TextUnformatted("Alignment"); ImGui.BeginDisabled(autoAlign); diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index 8a9e6c9..47f8f86 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -401,7 +401,7 @@ public class TopTabMenu try { var myCidHash = (await _dalamudUtilService.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); - await _apiController.TryPairWithContentId(request.HashedCid, myCidHash).ConfigureAwait(false); + await _apiController.TryPairWithContentId(request.HashedCid).ConfigureAwait(false); _pairRequestService.RemoveRequest(request.HashedCid); var display = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName; diff --git a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs index d268dd8..bbac3a6 100644 --- a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs +++ b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs @@ -35,16 +35,16 @@ public partial class ApiController await _lightlessHub!.SendAsync(nameof(UserAddPair), user).ConfigureAwait(false); } - public async Task TryPairWithContentId(string otherCid, string myCid) + public async Task TryPairWithContentId(string otherCid) { if (!IsConnected) return; - await _lightlessHub!.SendAsync(nameof(TryPairWithContentId), otherCid, myCid).ConfigureAwait(false); + await _lightlessHub!.SendAsync(nameof(TryPairWithContentId), otherCid).ConfigureAwait(false); } - public async Task SetBroadcastStatus(string hashedCid, bool enabled, GroupBroadcastRequestDto? groupDto = null) + public async Task SetBroadcastStatus(bool enabled, GroupBroadcastRequestDto? groupDto = null) { CheckConnection(); - await _lightlessHub!.InvokeAsync(nameof(SetBroadcastStatus), hashedCid, enabled, groupDto).ConfigureAwait(false); + await _lightlessHub!.InvokeAsync(nameof(SetBroadcastStatus), enabled, groupDto).ConfigureAwait(false); } public async Task IsUserBroadcasting(string hashedCid) @@ -59,10 +59,10 @@ public partial class ApiController return await _lightlessHub!.InvokeAsync(nameof(AreUsersBroadcasting), hashedCids).ConfigureAwait(false); } - public async Task GetBroadcastTtl(string hashedCid) + public async Task GetBroadcastTtl() { CheckConnection(); - return await _lightlessHub!.InvokeAsync(nameof(GetBroadcastTtl), hashedCid).ConfigureAwait(false); + return await _lightlessHub!.InvokeAsync(nameof(GetBroadcastTtl)).ConfigureAwait(false); } public async Task UserDelete() diff --git a/PenumbraAPI b/PenumbraAPI index dd14131..648b6fc 160000 --- a/PenumbraAPI +++ b/PenumbraAPI @@ -1 +1 @@ -Subproject commit dd14131793e5ae47cc8e9232f46469216017b5aa +Subproject commit 648b6fc2ce600a95ab2b2ced27e1639af2b04502 From 9b04976aa65a132c7c7975d32b263f64f5e53e4c Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Sat, 11 Oct 2025 00:48:19 +0900 Subject: [PATCH 2/5] add info options for server bar, direct settings button in lightfinder window and fix color swaps --- LightlessAPI | 2 +- .../LightfinderDtrDisplayMode.cs | 7 + .../Configurations/LightlessConfig.cs | 1 + LightlessSync/Plugin.cs | 5 +- .../Services/BroadcastScanningService.cs | 10 ++ LightlessSync/Services/Mediator/Messages.cs | 1 + LightlessSync/UI/BroadcastUI.cs | 16 +++ LightlessSync/UI/DtrEntry.cs | 121 +++++++++++++++++- LightlessSync/UI/SettingsUi.cs | 93 ++++++++++++-- 9 files changed, 238 insertions(+), 18 deletions(-) create mode 100644 LightlessSync/LightlessConfiguration/Configurations/LightfinderDtrDisplayMode.cs diff --git a/LightlessAPI b/LightlessAPI index f3c6064..4a0b6c3 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit f3c60648921abab03c3a6cc6142543f06ba02c45 +Subproject commit 4a0b6c3e4dccb11b1df5af86d28b99f184455b01 diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightfinderDtrDisplayMode.cs b/LightlessSync/LightlessConfiguration/Configurations/LightfinderDtrDisplayMode.cs new file mode 100644 index 0000000..5c7633c --- /dev/null +++ b/LightlessSync/LightlessConfiguration/Configurations/LightfinderDtrDisplayMode.cs @@ -0,0 +1,7 @@ +namespace LightlessSync.LightlessConfiguration.Configurations; + +public enum LightfinderDtrDisplayMode +{ + NearbyBroadcasts = 0, + PendingPairRequests = 1, +} diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs index c5e5b2f..3036bdb 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs @@ -28,6 +28,7 @@ public class LightlessConfig : ILightlessConfiguration public DtrEntry.Colors DtrColorsLightfinderDisabled { get; set; } = new(Foreground: 0xD44444u, Glow: 0x642222u); public DtrEntry.Colors DtrColorsLightfinderCooldown { get; set; } = new(Foreground: 0xFFE97Au, Glow: 0x766C3Au); public DtrEntry.Colors DtrColorsLightfinderUnavailable { get; set; } = new(Foreground: 0x000000u, Glow: 0x000000u); + public LightfinderDtrDisplayMode LightfinderDtrDisplayMode { get; set; } = LightfinderDtrDisplayMode.PendingPairRequests; public bool UseLightlessRedesign { get; set; } = true; public bool EnableRightClickMenus { get; set; } = true; public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both; diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index ff9b3d5..9a397be 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -148,9 +148,12 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), + s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), - s.GetRequiredService())); + s.GetRequiredService(), + s.GetRequiredService(), + s.GetRequiredService())); collection.AddSingleton(s => new PairManager(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), contextMenu, s.GetRequiredService())); collection.AddSingleton(); diff --git a/LightlessSync/Services/BroadcastScanningService.cs b/LightlessSync/Services/BroadcastScanningService.cs index 4619a5e..79fe984 100644 --- a/LightlessSync/Services/BroadcastScanningService.cs +++ b/LightlessSync/Services/BroadcastScanningService.cs @@ -211,6 +211,16 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos UpdateSyncshellBroadcasts(); } + public int CountActiveBroadcasts(string? excludeHashedCid = null) + { + var now = DateTime.UtcNow; + var comparer = StringComparer.Ordinal; + return _broadcastCache.Count(entry => + entry.Value.IsBroadcasting && + entry.Value.ExpiryTime > now && + (excludeHashedCid is null || !comparer.Equals(entry.Key, excludeHashedCid))); + } + protected override void Dispose(bool disposing) { base.Dispose(disposing); diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index 963bcbf..dba8427 100644 --- a/LightlessSync/Services/Mediator/Messages.cs +++ b/LightlessSync/Services/Mediator/Messages.cs @@ -17,6 +17,7 @@ namespace LightlessSync.Services.Mediator; public record SwitchToIntroUiMessage : MessageBase; public record SwitchToMainUiMessage : MessageBase; public record OpenSettingsUiMessage : MessageBase; +public record OpenLightfinderSettingsMessage : MessageBase; public record DalamudLoginMessage : MessageBase; public record DalamudLogoutMessage : MessageBase; public record PriorityFrameworkUpdateMessage : SameThreadMessage; diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index 2e2334a..c760a45 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -1,4 +1,5 @@ using Dalamud.Bindings.ImGui; +using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Utility; @@ -252,12 +253,27 @@ namespace LightlessSync.UI _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(); } diff --git a/LightlessSync/UI/DtrEntry.cs b/LightlessSync/UI/DtrEntry.cs index 1b3ab61..89c7389 100644 --- a/LightlessSync/UI/DtrEntry.cs +++ b/LightlessSync/UI/DtrEntry.cs @@ -10,6 +10,7 @@ using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; using LightlessSync.WebAPI; using LightlessSync.WebAPI.SignalR.Utils; +using LightlessSync.Utils; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Runtime.InteropServices; @@ -19,6 +20,9 @@ namespace LightlessSync.UI; public sealed class DtrEntry : IDisposable, IHostedService { + private static readonly TimeSpan _localHashedCidCacheDuration = TimeSpan.FromMinutes(2); + private static readonly TimeSpan _localHashedCidErrorCooldown = TimeSpan.FromMinutes(1); + private readonly ApiController _apiController; private readonly ServerConfigurationManager _serverManager; private readonly CancellationTokenSource _cancellationTokenSource = new(); @@ -28,8 +32,11 @@ public sealed class DtrEntry : IDisposable, IHostedService private readonly Lazy _lightfinderEntry; private readonly ILogger _logger; private readonly BroadcastService _broadcastService; + private readonly BroadcastScannerService _broadcastScannerService; private readonly LightlessMediator _lightlessMediator; private readonly PairManager _pairManager; + private readonly PairRequestService _pairRequestService; + private readonly DalamudUtilService _dalamudUtilService; private Task? _runTask; private string? _statusText; private string? _statusTooltip; @@ -37,6 +44,10 @@ public sealed class DtrEntry : IDisposable, IHostedService private string? _lightfinderText; private string? _lightfinderTooltip; private Colors _lightfinderColors; + private string? _localHashedCid; + private DateTime _localHashedCidFetchedAt = DateTime.MinValue; + private DateTime _localHashedCidNextErrorLog = DateTime.MinValue; + private DateTime _pairRequestNextErrorLog = DateTime.MinValue; public DtrEntry( ILogger logger, @@ -44,9 +55,12 @@ public sealed class DtrEntry : IDisposable, IHostedService ConfigurationServiceBase configService, LightlessMediator lightlessMediator, PairManager pairManager, + PairRequestService pairRequestService, ApiController apiController, ServerConfigurationManager serverManager, - BroadcastService broadcastService) + BroadcastService broadcastService, + BroadcastScannerService broadcastScannerService, + DalamudUtilService dalamudUtilService) { _logger = logger; _dtrBar = dtrBar; @@ -55,9 +69,12 @@ public sealed class DtrEntry : IDisposable, IHostedService _configService = configService; _lightlessMediator = lightlessMediator; _pairManager = pairManager; + _pairRequestService = pairRequestService; _apiController = apiController; _serverManager = serverManager; _broadcastService = broadcastService; + _broadcastScannerService = broadcastScannerService; + _dalamudUtilService = dalamudUtilService; } public void Dispose() @@ -318,27 +335,99 @@ public sealed class DtrEntry : IDisposable, IHostedService } } + private string? GetLocalHashedCid() + { + var now = DateTime.UtcNow; + if (_localHashedCid is not null && now - _localHashedCidFetchedAt < _localHashedCidCacheDuration) + return _localHashedCid; + + try + { + var cid = _dalamudUtilService.GetCIDAsync().GetAwaiter().GetResult(); + var hashedCid = cid.ToString().GetHash256(); + _localHashedCid = hashedCid; + _localHashedCidFetchedAt = now; + return hashedCid; + } + catch (Exception ex) + { + if (now >= _localHashedCidNextErrorLog) + { + _logger.LogDebug(ex, "Failed to refresh local hashed CID for Lightfinder DTR entry."); + _localHashedCidNextErrorLog = now + _localHashedCidErrorCooldown; + } + + _localHashedCid = null; + _localHashedCidFetchedAt = now; + return null; + } + } + + private int GetNearbyBroadcastCount() + { + var localHashedCid = GetLocalHashedCid(); + return _broadcastScannerService.CountActiveBroadcasts( + string.IsNullOrEmpty(localHashedCid) ? null : localHashedCid); + } + + private int GetPendingPairRequestCount() + { + try + { + return _pairRequestService.GetActiveRequests().Count; + } + catch (Exception ex) + { + var now = DateTime.UtcNow; + if (now >= _pairRequestNextErrorLog) + { + _logger.LogDebug(ex, "Failed to retrieve pair request count for Lightfinder DTR entry."); + _pairRequestNextErrorLog = now + _localHashedCidErrorCooldown; + } + + return 0; + } + } + private (string Text, Colors Colors, string Tooltip) BuildLightfinderIndicator() { var config = _configService.Current; const string icon = "\uE048"; if (!_broadcastService.IsLightFinderAvailable) { - return ($"{icon} --", config.DtrColorsLightfinderUnavailable, "Lightfinder - Unavailable on this server."); + return ($"{icon} --", SwapColorChannels(config.DtrColorsLightfinderUnavailable), "Lightfinder - Unavailable on this server."); } if (_broadcastService.IsBroadcasting) { - return ($"{icon} ON", config.DtrColorsLightfinderEnabled, "Lightfinder - Enabled"); + var tooltipBuilder = new StringBuilder("Lightfinder - Enabled"); + + switch (config.LightfinderDtrDisplayMode) + { + case LightfinderDtrDisplayMode.PendingPairRequests: + { + var requestCount = GetPendingPairRequestCount(); + tooltipBuilder.AppendLine(); + tooltipBuilder.Append("Pending pair requests: ").Append(requestCount); + return ($"{icon} Requests {requestCount}", SwapColorChannels(config.DtrColorsLightfinderEnabled), tooltipBuilder.ToString()); + } + default: + { + var broadcastCount = GetNearbyBroadcastCount(); + tooltipBuilder.AppendLine(); + tooltipBuilder.Append("Nearby Lightfinder users: ").Append(broadcastCount); + return ($"{icon} {broadcastCount}", SwapColorChannels(config.DtrColorsLightfinderEnabled), tooltipBuilder.ToString()); + } + } } var tooltip = new StringBuilder("Lightfinder - Disabled"); - var colors = config.DtrColorsLightfinderDisabled; + var colors = SwapColorChannels(config.DtrColorsLightfinderDisabled); if (_broadcastService.RemainingCooldown is { } cooldown && cooldown > TimeSpan.Zero) { tooltip.AppendLine(); tooltip.Append("Cooldown: ").Append(Math.Ceiling(cooldown.TotalSeconds)).Append("s"); - colors = config.DtrColorsLightfinderCooldown; + colors = SwapColorChannels(config.DtrColorsLightfinderCooldown); } return ($"{icon} OFF", colors, tooltip.ToString()); @@ -377,6 +466,17 @@ public sealed class DtrEntry : IDisposable, IHostedService private const byte _colorTypeForeground = 0x13; private const byte _colorTypeGlow = 0x14; + private static Colors SwapColorChannels(Colors colors) + => new(SwapColorComponent(colors.Foreground), SwapColorComponent(colors.Glow)); + + private static uint SwapColorComponent(uint color) + { + if (color == 0) + return 0; + + return ((color & 0xFFu) << 16) | (color & 0xFF00u) | ((color >> 16) & 0xFFu); + } + private static SeString BuildColoredSeString(string text, Colors colors) { var ssb = new SeStringBuilder(); @@ -385,7 +485,16 @@ public sealed class DtrEntry : IDisposable, IHostedService } private static RawPayload BuildColorStartPayload(byte colorType, uint color) - => new(unchecked([0x02, colorType, 0x05, 0xF6, byte.Max((byte)color, 0x01), byte.Max((byte)(color >> 8), 0x01), byte.Max((byte)(color >> 16), 0x01), 0x03])); + => new(unchecked([ + 0x02, + colorType, + 0x05, + 0xF6, + byte.Max((byte)color, (byte)0x01), + byte.Max((byte)(color >> 8), (byte)0x01), + byte.Max((byte)(color >> 16), (byte)0x01), + 0x03 + ])); private static RawPayload BuildColorEndPayload(byte colorType) => new([0x02, colorType, 0x02, 0xEC, 0x03]); diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 52de306..5b2ce6d 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -73,6 +73,8 @@ public class SettingsUi : WindowMediatorSubscriberBase private string _lightfinderIconInput = string.Empty; private bool _lightfinderIconInputInitialized = false; private int _lightfinderIconPresetIndex = -1; + private bool _selectGeneralTabOnNextDraw = false; + private bool _openLightfinderSectionOnNextDraw = false; private static readonly (string Label, SeIconChar Icon)[] LightfinderIconPresets = new[] { ("Link Marker", SeIconChar.LinkMarker), @@ -136,6 +138,12 @@ public class SettingsUi : WindowMediatorSubscriberBase }; Mediator.Subscribe(this, (_) => Toggle()); + Mediator.Subscribe(this, (_) => + { + IsOpen = true; + _selectGeneralTabOnNextDraw = true; + _openLightfinderSectionOnNextDraw = true; + }); Mediator.Subscribe(this, (_) => IsOpen = false); Mediator.Subscribe(this, (_) => UiSharedService_GposeStart()); Mediator.Subscribe(this, (_) => UiSharedService_GposeEnd()); @@ -222,6 +230,17 @@ public class SettingsUi : WindowMediatorSubscriberBase } } + private static DtrEntry.Colors SwapColorChannels(DtrEntry.Colors colors) + => new(SwapColorChannels(colors.Foreground), SwapColorChannels(colors.Glow)); + + private static uint SwapColorChannels(uint color) + { + if (color == 0) + return 0; + + return ((color & 0xFFu) << 16) | (color & 0xFF00u) | ((color >> 16) & 0xFFu); + } + private void DrawBlockedTransfers() { _lastTab = "BlockedTransfers"; @@ -1081,18 +1100,31 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Separator(); + var forceOpenLightfinder = _openLightfinderSectionOnNextDraw; + if (_openLightfinderSectionOnNextDraw) + { + ImGui.SetNextItemOpen(true, ImGuiCond.Always); + } + if (_uiShared.MediumTreeNode("Lightfinder", UIColors.Get("LightlessPurple"))) { + if (forceOpenLightfinder) + { + ImGui.SetScrollHereY(); + } + + _openLightfinderSectionOnNextDraw = false; + bool autoEnable = _configService.Current.LightfinderAutoEnableOnConnect; var autoAlign = _configService.Current.LightfinderAutoAlign; var offsetX = (int)_configService.Current.LightfinderLabelOffsetX; var offsetY = (int)_configService.Current.LightfinderLabelOffsetY; var labelScale = _configService.Current.LightfinderLabelScale; bool showLightfinderInDtr = _configService.Current.ShowLightfinderInDtr; - var dtrLightfinderEnabled = _configService.Current.DtrColorsLightfinderEnabled; - var dtrLightfinderDisabled = _configService.Current.DtrColorsLightfinderDisabled; - var dtrLightfinderCooldown = _configService.Current.DtrColorsLightfinderCooldown; - var dtrLightfinderUnavailable = _configService.Current.DtrColorsLightfinderUnavailable; + var dtrLightfinderEnabled = SwapColorChannels(_configService.Current.DtrColorsLightfinderEnabled); + var dtrLightfinderDisabled = SwapColorChannels(_configService.Current.DtrColorsLightfinderDisabled); + var dtrLightfinderCooldown = SwapColorChannels(_configService.Current.DtrColorsLightfinderCooldown); + var dtrLightfinderUnavailable = SwapColorChannels(_configService.Current.DtrColorsLightfinderUnavailable); ImGui.TextUnformatted("Connection"); if (ImGui.Checkbox("Auto-enable Lightfinder on server connection", ref autoEnable)) @@ -1112,6 +1144,40 @@ public class SettingsUi : WindowMediatorSubscriberBase } _uiShared.DrawHelpText("Adds a Lightfinder status to the Server info bar. Left click toggles Lightfinder when visible."); + var lightfinderDisplayMode = _configService.Current.LightfinderDtrDisplayMode; + var lightfinderDisplayLabel = lightfinderDisplayMode switch + { + LightfinderDtrDisplayMode.PendingPairRequests => "Pending pair requests", + _ => "Nearby Lightfinder users", + }; + + ImGui.BeginDisabled(!showLightfinderInDtr); + if (ImGui.BeginCombo("Info display", lightfinderDisplayLabel)) + { + foreach (var option in Enum.GetValues()) + { + var optionLabel = option switch + { + LightfinderDtrDisplayMode.PendingPairRequests => "Pending pair requests", + _ => "Nearby Lightfinder users", + }; + + var selected = option == lightfinderDisplayMode; + if (ImGui.Selectable(optionLabel, selected)) + { + _configService.Current.LightfinderDtrDisplayMode = option; + _configService.Save(); + } + + if (selected) + ImGui.SetItemDefaultFocus(); + } + + ImGui.EndCombo(); + } + ImGui.EndDisabled(); + _uiShared.DrawHelpText("Choose what the Lightfinder info bar displays while Lightfinder is active."); + bool useLightfinderColors = _configService.Current.UseLightfinderColorsInDtr; if (ImGui.Checkbox("Color-code the Lightfinder info bar according to status", ref useLightfinderColors)) { @@ -1120,24 +1186,24 @@ public class SettingsUi : WindowMediatorSubscriberBase } ImGui.BeginDisabled(!showLightfinderInDtr || !useLightfinderColors); - if (InputDtrColors("Enables", ref dtrLightfinderEnabled)) + if (InputDtrColors("Enabled", ref dtrLightfinderEnabled)) { - _configService.Current.DtrColorsLightfinderEnabled = dtrLightfinderEnabled; + _configService.Current.DtrColorsLightfinderEnabled = SwapColorChannels(dtrLightfinderEnabled); _configService.Save(); } if (InputDtrColors("Disabled", ref dtrLightfinderDisabled)) { - _configService.Current.DtrColorsLightfinderDisabled = dtrLightfinderDisabled; + _configService.Current.DtrColorsLightfinderDisabled = SwapColorChannels(dtrLightfinderDisabled); _configService.Save(); } if (InputDtrColors("Cooldown", ref dtrLightfinderCooldown)) { - _configService.Current.DtrColorsLightfinderCooldown = dtrLightfinderCooldown; + _configService.Current.DtrColorsLightfinderCooldown = SwapColorChannels(dtrLightfinderCooldown); _configService.Save(); } if (InputDtrColors("Unavailable", ref dtrLightfinderUnavailable)) { - _configService.Current.DtrColorsLightfinderUnavailable = dtrLightfinderUnavailable; + _configService.Current.DtrColorsLightfinderUnavailable = SwapColorChannels(dtrLightfinderUnavailable); _configService.Save(); } ImGui.EndDisabled(); @@ -2696,8 +2762,15 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Separator(); if (ImGui.BeginTabBar("mainTabBar")) { - if (ImGui.BeginTabItem("General")) + var generalTabFlags = ImGuiTabItemFlags.None; + if (_selectGeneralTabOnNextDraw) { + generalTabFlags |= ImGuiTabItemFlags.SetSelected; + } + + if (ImGui.BeginTabItem("General", generalTabFlags)) + { + _selectGeneralTabOnNextDraw = false; DrawGeneral(); ImGui.EndTabItem(); } From 2a9b5812edd39005ece2e2ffbe43b3d1e80f99c9 Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Sat, 11 Oct 2025 03:29:44 +0900 Subject: [PATCH 3/5] add theme override customizations --- .../Configurations/UiStyleOverride.cs | 21 + .../Configurations/UiThemeConfig.cs | 12 + .../UiThemeConfigService.cs | 14 + LightlessSync/Plugin.cs | 5 +- LightlessSync/UI/SettingsUi.cs | 254 +++++++++++- LightlessSync/UI/Style/MainStyle.cs | 382 ++++++++++-------- 6 files changed, 526 insertions(+), 162 deletions(-) create mode 100644 LightlessSync/LightlessConfiguration/Configurations/UiStyleOverride.cs create mode 100644 LightlessSync/LightlessConfiguration/Configurations/UiThemeConfig.cs create mode 100644 LightlessSync/LightlessConfiguration/UiThemeConfigService.cs diff --git a/LightlessSync/LightlessConfiguration/Configurations/UiStyleOverride.cs b/LightlessSync/LightlessConfiguration/Configurations/UiStyleOverride.cs new file mode 100644 index 0000000..3ea43eb --- /dev/null +++ b/LightlessSync/LightlessConfiguration/Configurations/UiStyleOverride.cs @@ -0,0 +1,21 @@ +using System; +using System.Numerics; + +namespace LightlessSync.LightlessConfiguration.Configurations; + +[Serializable] +public class UiStyleOverride +{ + public uint? Color { get; set; } + public float? Float { get; set; } + public Vector2Config? Vector2 { get; set; } + + public bool IsEmpty => Color is null && Float is null && Vector2 is null; +} + +[Serializable] +public record struct Vector2Config(float X, float Y) +{ + public static implicit operator Vector2(Vector2Config value) => new(value.X, value.Y); + public static implicit operator Vector2Config(Vector2 value) => new(value.X, value.Y); +} diff --git a/LightlessSync/LightlessConfiguration/Configurations/UiThemeConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/UiThemeConfig.cs new file mode 100644 index 0000000..aa0b219 --- /dev/null +++ b/LightlessSync/LightlessConfiguration/Configurations/UiThemeConfig.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace LightlessSync.LightlessConfiguration.Configurations; + +[Serializable] +public class UiThemeConfig : ILightlessConfiguration +{ + public Dictionary StyleOverrides { get; set; } = new(StringComparer.OrdinalIgnoreCase); + + public int Version { get; set; } = 1; +} diff --git a/LightlessSync/LightlessConfiguration/UiThemeConfigService.cs b/LightlessSync/LightlessConfiguration/UiThemeConfigService.cs new file mode 100644 index 0000000..21a5051 --- /dev/null +++ b/LightlessSync/LightlessConfiguration/UiThemeConfigService.cs @@ -0,0 +1,14 @@ +using LightlessSync.LightlessConfiguration.Configurations; + +namespace LightlessSync.LightlessConfiguration; + +public class UiThemeConfigService : ConfigurationServiceBase +{ + public const string ConfigName = "ui-theme.json"; + + public UiThemeConfigService(string configDir) : base(configDir) + { + } + + public override string ConfigurationName => ConfigName; +} diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 9a397be..7a54ce9 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -192,10 +192,12 @@ public sealed class Plugin : IDalamudPlugin httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LightlessSync", ver!.Major + "." + ver!.Minor + "." + ver!.Build)); return httpClient; }); + collection.AddSingleton((s) => new UiThemeConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => { var cfg = new LightlessConfigService(pluginInterface.ConfigDirectory.FullName); - LightlessSync.UI.Style.MainStyle.Init(cfg); + var theme = s.GetRequiredService(); + LightlessSync.UI.Style.MainStyle.Init(cfg, theme); return cfg; }); collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName)); @@ -207,6 +209,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton((s) => new PlayerPerformanceConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new CharaDataConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton>(s => s.GetRequiredService()); + collection.AddSingleton>(s => s.GetRequiredService()); collection.AddSingleton>(s => s.GetRequiredService()); collection.AddSingleton>(s => s.GetRequiredService()); collection.AddSingleton>(s => s.GetRequiredService()); diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 5b2ce6d..de20fb6 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -18,6 +18,7 @@ using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; +using LightlessSync.UI.Style; using LightlessSync.Utils; using LightlessSync.UtilsEnum.Enum; using LightlessSync.WebAPI; @@ -44,6 +45,7 @@ public class SettingsUi : WindowMediatorSubscriberBase private readonly ApiController _apiController; private readonly CacheMonitor _cacheMonitor; private readonly LightlessConfigService _configService; + private readonly UiThemeConfigService _themeConfigService; private readonly ConcurrentDictionary> _currentDownloads = new(); private readonly DalamudUtilService _dalamudUtilService; private readonly HttpClient _httpClient; @@ -94,7 +96,7 @@ public class SettingsUi : WindowMediatorSubscriberBase private bool _wasOpen = false; public SettingsUi(ILogger logger, - UiSharedService uiShared, LightlessConfigService configService, + UiSharedService uiShared, LightlessConfigService configService, UiThemeConfigService themeConfigService, PairManager pairManager, ServerConfigurationManager serverConfigurationManager, PlayerPerformanceConfigService playerPerformanceConfigService, @@ -110,6 +112,7 @@ public class SettingsUi : WindowMediatorSubscriberBase NameplateHandler nameplateHandler) : base(logger, mediator, "Lightless Sync Settings", performanceCollector) { _configService = configService; + _themeConfigService = themeConfigService; _pairManager = pairManager; _serverConfigurationManager = serverConfigurationManager; _playerPerformanceConfigService = playerPerformanceConfigService; @@ -241,6 +244,253 @@ public class SettingsUi : WindowMediatorSubscriberBase return ((color & 0xFFu) << 16) | (color & 0xFF00u) | ((color >> 16) & 0xFFu); } + private static Vector4 PackedThemeColorToVector4(uint packed) + => new( + (packed & 0xFF) / 255f, + ((packed >> 8) & 0xFF) / 255f, + ((packed >> 16) & 0xFF) / 255f, + ((packed >> 24) & 0xFF) / 255f); + + private static uint ThemeVector4ToPackedColor(Vector4 color) + { + static byte ToByte(float channel) + { + var scaled = MathF.Round(Math.Clamp(channel, 0f, 1f) * 255.0f); + return (byte)Math.Clamp((int)scaled, 0, 255); + } + + var r = ToByte(color.X); + var g = ToByte(color.Y); + var b = ToByte(color.Z); + var a = ToByte(color.W); + return (uint)(r | (g << 8) | (b << 16) | (a << 24)); + } + + private void UpdateStyleOverride(string key, Action updater) + { + var overrides = _themeConfigService.Current.StyleOverrides; + + if (!overrides.TryGetValue(key, out var entry)) + entry = new UiStyleOverride(); + + updater(entry); + + if (entry.IsEmpty) + overrides.Remove(key); + else + overrides[key] = entry; + + _themeConfigService.Save(); + } + + private void DrawThemeOverridesSection() + { + ImGui.TextUnformatted("Lightless Theme Overrides"); + _uiShared.DrawHelpText("Adjust the Lightless redesign theme. Overrides only apply when the redesign is enabled."); + + if (!_configService.Current.UseLightlessRedesign) + UiSharedService.ColorTextWrapped("The Lightless redesign is currently disabled. Enable it to see these changes take effect.", UIColors.Get("DimRed")); + + const ImGuiTableFlags flags = ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp; + if (!ImGui.BeginTable("##ThemeOverridesTable", 3, flags)) + return; + + ImGui.TableSetupColumn("Element", ImGuiTableColumnFlags.WidthFixed, 325f); + ImGui.TableSetupColumn("Value", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 70f); + ImGui.TableHeadersRow(); + + DrawThemeCategoryRow("Colors"); + foreach (var option in MainStyle.ColorOptions) + DrawThemeColorRow(option); + + DrawThemeCategoryRow("Spacing & Padding"); + foreach (var option in MainStyle.Vector2Options) + DrawThemeVectorRow(option); + + DrawThemeCategoryRow("Rounding & Sizes"); + foreach (var option in MainStyle.FloatOptions) + DrawThemeFloatRow(option); + + ImGui.EndTable(); + } + + private static void DrawThemeCategoryRow(string label) + { + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + ImGui.TextColored(UIColors.Get("LightlessPurple"), label); + ImGui.TableSetColumnIndex(1); + ImGui.TableSetColumnIndex(2); + } + + private void DrawThemeColorRow(MainStyle.StyleColorOption option) + { + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + ImGui.TextUnformatted(option.Label); + bool showTooltip = ImGui.IsItemHovered(); + + var tooltip = string.Empty; + if (!string.IsNullOrEmpty(option.Description)) + tooltip = option.Description; + + var overrides = _themeConfigService.Current.StyleOverrides; + overrides.TryGetValue(option.Key, out var existing); + + if (!string.IsNullOrEmpty(option.UiColorKey)) + { + if (!string.IsNullOrEmpty(tooltip)) + tooltip += "\n"; + tooltip += $"Default uses UIColors[\"{option.UiColorKey}\"]"; + + ImGui.SameLine(); + ImGui.TextDisabled($"(UIColors.{option.UiColorKey})"); + if (ImGui.IsItemHovered()) + showTooltip = true; + } + + ImGui.TableSetColumnIndex(2); + if (DrawStyleResetButton(option.Key, existing?.Color is not null)) + { + UpdateStyleOverride(option.Key, entry => + { + entry.Color = null; + entry.Float = null; + entry.Vector2 = null; + }); + + existing = null; + } + + if (showTooltip && !string.IsNullOrEmpty(tooltip)) + ImGui.SetTooltip(tooltip); + + var defaultColor = MainStyle.NormalizeColorVector(option.DefaultValue()); + var current = existing?.Color is { } packed ? PackedThemeColorToVector4(packed) : defaultColor; + var edit = current; + + ImGui.TableSetColumnIndex(1); + if (ImGui.ColorEdit4($"##theme-color-{option.Key}", ref edit, ImGuiColorEditFlags.AlphaPreviewHalf)) + { + UpdateStyleOverride(option.Key, entry => + { + entry.Color = ThemeVector4ToPackedColor(edit); + entry.Float = null; + entry.Vector2 = null; + }); + } + } + + private void DrawThemeVectorRow(MainStyle.StyleVector2Option option) + { + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + ImGui.TextUnformatted(option.Label); + if (!string.IsNullOrEmpty(option.Description) && ImGui.IsItemHovered()) + ImGui.SetTooltip(option.Description); + + var overrides = _themeConfigService.Current.StyleOverrides; + overrides.TryGetValue(option.Key, out var existing); + + ImGui.TableSetColumnIndex(2); + if (DrawStyleResetButton(option.Key, existing?.Vector2 is not null)) + { + UpdateStyleOverride(option.Key, entry => + { + entry.Vector2 = null; + entry.Color = null; + entry.Float = null; + }); + existing = null; + } + + var defaultValue = option.DefaultValue(); + var current = existing?.Vector2 is { } vectorOverride ? (Vector2)vectorOverride : defaultValue; + var edit = current; + + ImGui.TableSetColumnIndex(1); + if (ImGui.DragFloat2($"##theme-vector-{option.Key}", ref edit, option.Speed)) + { + if (option.Min is { } min) + edit = Vector2.Max(edit, min); + if (option.Max is { } max) + edit = Vector2.Min(edit, max); + + UpdateStyleOverride(option.Key, entry => + { + entry.Vector2 = new Vector2Config(edit.X, edit.Y); + entry.Color = null; + entry.Float = null; + }); + } + } + + private void DrawThemeFloatRow(MainStyle.StyleFloatOption option) + { + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + ImGui.TextUnformatted(option.Label); + if (!string.IsNullOrEmpty(option.Description) && ImGui.IsItemHovered()) + ImGui.SetTooltip(option.Description); + + var overrides = _themeConfigService.Current.StyleOverrides; + overrides.TryGetValue(option.Key, out var existing); + + ImGui.TableSetColumnIndex(2); + if (DrawStyleResetButton(option.Key, existing?.Float is not null)) + { + UpdateStyleOverride(option.Key, entry => + { + entry.Float = null; + entry.Color = null; + entry.Vector2 = null; + }); + existing = null; + } + + var current = existing?.Float ?? option.DefaultValue; + var edit = current; + + var min = option.Min ?? float.MinValue; + var max = option.Max ?? float.MaxValue; + + ImGui.TableSetColumnIndex(1); + if (ImGui.DragFloat($"##theme-float-{option.Key}", ref edit, option.Speed, min, max, "%.2f")) + { + if (option.Min.HasValue) + edit = MathF.Max(option.Min.Value, edit); + if (option.Max.HasValue) + edit = MathF.Min(option.Max.Value, edit); + + UpdateStyleOverride(option.Key, entry => + { + entry.Float = edit; + entry.Color = null; + entry.Vector2 = null; + }); + } + } + + private bool DrawStyleResetButton(string key, bool hasOverride, string? tooltipOverride = null) + { + using var id = ImRaii.PushId($"reset-{key}"); + using var disabled = ImRaii.Disabled(!hasOverride); + var availableWidth = ImGui.GetContentRegionAvail().X; + bool pressed = false; + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + if (ImGui.Button(FontAwesomeIcon.Undo.ToIconString(), new Vector2(availableWidth, 0))) + pressed = true; + } + + var tooltip = tooltipOverride ?? (hasOverride + ? "Reset this style override to its default value." + : "Value already matches the default."); + UiSharedService.AttachToolTip(tooltip); + return pressed; + } + private void DrawBlockedTransfers() { _lastTab = "BlockedTransfers"; @@ -1642,6 +1892,8 @@ public class SettingsUi : WindowMediatorSubscriberBase } _uiShared.DrawHelpText("This changes the vanity colored UID's in pair list."); + DrawThemeOverridesSection(); + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } diff --git a/LightlessSync/UI/Style/MainStyle.cs b/LightlessSync/UI/Style/MainStyle.cs index d40ed2e..d3d8b68 100644 --- a/LightlessSync/UI/Style/MainStyle.cs +++ b/LightlessSync/UI/Style/MainStyle.cs @@ -1,169 +1,231 @@ -// inspiration: brio because it's style is fucking amazing +// inspiration: brio because it's style is fucking amazing using Dalamud.Bindings.ImGui; using LightlessSync.LightlessConfiguration; +using LightlessSync.LightlessConfiguration.Configurations; +using System; +using System.Collections.Generic; using System.Numerics; -namespace LightlessSync.UI.Style +namespace LightlessSync.UI.Style; + +internal static class MainStyle { - internal static class MainStyle + public readonly record struct StyleColorOption(string Key, string Label, Func DefaultValue, ImGuiCol Target, string? Description = null, string? UiColorKey = null); + public readonly record struct StyleFloatOption(string Key, string Label, float DefaultValue, ImGuiStyleVar Target, float? Min = null, float? Max = null, float Speed = 0.25f, string? Description = null); + public readonly record struct StyleVector2Option(string Key, string Label, Func DefaultValue, ImGuiStyleVar Target, Vector2? Min = null, Vector2? Max = null, float Speed = 0.25f, string? Description = null); + + private static LightlessConfigService? _config; + private static UiThemeConfigService? _themeConfig; + public static void Init(LightlessConfigService config, UiThemeConfigService themeConfig) { - private static LightlessConfigService? _config; - public static void Init(LightlessConfigService config) => _config = config; - public static bool ShouldUseTheme => _config?.Current.UseLightlessRedesign ?? false; - - private static bool _hasPushed; - private static int _pushedColorCount; - private static int _pushedStyleVarCount; - - public static void PushStyle() - { - if (_hasPushed) - PopStyle(); - - if (!ShouldUseTheme) - { - _hasPushed = false; - return; - } - - _hasPushed = true; - _pushedColorCount = 0; - _pushedStyleVarCount = 0; - - Push(ImGuiCol.Text, new Vector4(255, 255, 255, 255)); - Push(ImGuiCol.TextDisabled, new Vector4(128, 128, 128, 255)); - - Push(ImGuiCol.WindowBg, new Vector4(23, 23, 23, 248)); - Push(ImGuiCol.ChildBg, new Vector4(23, 23, 23, 66)); - Push(ImGuiCol.PopupBg, new Vector4(23, 23, 23, 248)); - - Push(ImGuiCol.Border, new Vector4(65, 65, 65, 255)); - Push(ImGuiCol.BorderShadow, new Vector4(0, 0, 0, 150)); - - Push(ImGuiCol.FrameBg, new Vector4(40, 40, 40, 255)); - Push(ImGuiCol.FrameBgHovered, new Vector4(50, 50, 50, 255)); - Push(ImGuiCol.FrameBgActive, new Vector4(30, 30, 30, 255)); - - Push(ImGuiCol.TitleBg, new Vector4(24, 24, 24, 232)); - Push(ImGuiCol.TitleBgActive, new Vector4(30, 30, 30, 255)); - Push(ImGuiCol.TitleBgCollapsed, new Vector4(27, 27, 27, 255)); - - Push(ImGuiCol.MenuBarBg, new Vector4(36, 36, 36, 255)); - Push(ImGuiCol.ScrollbarBg, new Vector4(0, 0, 0, 0)); - Push(ImGuiCol.ScrollbarGrab, new Vector4(62, 62, 62, 255)); - Push(ImGuiCol.ScrollbarGrabHovered, new Vector4(70, 70, 70, 255)); - Push(ImGuiCol.ScrollbarGrabActive, new Vector4(70, 70, 70, 255)); - - Push(ImGuiCol.CheckMark, UIColors.Get("LightlessPurple")); - - Push(ImGuiCol.SliderGrab, new Vector4(101, 101, 101, 255)); - Push(ImGuiCol.SliderGrabActive, new Vector4(123, 123, 123, 255)); - - Push(ImGuiCol.Button, UIColors.Get("ButtonDefault")); - Push(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple")); - Push(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive")); - - Push(ImGuiCol.Header, new Vector4(0, 0, 0, 60)); - Push(ImGuiCol.HeaderHovered, new Vector4(0, 0, 0, 90)); - Push(ImGuiCol.HeaderActive, new Vector4(0, 0, 0, 120)); - - Push(ImGuiCol.Separator, new Vector4(75, 75, 75, 121)); - Push(ImGuiCol.SeparatorHovered, UIColors.Get("LightlessPurple")); - Push(ImGuiCol.SeparatorActive, UIColors.Get("LightlessPurpleActive")); - - Push(ImGuiCol.ResizeGrip, new Vector4(0, 0, 0, 0)); - Push(ImGuiCol.ResizeGripHovered, new Vector4(0, 0, 0, 0)); - Push(ImGuiCol.ResizeGripActive, UIColors.Get("LightlessPurpleActive")); - - Push(ImGuiCol.Tab, new Vector4(40, 40, 40, 255)); - Push(ImGuiCol.TabHovered, UIColors.Get("LightlessPurple")); - Push(ImGuiCol.TabActive, UIColors.Get("LightlessPurpleActive")); - Push(ImGuiCol.TabUnfocused, new Vector4(40, 40, 40, 255)); - Push(ImGuiCol.TabUnfocusedActive, UIColors.Get("LightlessPurpleActive")); - - Push(ImGuiCol.DockingPreview, UIColors.Get("LightlessPurpleActive")); - Push(ImGuiCol.DockingEmptyBg, new Vector4(50, 50, 50, 255)); - - Push(ImGuiCol.PlotLines, new Vector4(150, 150, 150, 255)); - - Push(ImGuiCol.TableHeaderBg, new Vector4(48, 48, 48, 255)); - Push(ImGuiCol.TableBorderStrong, new Vector4(79, 79, 89, 255)); - Push(ImGuiCol.TableBorderLight, new Vector4(59, 59, 64, 255)); - Push(ImGuiCol.TableRowBg, new Vector4(0, 0, 0, 0)); - Push(ImGuiCol.TableRowBgAlt, new Vector4(255, 255, 255, 15)); - - Push(ImGuiCol.TextSelectedBg, new Vector4(98, 75, 224, 255)); - Push(ImGuiCol.DragDropTarget, new Vector4(98, 75, 224, 255)); - - Push(ImGuiCol.NavHighlight, new Vector4(98, 75, 224, 179)); - Push(ImGuiCol.NavWindowingDimBg, new Vector4(204, 204, 204, 51)); - Push(ImGuiCol.NavWindowingHighlight, new Vector4(204, 204, 204, 89)); - - PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(6, 6)); - PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(4, 3)); - PushStyleVar(ImGuiStyleVar.CellPadding, new Vector2(4, 4)); - PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(4, 4)); - PushStyleVar(ImGuiStyleVar.ItemInnerSpacing, new Vector2(4, 4)); - - PushStyleVar(ImGuiStyleVar.IndentSpacing, 21.0f); - PushStyleVar(ImGuiStyleVar.ScrollbarSize, 10.0f); - PushStyleVar(ImGuiStyleVar.GrabMinSize, 20.0f); - - PushStyleVar(ImGuiStyleVar.WindowBorderSize, 1.5f); - PushStyleVar(ImGuiStyleVar.ChildBorderSize, 1.5f); - PushStyleVar(ImGuiStyleVar.PopupBorderSize, 1.5f); - PushStyleVar(ImGuiStyleVar.FrameBorderSize, 0f); - - PushStyleVar(ImGuiStyleVar.WindowRounding, 7f); - PushStyleVar(ImGuiStyleVar.ChildRounding, 4f); - PushStyleVar(ImGuiStyleVar.FrameRounding, 4f); - PushStyleVar(ImGuiStyleVar.PopupRounding, 4f); - PushStyleVar(ImGuiStyleVar.ScrollbarRounding, 4f); - PushStyleVar(ImGuiStyleVar.GrabRounding, 4f); - PushStyleVar(ImGuiStyleVar.TabRounding, 4f); - } - - public static void PopStyle() - { - if (!_hasPushed) - return; - - if (_pushedStyleVarCount > 0) - ImGui.PopStyleVar(_pushedStyleVarCount); - if (_pushedColorCount > 0) - ImGui.PopStyleColor(_pushedColorCount); - - _hasPushed = false; - _pushedColorCount = 0; - _pushedStyleVarCount = 0; - } - - private static void Push(ImGuiCol col, Vector4 rgba) - { - if (rgba.X > 1f || rgba.Y > 1f || rgba.Z > 1f || rgba.W > 1f) - rgba /= 255f; - - ImGui.PushStyleColor(col, rgba); - _pushedColorCount++; - } - - private static void Push(ImGuiCol col, uint packedRgba) - { - ImGui.PushStyleColor(col, packedRgba); - _pushedColorCount++; - } - - private static void PushStyleVar(ImGuiStyleVar var, float value) - { - ImGui.PushStyleVar(var, value); - _pushedStyleVarCount++; - } - - private static void PushStyleVar(ImGuiStyleVar var, Vector2 value) - { - ImGui.PushStyleVar(var, value); - _pushedStyleVarCount++; - } + _config = config; + _themeConfig = themeConfig; } + public static bool ShouldUseTheme => _config?.Current.UseLightlessRedesign ?? false; + + private static bool _hasPushed; + private static int _pushedColorCount; + private static int _pushedStyleVarCount; + + private static readonly StyleColorOption[] _colorOptions = + [ + new("color.text", "Text", () => Rgba(255, 255, 255, 255), ImGuiCol.Text), + new("color.textDisabled", "Text (Disabled)", () => Rgba(128, 128, 128, 255), ImGuiCol.TextDisabled), + new("color.windowBg", "Window Background", () => Rgba(23, 23, 23, 248), ImGuiCol.WindowBg), + new("color.childBg", "Child Background", () => Rgba(23, 23, 23, 66), ImGuiCol.ChildBg), + new("color.popupBg", "Popup Background", () => Rgba(23, 23, 23, 248), ImGuiCol.PopupBg), + new("color.border", "Border", () => Rgba(65, 65, 65, 255), ImGuiCol.Border), + new("color.borderShadow", "Border Shadow", () => Rgba(0, 0, 0, 150), ImGuiCol.BorderShadow), + new("color.frameBg", "Frame Background", () => Rgba(40, 40, 40, 255), ImGuiCol.FrameBg), + new("color.frameBgHovered", "Frame Background (Hover)", () => Rgba(50, 50, 50, 255), 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.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), + new("color.scrollbarGrabHovered", "Scrollbar Grab (Hover)", () => Rgba(70, 70, 70, 255), ImGuiCol.ScrollbarGrabHovered), + new("color.scrollbarGrabActive", "Scrollbar Grab (Active)", () => Rgba(70, 70, 70, 255), ImGuiCol.ScrollbarGrabActive), + new("color.checkMark", "Check Mark", () => UIColors.Get("LightlessPurple"), ImGuiCol.CheckMark, UiColorKey: "LightlessPurple"), + new("color.sliderGrab", "Slider Grab", () => Rgba(101, 101, 101, 255), ImGuiCol.SliderGrab), + new("color.sliderGrabActive", "Slider Grab (Active)", () => Rgba(123, 123, 123, 255), ImGuiCol.SliderGrabActive), + new("color.button", "Button", () => UIColors.Get("ButtonDefault"), ImGuiCol.Button, UiColorKey: "ButtonDefault"), + new("color.buttonHovered", "Button (Hover)", () => UIColors.Get("LightlessPurple"), ImGuiCol.ButtonHovered, UiColorKey: "LightlessPurple"), + new("color.buttonActive", "Button (Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.ButtonActive, UiColorKey: "LightlessPurpleActive"), + new("color.header", "Header", () => Rgba(0, 0, 0, 60), ImGuiCol.Header), + new("color.headerHovered", "Header (Hover)", () => Rgba(0, 0, 0, 90), ImGuiCol.HeaderHovered), + new("color.headerActive", "Header (Active)", () => Rgba(0, 0, 0, 120), ImGuiCol.HeaderActive), + new("color.separator", "Separator", () => Rgba(75, 75, 75, 121), ImGuiCol.Separator), + new("color.separatorHovered", "Separator (Hover)", () => UIColors.Get("LightlessPurple"), ImGuiCol.SeparatorHovered, UiColorKey: "LightlessPurple"), + new("color.separatorActive", "Separator (Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.SeparatorActive, UiColorKey: "LightlessPurpleActive"), + new("color.resizeGrip", "Resize Grip", () => Rgba(0, 0, 0, 0), ImGuiCol.ResizeGrip), + new("color.resizeGripHovered", "Resize Grip (Hover)", () => Rgba(0, 0, 0, 0), ImGuiCol.ResizeGripHovered), + new("color.resizeGripActive", "Resize Grip (Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.ResizeGripActive, UiColorKey: "LightlessPurpleActive"), + new("color.tab", "Tab", () => Rgba(40, 40, 40, 255), ImGuiCol.Tab), + new("color.tabHovered", "Tab (Hover)", () => UIColors.Get("LightlessPurple"), ImGuiCol.TabHovered, UiColorKey: "LightlessPurple"), + new("color.tabActive", "Tab (Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.TabActive, UiColorKey: "LightlessPurpleActive"), + new("color.tabUnfocused", "Tab (Unfocused)", () => Rgba(40, 40, 40, 255), ImGuiCol.TabUnfocused), + new("color.tabUnfocusedActive", "Tab (Unfocused Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.TabUnfocusedActive, UiColorKey: "LightlessPurpleActive"), + new("color.dockingPreview", "Docking Preview", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.DockingPreview, UiColorKey: "LightlessPurpleActive"), + new("color.dockingEmptyBg", "Docking Empty Background", () => Rgba(50, 50, 50, 255), ImGuiCol.DockingEmptyBg), + new("color.plotLines", "Plot Lines", () => Rgba(150, 150, 150, 255), ImGuiCol.PlotLines), + new("color.tableHeaderBg", "Table Header Background", () => Rgba(48, 48, 48, 255), ImGuiCol.TableHeaderBg), + new("color.tableBorderStrong", "Table Border Strong", () => Rgba(79, 79, 89, 255), ImGuiCol.TableBorderStrong), + new("color.tableBorderLight", "Table Border Light", () => Rgba(59, 59, 64, 255), ImGuiCol.TableBorderLight), + new("color.tableRowBg", "Table Row Background", () => Rgba(0, 0, 0, 0), ImGuiCol.TableRowBg), + new("color.tableRowBgAlt", "Table Row Background (Alt)", () => Rgba(255, 255, 255, 15), ImGuiCol.TableRowBgAlt), + new("color.textSelectedBg", "Text Selection Background", () => Rgba(173, 138, 245, 255), ImGuiCol.TextSelectedBg), + new("color.dragDropTarget", "Drag & Drop Target", () => Rgba(173, 138, 245, 255), ImGuiCol.DragDropTarget), + new("color.navHighlight", "Navigation Highlight", () => Rgba(173, 138, 245, 179), ImGuiCol.NavHighlight), + new("color.navWindowingDimBg", "Navigation Window Dim", () => Rgba(204, 204, 204, 51), ImGuiCol.NavWindowingDimBg), + new("color.navWindowingHighlight", "Navigation Window Highlight", () => Rgba(204, 204, 204, 89), ImGuiCol.NavWindowingHighlight) + ]; + + private static readonly StyleVector2Option[] _vector2Options = + [ + new("vector.windowPadding", "Window Padding", () => new Vector2(6f, 6f), ImGuiStyleVar.WindowPadding), + new("vector.framePadding", "Frame Padding", () => new Vector2(4f, 3f), ImGuiStyleVar.FramePadding), + new("vector.cellPadding", "Cell Padding", () => new Vector2(4f, 4f), ImGuiStyleVar.CellPadding), + new("vector.itemSpacing", "Item Spacing", () => new Vector2(4f, 4f), ImGuiStyleVar.ItemSpacing), + new("vector.itemInnerSpacing", "Item Inner Spacing", () => new Vector2(4f, 4f), ImGuiStyleVar.ItemInnerSpacing) + ]; + + private static readonly StyleFloatOption[] _floatOptions = + [ + new("float.indentSpacing", "Indent Spacing", 21f, ImGuiStyleVar.IndentSpacing, 0f, 100f, 0.5f), + new("float.scrollbarSize", "Scrollbar Size", 10f, ImGuiStyleVar.ScrollbarSize, 4f, 30f, 0.5f), + new("float.grabMinSize", "Grab Minimum Size", 20f, ImGuiStyleVar.GrabMinSize, 1f, 80f, 0.5f), + new("float.windowBorderSize", "Window Border Size", 1.5f, ImGuiStyleVar.WindowBorderSize, 0f, 5f, 0.1f), + new("float.childBorderSize", "Child Border Size", 1.5f, ImGuiStyleVar.ChildBorderSize, 0f, 5f, 0.1f), + new("float.popupBorderSize", "Popup Border Size", 1.5f, ImGuiStyleVar.PopupBorderSize, 0f, 5f, 0.1f), + new("float.frameBorderSize", "Frame Border Size", 0f, ImGuiStyleVar.FrameBorderSize, 0f, 5f, 0.1f), + new("float.windowRounding", "Window Rounding", 7f, ImGuiStyleVar.WindowRounding, 0f, 20f, 0.2f), + new("float.childRounding", "Child Rounding", 4f, ImGuiStyleVar.ChildRounding, 0f, 20f, 0.2f), + new("float.frameRounding", "Frame Rounding", 4f, ImGuiStyleVar.FrameRounding, 0f, 20f, 0.2f), + new("float.popupRounding", "Popup Rounding", 4f, ImGuiStyleVar.PopupRounding, 0f, 20f, 0.2f), + new("float.scrollbarRounding", "Scrollbar Rounding", 4f, ImGuiStyleVar.ScrollbarRounding, 0f, 20f, 0.2f), + new("float.grabRounding", "Grab Rounding", 4f, ImGuiStyleVar.GrabRounding, 0f, 20f, 0.2f), + new("float.tabRounding", "Tab Rounding", 4f, ImGuiStyleVar.TabRounding, 0f, 20f, 0.2f) + ]; + + public static IReadOnlyList ColorOptions => _colorOptions; + public static IReadOnlyList FloatOptions => _floatOptions; + public static IReadOnlyList Vector2Options => _vector2Options; + + public static void PushStyle() + { + if (_hasPushed) + PopStyle(); + + if (!ShouldUseTheme) + { + _hasPushed = false; + return; + } + + _hasPushed = true; + _pushedColorCount = 0; + _pushedStyleVarCount = 0; + + foreach (var option in _colorOptions) + Push(option.Target, ResolveColor(option)); + + foreach (var option in _vector2Options) + PushStyleVar(option.Target, ResolveVector(option)); + + foreach (var option in _floatOptions) + PushStyleVar(option.Target, ResolveFloat(option)); + } + + public static void PopStyle() + { + if (!_hasPushed) + return; + + if (_pushedStyleVarCount > 0) + ImGui.PopStyleVar(_pushedStyleVarCount); + if (_pushedColorCount > 0) + ImGui.PopStyleColor(_pushedColorCount); + + _hasPushed = false; + _pushedColorCount = 0; + _pushedStyleVarCount = 0; + } + + private static Vector4 ResolveColor(StyleColorOption option) + { + var defaultValue = NormalizeColorVector(option.DefaultValue()); + if (_themeConfig?.Current.StyleOverrides.TryGetValue(option.Key, out var overrideValue) == true && overrideValue.Color is { } packed) + return PackedColorToVector4(packed); + + return defaultValue; + } + + private static Vector2 ResolveVector(StyleVector2Option option) + { + var value = option.DefaultValue(); + if (_themeConfig?.Current.StyleOverrides.TryGetValue(option.Key, out var overrideValue) == true && overrideValue.Vector2 is { } vectorOverride) + { + value = vectorOverride; + } + + if (option.Min is { } min) + value = Vector2.Max(value, min); + if (option.Max is { } max) + value = Vector2.Min(value, max); + return value; + } + + private static float ResolveFloat(StyleFloatOption option) + { + var value = option.DefaultValue; + if (_themeConfig?.Current.StyleOverrides.TryGetValue(option.Key, out var overrideValue) == true && overrideValue.Float is { } floatOverride) + { + value = floatOverride; + } + + if (option.Min.HasValue) + value = MathF.Max(option.Min.Value, value); + if (option.Max.HasValue) + value = MathF.Min(option.Max.Value, value); + return value; + } + + private static void Push(ImGuiCol col, Vector4 rgba) + { + rgba = NormalizeColorVector(rgba); + ImGui.PushStyleColor(col, rgba); + _pushedColorCount++; + } + + private static void PushStyleVar(ImGuiStyleVar var, float value) + { + ImGui.PushStyleVar(var, value); + _pushedStyleVarCount++; + } + + private static void PushStyleVar(ImGuiStyleVar var, Vector2 value) + { + ImGui.PushStyleVar(var, value); + _pushedStyleVarCount++; + } + + private static Vector4 Rgba(byte r, byte g, byte b, byte a = 255) + => new Vector4(r / 255f, g / 255f, b / 255f, a / 255f); + + internal static Vector4 NormalizeColorVector(Vector4 rgba) + { + if (rgba.X > 1f || rgba.Y > 1f || rgba.Z > 1f || rgba.W > 1f) + rgba /= 255f; + return rgba; + } + + internal static Vector4 PackedColorToVector4(uint color) + => new( + (color & 0xFF) / 255f, + ((color >> 8) & 0xFF) / 255f, + ((color >> 16) & 0xFF) / 255f, + ((color >> 24) & 0xFF) / 255f); } From 6a0f8c507cd44d2f7889802d3bb9af87a4167a38 Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Sat, 11 Oct 2025 07:52:52 +0900 Subject: [PATCH 4/5] add seperate colors for labels and update color inputs --- LightlessSync/Services/NameplateHandler.cs | 4 +- LightlessSync/UI/SettingsUi.cs | 278 ++++++++++++++++----- LightlessSync/UI/UIColors.cs | 3 + 3 files changed, 215 insertions(+), 70 deletions(-) diff --git a/LightlessSync/Services/NameplateHandler.cs b/LightlessSync/Services/NameplateHandler.cs index 74edabc..0731d0e 100644 --- a/LightlessSync/Services/NameplateHandler.cs +++ b/LightlessSync/Services/NameplateHandler.cs @@ -259,8 +259,8 @@ public unsafe class NameplateHandler : IMediatorSubscriber continue; } - var labelColor = UIColors.Get("LightlessPurple"); - var edgeColor = UIColors.Get("FullBlack"); + var labelColor = UIColors.Get("Lightfinder"); + var edgeColor = UIColors.Get("LightfinderEdge"); var config = _configService.Current; var scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f); diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index de20fb6..6bb69a2 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -77,6 +77,7 @@ public class SettingsUi : WindowMediatorSubscriberBase private int _lightfinderIconPresetIndex = -1; private bool _selectGeneralTabOnNextDraw = false; private bool _openLightfinderSectionOnNextDraw = false; + private static readonly LightlessConfig DefaultConfig = new(); private static readonly (string Label, SeIconChar Icon)[] LightfinderIconPresets = new[] { ("Link Marker", SeIconChar.LinkMarker), @@ -185,52 +186,102 @@ public class SettingsUi : WindowMediatorSubscriberBase DrawSettingsContent(); } - private static bool InputDtrColors(string label, ref DtrEntry.Colors colors) + private static Vector3 PackedColorToVector3(uint color) + => new( + (color & 0xFF) / 255f, + ((color >> 8) & 0xFF) / 255f, + ((color >> 16) & 0xFF) / 255f); + + private static uint Vector3ToPackedColor(Vector3 color) + { + static byte ToByte(float channel) + { + var scaled = MathF.Round(Math.Clamp(channel, 0f, 1f) * 255.0f); + return (byte)Math.Clamp((int)scaled, 0, 255); + } + + var r = ToByte(color.X); + var g = ToByte(color.Y); + var b = ToByte(color.Z); + return (uint)(r | (g << 8) | (b << 16)); + } + + private static bool DrawDtrColorEditors(ref DtrEntry.Colors colors) { - using var id = ImRaii.PushId(label); var innerSpacing = ImGui.GetStyle().ItemInnerSpacing.X; - var foregroundColor = ConvertColor(colors.Foreground); - var glowColor = ConvertColor(colors.Glow); + var foregroundColor = PackedColorToVector3(colors.Foreground); + var glowColor = PackedColorToVector3(colors.Glow); const ImGuiColorEditFlags colorFlags = ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel; - var ret = ImGui.ColorEdit3("###foreground", ref foregroundColor, colorFlags); + var changed = ImGui.ColorEdit3("###foreground", ref foregroundColor, colorFlags); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Foreground Color - Set to pure black (#000000) to use the default color"); ImGui.SameLine(0.0f, innerSpacing); - ret |= ImGui.ColorEdit3("###glow", ref glowColor, colorFlags); + changed |= ImGui.ColorEdit3("###glow", ref glowColor, colorFlags); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Glow Color - Set to pure black (#000000) to use the default color"); + if (changed) + colors = new(Vector3ToPackedColor(foregroundColor), Vector3ToPackedColor(glowColor)); + + return changed; + } + + private void DrawDtrColorRow(string id, string label, string description, ref DtrEntry.Colors colors, DtrEntry.Colors defaultDisplay, Action applyConfig) + { + ImGui.TableNextRow(); + + ImGui.TableSetColumnIndex(0); + using (ImRaii.PushId(id)) + { + var edited = DrawDtrColorEditors(ref colors); + ImGui.SameLine(0.0f, ImGui.GetStyle().ItemInnerSpacing.X); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(label); + + if (edited) + { + applyConfig(colors); + _configService.Save(); + } + } + + ImGui.TableSetColumnIndex(1); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(description); + + ImGui.TableSetColumnIndex(2); + using var resetId = ImRaii.PushId($"reset-{id}"); + var availableWidth = ImGui.GetContentRegionAvail().X; + var isDefault = colors == defaultDisplay; + + using (ImRaii.Disabled(isDefault)) + { + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + if (ImGui.Button(FontAwesomeIcon.Undo.ToIconString(), new Vector2(availableWidth, 0))) + { + colors = defaultDisplay; + applyConfig(defaultDisplay); + _configService.Save(); + } + } + } + + UiSharedService.AttachToolTip(isDefault ? "Colors already match the default value." : "Reset these colors to their default values."); + } + + private static bool InputDtrColors(string label, ref DtrEntry.Colors colors) + { + using var id = ImRaii.PushId(label); + var innerSpacing = ImGui.GetStyle().ItemInnerSpacing.X; + var ret = DrawDtrColorEditors(ref colors); + ImGui.SameLine(0.0f, innerSpacing); ImGui.TextUnformatted(label); - if (ret) - colors = new(ConvertBackColor(foregroundColor), ConvertBackColor(glowColor)); - return ret; - - static Vector3 ConvertColor(uint color) - { - var r = (color & 0xFF) / 255.0f; - var g = ((color >> 8) & 0xFF) / 255.0f; - var b = ((color >> 16) & 0xFF) / 255.0f; - return new(r, g, b); - } - - static uint ConvertBackColor(Vector3 color) - { - static byte ToByte(float channel) - { - var scaled = MathF.Round(Math.Clamp(channel, 0f, 1f) * 255.0f); - return (byte)Math.Clamp((int)scaled, 0, 255); - } - - var r = ToByte(color.X); - var g = ToByte(color.Y); - var b = ToByte(color.Z); - return (uint)(r | (g << 8) | (b << 16)); - } } private static DtrEntry.Colors SwapColorChannels(DtrEntry.Colors colors) @@ -1386,6 +1437,62 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + ImGui.TextUnformatted("Lightfinder Nameplate Colors"); + if (ImGui.BeginTable("##LightfinderColorTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) + { + ImGui.TableSetupColumn("Color", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40f); + ImGui.TableHeadersRow(); + + var lightfinderColors = new (string Key, string Label, string Description)[] + { + ("Lightfinder", "Nameplate Text", "Color used for Lightfinder nameplate text."), + ("LightfinderEdge", "Nameplate Outline", "Outline color applied around Lightfinder nameplate text.") + }; + + foreach (var (key, label, description) in lightfinderColors) + { + ImGui.TableNextRow(); + + ImGui.TableSetColumnIndex(0); + var colorValue = UIColors.Get(key); + if (ImGui.ColorEdit4($"##color_{key}", ref colorValue, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf)) + { + UIColors.Set(key, colorValue); + } + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(label); + + ImGui.TableSetColumnIndex(1); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(description); + + ImGui.TableSetColumnIndex(2); + using var resetId = ImRaii.PushId($"Reset_{key}"); + var availableWidth = ImGui.GetContentRegionAvail().X; + var isCustom = UIColors.IsCustom(key); + using (ImRaii.Disabled(!isCustom)) + { + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + if (ImGui.Button(FontAwesomeIcon.Undo.ToIconString(), new Vector2(availableWidth, 0))) + { + UIColors.Reset(key); + } + } + } + UiSharedService.AttachToolTip(isCustom ? "Reset this color to default" : "Color is already at default value"); + } + + ImGui.EndTable(); + } + + ImGui.Spacing(); + + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + ImGui.TextUnformatted("Lightfinder Info Bar"); if (ImGui.Checkbox("Show Lightfinder status in Server info bar", ref showLightfinderInDtr)) { @@ -1436,25 +1543,47 @@ public class SettingsUi : WindowMediatorSubscriberBase } ImGui.BeginDisabled(!showLightfinderInDtr || !useLightfinderColors); - if (InputDtrColors("Enabled", ref dtrLightfinderEnabled)) + const ImGuiTableFlags lightfinderInfoTableFlags = ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit; + if (ImGui.BeginTable("##LightfinderInfoBarColorTable", 3, lightfinderInfoTableFlags)) { - _configService.Current.DtrColorsLightfinderEnabled = SwapColorChannels(dtrLightfinderEnabled); - _configService.Save(); - } - if (InputDtrColors("Disabled", ref dtrLightfinderDisabled)) - { - _configService.Current.DtrColorsLightfinderDisabled = SwapColorChannels(dtrLightfinderDisabled); - _configService.Save(); - } - if (InputDtrColors("Cooldown", ref dtrLightfinderCooldown)) - { - _configService.Current.DtrColorsLightfinderCooldown = SwapColorChannels(dtrLightfinderCooldown); - _configService.Save(); - } - if (InputDtrColors("Unavailable", ref dtrLightfinderUnavailable)) - { - _configService.Current.DtrColorsLightfinderUnavailable = SwapColorChannels(dtrLightfinderUnavailable); - _configService.Save(); + ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.WidthFixed, 220f); + ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40f); + ImGui.TableHeadersRow(); + + DrawDtrColorRow( + "enabled", + "Enabled", + "Displayed when Lightfinder is active.", + ref dtrLightfinderEnabled, + SwapColorChannels(DefaultConfig.DtrColorsLightfinderEnabled), + value => _configService.Current.DtrColorsLightfinderEnabled = SwapColorChannels(value)); + + DrawDtrColorRow( + "disabled", + "Disabled", + "Shown when Lightfinder is turned off.", + ref dtrLightfinderDisabled, + SwapColorChannels(DefaultConfig.DtrColorsLightfinderDisabled), + value => _configService.Current.DtrColorsLightfinderDisabled = SwapColorChannels(value)); + + DrawDtrColorRow( + "cooldown", + "Cooldown", + "Displayed while Lightfinder is on cooldown.", + ref dtrLightfinderCooldown, + SwapColorChannels(DefaultConfig.DtrColorsLightfinderCooldown), + value => _configService.Current.DtrColorsLightfinderCooldown = SwapColorChannels(value)); + + DrawDtrColorRow( + "unavailable", + "Unavailable", + "Used when Lightfinder is not available on the current server.", + ref dtrLightfinderUnavailable, + SwapColorChannels(DefaultConfig.DtrColorsLightfinderUnavailable), + value => _configService.Current.DtrColorsLightfinderUnavailable = SwapColorChannels(value)); + + ImGui.EndTable(); } ImGui.EndDisabled(); @@ -1798,29 +1927,42 @@ public class SettingsUi : WindowMediatorSubscriberBase } _uiShared.DrawHelpText("This will color the Server Info Bar entry based on connection status and visible pairs."); - using (ImRaii.Disabled(!useColorsInDtr)) + ImGui.BeginDisabled(!useColorsInDtr); + const ImGuiTableFlags serverInfoTableFlags = ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit; + if (ImGui.BeginTable("##ServerInfoBarColorTable", 3, serverInfoTableFlags)) { - using var indent = ImRaii.PushIndent(); - if (InputDtrColors("Default", ref dtrColorsDefault)) - { - _configService.Current.DtrColorsDefault = dtrColorsDefault; - _configService.Save(); - } + ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.WidthFixed, 220f); + ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40f); + ImGui.TableHeadersRow(); - ImGui.SameLine(); - if (InputDtrColors("Not Connected", ref dtrColorsNotConnected)) - { - _configService.Current.DtrColorsNotConnected = dtrColorsNotConnected; - _configService.Save(); - } + DrawDtrColorRow( + "server-default", + "Default", + "Displayed when connected without any special status.", + ref dtrColorsDefault, + DefaultConfig.DtrColorsDefault, + value => _configService.Current.DtrColorsDefault = value); - ImGui.SameLine(); - if (InputDtrColors("Pairs in Range", ref dtrColorsPairsInRange)) - { - _configService.Current.DtrColorsPairsInRange = dtrColorsPairsInRange; - _configService.Save(); - } + DrawDtrColorRow( + "server-not-connected", + "Not Connected", + "Shown while disconnected from the Lightless server.", + ref dtrColorsNotConnected, + DefaultConfig.DtrColorsNotConnected, + value => _configService.Current.DtrColorsNotConnected = value); + + DrawDtrColorRow( + "server-pairs", + "Pairs in Range", + "Used when nearby paired players are detected.", + ref dtrColorsPairsInRange, + DefaultConfig.DtrColorsPairsInRange, + value => _configService.Current.DtrColorsPairsInRange = value); + + ImGui.EndTable(); } + ImGui.EndDisabled(); ImGui.Spacing(); diff --git a/LightlessSync/UI/UIColors.cs b/LightlessSync/UI/UIColors.cs index 3bd288f..993573d 100644 --- a/LightlessSync/UI/UIColors.cs +++ b/LightlessSync/UI/UIColors.cs @@ -26,6 +26,9 @@ namespace LightlessSync.UI { "LightlessAdminGlow", "#b09343" }, { "LightlessModeratorText", "#94ffda" }, { "LightlessModeratorGlow", "#599c84" }, + + { "Lightfinder", "#ad8af5" }, + { "LightfinderEdge", "#000000" }, }; private static LightlessConfigService? _configService; From 4f3ab604dbc92da7e4cbb73fd2db52ca1b496573 Mon Sep 17 00:00:00 2001 From: defnotken Date: Fri, 10 Oct 2025 18:03:57 -0500 Subject: [PATCH 5/5] update lightlessapi pointer --- LightlessAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessAPI b/LightlessAPI index 4a0b6c3..44fbe10 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 4a0b6c3e4dccb11b1df5af86d28b99f184455b01 +Subproject commit 44fbe1045872fcae4df45e43625a9ff1a79bc2ef