Merge branch '1.12.2' into pair-notifs-ui
# Conflicts: # LightlessSync/UI/SettingsUi.cs
This commit is contained in:
Submodule LightlessAPI updated: 6c542c0ccc...44fbe10458
@@ -16,6 +16,8 @@ public sealed class FileCacheManager : IHostedService
|
||||
public const string CachePrefix = "{cache}";
|
||||
public const string CsvSplit = "|";
|
||||
public const string PenumbraPrefix = "{penumbra}";
|
||||
private const int FileCacheVersion = 1;
|
||||
private const string FileCacheVersionHeaderPrefix = "#lightless-file-cache-version:";
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly string _csvPath;
|
||||
@@ -54,6 +56,62 @@ public sealed class FileCacheManager : IHostedService
|
||||
return NormalizeSeparators(prefixedPath).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static bool TryBuildPrefixedPath(string path, string? baseDirectory, string prefix, out string prefixedPath, out int matchedLength)
|
||||
{
|
||||
prefixedPath = string.Empty;
|
||||
matchedLength = 0;
|
||||
|
||||
if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(baseDirectory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalizedPath = NormalizeSeparators(path).ToLowerInvariant();
|
||||
var normalizedBase = NormalizeSeparators(baseDirectory).TrimEnd('\\').ToLowerInvariant();
|
||||
|
||||
if (!normalizedPath.StartsWith(normalizedBase, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (normalizedPath.Length > normalizedBase.Length)
|
||||
{
|
||||
if (normalizedPath[normalizedBase.Length] != '\\')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
prefixedPath = prefix + normalizedPath.Substring(normalizedBase.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
prefixedPath = prefix;
|
||||
}
|
||||
|
||||
prefixedPath = prefixedPath.Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||
matchedLength = normalizedBase.Length;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string BuildVersionHeader() => $"{FileCacheVersionHeaderPrefix}{FileCacheVersion}";
|
||||
|
||||
private static bool TryParseVersionHeader(string? line, out int version)
|
||||
{
|
||||
version = 0;
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!line.StartsWith(FileCacheVersionHeaderPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var versionSpan = line.AsSpan(FileCacheVersionHeaderPrefix.Length);
|
||||
return int.TryParse(versionSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out version);
|
||||
}
|
||||
|
||||
private string NormalizeToPrefixedPath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) return string.Empty;
|
||||
@@ -66,27 +124,25 @@ public sealed class FileCacheManager : IHostedService
|
||||
return NormalizePrefixedPathKey(normalized);
|
||||
}
|
||||
|
||||
var penumbraDir = _ipcManager.Penumbra.ModDirectory;
|
||||
if (!string.IsNullOrEmpty(penumbraDir))
|
||||
string? chosenPrefixed = null;
|
||||
var chosenLength = -1;
|
||||
|
||||
if (TryBuildPrefixedPath(normalized, _ipcManager.Penumbra.ModDirectory, PenumbraPrefix, out var penumbraPrefixed, out var penumbraMatch))
|
||||
{
|
||||
var normalizedPenumbra = NormalizeSeparators(penumbraDir);
|
||||
var replacement = normalizedPenumbra.EndsWith("\\", StringComparison.Ordinal)
|
||||
? PenumbraPrefix + "\\"
|
||||
: PenumbraPrefix;
|
||||
normalized = normalized.Replace(normalizedPenumbra, replacement, StringComparison.OrdinalIgnoreCase);
|
||||
chosenPrefixed = penumbraPrefixed;
|
||||
chosenLength = penumbraMatch;
|
||||
}
|
||||
|
||||
var cacheFolder = _configService.Current.CacheFolder;
|
||||
if (!string.IsNullOrEmpty(cacheFolder))
|
||||
if (TryBuildPrefixedPath(normalized, _configService.Current.CacheFolder, CachePrefix, out var cachePrefixed, out var cacheMatch))
|
||||
{
|
||||
var normalizedCache = NormalizeSeparators(cacheFolder);
|
||||
var replacement = normalizedCache.EndsWith("\\", StringComparison.Ordinal)
|
||||
? CachePrefix + "\\"
|
||||
: CachePrefix;
|
||||
normalized = normalized.Replace(normalizedCache, replacement, StringComparison.OrdinalIgnoreCase);
|
||||
if (cacheMatch > chosenLength)
|
||||
{
|
||||
chosenPrefixed = cachePrefixed;
|
||||
chosenLength = cacheMatch;
|
||||
}
|
||||
}
|
||||
|
||||
return NormalizePrefixedPathKey(normalized);
|
||||
return NormalizePrefixedPathKey(chosenPrefixed ?? normalized);
|
||||
}
|
||||
|
||||
public FileCacheEntity? CreateCacheEntry(string path)
|
||||
@@ -94,7 +150,9 @@ public sealed class FileCacheManager : IHostedService
|
||||
FileInfo fi = new(path);
|
||||
if (!fi.Exists) return null;
|
||||
_logger.LogTrace("Creating cache entry for {path}", path);
|
||||
return CreateFileEntity(_configService.Current.CacheFolder.ToLowerInvariant(), CachePrefix, fi);
|
||||
var cacheFolder = _configService.Current.CacheFolder;
|
||||
if (string.IsNullOrEmpty(cacheFolder)) return null;
|
||||
return CreateFileEntity(cacheFolder, CachePrefix, fi);
|
||||
}
|
||||
|
||||
public FileCacheEntity? CreateFileEntry(string path)
|
||||
@@ -102,14 +160,18 @@ public sealed class FileCacheManager : IHostedService
|
||||
FileInfo fi = new(path);
|
||||
if (!fi.Exists) return null;
|
||||
_logger.LogTrace("Creating file entry for {path}", path);
|
||||
return CreateFileEntity(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), PenumbraPrefix, fi);
|
||||
var modDirectory = _ipcManager.Penumbra.ModDirectory;
|
||||
if (string.IsNullOrEmpty(modDirectory)) return null;
|
||||
return CreateFileEntity(modDirectory, PenumbraPrefix, fi);
|
||||
}
|
||||
|
||||
private FileCacheEntity? CreateFileEntity(string directory, string prefix, FileInfo fi)
|
||||
{
|
||||
var fullName = fi.FullName.ToLowerInvariant();
|
||||
if (!fullName.Contains(directory, StringComparison.Ordinal)) return null;
|
||||
string prefixedPath = fullName.Replace(directory, prefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||
if (!TryBuildPrefixedPath(fi.FullName, directory, prefix, out var prefixedPath, out _))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return CreateFileCacheEntity(fi, prefixedPath);
|
||||
}
|
||||
|
||||
@@ -367,6 +429,7 @@ public sealed class FileCacheManager : IHostedService
|
||||
lock (_fileWriteLock)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine(BuildVersionHeader());
|
||||
foreach (var entry in _fileCaches.Values.SelectMany(k => k.Values).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
sb.AppendLine(entry.CsvEntry);
|
||||
@@ -389,6 +452,53 @@ public sealed class FileCacheManager : IHostedService
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureCsvHeaderLocked()
|
||||
{
|
||||
if (!File.Exists(_csvPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string[] existingLines = File.ReadAllLines(_csvPath);
|
||||
if (existingLines.Length > 0 && TryParseVersionHeader(existingLines[0], out var existingVersion) && existingVersion == FileCacheVersion)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder rebuilt = new();
|
||||
rebuilt.AppendLine(BuildVersionHeader());
|
||||
foreach (var line in existingLines)
|
||||
{
|
||||
if (TryParseVersionHeader(line, out _))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(line))
|
||||
{
|
||||
rebuilt.AppendLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
File.WriteAllText(_csvPath, rebuilt.ToString());
|
||||
}
|
||||
|
||||
private void BackupUnsupportedCache(string suffix)
|
||||
{
|
||||
var sanitizedSuffix = string.IsNullOrWhiteSpace(suffix) ? "unsupported" : $"{suffix}.unsupported";
|
||||
var backupPath = _csvPath + "." + sanitizedSuffix;
|
||||
|
||||
try
|
||||
{
|
||||
File.Move(_csvPath, backupPath, overwrite: true);
|
||||
_logger.LogWarning("Backed up unsupported file cache to {path}", backupPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to back up unsupported file cache to {path}", backupPath);
|
||||
}
|
||||
}
|
||||
|
||||
internal FileCacheEntity MigrateFileHashToExtension(FileCacheEntity fileCache, string ext)
|
||||
{
|
||||
try
|
||||
@@ -427,8 +537,16 @@ public sealed class FileCacheManager : IHostedService
|
||||
AddHashedFile(entity);
|
||||
lock (_fileWriteLock)
|
||||
{
|
||||
if (!File.Exists(_csvPath))
|
||||
{
|
||||
File.WriteAllLines(_csvPath, new[] { BuildVersionHeader(), entity.CsvEntry });
|
||||
}
|
||||
else
|
||||
{
|
||||
EnsureCsvHeaderLocked();
|
||||
File.AppendAllLines(_csvPath, new[] { entity.CsvEntry });
|
||||
}
|
||||
}
|
||||
var result = GetFileCacheByPath(fileInfo.FullName);
|
||||
_logger.LogTrace("Creating cache entity for {name} success: {success}", fileInfo.FullName, (result != null));
|
||||
return result;
|
||||
@@ -546,16 +664,67 @@ public sealed class FileCacheManager : IHostedService
|
||||
_logger.LogWarning("Could not load entries from {path}, continuing with empty file cache", _csvPath);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Found {amount} files in {path}", entries.Length, _csvPath);
|
||||
bool rewriteRequired = false;
|
||||
bool parseEntries = entries.Length > 0;
|
||||
int startIndex = 0;
|
||||
|
||||
Dictionary<string, bool> processedFiles = new(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var entry in entries)
|
||||
if (entries.Length > 0)
|
||||
{
|
||||
var headerLine = entries[0];
|
||||
var hasHeader = !string.IsNullOrEmpty(headerLine) &&
|
||||
headerLine.StartsWith(FileCacheVersionHeaderPrefix, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (hasHeader)
|
||||
{
|
||||
if (!TryParseVersionHeader(headerLine, out var parsedVersion))
|
||||
{
|
||||
_logger.LogWarning("Failed to parse file cache version header \"{header}\". Backing up existing cache.", headerLine);
|
||||
BackupUnsupportedCache("invalid-version");
|
||||
parseEntries = false;
|
||||
rewriteRequired = true;
|
||||
entries = Array.Empty<string>();
|
||||
}
|
||||
else if (parsedVersion != FileCacheVersion)
|
||||
{
|
||||
_logger.LogWarning("Unsupported file cache version {version} detected (expected {expected}). Backing up existing cache.", parsedVersion, FileCacheVersion);
|
||||
BackupUnsupportedCache($"v{parsedVersion}");
|
||||
parseEntries = false;
|
||||
rewriteRequired = true;
|
||||
entries = Array.Empty<string>();
|
||||
}
|
||||
else
|
||||
{
|
||||
startIndex = 1;
|
||||
}
|
||||
}
|
||||
else if (entries.Length > 0)
|
||||
{
|
||||
_logger.LogInformation("File cache missing version header, scheduling rewrite.");
|
||||
rewriteRequired = true;
|
||||
}
|
||||
}
|
||||
|
||||
var totalEntries = Math.Max(0, entries.Length - startIndex);
|
||||
Dictionary<string, bool> processedFiles = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (parseEntries && totalEntries > 0)
|
||||
{
|
||||
_logger.LogInformation("Found {amount} files in {path}", totalEntries, _csvPath);
|
||||
|
||||
for (var index = startIndex; index < entries.Length; index++)
|
||||
{
|
||||
var entry = entries[index];
|
||||
if (string.IsNullOrWhiteSpace(entry))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var splittedEntry = entry.Split(CsvSplit, StringSplitOptions.None);
|
||||
try
|
||||
{
|
||||
var hash = splittedEntry[0];
|
||||
if (hash.Length != 40) throw new InvalidOperationException("Expected Hash length of 40, received " + hash.Length);
|
||||
if (hash.Length != 40)
|
||||
throw new InvalidOperationException("Expected Hash length of 40, received " + hash.Length);
|
||||
var path = splittedEntry[1];
|
||||
var time = splittedEntry[2];
|
||||
|
||||
@@ -575,7 +744,8 @@ public sealed class FileCacheManager : IHostedService
|
||||
{
|
||||
size = result;
|
||||
}
|
||||
if (long.TryParse(splittedEntry[4], CultureInfo.InvariantCulture, out long resultCompressed))
|
||||
if (splittedEntry.Length > 4 &&
|
||||
long.TryParse(splittedEntry[4], CultureInfo.InvariantCulture, out long resultCompressed))
|
||||
{
|
||||
compressed = resultCompressed;
|
||||
}
|
||||
@@ -588,7 +758,17 @@ public sealed class FileCacheManager : IHostedService
|
||||
}
|
||||
}
|
||||
|
||||
if (processedFiles.Count != entries.Length)
|
||||
if (processedFiles.Count != totalEntries)
|
||||
{
|
||||
rewriteRequired = true;
|
||||
}
|
||||
}
|
||||
else if (!parseEntries && entries.Length > 0)
|
||||
{
|
||||
_logger.LogInformation("Skipping existing file cache entries due to incompatible version.");
|
||||
}
|
||||
|
||||
if (rewriteRequired)
|
||||
{
|
||||
WriteOutFullCsv();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
public enum LightfinderDtrDisplayMode
|
||||
{
|
||||
NearbyBroadcasts = 0,
|
||||
PendingPairRequests = 1,
|
||||
}
|
||||
@@ -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;
|
||||
@@ -119,11 +126,13 @@ 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;
|
||||
public bool LightfinderLabelShowOwn { get; set; } = true;
|
||||
public bool LightfinderLabelShowPaired { get; set; } = true;
|
||||
public bool LightfinderLabelShowHidden { get; set; } = false;
|
||||
public string LightfinderLabelIconGlyph { get; set; } = SeIconCharExtensions.ToIconString(SeIconChar.Hyadelyn);
|
||||
public float LightfinderLabelScale { get; set; } = 1.0f;
|
||||
public bool LightfinderAutoAlign { get; set; } = true;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
14
LightlessSync/LightlessConfiguration/UiThemeConfigService.cs
Normal file
14
LightlessSync/LightlessConfiguration/UiThemeConfigService.cs
Normal 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;
|
||||
}
|
||||
@@ -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>();
|
||||
@@ -186,10 +196,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));
|
||||
@@ -201,6 +213,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>());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -43,7 +43,7 @@ public sealed class CommandManagerService : IDisposable
|
||||
"\t /light gpose - Opens the Lightless Character Data Hub window" + Environment.NewLine +
|
||||
"\t /light analyze - Opens the Lightless Character Data Analysis window" + Environment.NewLine +
|
||||
"\t /light settings - Opens the Lightless Settings window" + Environment.NewLine +
|
||||
"\t /light lightfinder - Opens the Lightfinder window"
|
||||
"\t /light finder - Opens the Lightfinder window"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ public sealed class CommandManagerService : IDisposable
|
||||
{
|
||||
_mediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
|
||||
}
|
||||
else if (string.Equals(splitArgs[0], "lightfinder", StringComparison.OrdinalIgnoreCase))
|
||||
else if (string.Equals(splitArgs[0], "finder", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_mediator.Publish(new UiToggleMessage(typeof(BroadcastUI)));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -247,7 +247,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
var pNameplateIconNode = nameplateObject.MarkerIcon;
|
||||
var pNameplateResNode = nameplateObject.NameContainer;
|
||||
var pNameplateTextNode = nameplateObject.NameText;
|
||||
bool IsVisible = pNameplateIconNode->AtkResNode.IsVisible() || (pNameplateResNode->IsVisible() && pNameplateTextNode->AtkResNode.IsVisible());
|
||||
bool IsVisible = pNameplateIconNode->AtkResNode.IsVisible() || (pNameplateResNode->IsVisible() && pNameplateTextNode->AtkResNode.IsVisible()) || _configService.Current.LightfinderLabelShowHidden;
|
||||
pNode->AtkResNode.ToggleVisibility(IsVisible);
|
||||
|
||||
var nameContainer = nameplateObject.NameContainer;
|
||||
@@ -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);
|
||||
@@ -360,33 +360,35 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
}
|
||||
int positionX;
|
||||
|
||||
if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset)
|
||||
{
|
||||
var nameplateWidth = (int)nameContainer->Width;
|
||||
|
||||
if (!config.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
|
||||
labelContent = DefaultLabelText;
|
||||
|
||||
pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed;
|
||||
|
||||
pNode->SetText(labelContent);
|
||||
|
||||
if (!config.LightfinderLabelUseIcon)
|
||||
{
|
||||
pNode->TextFlags &= ~TextFlags.AutoAdjustNodeSize;
|
||||
pNode->AtkResNode.Width = 0;
|
||||
pNode->SetText(labelContent);
|
||||
|
||||
nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
||||
if (nodeWidth <= 0)
|
||||
nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||
|
||||
if (nodeWidth > nameplateWidth)
|
||||
nodeWidth = nameplateWidth;
|
||||
|
||||
pNode->AtkResNode.Width = (ushort)nodeWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
pNode->TextFlags |= TextFlags.AutoAdjustNodeSize;
|
||||
pNode->AtkResNode.Width = 0;
|
||||
pNode->SetText(labelContent);
|
||||
nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
||||
nodeWidth = pNode->AtkResNode.GetWidth();
|
||||
}
|
||||
|
||||
|
||||
if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset)
|
||||
{
|
||||
var nameplateWidth = (int)nameContainer->Width;
|
||||
|
||||
int leftPos = nameplateWidth / 8;
|
||||
int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8);
|
||||
int centrePos = (nameplateWidth - nodeWidth) / 2;
|
||||
|
||||
@@ -607,8 +607,9 @@ public class ServerConfigurationManager
|
||||
{
|
||||
var baseUri = serverUri.Replace("wss://", "https://").Replace("ws://", "http://");
|
||||
var oauthCheckUri = LightlessAuth.GetUIDsFullPath(new Uri(baseUri));
|
||||
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
var response = await _httpClient.GetAsync(oauthCheckUri).ConfigureAwait(false);
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, oauthCheckUri);
|
||||
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
using var response = await _httpClient.SendAsync(request).ConfigureAwait(false);
|
||||
var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
return await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(responseStream).ConfigureAwait(false) ?? [];
|
||||
}
|
||||
|
||||
@@ -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 Lightfinder’s 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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,10 +45,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
private readonly ApiController _apiController;
|
||||
private readonly CacheMonitor _cacheMonitor;
|
||||
private readonly LightlessConfigService _configService;
|
||||
|
||||
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads =
|
||||
new();
|
||||
|
||||
private readonly UiThemeConfigService _themeConfigService;
|
||||
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
@@ -77,6 +76,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[]
|
||||
{
|
||||
@@ -92,7 +94,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,
|
||||
@@ -110,6 +112,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
performanceCollector)
|
||||
{
|
||||
_configService = configService;
|
||||
_themeConfigService = themeConfigService;
|
||||
_pairManager = pairManager;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||
@@ -138,6 +141,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());
|
||||
@@ -177,39 +186,360 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
|
||||
DrawSettingsContent();
|
||||
}
|
||||
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)
|
||||
{
|
||||
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);
|
||||
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 foregroundColor = ConvertColor(colors.Foreground);
|
||||
var glowColor = ConvertColor(colors.Glow);
|
||||
|
||||
var ret = ImGui.ColorEdit3("###foreground", ref foregroundColor,
|
||||
ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel | ImGuiColorEditFlags.Uint8);
|
||||
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);
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip("Glow Color - Set to pure black (#000000) to use the default color");
|
||||
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()
|
||||
@@ -1187,12 +1517,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);
|
||||
@@ -1344,6 +1855,17 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
|
||||
_uiShared.DrawHelpText("Toggles paired player(s) Lightfinder label.");
|
||||
|
||||
var showHidden = _configService.Current.LightfinderLabelShowHidden;
|
||||
if (ImGui.Checkbox("Show Lightfinder label when no nameplate(s) is visible", ref showHidden))
|
||||
{
|
||||
_configService.Current.LightfinderLabelShowHidden = showHidden;
|
||||
_configService.Save();
|
||||
_nameplateHandler.ClearNameplateCaches();
|
||||
_nameplateHandler.FlagRefresh();
|
||||
_nameplateService.RequestRedraw();
|
||||
}
|
||||
_uiShared.DrawHelpText("Toggles Lightfinder label when no nameplate(s) is visible.");
|
||||
|
||||
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
|
||||
|
||||
ImGui.TextUnformatted("Label");
|
||||
@@ -1389,7 +1911,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
var selected = i == _lightfinderIconPresetIndex;
|
||||
if (ImGui.Selectable(preview, selected))
|
||||
{
|
||||
ApplyLightfinderIcon(optionGlyph, i);
|
||||
_lightfinderIconInput = NameplateHandler.ToIconEditorString(optionGlyph);
|
||||
_lightfinderIconPresetIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1550,29 +2073,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();
|
||||
|
||||
@@ -1649,6 +2185,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();
|
||||
}
|
||||
@@ -2872,14 +3410,21 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Lightless Sync Discord"))
|
||||
{
|
||||
Util.OpenLink("https://discord.gg/mpNdkrTRjW");
|
||||
Util.OpenLink("https://discord.gg/Lightless");
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -462,7 +462,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;
|
||||
|
||||
@@ -26,6 +26,9 @@ namespace LightlessSync.UI
|
||||
{ "LightlessAdminGlow", "#b09343" },
|
||||
{ "LightlessModeratorText", "#94ffda" },
|
||||
{ "LightlessModeratorGlow", "#599c84" },
|
||||
|
||||
{ "Lightfinder", "#ad8af5" },
|
||||
{ "LightfinderEdge", "#000000" },
|
||||
};
|
||||
|
||||
private static LightlessConfigService? _configService;
|
||||
|
||||
@@ -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()
|
||||
|
||||
Submodule PenumbraAPI updated: dd14131793...648b6fc2ce
Reference in New Issue
Block a user