lightfinder-partial-rework #56

Merged
defnotken merged 6 commits from lightfinder-partial-rework into 1.12.2 2025-10-10 23:04:28 +00:00
19 changed files with 1455 additions and 359 deletions

View File

@@ -0,0 +1,7 @@
namespace LightlessSync.LightlessConfiguration.Configurations;
public enum LightfinderDtrDisplayMode
{
NearbyBroadcasts = 0,
PendingPairRequests = 1,
}

View File

@@ -22,6 +22,13 @@ 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 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;
@@ -75,6 +82,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;

View File

@@ -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);
}

View File

@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
namespace LightlessSync.LightlessConfiguration.Configurations;
[Serializable]
public class UiThemeConfig : ILightlessConfiguration
{
public Dictionary<string, UiStyleOverride> StyleOverrides { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public int Version { get; set; } = 1;
}

View File

@@ -0,0 +1,14 @@
using LightlessSync.LightlessConfiguration.Configurations;
namespace LightlessSync.LightlessConfiguration;
public class UiThemeConfigService : ConfigurationServiceBase<UiThemeConfig>
{
public const string ConfigName = "ui-theme.json";
public UiThemeConfigService(string configDir) : base(configDir)
{
}
public override string ConfigurationName => ConfigName;
}

View File

@@ -142,8 +142,18 @@ public sealed class Plugin : IDalamudPlugin
clientState, objectTable, framework, gameGui, condition, gameData, targetManager, gameConfig,
s.GetRequiredService<BlockedCharacterHandler>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(),
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<PlayerPerformanceConfigService>()));
collection.AddSingleton((s) => new DtrEntry(s.GetRequiredService<ILogger<DtrEntry>>(), dtrBar, s.GetRequiredService<LightlessConfigService>(),
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<ServerConfigurationManager>()));
collection.AddSingleton((s) => new DtrEntry(
s.GetRequiredService<ILogger<DtrEntry>>(),
dtrBar,
s.GetRequiredService<LightlessConfigService>(),
s.GetRequiredService<LightlessMediator>(),
s.GetRequiredService<PairManager>(),
s.GetRequiredService<PairRequestService>(),
s.GetRequiredService<ApiController>(),
s.GetRequiredService<ServerConfigurationManager>(),
s.GetRequiredService<BroadcastService>(),
s.GetRequiredService<BroadcastScannerService>(),
s.GetRequiredService<DalamudUtilService>()));
collection.AddSingleton(s => new PairManager(s.GetRequiredService<ILogger<PairManager>>(), s.GetRequiredService<PairFactory>(),
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), contextMenu, s.GetRequiredService<PairProcessingLimiter>()));
collection.AddSingleton<RedrawManager>();
@@ -182,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<UiThemeConfigService>();
LightlessSync.UI.Style.MainStyle.Init(cfg, theme);
return cfg;
});
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
@@ -197,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<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<LightlessConfigService>());
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<UiThemeConfigService>());
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<ServerConfigService>());
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<NotesConfigService>());
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<PairTagConfigService>());

View File

@@ -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);

View File

@@ -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<string?> 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<EnableBroadcastMessage>(this, OnEnableBroadcast);
_mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
_mediator.Subscribe<DisconnectedMessage>(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,47 +265,39 @@ 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;
try
{
TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false);
if (ttl is { } remaining && remaining > TimeSpan.Zero)
if (TryApplyBroadcastEnabled(ttl, "client request"))
{
_config.Current.BroadcastTtl = DateTime.UtcNow + remaining;
_config.Current.BroadcastEnabled = true;
_config.Save();
_logger.LogDebug("Fetched TTL from server: {TTL}", remaining);
_mediator.Publish(new BroadcastStatusChangedMessage(true, remaining));
_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.");
_config.Current.BroadcastEnabled = false;
_config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
}
}
finally
{
_waitingForTtlFetch = false;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to toggle broadcast for {Cid}", msg.HashedCid);
@@ -219,17 +337,24 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
return result;
}
public async Task<TimeSpan?> GetBroadcastTtlAsync(string cid)
public async Task<TimeSpan?> 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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -1,4 +1,5 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
@@ -209,7 +210,7 @@ namespace LightlessSync.UI
else
{
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
ImGui.Text("The Lightfinder<EFBFBD>s light wanes, but not in vain."); // cringe..
ImGui.Text("The Lightfinders light wanes, but not in vain."); // cringe..
ImGui.PopStyleColor();
}
}
@@ -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();
}

