Files
LightlessClient/LightlessSync/UI/SettingsUi.cs

4343 lines
196 KiB
C#

using Dalamud.Bindings.ImGui;
using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Utility;
using LightlessSync.API.Data;
using LightlessSync.API.Data.Comparer;
using LightlessSync.API.Routes;
using LightlessSync.FileCache;
using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Configurations;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Handlers;
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;
using LightlessSync.WebAPI.Files;
using LightlessSync.WebAPI.Files.Models;
using LightlessSync.WebAPI.SignalR.Utils;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Numerics;
using System.Text;
using System.Text.Json;
namespace LightlessSync.UI;
public class SettingsUi : WindowMediatorSubscriberBase
{
private readonly ApiController _apiController;
private readonly CacheMonitor _cacheMonitor;
private readonly LightlessConfigService _configService;
private readonly UiThemeConfigService _themeConfigService;
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
private readonly DalamudUtilService _dalamudUtilService;
private readonly HttpClient _httpClient;
private readonly FileCacheManager _fileCacheManager;
private readonly FileCompactor _fileCompactor;
private readonly FileUploadManager _fileTransferManager;
private readonly FileTransferOrchestrator _fileTransferOrchestrator;
private readonly IpcManager _ipcManager;
private readonly PairManager _pairManager;
private readonly PerformanceCollectorService _performanceCollector;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly PairProcessingLimiter _pairProcessingLimiter;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly UiSharedService _uiShared;
private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress;
private readonly NameplateService _nameplateService;
private readonly NameplateHandler _nameplateHandler;
private (int, int, FileCacheEntity) _currentProgress;
private bool _deleteAccountPopupModalShown = false;
private bool _deleteFilesPopupModalShown = false;
private string _lastTab = string.Empty;
private bool? _notesSuccessfullyApplied = null;
private bool _overwriteExistingLabels = false;
private bool _readClearCache = false;
private int _selectedEntry = -1;
private string _uidToAddForIgnore = string.Empty;
private string _lightfinderIconInput = string.Empty;
private bool _lightfinderIconInputInitialized = false;
private int _lightfinderIconPresetIndex = -1;
private bool _selectGeneralTabOnNextDraw = false;
private bool _openLightfinderSectionOnNextDraw = false;
private static readonly LightlessConfig DefaultConfig = new();
private static readonly (string Label, SeIconChar Icon)[] LightfinderIconPresets = new[]
{
("Link Marker", SeIconChar.LinkMarker), ("Hyadelyn", SeIconChar.Hyadelyn), ("Gil", SeIconChar.Gil),
("Quest Sync", SeIconChar.QuestSync), ("Glamoured", SeIconChar.Glamoured),
("Glamoured (Dyed)", SeIconChar.GlamouredDyed), ("Auto-Translate Open", SeIconChar.AutoTranslateOpen),
("Auto-Translate Close", SeIconChar.AutoTranslateClose), ("Boxed Star", SeIconChar.BoxedStar),
("Boxed Plus", SeIconChar.BoxedPlus)
};
private CancellationTokenSource? _validationCts;
private Task<List<FileCacheEntity>>? _validationTask;
private bool _wasOpen = false;
public SettingsUi(ILogger<SettingsUi> logger,
UiSharedService uiShared, LightlessConfigService configService, UiThemeConfigService themeConfigService,
PairManager pairManager,
ServerConfigurationManager serverConfigurationManager,
PlayerPerformanceConfigService playerPerformanceConfigService,
PairProcessingLimiter pairProcessingLimiter,
LightlessMediator mediator, PerformanceCollectorService performanceCollector,
FileUploadManager fileTransferManager,
FileTransferOrchestrator fileTransferOrchestrator,
FileCacheManager fileCacheManager,
FileCompactor fileCompactor, ApiController apiController,
IpcManager ipcManager, CacheMonitor cacheMonitor,
DalamudUtilService dalamudUtilService, HttpClient httpClient,
NameplateService nameplateService,
NameplateHandler nameplateHandler) : base(logger, mediator, "Lightless Sync Settings",
performanceCollector)
{
_configService = configService;
_themeConfigService = themeConfigService;
_pairManager = pairManager;
_serverConfigurationManager = serverConfigurationManager;
_playerPerformanceConfigService = playerPerformanceConfigService;
_pairProcessingLimiter = pairProcessingLimiter;
_performanceCollector = performanceCollector;
_fileTransferManager = fileTransferManager;
_fileTransferOrchestrator = fileTransferOrchestrator;
_fileCacheManager = fileCacheManager;
_apiController = apiController;
_ipcManager = ipcManager;
_cacheMonitor = cacheMonitor;
_dalamudUtilService = dalamudUtilService;
_httpClient = httpClient;
_fileCompactor = fileCompactor;
_uiShared = uiShared;
_nameplateService = nameplateService;
_nameplateHandler = nameplateHandler;
AllowClickthrough = false;
AllowPinning = true;
_validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v);
SizeConstraints = new WindowSizeConstraints()
{
MinimumSize = new Vector2(800, 400), MaximumSize = new Vector2(800, 2000),
};
TitleBarButtons = new()
{
new TitleBarButton()
{
Icon = FontAwesomeIcon.FileAlt,
Click = (msg) =>
{
Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi)));
},
IconOffset = new(2, 1),
ShowTooltip = () =>
{
ImGui.BeginTooltip();
ImGui.Text("View Update Notes");
ImGui.EndTooltip();
}
}
};
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());
Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) => LastCreatedCharacterData = msg.CharacterData);
Mediator.Subscribe<DownloadStartedMessage>(this,
(msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
_nameplateService = nameplateService;
}
public CharacterData? LastCreatedCharacterData { private get; set; }
private ApiController ApiController => _uiShared.ApiController;
public override void OnOpen()
{
_uiShared.ResetOAuthTasksState();
_speedTestCts = new();
}
public override void OnClose()
{
_uiShared.EditTrackerPosition = false;
_uidToAddForIgnore = string.Empty;
_secretKeysConversionCts = _secretKeysConversionCts.CancelRecreate();
_downloadServersTask = null;
_speedTestTask = null;
_speedTestCts?.Cancel();
_speedTestCts?.Dispose();
_speedTestCts = null;
base.OnClose();
}
protected override void DrawInternal()
{
_ = _uiShared.DrawOtherPluginState();
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 ret = DrawDtrColorEditors(ref colors);
ImGui.SameLine(0.0f, innerSpacing);
ImGui.TextUnformatted(label);
return ret;
}
private static DtrEntry.Colors SwapColorChannels(DtrEntry.Colors colors)
=> new(SwapColorChannels(colors.Foreground), SwapColorChannels(colors.Glow));
private static uint SwapColorChannels(uint color)
{
if (color == 0)
return 0;
return ((color & 0xFFu) << 16) | (color & 0xFF00u) | ((color >> 16) & 0xFFu);
}
private 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()
{
_lastTab = "BlockedTransfers";
UiSharedService.ColorTextWrapped(
"Files that you attempted to upload or download that were forbidden to be transferred by their creators will appear here. " +
"If you see file paths from your drive here, then those files were not allowed to be uploaded. If you see hashes, those files were not allowed to be downloaded. " +
"Ask your paired friend to send you the mod in question through other means, acquire the mod yourself or pester the mod creator to allow it to be sent over Lightless.",
ImGuiColors.DalamudGrey);
if (ImGui.BeginTable("TransfersTable", 2, ImGuiTableFlags.SizingStretchProp))
{
ImGui.TableSetupColumn(
$"Hash/Filename");
ImGui.TableSetupColumn($"Forbidden by");
ImGui.TableHeadersRow();
foreach (var item in _fileTransferOrchestrator.ForbiddenTransfers)
{
ImGui.TableNextColumn();
if (item is UploadFileTransfer transfer)
{
ImGui.TextUnformatted(transfer.LocalFile);
}
else
{
ImGui.TextUnformatted(item.Hash);
}
ImGui.TableNextColumn();
ImGui.TextUnformatted(item.ForbiddenBy);
}
ImGui.EndTable();
}
}
private void DrawCurrentTransfers()
{
_lastTab = "Transfers";
_uiShared.UnderlinedBigText("Transfer Settings", UIColors.Get("LightlessBlue"));
ImGuiHelpers.ScaledDummy(5);
int maxParallelDownloads = _configService.Current.ParallelDownloads;
int maxParallelUploads = _configService.Current.ParallelUploads;
int maxPairApplications = _configService.Current.MaxConcurrentPairApplications;
bool limitPairApplications = _configService.Current.EnablePairProcessingLimiter;
bool useAlternativeUpload = _configService.Current.UseAlternativeFileUpload;
int downloadSpeedLimit = _configService.Current.DownloadSpeedLimitInBytes;
bool enableDirectDownloads = _configService.Current.EnableDirectDownloads;
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Global Download Speed Limit");
ImGui.SameLine();
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
if (ImGui.InputInt("###speedlimit", ref downloadSpeedLimit))
{
_configService.Current.DownloadSpeedLimitInBytes = downloadSpeedLimit;
_configService.Save();
Mediator.Publish(new DownloadLimitChangedMessage());
}
ImGui.SameLine();
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
_uiShared.DrawCombo("###speed", [DownloadSpeeds.Bps, DownloadSpeeds.KBps, DownloadSpeeds.MBps],
(s) => s switch
{
DownloadSpeeds.Bps => "Byte/s",
DownloadSpeeds.KBps => "KB/s",
DownloadSpeeds.MBps => "MB/s",
_ => throw new NotSupportedException()
}, (s) =>
{
_configService.Current.DownloadSpeedType = s;
_configService.Save();
Mediator.Publish(new DownloadLimitChangedMessage());
}, _configService.Current.DownloadSpeedType);
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("0 = No limit/infinite");
if (ImGui.Checkbox("[BETA] Enable Lightspeed Downloads", ref enableDirectDownloads))
{
_configService.Current.EnableDirectDownloads = enableDirectDownloads;
_configService.Save();
}
_uiShared.DrawHelpText("Uses signed CDN links when available. Disable to force the legacy queued download flow.");
if (ImGui.SliderInt("Maximum Parallel Downloads", ref maxParallelDownloads, 1, 10))
{
_configService.Current.ParallelDownloads = maxParallelDownloads;
_configService.Save();
Mediator.Publish(new DownloadLimitChangedMessage());
}
_uiShared.DrawHelpText("Controls how many download slots can be active at once.");
if (ImGui.SliderInt("Maximum Parallel Uploads", ref maxParallelUploads, 1, 8))
{
_configService.Current.ParallelUploads = maxParallelUploads;
_configService.Save();
}
_uiShared.DrawHelpText("Controls how many uploads can run at once.");
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
if (ImGui.Checkbox("Enable Pair Download Limiter", ref limitPairApplications))
{
_configService.Current.EnablePairProcessingLimiter = limitPairApplications;
_configService.Save();
Mediator.Publish(new PairProcessingLimitChangedMessage());
}
_uiShared.DrawHelpText(
"When enabled we stagger pair downloads to avoid large network and game lag caused by attempting to download everyone at once.");
var limiterDisabledScope = !limitPairApplications;
if (limiterDisabledScope)
{
ImGui.BeginDisabled();
}
if (ImGui.SliderInt("Maximum Concurrent Pair Downloads", ref maxPairApplications, 1, 6))
{
_configService.Current.MaxConcurrentPairApplications = maxPairApplications;
_configService.Save();
Mediator.Publish(new PairProcessingLimitChangedMessage());
}
_uiShared.DrawHelpText("How many pair downloads/applications can run simultaneously when the limit is on.");
if (limiterDisabledScope)
{
ImGui.EndDisabled();
}
var limiterSnapshot = _pairProcessingLimiter.GetSnapshot();
if (limiterSnapshot.IsEnabled)
{
var queueColor = limiterSnapshot.Waiting > 0 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudGrey;
var queueText = $"Pair queue {limiterSnapshot.InFlight}/{limiterSnapshot.Limit}";
queueText += limiterSnapshot.Waiting > 0
? $" ({limiterSnapshot.Waiting} waiting, {limiterSnapshot.Remaining} free)"
: $" ({limiterSnapshot.Remaining} free)";
ImGui.TextColored(queueColor, queueText);
}
else
{
ImGui.TextColored(ImGuiColors.DalamudGrey, "Pair apply limiter is disabled.");
}
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
if (ImGui.Checkbox("Use Alternative Upload Method", ref useAlternativeUpload))
{
_configService.Current.UseAlternativeFileUpload = useAlternativeUpload;
_configService.Save();
}
_uiShared.DrawHelpText(
"This will attempt to upload files in one go instead of a stream. Typically not necessary to enable. Use if you have upload issues.");
ImGui.Separator();
_uiShared.UnderlinedBigText("Transfer UI", UIColors.Get("LightlessBlue"));
ImGuiHelpers.ScaledDummy(5);
_uiShared.DrawHelpText(
"Download progress notification settings have been moved to the 'Enhanced Notifications' tab for better organization.");
ImGuiHelpers.ScaledDummy(5);
bool showTransferWindow = _configService.Current.ShowTransferWindow;
if (ImGui.Checkbox("Show separate transfer window", ref showTransferWindow))
{
_configService.Current.ShowTransferWindow = showTransferWindow;
_configService.Save();
}
_uiShared.DrawHelpText(
$"The download window will show the current progress of outstanding downloads.{Environment.NewLine}{Environment.NewLine}" +
$"What do W/Q/P/D stand for?{Environment.NewLine}W = Waiting for Slot (see Maximum Parallel Downloads){Environment.NewLine}" +
$"Q = Queued on Server, waiting for queue ready signal{Environment.NewLine}" +
$"P = Processing download (aka downloading){Environment.NewLine}" +
$"D = Decompressing download");
if (!_configService.Current.ShowTransferWindow) ImGui.BeginDisabled();
ImGui.Indent();
bool editTransferWindowPosition = _uiShared.EditTrackerPosition;
if (ImGui.Checkbox("Edit Transfer Window position", ref editTransferWindowPosition))
{
_uiShared.EditTrackerPosition = editTransferWindowPosition;
}
ImGui.Unindent();
if (!_configService.Current.ShowTransferWindow) ImGui.EndDisabled();
bool showTransferBars = _configService.Current.ShowTransferBars;
if (ImGui.Checkbox("Show transfer bars rendered below players", ref showTransferBars))
{
_configService.Current.ShowTransferBars = showTransferBars;
_configService.Save();
}
_uiShared.DrawHelpText(
"This will render a progress bar during the download at the feet of the player you are downloading from.");
if (!showTransferBars) ImGui.BeginDisabled();
ImGui.Indent();
bool transferBarShowText = _configService.Current.TransferBarsShowText;
if (ImGui.Checkbox("Show Download Text", ref transferBarShowText))
{
_configService.Current.TransferBarsShowText = transferBarShowText;
_configService.Save();
}
_uiShared.DrawHelpText("Shows download text (amount of MiB downloaded) in the transfer bars");
int transferBarWidth = _configService.Current.TransferBarsWidth;
if (ImGui.SliderInt("Transfer Bar Width", ref transferBarWidth, 10, 500))
{
_configService.Current.TransferBarsWidth = transferBarWidth;
_configService.Save();
}
_uiShared.DrawHelpText(
"Width of the displayed transfer bars (will never be less wide than the displayed text)");
int transferBarHeight = _configService.Current.TransferBarsHeight;
if (ImGui.SliderInt("Transfer Bar Height", ref transferBarHeight, 2, 50))
{
_configService.Current.TransferBarsHeight = transferBarHeight;
_configService.Save();
}
_uiShared.DrawHelpText(
"Height of the displayed transfer bars (will never be less tall than the displayed text)");
bool showUploading = _configService.Current.ShowUploading;
if (ImGui.Checkbox("Show 'Uploading' text below players that are currently uploading", ref showUploading))
{
_configService.Current.ShowUploading = showUploading;
_configService.Save();
}
_uiShared.DrawHelpText(
"This will render an 'Uploading' text at the feet of the player that is in progress of uploading data.");
ImGui.Unindent();
if (!showUploading) ImGui.BeginDisabled();
ImGui.Indent();
bool showUploadingBigText = _configService.Current.ShowUploadingBigText;
if (ImGui.Checkbox("Large font for 'Uploading' text", ref showUploadingBigText))
{
_configService.Current.ShowUploadingBigText = showUploadingBigText;
_configService.Save();
}
_uiShared.DrawHelpText("This will render an 'Uploading' text in a larger font.");
ImGui.Unindent();
if (!showUploading) ImGui.EndDisabled();
if (!showTransferBars) ImGui.EndDisabled();
if (_apiController.IsConnected)
{
ImGuiHelpers.ScaledDummy(5);
ImGui.Separator();
ImGuiHelpers.ScaledDummy(10);
using var tree = ImRaii.TreeNode("Speed Test to Servers");
if (tree)
{
if (_downloadServersTask == null || ((_downloadServersTask?.IsCompleted ?? false) &&
(!_downloadServersTask?.IsCompletedSuccessfully ?? false)))
{
if (_uiShared.IconTextButton(FontAwesomeIcon.GroupArrowsRotate, "Update Download Server List"))
{
_downloadServersTask = GetDownloadServerList();
}
}
if (_downloadServersTask != null && _downloadServersTask.IsCompleted &&
!_downloadServersTask.IsCompletedSuccessfully)
{
UiSharedService.ColorTextWrapped(
"Failed to get download servers from service, see /xllog for more information",
ImGuiColors.DalamudRed);
}
if (_downloadServersTask != null && _downloadServersTask.IsCompleted &&
_downloadServersTask.IsCompletedSuccessfully)
{
if (_speedTestTask == null || _speedTestTask.IsCompleted)
{
if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowRight, "Start Speedtest"))
{
_speedTestTask = RunSpeedTest(_downloadServersTask.Result!,
_speedTestCts?.Token ?? CancellationToken.None);
}
}
else if (!_speedTestTask.IsCompleted)
{
UiSharedService.ColorTextWrapped("Running Speedtest to File Servers...",
UIColors.Get("LightlessYellow"));
UiSharedService.ColorTextWrapped(
"Please be patient, depending on usage and load this can take a while.",
UIColors.Get("LightlessYellow"));
if (_uiShared.IconTextButton(FontAwesomeIcon.Ban, "Cancel speedtest"))
{
_speedTestCts?.Cancel();
_speedTestCts?.Dispose();
_speedTestCts = new();
}
}
if (_speedTestTask != null && _speedTestTask.IsCompleted)
{
if (_speedTestTask.Result != null && _speedTestTask.Result.Count != 0)
{
foreach (var result in _speedTestTask.Result)
{
UiSharedService.TextWrapped(result);
}
}
else
{
UiSharedService.ColorTextWrapped("Speedtest completed with no results",
UIColors.Get("LightlessYellow"));
}
}
}
}
ImGuiHelpers.ScaledDummy(10);
}
ImGui.Separator();
_uiShared.UnderlinedBigText("Current Transfers", UIColors.Get("LightlessBlue"));
ImGuiHelpers.ScaledDummy(5);
if (ImGui.BeginTabBar("TransfersTabBar"))
{
if (ApiController.ServerState is ServerState.Connected && ImGui.BeginTabItem("Transfers"))
{
var uploadsSnapshot = _fileTransferManager.GetCurrentUploadsSnapshot();
var activeUploads = uploadsSnapshot.Count(c => !c.IsTransferred);
var uploadSlotLimit = Math.Clamp(_configService.Current.ParallelUploads, 1, 8);
ImGui.TextUnformatted($"Uploads (slots {activeUploads}/{uploadSlotLimit})");
if (ImGui.BeginTable("UploadsTable", 3))
{
ImGui.TableSetupColumn("File");
ImGui.TableSetupColumn("Uploaded");
ImGui.TableSetupColumn("Size");
ImGui.TableHeadersRow();
foreach (var transfer in uploadsSnapshot)
{
var color = UiSharedService.UploadColor((transfer.Transferred, transfer.Total));
using var col = ImRaii.PushColor(ImGuiCol.Text, color);
ImGui.TableNextColumn();
if (transfer is UploadFileTransfer uploadTransfer)
{
ImGui.TextUnformatted(uploadTransfer.LocalFile);
}
else
{
ImGui.TextUnformatted(transfer.Hash);
}
ImGui.TableNextColumn();
ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Transferred));
ImGui.TableNextColumn();
ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Total));
}
ImGui.EndTable();
}
ImGui.Separator();
ImGui.TextUnformatted("Downloads");
if (ImGui.BeginTable("DownloadsTable", 4))
{
ImGui.TableSetupColumn("User");
ImGui.TableSetupColumn("Server");
ImGui.TableSetupColumn("Files");
ImGui.TableSetupColumn("Download");
ImGui.TableHeadersRow();
foreach (var transfer in _currentDownloads.ToArray())
{
var userName = transfer.Key.Name;
foreach (var entry in transfer.Value)
{
var color = UiSharedService.UploadColor((entry.Value.TransferredBytes,
entry.Value.TotalBytes));
ImGui.TableNextColumn();
ImGui.TextUnformatted(userName);
ImGui.TableNextColumn();
ImGui.TextUnformatted(entry.Key);
var col = ImRaii.PushColor(ImGuiCol.Text, color);
ImGui.TableNextColumn();
ImGui.TextUnformatted(entry.Value.TransferredFiles + "/" + entry.Value.TotalFiles);
ImGui.TableNextColumn();
ImGui.TextUnformatted(UiSharedService.ByteToString(entry.Value.TransferredBytes) + "/" +
UiSharedService.ByteToString(entry.Value.TotalBytes));
ImGui.TableNextColumn();
col.Dispose();
ImGui.TableNextRow();
}
}
ImGui.EndTable();
}
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Blocked Transfers"))
{
DrawBlockedTransfers();
ImGui.EndTabItem();
}
ImGui.EndTabBar();
}
}
private Task<List<string>?>? _downloadServersTask = null;
private Task<List<string>?>? _speedTestTask = null;
private CancellationTokenSource? _speedTestCts;
private async Task<List<string>?> RunSpeedTest(List<string> servers, CancellationToken token)
{
List<string> speedTestResults = new();
foreach (var server in servers)
{
HttpResponseMessage? result = null;
Stopwatch? st = null;
try
{
result = await _fileTransferOrchestrator.SendRequestAsync(HttpMethod.Get,
new Uri(new Uri(server), "speedtest/run"), token, HttpCompletionOption.ResponseHeadersRead)
.ConfigureAwait(false);
result.EnsureSuccessStatusCode();
using CancellationTokenSource speedtestTimeCts = new();
speedtestTimeCts.CancelAfter(TimeSpan.FromSeconds(10));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(speedtestTimeCts.Token, token);
long readBytes = 0;
st = Stopwatch.StartNew();
try
{
var stream = await result.Content.ReadAsStreamAsync(linkedCts.Token).ConfigureAwait(false);
byte[] buffer = new byte[8192];
while (!speedtestTimeCts.Token.IsCancellationRequested)
{
var currentBytes = await stream.ReadAsync(buffer, linkedCts.Token).ConfigureAwait(false);
if (currentBytes == 0)
break;
readBytes += currentBytes;
}
}
catch (OperationCanceledException)
{
_logger.LogWarning("Speedtest to {server} cancelled", server);
}
st.Stop();
_logger.LogInformation("Downloaded {bytes} from {server} in {time}",
UiSharedService.ByteToString(readBytes), server, st.Elapsed);
var bps = (long)((readBytes) / st.Elapsed.TotalSeconds);
speedTestResults.Add($"{server}: ~{UiSharedService.ByteToString(bps)}/s");
}
catch (HttpRequestException ex)
{
if (result != null)
{
var res = await result!.Content.ReadAsStringAsync().ConfigureAwait(false);
speedTestResults.Add($"{server}: {ex.Message} - {res}");
}
}
catch (OperationCanceledException)
{
_logger.LogWarning("Speedtest on {server} cancelled", server);
speedTestResults.Add($"{server}: Cancelled by user");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Some exception");
}
finally
{
st?.Stop();
}
}
return speedTestResults;
}
private async Task<List<string>?> GetDownloadServerList()
{
try
{
var result = await _fileTransferOrchestrator.SendRequestAsync(HttpMethod.Get,
new Uri(_fileTransferOrchestrator.FilesCdnUri!, "files/downloadServers"), CancellationToken.None)
.ConfigureAwait(false);
result.EnsureSuccessStatusCode();
return await JsonSerializer
.DeserializeAsync<List<string>>(await result.Content.ReadAsStreamAsync().ConfigureAwait(false))
.ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to get download server list");
throw;
}
}
private void DrawDebug()
{
_lastTab = "Debug";
_uiShared.UnderlinedBigText("Debug", UIColors.Get("LightlessYellow"));
ImGuiHelpers.ScaledDummy(10);
#if DEBUG
if (LastCreatedCharacterData != null && ImGui.TreeNode("Last created character data"))
{
foreach (var l in JsonSerializer
.Serialize(LastCreatedCharacterData, new JsonSerializerOptions() { WriteIndented = true })
.Split('\n'))
{
ImGui.TextUnformatted($"{l}");
}
ImGui.TreePop();
}
#endif
if (_uiShared.IconTextButton(FontAwesomeIcon.Copy, "[DEBUG] Copy Last created Character Data to clipboard"))
{
if (LastCreatedCharacterData != null)
{
ImGui.SetClipboardText(JsonSerializer.Serialize(LastCreatedCharacterData,
new JsonSerializerOptions() { WriteIndented = true }));
}
else
{
ImGui.SetClipboardText("ERROR: No created character data, cannot copy.");
}
}
UiSharedService.AttachToolTip("Use this when reporting mods being rejected from the server.");
_uiShared.DrawCombo("Log Level", Enum.GetValues<LogLevel>(), (l) => l.ToString(), (l) =>
{
_configService.Current.LogLevel = l;
_configService.Save();
}, _configService.Current.LogLevel);
bool logPerformance = _configService.Current.LogPerformance;
if (ImGui.Checkbox("Log Performance Counters", ref logPerformance))
{
_configService.Current.LogPerformance = logPerformance;
_configService.Save();
}
_uiShared.DrawHelpText(
"Enabling this can incur a (slight) performance impact. Enabling this for extended periods of time is not recommended.");
using (ImRaii.Disabled(!logPerformance))
{
if (_uiShared.IconTextButton(FontAwesomeIcon.StickyNote, "Print Performance Stats to /xllog"))
{
_performanceCollector.PrintPerformanceStats();
}
ImGui.SameLine();
if (_uiShared.IconTextButton(FontAwesomeIcon.StickyNote, "Print Performance Stats (last 60s) to /xllog"))
{
_performanceCollector.PrintPerformanceStats(60);
}
}
bool stopWhining = _configService.Current.DebugStopWhining;
if (ImGui.Checkbox("Do not notify for modified game files or enabled LOD", ref stopWhining))
{
_configService.Current.DebugStopWhining = stopWhining;
_configService.Save();
}
_uiShared.DrawHelpText(
"Having modified game files will still mark your logs with UNSUPPORTED and you will not receive support, message shown or not." +
UiSharedService.TooltipSeparator
+ "Keeping LOD enabled can lead to more crashes. Use at your own risk.");
_uiShared.ColoredSeparator(UIColors.Get("LightlessYellow"), 2f);
}
private void DrawFileStorageSettings()
{
_lastTab = "FileCache";
_uiShared.UnderlinedBigText("Storage", UIColors.Get("LightlessBlue"));
ImGuiHelpers.ScaledDummy(5);
UiSharedService.TextWrapped(
"Lightless stores downloaded files from paired people permanently. This is to improve loading performance and requiring less downloads. " +
"The storage governs itself by clearing data beyond the set storage size. Please set the storage size accordingly. It is not necessary to manually clear the storage.");
_uiShared.DrawFileScanState();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(
"Monitoring Penumbra Folder: " + (_cacheMonitor.PenumbraWatcher?.Path ?? "Not monitoring"));
if (string.IsNullOrEmpty(_cacheMonitor.PenumbraWatcher?.Path))
{
ImGui.SameLine();
using var id = ImRaii.PushId("penumbraMonitor");
if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowsToCircle, "Try to reinitialize Monitor"))
{
_cacheMonitor.StartPenumbraWatcher(_ipcManager.Penumbra.ModDirectory);
}
}
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Monitoring Lightless Storage Folder: " +
(_cacheMonitor.LightlessWatcher?.Path ?? "Not monitoring"));
if (string.IsNullOrEmpty(_cacheMonitor.LightlessWatcher?.Path))
{
ImGui.SameLine();
using var id = ImRaii.PushId("lightlessMonitor");
if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowsToCircle, "Try to reinitialize Monitor"))
{
_cacheMonitor.StartLightlessWatcher(_configService.Current.CacheFolder);
}
}
if (_cacheMonitor.LightlessWatcher == null || _cacheMonitor.PenumbraWatcher == null)
{
if (_uiShared.IconTextButton(FontAwesomeIcon.Play, "Resume Monitoring"))
{
_cacheMonitor.StartLightlessWatcher(_configService.Current.CacheFolder);
_cacheMonitor.StartPenumbraWatcher(_ipcManager.Penumbra.ModDirectory);
_cacheMonitor.InvokeScan();
}
UiSharedService.AttachToolTip("Attempts to resume monitoring for both Penumbra and Lightless Storage. "
+ "Resuming the monitoring will also force a full scan to run." +
Environment.NewLine
+ "If the button remains present after clicking it, consult /xllog for errors");
}
else
{
using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
{
if (_uiShared.IconTextButton(FontAwesomeIcon.Stop, "Stop Monitoring"))
{
_cacheMonitor.StopMonitoring();
}
}
UiSharedService.AttachToolTip("Stops the monitoring for both Penumbra and Lightless Storage. "
+ "Do not stop the monitoring, unless you plan to move the Penumbra and Lightless Storage folders, to ensure correct functionality of Lightless." +
Environment.NewLine
+ "If you stop the monitoring to move folders around, resume it after you are finished moving the files."
+ UiSharedService.TooltipSeparator + "Hold CTRL to enable this button");
}
_uiShared.DrawCacheDirectorySetting();
ImGui.AlignTextToFramePadding();
if (_cacheMonitor.FileCacheSize >= 0)
ImGui.TextUnformatted(
$"Currently utilized local storage: {UiSharedService.ByteToString(_cacheMonitor.FileCacheSize)}");
else
ImGui.TextUnformatted($"Currently utilized local storage: Calculating...");
ImGui.TextUnformatted(
$"Remaining space free on drive: {UiSharedService.ByteToString(_cacheMonitor.FileCacheDriveFree)}");
bool useFileCompactor = _configService.Current.UseCompactor;
if (!useFileCompactor)
{
UiSharedService.ColorTextWrapped(
"Hint: To free up space when using Lightless consider enabling the File Compactor",
UIColors.Get("LightlessYellow"));
}
if (!_cacheMonitor.StorageIsBtrfs && !_cacheMonitor.StorageisNTFS) ImGui.BeginDisabled();
if (ImGui.Checkbox("Use file compactor", ref useFileCompactor))
{
_configService.Current.UseCompactor = useFileCompactor;
_configService.Save();
}
_uiShared.DrawHelpText(
"The file compactor can massively reduce your saved files. It might incur a minor penalty on loading files on a slow CPU." +
Environment.NewLine
+ "It is recommended to leave it enabled to save on space.");
ImGui.SameLine();
if (!_fileCompactor.MassCompactRunning)
{
if (_uiShared.IconTextButton(FontAwesomeIcon.FileArchive, "Compact all files in storage"))
{
_ = Task.Run(() =>
{
_fileCompactor.CompactStorage(compress: true);
_cacheMonitor.RecalculateFileCacheSize(CancellationToken.None);
});
}
UiSharedService.AttachToolTip("This will run compression on all files in your current Lightless Storage." +
Environment.NewLine
+ "You do not need to run this manually if you keep the file compactor enabled.");
ImGui.SameLine();
if (_uiShared.IconTextButton(FontAwesomeIcon.File, "Decompact all files in storage"))
{
_ = Task.Run(() =>
{
_fileCompactor.CompactStorage(compress: false);
_cacheMonitor.RecalculateFileCacheSize(CancellationToken.None);
});
}
UiSharedService.AttachToolTip(
"This will run decompression on all files in your current Lightless Storage.");
}
else
{
UiSharedService.ColorText($"File compactor currently running ({_fileCompactor.Progress})",
UIColors.Get("LightlessYellow"));
}
if (!_cacheMonitor.StorageIsBtrfs && !_cacheMonitor.StorageisNTFS)
{
ImGui.EndDisabled();
ImGui.TextUnformatted("The file compactor is only available on BTRFS and NTFS drives.");
}
if (_cacheMonitor.StorageisNTFS)
{
ImGui.TextUnformatted("The file compactor is running on NTFS Drive.");
}
if (_cacheMonitor.StorageIsBtrfs)
{
ImGui.TextUnformatted("The file compactor is running on Btrfs Drive.");
}
ImGuiHelpers.ScaledDummy(new Vector2(10, 10));
ImGui.Separator();
if (_uiShared.MediumTreeNode("Storage Validation", UIColors.Get("LightlessYellow")))
{
UiSharedService.TextWrapped(
"File Storage validation can make sure that all files in your local Lightless Storage are valid. " +
"Run the validation before you clear the Storage for no reason. " + Environment.NewLine +
"This operation, depending on how many files you have in your storage, can take a while and will be CPU and drive intensive.");
using (ImRaii.Disabled(_validationTask != null && !_validationTask.IsCompleted))
{
if (_uiShared.IconTextButton(FontAwesomeIcon.Check, "Start File Storage Validation"))
{
_validationCts?.Cancel();
_validationCts?.Dispose();
_validationCts = new();
var token = _validationCts.Token;
_validationTask = Task.Run(() =>
_fileCacheManager.ValidateLocalIntegrity(_validationProgress, token));
}
}
if (_validationTask != null && !_validationTask.IsCompleted)
{
ImGui.SameLine();
if (_uiShared.IconTextButton(FontAwesomeIcon.Times, "Cancel"))
{
_validationCts?.Cancel();
}
}
if (_validationTask != null)
{
using (ImRaii.PushIndent(20f))
{
if (_validationTask.IsCompleted)
{
UiSharedService.TextWrapped(
$"The storage validation has completed and removed {_validationTask.Result.Count} invalid files from storage.");
}
else
{
UiSharedService.TextWrapped(
$"Storage validation is running: {_currentProgress.Item1}/{_currentProgress.Item2}");
UiSharedService.TextWrapped($"Current item: {_currentProgress.Item3.ResolvedFilepath}");
}
}
}
_uiShared.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f);
ImGui.TreePop();
}
ImGui.Separator();
if (_uiShared.MediumTreeNode("Storage Clearing", UIColors.Get("DimRed")))
{
ImGui.TextUnformatted("To clear the local storage accept the following disclaimer");
ImGui.Indent();
ImGui.Checkbox("##readClearCache", ref _readClearCache);
ImGui.SameLine();
UiSharedService.TextWrapped("I understand that: " + Environment.NewLine +
"- By clearing the local storage I put the file servers of my connected service under extra strain by having to redownload all data."
+ Environment.NewLine + "- This is not a step to try to fix sync issues."
+ Environment.NewLine +
"- This can make the situation of not getting other players data worse in situations of heavy file server load.");
if (!_readClearCache)
ImGui.BeginDisabled();
if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Clear local storage") &&
UiSharedService.CtrlPressed() && _readClearCache)
{
_ = Task.Run(() =>
{
foreach (var file in Directory.GetFiles(_configService.Current.CacheFolder))
{
try
{
File.Delete(file);
}
catch (IOException ex)
{
_logger.LogWarning(ex, $"Could not delete file {file} because it is in use.");
}
}
});
}
UiSharedService.AttachToolTip(
"You normally do not need to do this. THIS IS NOT SOMETHING YOU SHOULD BE DOING TO TRY TO FIX SYNC ISSUES." +
Environment.NewLine
+ "This will solely remove all downloaded data from all players and will require you to re-download everything again." +
Environment.NewLine
+ "Lightless storage is self-clearing and will not surpass the limit you have set it to." +
Environment.NewLine
+ "If you still think you need to do this hold CTRL while pressing the button.");
if (!_readClearCache)
ImGui.EndDisabled();
ImGui.Unindent();
_uiShared.ColoredSeparator(UIColors.Get("DimRed"), 1.5f);
ImGui.TreePop();
}
}
private void DrawGeneral()
{
if (!string.Equals(_lastTab, "General", StringComparison.OrdinalIgnoreCase))
{
_notesSuccessfullyApplied = null;
}
_lastTab = "General";
//UiSharedService.FontText("Experimental", _uiShared.UidFont);
//ImGui.Separator();
_uiShared.UnderlinedBigText("General Settings", UIColors.Get("LightlessBlue"));
ImGui.Dummy(new Vector2(10));
_uiShared.BigText("Notes");
if (_uiShared.MediumTreeNode("Import & Export", UIColors.Get("LightlessPurple")))
{
if (_uiShared.IconTextButton(FontAwesomeIcon.StickyNote, "Export all your user notes to clipboard"))
{
ImGui.SetClipboardText(UiSharedService.GetNotes(_pairManager.DirectPairs
.UnionBy(_pairManager.GroupPairs.SelectMany(p => p.Value), p => p.UserData,
UserDataComparer.Instance).ToList()));
}
if (_uiShared.IconTextButton(FontAwesomeIcon.FileImport, "Import notes from clipboard"))
{
_notesSuccessfullyApplied = null;
var notes = ImGui.GetClipboardText();
_notesSuccessfullyApplied = _uiShared.ApplyNotesFromClipboard(notes, _overwriteExistingLabels);
}
ImGui.SameLine();
ImGui.Checkbox("Overwrite existing notes", ref _overwriteExistingLabels);
_uiShared.DrawHelpText(
"If this option is selected all already existing notes for UIDs will be overwritten by the imported notes.");
if (_notesSuccessfullyApplied.HasValue && _notesSuccessfullyApplied.Value)
{
UiSharedService.ColorTextWrapped("User Notes successfully imported", UIColors.Get("LightlessBlue"));
}
else if (_notesSuccessfullyApplied.HasValue && !_notesSuccessfullyApplied.Value)
{
UiSharedService.ColorTextWrapped(
"Attempt to import notes from clipboard failed. Check formatting and try again",
ImGuiColors.DalamudRed);
}
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop();
}
ImGui.Separator();
var openPopupOnAddition = _configService.Current.OpenPopupOnAdd;
if (_uiShared.MediumTreeNode("Popup & Auto Fill", UIColors.Get("LightlessPurple")))
{
if (ImGui.Checkbox("Open Notes Popup on user addition", ref openPopupOnAddition))
{
_configService.Current.OpenPopupOnAdd = openPopupOnAddition;
_configService.Save();
}
_uiShared.DrawHelpText(
"This will open a popup that allows you to set the notes for a user after successfully adding them to your individual pairs.");
var autoPopulateNotes = _configService.Current.AutoPopulateEmptyNotesFromCharaName;
if (ImGui.Checkbox("Automatically populate notes using player names", ref autoPopulateNotes))
{
_configService.Current.AutoPopulateEmptyNotesFromCharaName = autoPopulateNotes;
_configService.Save();
}
_uiShared.DrawHelpText(
"This will automatically populate user notes using the first encountered player name if the note was not set prior");
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop();
}
ImGui.Separator();
ImGui.Dummy(new Vector2(10));
_uiShared.BigText("UI");
var showNameInsteadOfNotes = _configService.Current.ShowCharacterNameInsteadOfNotesForVisible;
var showVisibleSeparate = _configService.Current.ShowVisibleUsersSeparately;
var showOfflineSeparate = _configService.Current.ShowOfflineUsersSeparately;
var showProfiles = _configService.Current.ProfilesShow;
var showNsfwProfiles = _configService.Current.ProfilesAllowNsfw;
var profileDelay = _configService.Current.ProfileDelay;
var profileOnRight = _configService.Current.ProfilePopoutRight;
var enableRightClickMenu = _configService.Current.EnableRightClickMenus;
var enableDtrEntry = _configService.Current.EnableDtrEntry;
var showUidInDtrTooltip = _configService.Current.ShowUidInDtrTooltip;
var preferNoteInDtrTooltip = _configService.Current.PreferNoteInDtrTooltip;
var useColorsInDtr = _configService.Current.UseColorsInDtr;
var useLightlessRedesign = _configService.Current.UseLightlessRedesign;
var dtrColorsDefault = _configService.Current.DtrColorsDefault;
var dtrColorsNotConnected = _configService.Current.DtrColorsNotConnected;
var dtrColorsPairsInRange = _configService.Current.DtrColorsPairsInRange;
var preferNotesInsteadOfName = _configService.Current.PreferNotesOverNamesForVisible;
var useFocusTarget = _configService.Current.UseFocusTarget;
var groupUpSyncshells = _configService.Current.GroupUpSyncshells;
var groupedSyncshells = _configService.Current.ShowGroupedSyncshellsInAll;
var groupInVisible = _configService.Current.ShowSyncshellUsersInVisible;
var syncshellOfflineSeparate = _configService.Current.ShowSyncshellOfflineUsersSeparately;
if (_uiShared.MediumTreeNode("Behavior", UIColors.Get("LightlessPurple")))
{
if (ImGui.Checkbox("Enable Game Right Click Menu Entries", ref enableRightClickMenu))
{
_configService.Current.EnableRightClickMenus = enableRightClickMenu;
_configService.Save();
}
_uiShared.DrawHelpText("This will add all Lightless related right click menu entries in the game UI.");
if (ImGui.Checkbox("Display status and visible pair count in Server Info Bar", ref enableDtrEntry))
{
_configService.Current.EnableDtrEntry = enableDtrEntry;
_configService.Save();
}
_uiShared.DrawHelpText(
"This will add Lightless connection status and visible pair count in the Server Info Bar.\nYou can further configure this through your Dalamud Settings.");
using (ImRaii.Disabled(!enableDtrEntry))
{
using var indent = ImRaii.PushIndent();
if (ImGui.Checkbox("Show visible character's UID in tooltip", ref showUidInDtrTooltip))
{
_configService.Current.ShowUidInDtrTooltip = showUidInDtrTooltip;
_configService.Save();
}
if (ImGui.Checkbox("Prefer notes over player names in tooltip", ref preferNoteInDtrTooltip))
{
_configService.Current.PreferNoteInDtrTooltip = preferNoteInDtrTooltip;
_configService.Save();
}
}
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop();
}
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);
if (ImGui.SliderInt("Label Offset X", ref offsetX, -200, 200))
{
_configService.Current.LightfinderLabelOffsetX = (short)offsetX;
_configService.Save();
_nameplateHandler.ClearNameplateCaches();
_nameplateHandler.FlagRefresh();
_nameplateService.RequestRedraw();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_configService.Current.LightfinderLabelOffsetX = 0;
_configService.Save();
_nameplateHandler.ClearNameplateCaches();
_nameplateHandler.FlagRefresh();
_nameplateService.RequestRedraw();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default.");
ImGui.EndDisabled();
_uiShared.DrawHelpText(
"Moves the Lightfinder label horizontally on player nameplates.\nUnavailable when automatic alignment is enabled.");
if (ImGui.SliderInt("Label Offset Y", ref offsetY, -200, 200))
{
_configService.Current.LightfinderLabelOffsetY = (short)offsetY;
_configService.Save();
_nameplateHandler.ClearNameplateCaches();
_nameplateHandler.FlagRefresh();
_nameplateService.RequestRedraw();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_configService.Current.LightfinderLabelOffsetY = 0;
_configService.Save();
_nameplateHandler.ClearNameplateCaches();
_nameplateHandler.FlagRefresh();
_nameplateService.RequestRedraw();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default.");
_uiShared.DrawHelpText("Moves the Lightfinder label vertically on player nameplates.");
if (ImGui.SliderFloat("Label Size", ref labelScale, 0.5f, 2.0f, "%.2fx"))
{
_configService.Current.LightfinderLabelScale = labelScale;
_configService.Save();
_nameplateHandler.ClearNameplateCaches();
_nameplateHandler.FlagRefresh();
_nameplateService.RequestRedraw();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_configService.Current.LightfinderLabelScale = 1.0f;
_configService.Save();
_nameplateHandler.ClearNameplateCaches();
_nameplateHandler.FlagRefresh();
_nameplateService.RequestRedraw();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default.");
_uiShared.DrawHelpText("Adjusts the Lightfinder label size for both text and icon modes.");
ImGui.Dummy(new Vector2(8));
if (ImGui.Checkbox("Automatically align with nameplate", ref autoAlign))
{
_configService.Current.LightfinderAutoAlign = autoAlign;
_configService.Save();
_nameplateHandler.ClearNameplateCaches();
_nameplateHandler.FlagRefresh();
_nameplateService.RequestRedraw();
}
_uiShared.DrawHelpText(
"Automatically position the label relative to the in-game nameplate. Turn off to rely entirely on manual offsets.");
if (autoAlign)
{
var alignmentOption = _configService.Current.LabelAlignment;
var alignmentLabel = alignmentOption switch
{
LabelAlignment.Left => "Left",
LabelAlignment.Right => "Right",
_ => "Center",
};
if (ImGui.BeginCombo("Horizontal Alignment", alignmentLabel))
{
foreach (LabelAlignment option in Enum.GetValues<LabelAlignment>())
{
var optionLabel = option switch
{
LabelAlignment.Left => "Left",
LabelAlignment.Right => "Right",
_ => "Center",
};
var selected = option == alignmentOption;
if (ImGui.Selectable(optionLabel, selected))
{
_configService.Current.LabelAlignment = option;
_configService.Save();
_nameplateHandler.FlagRefresh();
_nameplateService.RequestRedraw();
}
if (selected)
ImGui.SetItemDefaultFocus();
}
ImGui.EndCombo();
}
}
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TextUnformatted("Visibility");
var showOwn = _configService.Current.LightfinderLabelShowOwn;
if (ImGui.Checkbox("Show your own Lightfinder label", ref showOwn))
{
_configService.Current.LightfinderLabelShowOwn = showOwn;
_configService.Save();
_nameplateHandler.ClearNameplateCaches();
_nameplateHandler.FlagRefresh();
_nameplateService.RequestRedraw();
}
_uiShared.DrawHelpText("Toggles your own Lightfinder label.");
var showPaired = _configService.Current.LightfinderLabelShowPaired;
if (ImGui.Checkbox("Show paired player(s) Lightfinder label", ref showPaired))
{
_configService.Current.LightfinderLabelShowPaired = showPaired;
_configService.Save();
_nameplateHandler.ClearNameplateCaches();
_nameplateHandler.FlagRefresh();
_nameplateService.RequestRedraw();
}
_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");
var useIcon = _configService.Current.LightfinderLabelUseIcon;
if (ImGui.Checkbox("Show icon instead of text", ref useIcon))
{
_configService.Current.LightfinderLabelUseIcon = useIcon;
_configService.Save();
_nameplateHandler.ClearNameplateCaches();
_nameplateHandler.FlagRefresh();
_nameplateService.RequestRedraw();
if (useIcon)
{
RefreshLightfinderIconState();
}
else
{
_lightfinderIconInputInitialized = false;
_lightfinderIconPresetIndex = -1;
}
}
_uiShared.DrawHelpText("Switch between the Lightfinder text label and an icon on nameplates.");
if (useIcon)
{
if (!_lightfinderIconInputInitialized)
{
RefreshLightfinderIconState();
}
var currentPresetLabel = _lightfinderIconPresetIndex >= 0
? $"{GetLightfinderPresetGlyph(_lightfinderIconPresetIndex)} {LightfinderIconPresets[_lightfinderIconPresetIndex].Label}"
: "Custom";
if (ImGui.BeginCombo("Preset Icon", currentPresetLabel))
{
for (int i = 0; i < LightfinderIconPresets.Length; i++)
{
var optionGlyph = GetLightfinderPresetGlyph(i);
var preview = $"{optionGlyph} {LightfinderIconPresets[i].Label}";
var selected = i == _lightfinderIconPresetIndex;
if (ImGui.Selectable(preview, selected))
{
_lightfinderIconInput = NameplateHandler.ToIconEditorString(optionGlyph);
_lightfinderIconPresetIndex = i;
}
}
if (ImGui.Selectable("Custom", _lightfinderIconPresetIndex == -1))
{
_lightfinderIconPresetIndex = -1;
}
ImGui.EndCombo();
}
var editorBuffer = _lightfinderIconInput;
if (ImGui.InputText("Icon Glyph", ref editorBuffer, 16))
{
_lightfinderIconInput = editorBuffer;
_lightfinderIconPresetIndex = -1;
}
if (ImGui.Button("Apply Icon"))
{
var normalized = NameplateHandler.NormalizeIconGlyph(_lightfinderIconInput);
ApplyLightfinderIcon(normalized, _lightfinderIconPresetIndex);
}
ImGui.SameLine();
if (ImGui.Button("Reset Icon"))
{
var defaultGlyph = NameplateHandler.NormalizeIconGlyph(null);
var defaultIndex = -1;
for (int i = 0; i < LightfinderIconPresets.Length; i++)
{
if (string.Equals(GetLightfinderPresetGlyph(i), defaultGlyph, StringComparison.Ordinal))
{
defaultIndex = i;
break;
}
}
if (defaultIndex < 0)
{
defaultIndex = 0;
}
ApplyLightfinderIcon(GetLightfinderPresetGlyph(defaultIndex), defaultIndex);
}
var previewGlyph = NameplateHandler.NormalizeIconGlyph(_lightfinderIconInput);
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.Text($"Preview: {previewGlyph}");
_uiShared.DrawHelpText(
"Enter a hex code (e.g. E0BB), pick a preset, or paste an icon character directly.");
}
else
{
_lightfinderIconInputInitialized = false;
_lightfinderIconPresetIndex = -1;
}
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop();
}
ImGui.Separator();
if (_uiShared.MediumTreeNode("Colors", UIColors.Get("LightlessPurple")))
{
ImGui.TextUnformatted("UI Theme Colors");
var colorNames = new[]
{
("LightlessPurple", "Primary Purple", "Section titles and dividers"),
("LightlessPurpleActive", "Primary Purple (Active)", "Active tabs and hover highlights"),
("LightlessPurpleDefault", "Primary Purple (Inactive)", "Inactive tabs and default dividers"),
("LightlessBlue", "Secondary Blue", "Secondary title colors, visable pairs"),
("LightlessGreen", "Success Green", "Join buttons and success messages"),
("LightlessYellow", "Warning Yellow", "Warning colors"),
("LightlessOrange", "Performance Orange", "Performance notifications and warnings"),
("PairBlue", "Syncshell Blue", "Syncshell headers, toggle highlights, and moderator actions"),
("DimRed", "Error Red", "Error and offline colors")
};
if (ImGui.BeginTable("##ColorTable", 3,
ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit))
{
ImGui.TableSetupColumn("Color", ImGuiTableColumnFlags.WidthFixed);
ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40);
ImGui.TableHeadersRow();
foreach (var (colorKey, displayName, description) in colorNames)
{
ImGui.TableNextRow();
// color column
ImGui.TableSetColumnIndex(0);
var currentColor = UIColors.Get(colorKey);
var colorToEdit = currentColor;
if (ImGui.ColorEdit4($"##color_{colorKey}", ref colorToEdit,
ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf))
{
UIColors.Set(colorKey, colorToEdit);
}
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(displayName);
// description column
ImGui.TableSetColumnIndex(1);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(description);
// actions column
ImGui.TableSetColumnIndex(2);
using var resetId = ImRaii.PushId($"Reset_{colorKey}");
var availableWidth = ImGui.GetContentRegionAvail().X;
var isCustom = UIColors.IsCustom(colorKey);
using (ImRaii.Disabled(!isCustom))
{
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button(FontAwesomeIcon.Undo.ToIconString(), new Vector2(availableWidth, 0)))
{
UIColors.Reset(colorKey);
}
}
}
UiSharedService.AttachToolTip(isCustom
? "Reset this color to default"
: "Color is already at default value");
}
ImGui.EndTable();
}
ImGui.Spacing();
if (_uiShared.IconTextButton(FontAwesomeIcon.Undo, "Reset All Theme Colors"))
{
UIColors.ResetAll();
}
_uiShared.DrawHelpText("This will reset all theme colors to their default values");
ImGui.Spacing();
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TextUnformatted("Server Info Bar Colors");
if (ImGui.Checkbox("Color-code the Server Info Bar entry according to status", ref useColorsInDtr))
{
_configService.Current.UseColorsInDtr = useColorsInDtr;
_configService.Save();
}
_uiShared.DrawHelpText(
"This will color the Server Info Bar entry based on connection status and visible pairs.");
ImGui.BeginDisabled(!useColorsInDtr);
const ImGuiTableFlags serverInfoTableFlags = ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit;
if (ImGui.BeginTable("##ServerInfoBarColorTable", 3, serverInfoTableFlags))
{
ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.WidthFixed, 220f);
ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40f);
ImGui.TableHeadersRow();
DrawDtrColorRow(
"server-default",
"Default",
"Displayed when connected without any special status.",
ref dtrColorsDefault,
DefaultConfig.DtrColorsDefault,
value => _configService.Current.DtrColorsDefault = value);
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();
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TextUnformatted("Nameplate Colors");
var nameColorsEnabled = _configService.Current.IsNameplateColorsEnabled;
var nameColors = _configService.Current.NameplateColors;
var isFriendOverride = _configService.Current.overrideFriendColor;
var isPartyOverride = _configService.Current.overridePartyColor;
var isFcTagOverride = _configService.Current.overrideFcTagColor;
if (ImGui.Checkbox("Override name color of visible paired players", ref nameColorsEnabled))
{
_configService.Current.IsNameplateColorsEnabled = nameColorsEnabled;
_configService.Save();
_nameplateService.RequestRedraw();
}
_uiShared.DrawHelpText("This will override the nameplate colors for visible paired players in-game.");
using (ImRaii.Disabled(!nameColorsEnabled))
{
using var indent = ImRaii.PushIndent();
if (InputDtrColors("Name color", ref nameColors))
{
_configService.Current.NameplateColors = nameColors;
_configService.Save();
_nameplateService.RequestRedraw();
}
if (ImGui.Checkbox("Override friend color", ref isFriendOverride))
{
_configService.Current.overrideFriendColor = isFriendOverride;
_configService.Save();
_nameplateService.RequestRedraw();
}
if (ImGui.Checkbox("Override party color", ref isPartyOverride))
{
_configService.Current.overridePartyColor = isPartyOverride;
_configService.Save();
_nameplateService.RequestRedraw();
}
if (ImGui.Checkbox("Override FC tag color", ref isFcTagOverride))
{
_configService.Current.overrideFcTagColor = isFcTagOverride;
_configService.Save();
_nameplateService.RequestRedraw();
}
}
ImGui.Spacing();
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TextUnformatted("UI Theme");
if (ImGui.Checkbox("Use the redesign of the UI for Lightless client", ref useLightlessRedesign))
{
_configService.Current.UseLightlessRedesign = useLightlessRedesign;
_configService.Save();
}
var usePairColoredUIDs = _configService.Current.useColoredUIDs;
if (ImGui.Checkbox("Toggle the colored UID's in pair list", ref usePairColoredUIDs))
{
_configService.Current.useColoredUIDs = usePairColoredUIDs;
_configService.Save();
}
_uiShared.DrawHelpText("This changes the vanity colored UID's in pair list.");
DrawThemeOverridesSection();
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop();
}
ImGui.Separator();
if (_uiShared.MediumTreeNode("Pair List", UIColors.Get("LightlessPurple")))
{
if (ImGui.Checkbox("Show separate Visible group", ref showVisibleSeparate))
{
_configService.Current.ShowVisibleUsersSeparately = showVisibleSeparate;
_configService.Save();
Mediator.Publish(new RefreshUiMessage());
}
_uiShared.DrawHelpText(
"This will show all currently visible users in a special 'Visible' group in the main UI.");
using (ImRaii.Disabled(!showVisibleSeparate))
{
using var indent = ImRaii.PushIndent();
if (ImGui.Checkbox("Show Syncshell Users in Visible Group", ref groupInVisible))
{
_configService.Current.ShowSyncshellUsersInVisible = groupInVisible;
_configService.Save();
Mediator.Publish(new RefreshUiMessage());
}
}
if (ImGui.Checkbox("Show separate Offline group", ref showOfflineSeparate))
{
_configService.Current.ShowOfflineUsersSeparately = showOfflineSeparate;
_configService.Save();
Mediator.Publish(new RefreshUiMessage());
}
_uiShared.DrawHelpText(
"This will show all currently offline users in a special 'Offline' group in the main UI.");
using (ImRaii.Disabled(!showOfflineSeparate))
{
using var indent = ImRaii.PushIndent();
if (ImGui.Checkbox("Show separate Offline group for Syncshell users", ref syncshellOfflineSeparate))
{
_configService.Current.ShowSyncshellOfflineUsersSeparately = syncshellOfflineSeparate;
_configService.Save();
Mediator.Publish(new RefreshUiMessage());
}
}
if (ImGui.Checkbox("Group up all syncshells in one folder", ref groupUpSyncshells))
{
_configService.Current.GroupUpSyncshells = groupUpSyncshells;
_configService.Save();
Mediator.Publish(new RefreshUiMessage());
}
_uiShared.DrawHelpText(
"This will group up all Syncshells in a special 'All Syncshells' folder in the main UI.");
if (ImGui.Checkbox("Show grouped syncshells in main screen/all syncshells", ref groupedSyncshells))
{
_configService.Current.ShowGroupedSyncshellsInAll = groupedSyncshells;
_configService.Save();
Mediator.Publish(new RefreshUiMessage());
}
_uiShared.DrawHelpText("This will show grouped syncshells in main screen or group 'All Syncshells'.");
if (ImGui.Checkbox("Show player name for visible players", ref showNameInsteadOfNotes))
{
_configService.Current.ShowCharacterNameInsteadOfNotesForVisible = showNameInsteadOfNotes;
_configService.Save();
Mediator.Publish(new RefreshUiMessage());
}
_uiShared.DrawHelpText(
"This will show the character name instead of custom set note when a character is visible");
ImGui.Indent();
if (!_configService.Current.ShowCharacterNameInsteadOfNotesForVisible) ImGui.BeginDisabled();
if (ImGui.Checkbox("Prefer notes over player names for visible players", ref preferNotesInsteadOfName))
{
_configService.Current.PreferNotesOverNamesForVisible = preferNotesInsteadOfName;
_configService.Save();
Mediator.Publish(new RefreshUiMessage());
}
_uiShared.DrawHelpText("If you set a note for a player it will be shown instead of the player name");
if (!_configService.Current.ShowCharacterNameInsteadOfNotesForVisible) ImGui.EndDisabled();
ImGui.Unindent();
if (ImGui.Checkbox("Set visible pairs as focus targets when clicking the eye", ref useFocusTarget))
{
_configService.Current.UseFocusTarget = useFocusTarget;
_configService.Save();
}
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop();
}
ImGui.Separator();
if (_uiShared.MediumTreeNode("Profiles", UIColors.Get("LightlessPurple")))
{
if (ImGui.Checkbox("Show Lightless Profiles on Hover", ref showProfiles))
{
Mediator.Publish(new ClearProfileUserDataMessage());
_configService.Current.ProfilesShow = showProfiles;
_configService.Save();
}
_uiShared.DrawHelpText("This will show the configured user profile after a set delay");
ImGui.Indent();
if (!showProfiles) ImGui.BeginDisabled();
if (ImGui.Checkbox("Popout profiles on the right", ref profileOnRight))
{
_configService.Current.ProfilePopoutRight = profileOnRight;
_configService.Save();
Mediator.Publish(new CompactUiChange(Vector2.Zero, Vector2.Zero));
}
_uiShared.DrawHelpText("Will show profiles on the right side of the main UI");
if (ImGui.SliderFloat("Hover Delay", ref profileDelay, 1, 10))
{
_configService.Current.ProfileDelay = profileDelay;
_configService.Save();
}
_uiShared.DrawHelpText("Delay until the profile should be displayed");
if (!showProfiles) ImGui.EndDisabled();
ImGui.Unindent();
if (ImGui.Checkbox("Show profiles marked as NSFW", ref showNsfwProfiles))
{
Mediator.Publish(new ClearProfileUserDataMessage());
_configService.Current.ProfilesAllowNsfw = showNsfwProfiles;
_configService.Save();
}
_uiShared.DrawHelpText("Will show profiles that have the NSFW tag enabled");
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop();
}
ImGui.Separator();
}
private void DrawPerformance()
{
_uiShared.UnderlinedBigText("Performance Settings", UIColors.Get("LightlessBlue"));
ImGui.Dummy(new Vector2(10));
UiSharedService.TextWrapped(
"The configuration options here are to give you more informed warnings and automation when it comes to other performance-intensive synced players.");
bool showPerformanceIndicator = _playerPerformanceConfigService.Current.ShowPerformanceIndicator;
if (_uiShared.MediumTreeNode("Warnings", UIColors.Get("LightlessPurple")))
{
if (ImGui.Checkbox("Show performance indicator", ref showPerformanceIndicator))
{
_playerPerformanceConfigService.Current.ShowPerformanceIndicator = showPerformanceIndicator;
_playerPerformanceConfigService.Save();
}
_uiShared.DrawHelpText(
"Will show a performance indicator when players exceed defined thresholds in Lightless UI." +
Environment.NewLine + "Will use warning thresholds.");
bool warnOnExceedingThresholds = _playerPerformanceConfigService.Current.WarnOnExceedingThresholds;
if (ImGui.Checkbox("Warn on loading in players exceeding performance thresholds",
ref warnOnExceedingThresholds))
{
_playerPerformanceConfigService.Current.WarnOnExceedingThresholds = warnOnExceedingThresholds;
_playerPerformanceConfigService.Save();
}
_uiShared.DrawHelpText(
"Lightless will print a warning in chat once per session of meeting those people. Will not warn on players with preferred permissions.");
using (ImRaii.Disabled(!warnOnExceedingThresholds && !showPerformanceIndicator))
{
using var indent = ImRaii.PushIndent();
var warnOnPref = _playerPerformanceConfigService.Current.WarnOnPreferredPermissionsExceedingThresholds;
if (ImGui.Checkbox("Warn/Indicate also on players with preferred permissions", ref warnOnPref))
{
_playerPerformanceConfigService.Current.WarnOnPreferredPermissionsExceedingThresholds = warnOnPref;
_playerPerformanceConfigService.Save();
}
_uiShared.DrawHelpText(
"Lightless will also print warnings and show performance indicator for players where you enabled preferred permissions. If warning in general is disabled, this will not produce any warnings.");
}
using (ImRaii.Disabled(!showPerformanceIndicator && !warnOnExceedingThresholds))
{
var vram = _playerPerformanceConfigService.Current.VRAMSizeWarningThresholdMiB;
var tris = _playerPerformanceConfigService.Current.TrisWarningThresholdThousands;
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
if (ImGui.InputInt("Warning VRAM threshold", ref vram))
{
_playerPerformanceConfigService.Current.VRAMSizeWarningThresholdMiB = vram;
_playerPerformanceConfigService.Save();
}
ImGui.SameLine();
ImGui.Text("(MiB)");
_uiShared.DrawHelpText(
"Limit in MiB of approximate VRAM usage to trigger warning or performance indicator on UI." +
UiSharedService.TooltipSeparator
+ "Default: 375 MiB");
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
if (ImGui.InputInt("Warning Triangle threshold", ref tris))
{
_playerPerformanceConfigService.Current.TrisWarningThresholdThousands = tris;
_playerPerformanceConfigService.Save();
}
ImGui.SameLine();
ImGui.Text("(thousand triangles)");
_uiShared.DrawHelpText(
"Limit in approximate used triangles from mods to trigger warning or performance indicator on UI." +
UiSharedService.TooltipSeparator
+ "Default: 165 thousand");
}
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop();
}
ImGui.Separator();
bool autoPause = _playerPerformanceConfigService.Current.AutoPausePlayersExceedingThresholds;
bool autoPauseEveryone = _playerPerformanceConfigService.Current
.AutoPausePlayersWithPreferredPermissionsExceedingThresholds;
bool autoPauseInDuty = _playerPerformanceConfigService.Current.PauseInInstanceDuty;
bool autoPauseInCombat = _playerPerformanceConfigService.Current.PauseInCombat;
bool autoPauseWhilePerforming = _playerPerformanceConfigService.Current.PauseWhilePerforming;
if (_uiShared.MediumTreeNode("Auto Pause", UIColors.Get("LightlessPurple")))
{
if (ImGui.Checkbox("Auto pause sync while combat", ref autoPauseInCombat))
{
_playerPerformanceConfigService.Current.PauseInCombat = autoPauseInCombat;
_playerPerformanceConfigService.Save();
}
_uiShared.DrawHelpText(
"AUTO-ENABLED: Your risk of crashing during a fight increases when this is disabled. For example: VFX mods Loading mid fight can cause a crash." +
Environment.NewLine
+ UiSharedService.TooltipSeparator + "WARNING: DISABLE AT YOUR OWN RISK.");
if (ImGui.Checkbox("Auto pause sync while in Perfomance as Bard", ref autoPauseWhilePerforming))
{
_playerPerformanceConfigService.Current.PauseWhilePerforming = autoPauseWhilePerforming;
_playerPerformanceConfigService.Save();
}
_uiShared.DrawHelpText(
"AUTO-ENABLED: Your risk of crashing during a performance increases when this is disabled. For example: Some mods can crash you mid performance" +
Environment.NewLine
+ UiSharedService.TooltipSeparator + "WARNING: DISABLE AT YOUR OWN RISK.");
if (ImGui.Checkbox("Auto pause sync while in instances and duties", ref autoPauseInDuty))
{
_playerPerformanceConfigService.Current.PauseInInstanceDuty = autoPauseInDuty;
_playerPerformanceConfigService.Save();
}
_uiShared.DrawHelpText(
"When enabled, it will automatically pause all players while you are in an instance, such as a dungeon or raid." +
Environment.NewLine
+ UiSharedService.TooltipSeparator +
"Warning: You may have to leave the dungeon to resync with people again");
if (ImGui.Checkbox("Automatically pause players exceeding thresholds", ref autoPause))
{
_playerPerformanceConfigService.Current.AutoPausePlayersExceedingThresholds = autoPause;
_playerPerformanceConfigService.Save();
}
_uiShared.DrawHelpText(
"When enabled, it will automatically pause all players without preferred permissions that exceed the thresholds defined below." +
Environment.NewLine
+ "Will print a warning in chat when a player got paused automatically."
+ UiSharedService.TooltipSeparator +
"Warning: this will not automatically unpause those people again, you will have to do this manually.");
using (ImRaii.Disabled(!autoPause))
{
using var indent = ImRaii.PushIndent();
if (ImGui.Checkbox("Automatically pause also players with preferred permissions",
ref autoPauseEveryone))
{
_playerPerformanceConfigService.Current
.AutoPausePlayersWithPreferredPermissionsExceedingThresholds = autoPauseEveryone;
_playerPerformanceConfigService.Save();
}
_uiShared.DrawHelpText(
"When enabled, will automatically pause all players regardless of preferred permissions that exceed thresholds defined below." +
UiSharedService.TooltipSeparator +
"Warning: this will not automatically unpause those people again, you will have to do this manually.");
var vramAuto = _playerPerformanceConfigService.Current.VRAMSizeAutoPauseThresholdMiB;
var trisAuto = _playerPerformanceConfigService.Current.TrisAutoPauseThresholdThousands;
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
if (ImGui.InputInt("Auto Pause VRAM threshold", ref vramAuto))
{
_playerPerformanceConfigService.Current.VRAMSizeAutoPauseThresholdMiB = vramAuto;
_playerPerformanceConfigService.Save();
}
ImGui.SameLine();
ImGui.Text("(MiB)");
_uiShared.DrawHelpText(
"When a loading in player and their VRAM usage exceeds this amount, automatically pauses the synced player." +
UiSharedService.TooltipSeparator
+ "Default: 550 MiB");
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
if (ImGui.InputInt("Auto Pause Triangle threshold", ref trisAuto))
{
_playerPerformanceConfigService.Current.TrisAutoPauseThresholdThousands = trisAuto;
_playerPerformanceConfigService.Save();
}
ImGui.SameLine();
ImGui.Text("(thousand triangles)");
_uiShared.DrawHelpText(
"When a loading in player and their triangle count exceeds this amount, automatically pauses the synced player." +
UiSharedService.TooltipSeparator
+ "Default: 250 thousand");
}
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop();
}
ImGui.Separator();
ImGui.Dummy(new Vector2(10));
_uiShared.UnderlinedBigText("Whitelisted UIDs", UIColors.Get("LightlessBlue"));
ImGuiHelpers.ScaledDummy(5);
UiSharedService.TextWrapped(
"The entries in the list below will be ignored for all warnings and auto pause operations.");
ImGui.Dummy(new Vector2(10));
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
ImGui.InputText("##ignoreuid", ref _uidToAddForIgnore, 20);
ImGui.SameLine();
using (ImRaii.Disabled(string.IsNullOrEmpty(_uidToAddForIgnore)))
{
if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Add UID/Vanity ID to whitelist"))
{
if (!_playerPerformanceConfigService.Current.UIDsToIgnore.Contains(_uidToAddForIgnore,
StringComparer.Ordinal))
{
_playerPerformanceConfigService.Current.UIDsToIgnore.Add(_uidToAddForIgnore);
_playerPerformanceConfigService.Save();
}
_uidToAddForIgnore = string.Empty;
}
}
_uiShared.DrawHelpText("Hint: UIDs are case sensitive.");
var playerList = _playerPerformanceConfigService.Current.UIDsToIgnore;
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
using (var lb = ImRaii.ListBox("UID whitelist"))
{
if (lb)
{
for (int i = 0; i < playerList.Count; i++)
{
bool shouldBeSelected = _selectedEntry == i;
if (ImGui.Selectable(playerList[i] + "##" + i, shouldBeSelected))
{
_selectedEntry = i;
}
}
}
}
using (ImRaii.Disabled(_selectedEntry == -1))
{
if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete selected UID"))
{
_playerPerformanceConfigService.Current.UIDsToIgnore.RemoveAt(_selectedEntry);
_selectedEntry = -1;
_playerPerformanceConfigService.Save();
}
}
}
private void DrawServerConfiguration()
{
_lastTab = "Service Settings";
if (ApiController.ServerAlive)
{
_uiShared.UnderlinedBigText("Service Actions", UIColors.Get("LightlessBlue"));
ImGui.Dummy(new Vector2(10));
if (_uiShared.MediumTreeNode("Deletion", UIColors.Get("DimRed")))
{
ImGuiHelpers.ScaledDummy(new Vector2(5, 5));
if (ImGui.Button("Delete all my files"))
{
_deleteFilesPopupModalShown = true;
ImGui.OpenPopup("Delete all your files?");
}
_uiShared.DrawHelpText("Completely deletes all your uploaded files on the service.");
if (ImGui.BeginPopupModal("Delete all your files?", ref _deleteFilesPopupModalShown,
UiSharedService.PopupWindowFlags))
{
UiSharedService.TextWrapped(
"All your own uploaded files on the service will be deleted.\nThis operation cannot be undone.");
ImGui.TextUnformatted("Are you sure you want to continue?");
ImGui.Separator();
ImGui.Spacing();
var buttonSize = (ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X -
ImGui.GetStyle().ItemSpacing.X) / 2;
if (ImGui.Button("Delete everything", new Vector2(buttonSize, 0)))
{
_ = Task.Run(_fileTransferManager.DeleteAllFiles);
_deleteFilesPopupModalShown = false;
}
ImGui.SameLine();
if (ImGui.Button("Cancel##cancelDelete", new Vector2(buttonSize, 0)))
{
_deleteFilesPopupModalShown = false;
}
UiSharedService.SetScaledWindowSize(325);
ImGui.EndPopup();
}
ImGui.SameLine();
if (ImGui.Button("Delete account"))
{
_deleteAccountPopupModalShown = true;
ImGui.OpenPopup("Delete your account?");
}
_uiShared.DrawHelpText("Completely deletes your account and all uploaded files to the service.");
if (ImGui.BeginPopupModal("Delete your account?", ref _deleteAccountPopupModalShown,
UiSharedService.PopupWindowFlags))
{
UiSharedService.TextWrapped(
"Your account and all associated files and data on the service will be deleted.");
UiSharedService.TextWrapped("Your UID will be removed from all pairing lists.");
ImGui.TextUnformatted("Are you sure you want to continue?");
ImGui.Separator();
ImGui.Spacing();
var buttonSize = (ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X -
ImGui.GetStyle().ItemSpacing.X) / 2;
if (ImGui.Button("Delete account", new Vector2(buttonSize, 0)))
{
_ = Task.Run(ApiController.UserDelete);
_deleteAccountPopupModalShown = false;
Mediator.Publish(new SwitchToIntroUiMessage());
}
ImGui.SameLine();
if (ImGui.Button("Cancel##cancelDelete", new Vector2(buttonSize, 0)))
{
_deleteAccountPopupModalShown = false;
}
UiSharedService.SetScaledWindowSize(325);
ImGui.EndPopup();
}
_uiShared.ColoredSeparator(UIColors.Get("DimRed"), 1.5f);
ImGui.TreePop();
}
ImGui.Separator();
}
ImGui.Dummy(new Vector2(10));
_uiShared.MediumText("Service & Character Settings", UIColors.Get("LightlessPurple"));
ImGui.Dummy(new Vector2(5));
var sendCensus = _serverConfigurationManager.SendCensusData;
if (ImGui.Checkbox("Send Statistical Census Data", ref sendCensus))
{
_serverConfigurationManager.SendCensusData = sendCensus;
}
_uiShared.DrawHelpText("This will allow sending census data to the currently connected service." +
UiSharedService.TooltipSeparator
+ "Census data contains:" + Environment.NewLine
+ "- Current World" + Environment.NewLine
+ "- Current Gender" + Environment.NewLine
+ "- Current Race" + Environment.NewLine
+ "- Current Clan (this is not your Free Company, this is e.g. Keeper or Seeker for Miqo'te)" +
UiSharedService.TooltipSeparator
+ "The census data is only saved temporarily and will be removed from the server on disconnect. It is stored temporarily associated with your UID while you are connected." +
UiSharedService.TooltipSeparator
+ "If you do not wish to participate in the statistical census, untick this box and reconnect to the server.");
ImGuiHelpers.ScaledDummy(new Vector2(10, 10));
var idx = _uiShared.DrawServiceSelection();
if (_lastSelectedServerIndex != idx)
{
_uiShared.ResetOAuthTasksState();
_secretKeysConversionCts = _secretKeysConversionCts.CancelRecreate();
_secretKeysConversionTask = null;
_lastSelectedServerIndex = idx;
}
ImGuiHelpers.ScaledDummy(new Vector2(10, 10));
var selectedServer = _serverConfigurationManager.GetServerByIndex(idx);
if (selectedServer == _serverConfigurationManager.CurrentServer)
{
UiSharedService.ColorTextWrapped(
"For any changes to be applied to the current service you need to reconnect to the service.",
UIColors.Get("LightlessYellow"));
}
bool useOauth = selectedServer.UseOAuth2;
if (ImGui.BeginTabBar("serverTabBar"))
{
if (ImGui.BeginTabItem("Character Management"))
{
if (selectedServer.SecretKeys.Any() || useOauth)
{
UiSharedService.ColorTextWrapped(
"Characters listed here will automatically connect to the selected Lightless service with the settings as provided below." +
" Make sure to enter the character names correctly or use the 'Add current character' button at the bottom.",
UIColors.Get("LightlessYellow"));
int i = 0;
_uiShared.DrawUpdateOAuthUIDsButton(selectedServer);
if (selectedServer.UseOAuth2 && !string.IsNullOrEmpty(selectedServer.OAuthToken))
{
bool hasSetSecretKeysButNoUid =
selectedServer.Authentications.Exists(u =>
u.SecretKeyIdx != -1 && string.IsNullOrEmpty(u.UID));
if (hasSetSecretKeysButNoUid)
{
ImGui.Dummy(new(5f, 5f));
UiSharedService.TextWrapped(
"Some entries have been detected that have previously been assigned secret keys but not UIDs. " +
"Press this button below to attempt to convert those entries.");
using (ImRaii.Disabled(_secretKeysConversionTask != null &&
!_secretKeysConversionTask.IsCompleted))
{
if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowsLeftRight,
"Try to Convert Secret Keys to UIDs"))
{
_secretKeysConversionTask =
ConvertSecretKeysToUIDs(selectedServer, _secretKeysConversionCts.Token);
}
}
if (_secretKeysConversionTask != null && !_secretKeysConversionTask.IsCompleted)
{
UiSharedService.ColorTextWrapped("Converting Secret Keys to UIDs",
UIColors.Get("LightlessYellow"));
}
if (_secretKeysConversionTask != null && _secretKeysConversionTask.IsCompletedSuccessfully)
{
Vector4? textColor = null;
if (_secretKeysConversionTask.Result.PartialSuccess)
{
textColor = UIColors.Get("LightlessYellow");
}
if (!_secretKeysConversionTask.Result.Success)
{
textColor = ImGuiColors.DalamudRed;
}
string text = $"Conversion has completed: {_secretKeysConversionTask.Result.Result}";
if (textColor == null)
{
UiSharedService.TextWrapped(text);
}
else
{
UiSharedService.ColorTextWrapped(text, textColor!.Value);
}
if (!_secretKeysConversionTask.Result.Success ||
_secretKeysConversionTask.Result.PartialSuccess)
{
UiSharedService.TextWrapped(
"In case of conversion failures, please set the UIDs for the failed conversions manually.");
}
}
}
}
ImGui.Separator();
string youName = _dalamudUtilService.GetPlayerName();
uint youWorld = _dalamudUtilService.GetHomeWorldId();
ulong youCid = _dalamudUtilService.GetCID();
if (!selectedServer.Authentications.Exists(a =>
string.Equals(a.CharacterName, youName, StringComparison.Ordinal) && a.WorldId == youWorld))
{
_uiShared.BigText("Your Character is not Configured", ImGuiColors.DalamudRed);
UiSharedService.ColorTextWrapped(
"You have currently no character configured that corresponds to your current name and world.",
ImGuiColors.DalamudRed);
var authWithCid = selectedServer.Authentications.Find(f => f.LastSeenCID == youCid);
if (authWithCid != null)
{
ImGuiHelpers.ScaledDummy(5);
UiSharedService.ColorText(
"A potential rename/world change from this character was detected:",
UIColors.Get("LightlessYellow"));
using (ImRaii.PushIndent(10f))
UiSharedService.ColorText(
"Entry: " + authWithCid.CharacterName + " - " +
_dalamudUtilService.WorldData.Value[(ushort)authWithCid.WorldId],
UIColors.Get("LightlessBlue"));
UiSharedService.ColorText(
"Press the button below to adjust that entry to your current character:",
UIColors.Get("LightlessYellow"));
using (ImRaii.PushIndent(10f))
UiSharedService.ColorText(
"Current: " + youName + " - " +
_dalamudUtilService.WorldData.Value[(ushort)youWorld],
UIColors.Get("LightlessBlue"));
ImGuiHelpers.ScaledDummy(5);
if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowRight,
"Update Entry to Current Character"))
{
authWithCid.CharacterName = youName;
authWithCid.WorldId = youWorld;
_serverConfigurationManager.Save();
}
}
ImGuiHelpers.ScaledDummy(5);
ImGui.Separator();
ImGuiHelpers.ScaledDummy(5);
}
foreach (var item in selectedServer.Authentications.ToList())
{
using var charaId = ImRaii.PushId("selectedChara" + i);
var worldIdx = (ushort)item.WorldId;
var data = _uiShared.WorldData.OrderBy(u => u.Value, StringComparer.Ordinal)
.ToDictionary(k => k.Key, k => k.Value);
if (!data.TryGetValue(worldIdx, out string? worldPreview))
{
worldPreview = data.First().Value;
}
Dictionary<int, SecretKey> keys = [];
if (!useOauth)
{
var secretKeyIdx = item.SecretKeyIdx;
keys = selectedServer.SecretKeys;
if (!keys.TryGetValue(secretKeyIdx, out var secretKey))
{
secretKey = new();
}
}
bool thisIsYou = false;
if (string.Equals(youName, item.CharacterName, StringComparison.OrdinalIgnoreCase)
&& youWorld == worldIdx)
{
thisIsYou = true;
}
bool misManaged = false;
if (selectedServer.UseOAuth2 && !string.IsNullOrEmpty(selectedServer.OAuthToken) &&
string.IsNullOrEmpty(item.UID))
{
misManaged = true;
}
if (!selectedServer.UseOAuth2 && item.SecretKeyIdx == -1)
{
misManaged = true;
}
Vector4 color = UIColors.Get("LightlessBlue");
string text = thisIsYou ? "Your Current Character" : string.Empty;
if (misManaged)
{
text += " [MISMANAGED (" + (selectedServer.UseOAuth2 ? "No UID Set" : "No Secret Key Set") +
")]";
color = ImGuiColors.DalamudRed;
}
if (selectedServer.Authentications.Where(e => e != item).Any(e =>
string.Equals(e.CharacterName, item.CharacterName, StringComparison.Ordinal)
&& e.WorldId == item.WorldId))
{
text += " [DUPLICATE]";
color = ImGuiColors.DalamudRed;
}
if (!string.IsNullOrEmpty(text))
{
text = text.Trim();
_uiShared.BigText(text, color);
}
var charaName = item.CharacterName;
if (ImGui.InputText("Character Name", ref charaName, 64))
{
item.CharacterName = charaName;
_serverConfigurationManager.Save();
}
_uiShared.DrawCombo("World##" + item.CharacterName + i, data, (w) => w.Value,
(w) =>
{
if (item.WorldId != w.Key)
{
item.WorldId = w.Key;
_serverConfigurationManager.Save();
}
},
EqualityComparer<KeyValuePair<ushort, string>>.Default.Equals(
data.FirstOrDefault(f => f.Key == worldIdx), default)
? data.First()
: data.First(f => f.Key == worldIdx));
if (!useOauth)
{
_uiShared.DrawCombo("Secret Key###" + item.CharacterName + i, keys,
(w) => w.Value.FriendlyName,
(w) =>
{
if (w.Key != item.SecretKeyIdx)
{
item.SecretKeyIdx = w.Key;
_serverConfigurationManager.Save();
}
},
EqualityComparer<KeyValuePair<int, SecretKey>>.Default.Equals(
keys.FirstOrDefault(f => f.Key == item.SecretKeyIdx), default)
? keys.First()
: keys.First(f => f.Key == item.SecretKeyIdx));
}
else
{
_uiShared.DrawUIDComboForAuthentication(i, item, selectedServer.ServerUri, _logger);
}
bool isAutoLogin = item.AutoLogin;
if (ImGui.Checkbox("Automatically login to Lightless", ref isAutoLogin))
{
item.AutoLogin = isAutoLogin;
_serverConfigurationManager.Save();
}
_uiShared.DrawHelpText(
"When enabled and logging into this character in XIV, Lightless will automatically connect to the current service.");
if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Character") &&
UiSharedService.CtrlPressed())
_serverConfigurationManager.RemoveCharacterFromServer(idx, item);
UiSharedService.AttachToolTip("Hold CTRL to delete this entry.");
i++;
if (item != selectedServer.Authentications.ToList()[^1])
{
ImGuiHelpers.ScaledDummy(5);
ImGui.Separator();
ImGuiHelpers.ScaledDummy(5);
}
}
if (selectedServer.Authentications.Any())
ImGui.Separator();
if (!selectedServer.Authentications.Exists(c =>
string.Equals(c.CharacterName, youName, StringComparison.Ordinal)
&& c.WorldId == youWorld))
{
if (_uiShared.IconTextButton(FontAwesomeIcon.User, "Add current character"))
{
_serverConfigurationManager.AddCurrentCharacterToServer(idx);
}
ImGui.SameLine();
}
if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Add new character"))
{
_serverConfigurationManager.AddEmptyCharacterToServer(idx);
}
}
else
{
UiSharedService.ColorTextWrapped("You need to add a Secret Key first before adding Characters.",
UIColors.Get("LightlessYellow"));
}
ImGui.EndTabItem();
}
if (!useOauth && ImGui.BeginTabItem("Secret Key Management"))
{
foreach (var item in selectedServer.SecretKeys.ToList())
{
using var id = ImRaii.PushId("key" + item.Key);
var friendlyName = item.Value.FriendlyName;
if (ImGui.InputText("Secret Key Display Name", ref friendlyName, 255))
{
item.Value.FriendlyName = friendlyName;
_serverConfigurationManager.Save();
}
var key = item.Value.Key;
if (ImGui.InputText("Secret Key", ref key, 64))
{
item.Value.Key = key;
_serverConfigurationManager.Save();
}
if (!selectedServer.Authentications.Exists(p => p.SecretKeyIdx == item.Key))
{
if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Secret Key") &&
UiSharedService.CtrlPressed())
{
selectedServer.SecretKeys.Remove(item.Key);
_serverConfigurationManager.Save();
}
UiSharedService.AttachToolTip("Hold CTRL to delete this secret key entry");
}
else
{
UiSharedService.ColorTextWrapped("This key is in use and cannot be deleted",
UIColors.Get("LightlessYellow"));
}
if (item.Key != selectedServer.SecretKeys.Keys.LastOrDefault())
ImGui.Separator();
}
ImGui.Separator();
if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Add new Secret Key"))
{
selectedServer.SecretKeys.Add(
selectedServer.SecretKeys.Any() ? selectedServer.SecretKeys.Max(p => p.Key) + 1 : 0,
new SecretKey() { FriendlyName = "New Secret Key", });
_serverConfigurationManager.Save();
}
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Service Configuration"))
{
var serverName = selectedServer.ServerName;
var serverUri = selectedServer.ServerUri;
var isMain = string.Equals(serverName, ApiController.MainServer, StringComparison.OrdinalIgnoreCase);
var flags = isMain ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None;
if (ImGui.InputText("Service URI", ref serverUri, 255, flags))
{
selectedServer.ServerUri = serverUri;
}
if (isMain)
{
_uiShared.DrawHelpText("You cannot edit the URI of the main service.");
}
if (ImGui.InputText("Service Name", ref serverName, 255, flags))
{
selectedServer.ServerName = serverName;
_serverConfigurationManager.Save();
}
if (isMain)
{
_uiShared.DrawHelpText("You cannot edit the name of the main service.");
}
ImGui.SetNextItemWidth(200);
var serverTransport = _serverConfigurationManager.GetTransport();
_uiShared.DrawCombo("Server Transport Type",
Enum.GetValues<HttpTransportType>().Where(t => t != HttpTransportType.None),
(v) => v.ToString(),
onSelected: (t) => _serverConfigurationManager.SetTransportType(t),
serverTransport);
_uiShared.DrawHelpText(
"You normally do not need to change this, if you don't know what this is or what it's for, keep it to WebSockets." +
Environment.NewLine
+ "If you run into connection issues with e.g. VPNs, try ServerSentEvents first before trying out LongPolling." +
UiSharedService.TooltipSeparator
+ "Note: if the server does not support a specific Transport Type it will fall through to the next automatically: WebSockets > ServerSentEvents > LongPolling");
ImGuiHelpers.ScaledDummy(5);
if (ImGui.Checkbox("Use Discord OAuth2 Authentication", ref useOauth))
{
selectedServer.UseOAuth2 = useOauth;
_serverConfigurationManager.Save();
}
_uiShared.DrawHelpText(
"Use Discord OAuth2 Authentication to identify with this server instead of secret keys");
if (useOauth)
{
_uiShared.DrawOAuth(selectedServer);
if (string.IsNullOrEmpty(_serverConfigurationManager.GetDiscordUserFromToken(selectedServer)))
{
ImGuiHelpers.ScaledDummy(10f);
UiSharedService.ColorTextWrapped(
"You have enabled OAuth2 but it is not linked. Press the buttons Check, then Authenticate to link properly.",
ImGuiColors.DalamudRed);
}
if (!string.IsNullOrEmpty(_serverConfigurationManager.GetDiscordUserFromToken(selectedServer))
&& selectedServer.Authentications.TrueForAll(u => string.IsNullOrEmpty(u.UID)))
{
ImGuiHelpers.ScaledDummy(10f);
UiSharedService.ColorTextWrapped(
"You have enabled OAuth2 but no characters configured. Set the correct UIDs for your characters in \"Character Management\".",
ImGuiColors.DalamudRed);
}
}
if (!isMain && selectedServer != _serverConfigurationManager.CurrentServer)
{
ImGui.Separator();
if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Service") &&
UiSharedService.CtrlPressed())
{
_serverConfigurationManager.DeleteServer(selectedServer);
}
_uiShared.DrawHelpText("Hold CTRL to delete this service");
}
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Permission Settings"))
{
_uiShared.BigText("Default Permission Settings");
if (selectedServer == _serverConfigurationManager.CurrentServer && _apiController.IsConnected)
{
UiSharedService.TextWrapped(
"Note: The default permissions settings here are not applied retroactively to existing pairs or joined Syncshells.");
UiSharedService.TextWrapped(
"Note: The default permissions settings here are sent and stored on the connected service.");
ImGuiHelpers.ScaledDummy(5f);
var perms = _apiController.DefaultPermissions!;
bool individualIsSticky = perms.IndividualIsSticky;
bool disableIndividualSounds = perms.DisableIndividualSounds;
bool disableIndividualAnimations = perms.DisableIndividualAnimations;
bool disableIndividualVFX = perms.DisableIndividualVFX;
if (ImGui.Checkbox("Individually set permissions become preferred permissions",
ref individualIsSticky))
{
perms.IndividualIsSticky = individualIsSticky;
_ = _apiController.UserUpdateDefaultPermissions(perms);
}
_uiShared.DrawHelpText(
"The preferred attribute means that the permissions to that user will never change through any of your permission changes to Syncshells " +
"(i.e. if you have paused one specific user in a Syncshell and they become preferred permissions, then pause and unpause the same Syncshell, the user will remain paused - " +
"if a user does not have preferred permissions, it will follow the permissions of the Syncshell and be unpaused)." +
Environment.NewLine + Environment.NewLine +
"This setting means:" + Environment.NewLine +
" - All new individual pairs get their permissions defaulted to preferred permissions." +
Environment.NewLine +
" - All individually set permissions for any pair will also automatically become preferred permissions. This includes pairs in Syncshells." +
Environment.NewLine + Environment.NewLine +
"It is possible to remove or set the preferred permission state for any pair at any time." +
Environment.NewLine + Environment.NewLine +
"If unsure, leave this setting off.");
ImGuiHelpers.ScaledDummy(3f);
if (ImGui.Checkbox("Disable individual pair sounds", ref disableIndividualSounds))
{
perms.DisableIndividualSounds = disableIndividualSounds;
_ = _apiController.UserUpdateDefaultPermissions(perms);
}
_uiShared.DrawHelpText("This setting will disable sound sync for all new individual pairs.");
if (ImGui.Checkbox("Disable individual pair animations", ref disableIndividualAnimations))
{
perms.DisableIndividualAnimations = disableIndividualAnimations;
_ = _apiController.UserUpdateDefaultPermissions(perms);
}
_uiShared.DrawHelpText("This setting will disable animation sync for all new individual pairs.");
if (ImGui.Checkbox("Disable individual pair VFX", ref disableIndividualVFX))
{
perms.DisableIndividualVFX = disableIndividualVFX;
_ = _apiController.UserUpdateDefaultPermissions(perms);
}
_uiShared.DrawHelpText("This setting will disable VFX sync for all new individual pairs.");
ImGuiHelpers.ScaledDummy(5f);
bool disableGroundSounds = perms.DisableGroupSounds;
bool disableGroupAnimations = perms.DisableGroupAnimations;
bool disableGroupVFX = perms.DisableGroupVFX;
if (ImGui.Checkbox("Disable Syncshell pair sounds", ref disableGroundSounds))
{
perms.DisableGroupSounds = disableGroundSounds;
_ = _apiController.UserUpdateDefaultPermissions(perms);
}
_uiShared.DrawHelpText(
"This setting will disable sound sync for all non-sticky pairs in newly joined syncshells.");
if (ImGui.Checkbox("Disable Syncshell pair animations", ref disableGroupAnimations))
{
perms.DisableGroupAnimations = disableGroupAnimations;
_ = _apiController.UserUpdateDefaultPermissions(perms);
}
_uiShared.DrawHelpText(
"This setting will disable animation sync for all non-sticky pairs in newly joined syncshells.");
if (ImGui.Checkbox("Disable Syncshell pair VFX", ref disableGroupVFX))
{
perms.DisableGroupVFX = disableGroupVFX;
_ = _apiController.UserUpdateDefaultPermissions(perms);
}
_uiShared.DrawHelpText(
"This setting will disable VFX sync for all non-sticky pairs in newly joined syncshells.");
}
else
{
UiSharedService.ColorTextWrapped("Default Permission Settings unavailable for this service. " +
"You need to connect to this service to change the default permissions since they are stored on the service.",
UIColors.Get("LightlessYellow"));
}
ImGui.EndTabItem();
}
ImGui.EndTabBar();
}
ImGui.Dummy(new Vector2(10));
}
private int _lastSelectedServerIndex = -1;
private Task<(bool Success, bool PartialSuccess, string Result)>? _secretKeysConversionTask = null;
private CancellationTokenSource _secretKeysConversionCts = new CancellationTokenSource();
private async Task<(bool Success, bool partialSuccess, string Result)> ConvertSecretKeysToUIDs(
ServerStorage serverStorage, CancellationToken token)
{
List<Authentication> failedConversions = serverStorage.Authentications
.Where(u => u.SecretKeyIdx == -1 && string.IsNullOrEmpty(u.UID)).ToList();
List<Authentication> conversionsToAttempt = serverStorage.Authentications
.Where(u => u.SecretKeyIdx != -1 && string.IsNullOrEmpty(u.UID)).ToList();
List<Authentication> successfulConversions = [];
Dictionary<string, List<Authentication>> secretKeyMapping = new(StringComparer.Ordinal);
foreach (var authEntry in conversionsToAttempt)
{
if (!serverStorage.SecretKeys.TryGetValue(authEntry.SecretKeyIdx, out var secretKey))
{
failedConversions.Add(authEntry);
continue;
}
if (!secretKeyMapping.TryGetValue(secretKey.Key, out List<Authentication>? authList))
{
secretKeyMapping[secretKey.Key] = authList = [];
}
authList.Add(authEntry);
}
if (secretKeyMapping.Count == 0)
{
return (false, false,
$"Failed to convert {failedConversions.Count} entries: " +
string.Join(", ", failedConversions.Select(k => k.CharacterName)));
}
var baseUri = serverStorage.ServerUri.Replace("wss://", "https://").Replace("ws://", "http://");
var oauthCheckUri = LightlessAuth.GetUIDsBasedOnSecretKeyFullPath(new Uri(baseUri));
var requestContent = JsonContent.Create(secretKeyMapping.Select(k => k.Key).ToList());
HttpRequestMessage requestMessage = new(HttpMethod.Post, oauthCheckUri);
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", serverStorage.OAuthToken);
requestMessage.Content = requestContent;
using var response = await _httpClient.SendAsync(requestMessage, token).ConfigureAwait(false);
Dictionary<string, string>? secretKeyUidMapping = await JsonSerializer
.DeserializeAsync<Dictionary<string, string>>
(await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false), cancellationToken: token)
.ConfigureAwait(false);
if (secretKeyUidMapping == null)
{
return (false, false, $"Failed to parse the server response. Failed to convert all entries.");
}
foreach (var entry in secretKeyMapping)
{
if (!secretKeyUidMapping.TryGetValue(entry.Key, out var assignedUid) || string.IsNullOrEmpty(assignedUid))
{
failedConversions.AddRange(entry.Value);
continue;
}
foreach (var auth in entry.Value)
{
auth.UID = assignedUid;
successfulConversions.Add(auth);
}
}
if (successfulConversions.Count > 0)
_serverConfigurationManager.Save();
StringBuilder sb = new();
sb.Append("Conversion complete." + Environment.NewLine);
sb.Append($"Successfully converted {successfulConversions.Count} entries." + Environment.NewLine);
if (failedConversions.Count > 0)
{
sb.Append($"Failed to convert {failedConversions.Count} entries, assign those manually: ");
sb.Append(string.Join(", ", failedConversions.Select(k => k.CharacterName)));
}
return (true, failedConversions.Count != 0, sb.ToString());
}
private static string GetLightfinderPresetGlyph(int index)
{
return NameplateHandler.NormalizeIconGlyph(
SeIconCharExtensions.ToIconString(LightfinderIconPresets[index].Icon));
}
private void RefreshLightfinderIconState()
{
var normalized = NameplateHandler.NormalizeIconGlyph(_configService.Current.LightfinderLabelIconGlyph);
_lightfinderIconInput = NameplateHandler.ToIconEditorString(normalized);
_lightfinderIconInputInitialized = true;
_lightfinderIconPresetIndex = -1;
for (int i = 0; i < LightfinderIconPresets.Length; i++)
{
if (string.Equals(GetLightfinderPresetGlyph(i), normalized, StringComparison.Ordinal))
{
_lightfinderIconPresetIndex = i;
break;
}
}
}
private void ApplyLightfinderIcon(string normalizedGlyph, int presetIndex)
{
_configService.Current.LightfinderLabelIconGlyph = normalizedGlyph;
_configService.Save();
_nameplateHandler.FlagRefresh();
_nameplateService.RequestRedraw();
_lightfinderIconInput = NameplateHandler.ToIconEditorString(normalizedGlyph);
_lightfinderIconPresetIndex = presetIndex;
_lightfinderIconInputInitialized = true;
}
private void DrawSettingsContent()
{
if (_apiController.ServerState is ServerState.Connected)
{
ImGui.TextUnformatted("Service " + _serverConfigurationManager.CurrentServer!.ServerName + ":");
ImGui.SameLine();
ImGui.TextColored(UIColors.Get("LightlessBlue"), "Available");
ImGui.SameLine();
ImGui.TextUnformatted("(");
ImGui.SameLine();
ImGui.TextColored(UIColors.Get("LightlessBlue"),
_apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture));
ImGui.SameLine();
ImGui.TextUnformatted("Users Online");
ImGui.SameLine();
ImGui.TextUnformatted(")");
}
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Community and Support:");
ImGui.SameLine();
if (ImGui.Button("Lightless Sync Discord"))
{
Util.OpenLink("https://discord.gg/Lightless");
}
ImGui.Separator();
if (ImGui.BeginTabBar("mainTabBar"))
{
var generalTabFlags = ImGuiTabItemFlags.None;
if (_selectGeneralTabOnNextDraw)
{
generalTabFlags |= ImGuiTabItemFlags.SetSelected;
}
if (ImGui.BeginTabItem("General", generalTabFlags))
{
_selectGeneralTabOnNextDraw = false;
DrawGeneral();
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Performance"))
{
DrawPerformance();
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Storage"))
{
DrawFileStorageSettings();
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Transfers"))
{
DrawCurrentTransfers();
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Service Settings"))
{
DrawServerConfiguration();
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Notifications"))
{
DrawNotificationSettings();
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Debug"))
{
DrawDebug();
ImGui.EndTabItem();
}
ImGui.EndTabBar();
}
}
private void UiSharedService_GposeEnd()
{
IsOpen = _wasOpen;
}
private void UiSharedService_GposeStart()
{
_wasOpen = IsOpen;
IsOpen = false;
}
private void DrawNotificationSettings()
{
_lastTab = "Notifications";
_uiShared.UnderlinedBigText("Notification System", UIColors.Get("LightlessBlue"));
ImGuiHelpers.ScaledDummy(5);
bool useLightlessNotifications = _configService.Current.UseLightlessNotifications;
if (ImGui.Checkbox("Use Lightless Notifications", ref useLightlessNotifications))
{
_configService.Current.UseLightlessNotifications = useLightlessNotifications;
_configService.Save();
}
_uiShared.DrawHelpText(
"Enable modern notifications with interactive buttons, animations, and progress tracking. Disable for classic Dalamud toast/chat notifications.");
ImGuiHelpers.ScaledDummy(5);
ImGui.Separator();
if (_uiShared.MediumTreeNode("Notification Locations", UIColors.Get("LightlessPurple")))
{
UiSharedService.ColorTextWrapped("Choose where each notification type appears.", ImGuiColors.DalamudGrey);
ImGuiHelpers.ScaledDummy(5);
if (useLightlessNotifications)
{
// Lightless notification locations
var lightlessLocations = GetLightlessNotificationLocations();
var downloadLocations = GetDownloadNotificationLocations();
if (ImGui.BeginTable("##NotificationLocationTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit))
{
ImGui.TableSetupColumn("Notification Type", ImGuiTableColumnFlags.WidthFixed, 200f * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Location", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Test", ImGuiTableColumnFlags.WidthFixed, 40f * ImGuiHelpers.GlobalScale);
ImGui.TableHeadersRow();
ImGui.TableNextRow();
ImGui.TableSetColumnIndex(0);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Info Notifications");
ImGui.TableSetColumnIndex(1);
ImGui.SetNextItemWidth(-1);
_uiShared.DrawCombo("###enhanced_info", lightlessLocations, GetNotificationLocationLabel, (location) =>
{
_configService.Current.LightlessInfoNotification = location;
_configService.Save();
}, _configService.Current.LightlessInfoNotification);
ImGui.TableSetColumnIndex(2);
var availableWidth = ImGui.GetContentRegionAvail().X;
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_info", new Vector2(availableWidth, 0)))
{
Mediator.Publish(new NotificationMessage("Test Info",
"This is a test info notification to let you know Chocola is cute :3", NotificationType.Info));
}
}
UiSharedService.AttachToolTip("Test info notification");
ImGui.TableNextRow();
ImGui.TableSetColumnIndex(0);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Warning Notifications");
ImGui.TableSetColumnIndex(1);
ImGui.SetNextItemWidth(-1);
_uiShared.DrawCombo("###enhanced_warning", lightlessLocations, GetNotificationLocationLabel,
(location) =>
{
_configService.Current.LightlessWarningNotification = location;
_configService.Save();
}, _configService.Current.LightlessWarningNotification);
ImGui.TableSetColumnIndex(2);
availableWidth = ImGui.GetContentRegionAvail().X;
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_warning", new Vector2(availableWidth, 0)))
{
Mediator.Publish(new NotificationMessage("Test Warning", "This is a test warning notification!",
NotificationType.Warning));
}
}
UiSharedService.AttachToolTip("Test warning notification");
ImGui.TableNextRow();
ImGui.TableSetColumnIndex(0);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Error Notifications");
ImGui.TableSetColumnIndex(1);
ImGui.SetNextItemWidth(-1);
_uiShared.DrawCombo("###enhanced_error", lightlessLocations, GetNotificationLocationLabel, (location) =>
{
_configService.Current.LightlessErrorNotification = location;
_configService.Save();
}, _configService.Current.LightlessErrorNotification);
ImGui.TableSetColumnIndex(2);
availableWidth = ImGui.GetContentRegionAvail().X;
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_error", new Vector2(availableWidth, 0)))
{
Mediator.Publish(new NotificationMessage("Test Error", "This is a test error notification!",
NotificationType.Error));
}
}
UiSharedService.AttachToolTip("Test error notification");
ImGui.TableNextRow();
ImGui.TableSetColumnIndex(0);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Pair Request Notifications");
ImGui.TableSetColumnIndex(1);
ImGui.SetNextItemWidth(-1);
_uiShared.DrawCombo("###enhanced_pairrequest", lightlessLocations, GetNotificationLocationLabel,
(location) =>
{
_configService.Current.LightlessPairRequestNotification = location;
_configService.Save();
}, _configService.Current.LightlessPairRequestNotification);
ImGui.TableSetColumnIndex(2);
availableWidth = ImGui.GetContentRegionAvail().X;
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_pair", new Vector2(availableWidth, 0)))
{
Mediator.Publish(new PairRequestReceivedMessage("test-uid-123", "Test User wants to pair with you."));
}
}
UiSharedService.AttachToolTip("Test pair request notification");
ImGui.TableNextRow();
ImGui.TableSetColumnIndex(0);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Download Progress Notifications");
ImGui.TableSetColumnIndex(1);
ImGui.SetNextItemWidth(-1);
_uiShared.DrawCombo("###enhanced_download", downloadLocations, GetNotificationLocationLabel,
(location) =>
{
_configService.Current.LightlessDownloadNotification = location;
_configService.Save();
}, _configService.Current.LightlessDownloadNotification);
ImGui.TableSetColumnIndex(2);
availableWidth = ImGui.GetContentRegionAvail().X;
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_download", new Vector2(availableWidth, 0)))
{
Mediator.Publish(new PairDownloadStatusMessage(
[
("Player One", 0.35f, "downloading"),
("Player Two", 0.75f, "downloading"),
("Player Three", 1.0f, "downloading")
],
2
));
}
}
UiSharedService.AttachToolTip("Test download progress notification");
ImGui.TableNextRow();
ImGui.TableSetColumnIndex(0);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Performance Notifications");
ImGui.TableSetColumnIndex(1);
ImGui.SetNextItemWidth(-1);
_uiShared.DrawCombo("###enhanced_performance", lightlessLocations, GetNotificationLocationLabel,
(location) =>
{
_configService.Current.LightlessPerformanceNotification = location;
_configService.Save();
}, _configService.Current.LightlessPerformanceNotification);
ImGui.TableSetColumnIndex(2);
availableWidth = ImGui.GetContentRegionAvail().X;
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_performance", new Vector2(availableWidth, 0)))
{
var testUserData = new UserData("TEST123", "TestUser", false, false, false, null, null);
Mediator.Publish(new PerformanceNotificationMessage(
"Test Player (TestUser) exceeds performance threshold(s)",
"Player Test Player (TestUser) exceeds your configured VRAM warning threshold\n500 MB/300 MB",
testUserData,
false,
"Test Player"
));
}
}
UiSharedService.AttachToolTip("Test performance notification");
ImGui.EndTable();
}
ImGuiHelpers.ScaledDummy(5);
if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Clear All Notifications"))
{
Mediator.Publish(new ClearAllNotificationsMessage());
}
_uiShared.DrawHelpText("Dismiss all active notifications immediately.");
}
else
{
// Classic notifications when lightless notifs is disabled
var classicLocations = GetClassicNotificationLocations();
ImGui.Indent();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Info Notifications:");
ImGui.SameLine();
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
_uiShared.DrawCombo("###info", classicLocations, GetNotificationLocationLabel, (location) =>
{
_configService.Current.InfoNotification = location;
_configService.Save();
}, _configService.Current.InfoNotification);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Warning Notifications:");
ImGui.SameLine();
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
_uiShared.DrawCombo("###warning", classicLocations, GetNotificationLocationLabel, (location) =>
{
_configService.Current.WarningNotification = location;
_configService.Save();
}, _configService.Current.WarningNotification);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Error Notifications:");
ImGui.SameLine();
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
_uiShared.DrawCombo("###error", classicLocations, GetNotificationLocationLabel, (location) =>
{
_configService.Current.ErrorNotification = location;
_configService.Save();
}, _configService.Current.ErrorNotification);
ImGui.Unindent();
}
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop();
}
ImGui.Separator();
if (useLightlessNotifications)
{
if (_uiShared.MediumTreeNode("Basic Settings", UIColors.Get("LightlessPurple")))
{
int maxNotifications = _configService.Current.MaxSimultaneousNotifications;
if (ImGui.SliderInt("Max Simultaneous Notifications", ref maxNotifications, 1, 10))
{
_configService.Current.MaxSimultaneousNotifications = Math.Clamp(maxNotifications, 1, 10);
_configService.Save();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_configService.Current.MaxSimultaneousNotifications = 5;
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default (5).");
_uiShared.DrawHelpText("Maximum number of notifications that can be shown at once.");
bool showTimestamp = _configService.Current.ShowNotificationTimestamp;
if (ImGui.Checkbox("Show Timestamps", ref showTimestamp))
{
_configService.Current.ShowNotificationTimestamp = showTimestamp;
_configService.Save();
}
_uiShared.DrawHelpText("Display the time when each notification was created.");
bool dismissOnClick = _configService.Current.DismissNotificationOnClick;
if (ImGui.Checkbox("Dismiss on Click", ref dismissOnClick))
{
_configService.Current.DismissNotificationOnClick = dismissOnClick;
_configService.Save();
}
_uiShared.DrawHelpText("Click anywhere on a notification to dismiss it. Notifications with action buttons (like pair requests) are excluded.");
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop();
}
if (useLightlessNotifications)
{
ImGui.Separator();
if (_uiShared.MediumTreeNode("Appearance & Animation", UIColors.Get("LightlessPurple")))
{
float opacity = _configService.Current.NotificationOpacity;
if (ImGui.SliderFloat("Notification Opacity", ref opacity, 0.1f, 1.0f, "%.2f"))
{
_configService.Current.NotificationOpacity = Math.Clamp(opacity, 0.1f, 1.0f);
_configService.Save();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_configService.Current.NotificationOpacity = 0.95f;
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default (0.95).");
_uiShared.DrawHelpText("Transparency level of notification windows.");
ImGui.Spacing();
ImGui.TextUnformatted("Size & Layout");
float notifWidth = _configService.Current.NotificationWidth;
if (ImGui.SliderFloat("Notification Width", ref notifWidth, 250f, 600f, "%.0f"))
{
_configService.Current.NotificationWidth = Math.Clamp(notifWidth, 250f, 600f);
_configService.Save();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_configService.Current.NotificationWidth = 350f;
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default (350).");
_uiShared.DrawHelpText("Width of notification windows.");
float notifSpacing = _configService.Current.NotificationSpacing;
if (ImGui.SliderFloat("Notification Spacing", ref notifSpacing, 0f, 30f, "%.0f"))
{
_configService.Current.NotificationSpacing = Math.Clamp(notifSpacing, 0f, 30f);
_configService.Save();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_configService.Current.NotificationSpacing = 8f;
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default (8).");
_uiShared.DrawHelpText("Gap between stacked notifications.");
ImGui.Spacing();
ImGui.TextUnformatted("Position");
var currentCorner = _configService.Current.NotificationCorner;
if (ImGui.BeginCombo("Notification Position", GetNotificationCornerLabel(currentCorner)))
{
foreach (NotificationCorner corner in Enum.GetValues(typeof(NotificationCorner)))
{
bool isSelected = currentCorner == corner;
if (ImGui.Selectable(GetNotificationCornerLabel(corner), isSelected))
{
_configService.Current.NotificationCorner = corner;
_configService.Save();
}
if (isSelected)
ImGui.SetItemDefaultFocus();
}
ImGui.EndCombo();
}
_uiShared.DrawHelpText("Choose which corner of the screen notifications appear in.");
int offsetY = _configService.Current.NotificationOffsetY;
if (ImGui.SliderInt("Vertical Offset", ref offsetY, 0, 1000))
{
_configService.Current.NotificationOffsetY = Math.Clamp(offsetY, 0, 1000);
_configService.Save();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_configService.Current.NotificationOffsetY = 50;
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default (50).");
_uiShared.DrawHelpText("Distance from the top edge of the screen.");
int offsetX = _configService.Current.NotificationOffsetX;
if (ImGui.SliderInt("Horizontal Offset", ref offsetX, 0, 500))
{
_configService.Current.NotificationOffsetX = Math.Clamp(offsetX, 0, 500);
_configService.Save();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_configService.Current.NotificationOffsetX = 0;
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default (0).");
_uiShared.DrawHelpText("Move notifications left from the right edge.");
ImGui.Spacing();
ImGui.TextUnformatted("Animation Settings");
float animSpeed = _configService.Current.NotificationAnimationSpeed;
if (ImGui.SliderFloat("Animation Speed", ref animSpeed, 1f, 20f, "%.1f"))
{
_configService.Current.NotificationAnimationSpeed = Math.Clamp(animSpeed, 1f, 20f);
_configService.Save();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_configService.Current.NotificationAnimationSpeed = 10f;
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default (10).");
_uiShared.DrawHelpText("How fast notifications slide in/out. Higher = faster.");
float slideSpeed = _configService.Current.NotificationSlideSpeed;
if (ImGui.SliderFloat("Slide Speed", ref slideSpeed, 1f, 20f, "%.1f"))
{
_configService.Current.NotificationSlideSpeed = Math.Clamp(slideSpeed, 1f, 20f);
_configService.Save();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_configService.Current.NotificationSlideSpeed = 10f;
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default (10).");
_uiShared.DrawHelpText("How fast notifications slide into position when others disappear. Higher = faster.");
ImGui.Spacing();
ImGui.TextUnformatted("Visual Effects");
float accentWidth = _configService.Current.NotificationAccentBarWidth;
if (ImGui.SliderFloat("Accent Bar Width", ref accentWidth, 0f, 10f, "%.1f"))
{
_configService.Current.NotificationAccentBarWidth = Math.Clamp(accentWidth, 0f, 10f);
_configService.Save();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_configService.Current.NotificationAccentBarWidth = 3f;
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default (3).");
_uiShared.DrawHelpText("Width of the colored accent bar on the left side.");
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop();
}
}
ImGui.Separator();
if (_uiShared.MediumTreeNode("Duration Settings", UIColors.Get("LightlessPurple")))
{
UiSharedService.ColorTextWrapped("Configure how long each notification type stays visible.", ImGuiColors.DalamudGrey);
ImGuiHelpers.ScaledDummy(5);
int infoDuration = _configService.Current.InfoNotificationDurationSeconds;
if (ImGui.SliderInt("Info Duration (seconds)", ref infoDuration, 3, 60))
{
_configService.Current.InfoNotificationDurationSeconds = Math.Clamp(infoDuration, 3, 60);
_configService.Save();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_configService.Current.InfoNotificationDurationSeconds = 10;
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default (10).");
int warningDuration = _configService.Current.WarningNotificationDurationSeconds;
if (ImGui.SliderInt("Warning Duration (seconds)", ref warningDuration, 3, 60))
{
_configService.Current.WarningNotificationDurationSeconds = Math.Clamp(warningDuration, 3, 60);
_configService.Save();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_configService.Current.WarningNotificationDurationSeconds = 15;
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default (15).");
int errorDuration = _configService.Current.ErrorNotificationDurationSeconds;
if (ImGui.SliderInt("Error Duration (seconds)", ref errorDuration, 3, 120))
{
_configService.Current.ErrorNotificationDurationSeconds = Math.Clamp(errorDuration, 3, 120);
_configService.Save();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_configService.Current.ErrorNotificationDurationSeconds = 20;
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default (20).");
int pairRequestDuration = _configService.Current.PairRequestDurationSeconds;
if (ImGui.SliderInt("Pair Request Duration (seconds)", ref pairRequestDuration, 30, 600))
{
_configService.Current.PairRequestDurationSeconds = Math.Clamp(pairRequestDuration, 30, 600);
_configService.Save();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_configService.Current.PairRequestDurationSeconds = 180;
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default (180).");
int downloadDuration = _configService.Current.DownloadNotificationDurationSeconds;
if (ImGui.SliderInt("Download Duration (seconds)", ref downloadDuration, 60, 600))
{
_configService.Current.DownloadNotificationDurationSeconds = Math.Clamp(downloadDuration, 60, 600);
_configService.Save();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_configService.Current.DownloadNotificationDurationSeconds = 300;
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default (300).");
int performanceDuration = _configService.Current.PerformanceNotificationDurationSeconds;
if (ImGui.SliderInt("Performance Duration (seconds)", ref performanceDuration, 5, 60))
{
_configService.Current.PerformanceNotificationDurationSeconds = Math.Clamp(performanceDuration, 5, 60);
_configService.Save();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_configService.Current.PerformanceNotificationDurationSeconds = 20;
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default (20).");
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop();
}
ImGui.Separator();
if (_uiShared.MediumTreeNode("Sound Settings", UIColors.Get("LightlessPurple")))
{
ImGui.TextUnformatted("Notification Sounds");
ImGuiHelpers.ScaledDummy(5);
DrawSoundTable();
_uiShared.DrawHelpText(
"Configure which sounds play for each notification type. Use the play button to preview sounds.");
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop();
}
ImGui.Separator();
_uiShared.UnderlinedBigText("Specific Notification Types", UIColors.Get("LightlessBlue"));
ImGuiHelpers.ScaledDummy(5);
UiSharedService.ColorTextWrapped("Configure specific types of notifications and their behavior.",
ImGuiColors.DalamudGrey);
ImGuiHelpers.ScaledDummy(3);
// Online Notifications Section
if (_uiShared.MediumTreeNode("Online Status Notifications", UIColors.Get("LightlessGreen")))
{
var onlineNotifs = _configService.Current.ShowOnlineNotifications;
var onlineNotifsPairsOnly = _configService.Current.ShowOnlineNotificationsOnlyForIndividualPairs;
var onlineNotifsNamedOnly = _configService.Current.ShowOnlineNotificationsOnlyForNamedPairs;
if (ImGui.Checkbox("Enable online notifications", ref onlineNotifs))
{
_configService.Current.ShowOnlineNotifications = onlineNotifs;
_configService.Save();
}
_uiShared.DrawHelpText(
"Show notifications when pairs come online. These will use the Info notification location settings above.");
using var disabled = ImRaii.Disabled(!onlineNotifs);
ImGui.Indent();
if (ImGui.Checkbox("Notify only for individual pairs", ref onlineNotifsPairsOnly))
{
_configService.Current.ShowOnlineNotificationsOnlyForIndividualPairs = onlineNotifsPairsOnly;
_configService.Save();
}
_uiShared.DrawHelpText("Only show online notifications for individual pairs (not syncshell members).");
if (ImGui.Checkbox("Notify only for named pairs", ref onlineNotifsNamedOnly))
{
_configService.Current.ShowOnlineNotificationsOnlyForNamedPairs = onlineNotifsNamedOnly;
_configService.Save();
}
_uiShared.DrawHelpText(
"Only show online notifications for pairs where you have set an individual note.");
ImGui.Unindent();
_uiShared.ColoredSeparator(UIColors.Get("LightlessGreen"), 1.5f);
ImGui.TreePop();
}
if (_uiShared.MediumTreeNode("Pair Request Notifications", UIColors.Get("PairBlue")))
{
var showPairRequestActions = _configService.Current.ShowPairRequestNotificationActions;
if (ImGui.Checkbox("Show action buttons on pair requests", ref showPairRequestActions))
{
_configService.Current.ShowPairRequestNotificationActions = showPairRequestActions;
_configService.Save();
}
_uiShared.DrawHelpText(
"When you receive a pair request, show Accept/Decline buttons in the notification.");
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop();
}
if (_uiShared.MediumTreeNode("Performance Notifications", UIColors.Get("LightlessOrange")))
{
var showPerformanceActions = _configService.Current.ShowPerformanceNotificationActions;
if (ImGui.Checkbox("Show action buttons on performance warnings", ref showPerformanceActions))
{
_configService.Current.ShowPerformanceNotificationActions = showPerformanceActions;
_configService.Save();
}
_uiShared.DrawHelpText(
"When a player exceeds performance thresholds or is auto-paused, show Pause/Unpause buttons in the notification.");
_uiShared.ColoredSeparator(UIColors.Get("LightlessOrange"), 1.5f);
ImGui.TreePop();
}
if (_uiShared.MediumTreeNode("System Notifications", UIColors.Get("LightlessYellow")))
{
var disableOptionalPluginWarnings = _configService.Current.DisableOptionalPluginWarnings;
if (ImGui.Checkbox("Disable optional plugin warnings", ref disableOptionalPluginWarnings))
{
_configService.Current.DisableOptionalPluginWarnings = disableOptionalPluginWarnings;
_configService.Save();
}
_uiShared.DrawHelpText("Disable warning notifications for missing optional plugins.");
_uiShared.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f);
ImGui.TreePop();
}
ImGui.Separator();
// Location descriptions removed - information is now inline with each setting
}
}
private NotificationLocation[] GetLightlessNotificationLocations()
{
return new[]
{
NotificationLocation.LightlessUi, NotificationLocation.ChatAndLightlessUi, NotificationLocation.Nowhere
};
}
private NotificationLocation[] GetDownloadNotificationLocations()
{
return new[]
{
NotificationLocation.LightlessUi, NotificationLocation.TextOverlay, NotificationLocation.Nowhere
};
}
private NotificationLocation[] GetClassicNotificationLocations()
{
return new[]
{
NotificationLocation.Toast, NotificationLocation.Chat, NotificationLocation.Both,
NotificationLocation.Nowhere
};
}
private string GetNotificationLocationLabel(NotificationLocation location)
{
return location switch
{
NotificationLocation.Nowhere => "Nowhere",
NotificationLocation.Chat => "Chat",
NotificationLocation.Toast => "Toast",
NotificationLocation.Both => "Toast + Chat",
NotificationLocation.LightlessUi => "Lightless Notifications",
NotificationLocation.ChatAndLightlessUi => "Chat + Lightless Notifications",
NotificationLocation.TextOverlay => "Text Overlay",
_ => location.ToString()
};
}
private string GetNotificationCornerLabel(NotificationCorner corner)
{
return corner switch
{
NotificationCorner.Right => "Right",
NotificationCorner.Left => "Left",
_ => corner.ToString()
};
}
private void DrawSoundTable()
{
var soundEffects = new[]
{
(1u, "Se1 - Soft chime"), (2u, "Se2 - Higher chime"), (3u, "Se3 - Bell tone"), (4u, "Se4 - Harp tone"),
(5u, "Se5 - Mechanical click"), (6u, "Se6 - Drum/percussion"), (7u, "Se7 - Metallic chime"),
(8u, "Se8 - Wooden tone"), (9u, "Se9 - Wind/flute tone"), (11u, "Se10 - Magical sparkle"),
(12u, "Se11 - Metallic ring"), (13u, "Se12 - Deep thud"), (14u, "Se13 - Tell received ping"),
(15u, "Se14 - Success fanfare"), (16u, "Se15 - System warning")
};
if (ImGui.BeginTable("##SoundTable", 3,
ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit))
{
ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed, 120 * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Sound", ImGuiTableColumnFlags.WidthStretch, 280 * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, 120 * ImGuiHelpers.GlobalScale);
ImGui.TableHeadersRow();
var soundTypes = new[]
{
("Info", 0, _configService.Current.CustomInfoSoundId, _configService.Current.DisableInfoSound, 2u),
("Warning", 1, _configService.Current.CustomWarningSoundId, _configService.Current.DisableWarningSound, 16u),
("Error", 2, _configService.Current.CustomErrorSoundId, _configService.Current.DisableErrorSound, 16u),
("Pair Request", 3, _configService.Current.PairRequestSoundId, _configService.Current.DisablePairRequestSound, 5u),
("Performance", 4, _configService.Current.PerformanceSoundId, _configService.Current.DisablePerformanceSound, 16u)
};
foreach (var (typeName, typeIndex, currentSoundId, isDisabled, defaultSoundId) in soundTypes)
{
ImGui.TableNextRow();
// Type column
ImGui.TableSetColumnIndex(0);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(typeName);
// Sound picker column
ImGui.TableSetColumnIndex(1);
using (ImRaii.Disabled(isDisabled))
{
var currentIndex = Array.FindIndex(soundEffects, s => s.Item1 == currentSoundId);
if (currentIndex == -1) currentIndex = 1;
ImGui.SetNextItemWidth(-1);
if (ImGui.Combo($"##sound_{typeIndex}", ref currentIndex,
soundEffects.Select(s => s.Item2).ToArray(), soundEffects.Length))
{
var newSoundId = soundEffects[currentIndex].Item1;
switch (typeIndex)
{
case 0: _configService.Current.CustomInfoSoundId = newSoundId; break;
case 1: _configService.Current.CustomWarningSoundId = newSoundId; break;
case 2: _configService.Current.CustomErrorSoundId = newSoundId; break;
case 3: _configService.Current.PairRequestSoundId = newSoundId; break;
case 4: _configService.Current.PerformanceSoundId = newSoundId; break;
}
_configService.Save();
}
}
// Actions column
ImGui.TableSetColumnIndex(2);
var availableWidth = ImGui.GetContentRegionAvail().X;
var buttonWidth = (availableWidth - ImGui.GetStyle().ItemSpacing.X * 2) / 3;
// Play button
using var playId = ImRaii.PushId($"Play_{typeIndex}");
using (ImRaii.Disabled(isDisabled))
{
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button(FontAwesomeIcon.Play.ToIconString(), new Vector2(buttonWidth, 0)))
{
try
{
FFXIVClientStructs.FFXIV.Client.UI.UIGlobals.PlayChatSoundEffect(currentSoundId);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to play test sound");
}
}
}
}
UiSharedService.AttachToolTip("Test this sound");
// Disable toggle button
ImGui.SameLine();
using var disableId = ImRaii.PushId($"Disable_{typeIndex}");
using (ImRaii.PushFont(UiBuilder.IconFont))
{
var icon = isDisabled ? FontAwesomeIcon.VolumeOff : FontAwesomeIcon.VolumeUp;
var color = isDisabled ? UIColors.Get("DimRed") : UIColors.Get("LightlessGreen");
ImGui.PushStyleColor(ImGuiCol.Button, color);
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, color * new Vector4(1.2f, 1.2f, 1.2f, 1f));
ImGui.PushStyleColor(ImGuiCol.ButtonActive, color * new Vector4(0.8f, 0.8f, 0.8f, 1f));
if (ImGui.Button(icon.ToIconString(), new Vector2(buttonWidth, 0)))
{
bool newDisabled = !isDisabled;
switch (typeIndex)
{
case 0: _configService.Current.DisableInfoSound = newDisabled; break;
case 1: _configService.Current.DisableWarningSound = newDisabled; break;
case 2: _configService.Current.DisableErrorSound = newDisabled; break;
case 3: _configService.Current.DisablePairRequestSound = newDisabled; break;
case 4: _configService.Current.DisablePerformanceSound = newDisabled; break;
}
_configService.Save();
}
ImGui.PopStyleColor(3);
}
UiSharedService.AttachToolTip(isDisabled ? "Sound is disabled - click to enable" : "Sound is enabled - click to disable");
// Reset button
ImGui.SameLine();
using var resetId = ImRaii.PushId($"Reset_{typeIndex}");
bool isDefault = currentSoundId == defaultSoundId;
using (ImRaii.Disabled(isDefault))
{
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button(FontAwesomeIcon.Undo.ToIconString(), new Vector2(buttonWidth, 0)))
{
switch (typeIndex)
{
case 0: _configService.Current.CustomInfoSoundId = defaultSoundId; break;
case 1: _configService.Current.CustomWarningSoundId = defaultSoundId; break;
case 2: _configService.Current.CustomErrorSoundId = defaultSoundId; break;
case 3: _configService.Current.PairRequestSoundId = defaultSoundId; break;
case 4: _configService.Current.PerformanceSoundId = defaultSoundId; break;
}
_configService.Save();
}
}
}
UiSharedService.AttachToolTip(isDefault ? "Sound is already at default value" : "Reset to default sound");
}
ImGui.EndTable();
}
}
}