View File

@@ -1,56 +1,92 @@
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;
using LightlessSync.WebAPI.SignalR.Utils;
using LightlessSync.Utils;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Runtime.InteropServices;
using System.Text;
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();
private readonly ConfigurationServiceBase<LightlessConfig> _configService;
private readonly IDtrBar _dtrBar;
private readonly Lazy<IDtrBarEntry> _entry;
private readonly Lazy<IDtrBarEntry> _statusEntry;
private readonly Lazy<IDtrBarEntry> _lightfinderEntry;
private readonly ILogger<DtrEntry> _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? _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;
private string? _localHashedCid;
private DateTime _localHashedCidFetchedAt = DateTime.MinValue;
private DateTime _localHashedCidNextErrorLog = DateTime.MinValue;
private DateTime _pairRequestNextErrorLog = DateTime.MinValue;
public DtrEntry(ILogger<DtrEntry> logger, IDtrBar dtrBar, ConfigurationServiceBase<LightlessConfig> configService, LightlessMediator lightlessMediator, PairManager pairManager, ApiController apiController, ServerConfigurationManager serverManager)
public DtrEntry(
ILogger<DtrEntry> logger,
IDtrBar dtrBar,
ConfigurationServiceBase<LightlessConfig> configService,
LightlessMediator lightlessMediator,
PairManager pairManager,
PairRequestService pairRequestService,
ApiController apiController,
ServerConfigurationManager serverManager,
BroadcastService broadcastService,
BroadcastScannerService broadcastScannerService,
DalamudUtilService dalamudUtilService)
{
_logger = logger;
_dtrBar = dtrBar;
_entry = new(CreateEntry);
_statusEntry = new(CreateStatusEntry);
_lightfinderEntry = new(CreateLightfinderEntry);
_configService = configService;
_lightlessMediator = lightlessMediator;
_pairManager = pairManager;
_pairRequestService = pairRequestService;
_apiController = apiController;
_serverManager = serverManager;
_broadcastService = broadcastService;
_broadcastScannerService = broadcastScannerService;
_dalamudUtilService = dalamudUtilService;
}
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 +106,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
}
catch (OperationCanceledException)
{
// ignore cancelled
}
finally
{
@@ -80,33 +116,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))
{
if (interactionEvent.ModifierKeys.HasFlag(ClickModifierKeys.Shift))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
}
else
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(CompactUi)));
}
else if (interactionEvent.ClickType.Equals(MouseClickType.Left) && interactionEvent.ModifierKeys.Equals(ClickModifierKeys.Shift))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
return;
}
if (interactionEvent.ClickType.Equals(MouseClickType.Right))
@@ -131,6 +200,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,96 +223,278 @@ 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<string> 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<string> 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? 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} --", SwapColorChannels(config.DtrColorsLightfinderUnavailable), "Lightfinder - Unavailable on this server.");
}
if (_broadcastService.IsBroadcasting)
{
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 = SwapColorChannels(config.DtrColorsLightfinderDisabled);
if (_broadcastService.RemainingCooldown is { } cooldown && cooldown > TimeSpan.Zero)
{
tooltip.AppendLine();
tooltip.Append("Cooldown: ").Append(Math.Ceiling(cooldown.TotalSeconds)).Append("s");
colors = SwapColorChannels(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
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();
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();
}
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]);

View File

@@ -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<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
private readonly DalamudUtilService _dalamudUtilService;
private readonly HttpClient _httpClient;
@@ -73,6 +75,9 @@ 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 LightlessConfig DefaultConfig = new();
private static readonly (string Label, SeIconChar Icon)[] LightfinderIconPresets = new[]
{
("Link Marker", SeIconChar.LinkMarker),
@@ -92,7 +97,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
private bool _wasOpen = false;
public SettingsUi(ILogger<SettingsUi> logger,
UiSharedService uiShared, LightlessConfigService configService,
UiSharedService uiShared, LightlessConfigService configService, UiThemeConfigService themeConfigService,
PairManager pairManager,
ServerConfigurationManager serverConfigurationManager,
PlayerPerformanceConfigService playerPerformanceConfigService,
@@ -108,6 +113,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
NameplateHandler nameplateHandler) : base(logger, mediator, "Lightless Sync Settings", performanceCollector)
{
_configService = configService;
_themeConfigService = themeConfigService;
_pairManager = pairManager;
_serverConfigurationManager = serverConfigurationManager;
_playerPerformanceConfigService = playerPerformanceConfigService;
@@ -136,6 +142,12 @@ public class SettingsUi : WindowMediatorSubscriberBase
};
Mediator.Subscribe<OpenSettingsUiMessage>(this, (_) => Toggle());
Mediator.Subscribe<OpenLightfinderSettingsMessage>(this, (_) =>
{
IsOpen = true;
_selectGeneralTabOnNextDraw = true;
_openLightfinderSectionOnNextDraw = true;
});
Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) => IsOpen = false);
Mediator.Subscribe<CutsceneStartMessage>(this, (_) => UiSharedService_GposeStart());
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd());
@@ -174,35 +186,360 @@ public class SettingsUi : WindowMediatorSubscriberBase
DrawSettingsContent();
}
private static bool InputDtrColors(string label, 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);
private static Vector3 PackedColorToVector3(uint color)
=> new(
(color & 0xFF) / 255f,
((color >> 8) & 0xFF) / 255f,
((color >> 16) & 0xFF) / 255f);
var ret = ImGui.ColorEdit3("###foreground", ref foregroundColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel | ImGuiColorEditFlags.Uint8);
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)
{
var innerSpacing = ImGui.GetStyle().ItemInnerSpacing.X;
var foregroundColor = PackedColorToVector3(colors.Foreground);
var glowColor = PackedColorToVector3(colors.Glow);
const ImGuiColorEditFlags colorFlags = ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel;
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, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel | ImGuiColorEditFlags.Uint8);
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<DtrEntry.Colors> 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)
=> unchecked(new((byte)color / 255.0f, (byte)(color >> 8) / 255.0f, (byte)(color >> 16) / 255.0f));
private static DtrEntry.Colors SwapColorChannels(DtrEntry.Colors colors)
=> new(SwapColorChannels(colors.Foreground), SwapColorChannels(colors.Glow));
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);
private static uint SwapColorChannels(uint color)
{
if (color == 0)
return 0;
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<UiStyleOverride> 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()
@@ -1064,12 +1401,193 @@ 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 = 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))
{
_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 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))
{
_configService.Current.ShowLightfinderInDtr = showLightfinderInDtr;
_configService.Save();
}
_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<LightfinderDtrDisplayMode>())
{
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))
{
_configService.Current.UseLightfinderColorsInDtr = useLightfinderColors;
_configService.Save();
}
ImGui.BeginDisabled(!showLightfinderInDtr || !useLightfinderColors);
const ImGuiTableFlags lightfinderInfoTableFlags = ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit;
if (ImGui.BeginTable("##LightfinderInfoBarColorTable", 3, lightfinderInfoTableFlags))
{
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();
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TextUnformatted("Alignment");
ImGui.BeginDisabled(autoAlign);
@@ -1421,29 +1939,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();
@@ -1515,6 +2046,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();
}
@@ -2635,8 +3168,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();
}

View File

@@ -1,21 +1,119 @@
// 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
{
public readonly record struct StyleColorOption(string Key, string Label, Func<Vector4> 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<Vector2> DefaultValue, ImGuiStyleVar Target, Vector2? Min = null, Vector2? Max = null, float Speed = 0.25f, string? Description = null);
private static LightlessConfigService? _config;
public static void Init(LightlessConfigService config) => _config = config;
private static UiThemeConfigService? _themeConfig;
public static void Init(LightlessConfigService config, UiThemeConfigService themeConfig)
{
_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<StyleColorOption> ColorOptions => _colorOptions;
public static IReadOnlyList<StyleFloatOption> FloatOptions => _floatOptions;
public static IReadOnlyList<StyleVector2Option> Vector2Options => _vector2Options;
public static void PushStyle()
{
if (_hasPushed)
@@ -31,97 +129,14 @@ namespace LightlessSync.UI.Style
_pushedColorCount = 0;
_pushedStyleVarCount = 0;
Push(ImGuiCol.Text, new Vector4(255, 255, 255, 255));
Push(ImGuiCol.TextDisabled, new Vector4(128, 128, 128, 255));
foreach (var option in _colorOptions)
Push(option.Target, ResolveColor(option));
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));
foreach (var option in _vector2Options)
PushStyleVar(option.Target, ResolveVector(option));
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);
foreach (var option in _floatOptions)
PushStyleVar(option.Target, ResolveFloat(option));
}
public static void PopStyle()
@@ -139,18 +154,49 @@ namespace LightlessSync.UI.Style
_pushedStyleVarCount = 0;
}
private static void Push(ImGuiCol col, Vector4 rgba)
private static Vector4 ResolveColor(StyleColorOption option)
{
if (rgba.X > 1f || rgba.Y > 1f || rgba.Z > 1f || rgba.W > 1f)
rgba /= 255f;
var defaultValue = NormalizeColorVector(option.DefaultValue());
if (_themeConfig?.Current.StyleOverrides.TryGetValue(option.Key, out var overrideValue) == true && overrideValue.Color is { } packed)
return PackedColorToVector4(packed);
ImGui.PushStyleColor(col, rgba);
_pushedColorCount++;
return defaultValue;
}
private static void Push(ImGuiCol col, uint packedRgba)
private static Vector2 ResolveVector(StyleVector2Option option)
{
ImGui.PushStyleColor(col, packedRgba);
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++;
}
@@ -165,5 +211,21 @@ namespace LightlessSync.UI.Style
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);
}

View File

@@ -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;

View File

@@ -26,6 +26,9 @@ namespace LightlessSync.UI
{ "LightlessAdminGlow", "#b09343" },
{ "LightlessModeratorText", "#94ffda" },
{ "LightlessModeratorGlow", "#599c84" },
{ "Lightfinder", "#ad8af5" },
{ "LightfinderEdge", "#000000" },
};
private static LightlessConfigService? _configService;

View File

@@ -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<BroadcastStatusInfoDto?> IsUserBroadcasting(string hashedCid)
@@ -59,10 +59,10 @@ public partial class ApiController
return await _lightlessHub!.InvokeAsync<BroadcastStatusBatchDto>(nameof(AreUsersBroadcasting), hashedCids).ConfigureAwait(false);
}
public async Task<TimeSpan?> GetBroadcastTtl(string hashedCid)
public async Task<TimeSpan?> GetBroadcastTtl()
{
CheckConnection();
return await _lightlessHub!.InvokeAsync<TimeSpan?>(nameof(GetBroadcastTtl), hashedCid).ConfigureAwait(false);
return await _lightlessHub!.InvokeAsync<TimeSpan?>(nameof(GetBroadcastTtl)).ConfigureAwait(false);
}
public async Task UserDelete()