2.0.0 (#92)
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m27s

2.0.0 Changes:

- Reworked shell finder UI with compact or list view with profile tags showing with the listing, allowing moderators to broadcast the syncshell as well to have it be used more.
- Reworked user list in syncshell admin screen to have filter visible and moved away from table to its own thing, allowing to copy uid/note/alias when clicking on the name.
- Reworked download bars and download box to make it look more modern, removed the jitter around, so it shouldn't vibrate around much.
- Chat has been added to the top menu, working in Zone or in Syncshells to be used there.
- Paired system has been revamped to make pausing and unpausing faster, and loading people should be faster as well.
- Moved to the internal object table to have faster load times for users; people should load in faster
- Compactor is running on a multi-threaded level instead of single-threaded; this should increase the speed of compacting files
- Nameplate Service has been reworked so it wouldn't use the nameplate handler anymore.
- Files can be resized when downloading to reduce load on users if they aren't compressed. (can be toggled to resize all).
- Penumbra Collections are now only made when people are visible, reducing the load on boot-up when having many syncshells in your list.
- Lightfinder plates have been moved away from using Nameplates, but will use an overlay.
- Main UI has been changed a bit with a gradient, and on hover will glow up now.
- Reworked Profile UI for Syncshell and Users to be more user-facing with more customizable items.
- Reworked Settings UI to look more modern.
- Performance should be better due to new systems that would dispose of the collections and better caching of items.

Co-authored-by: defnotken <itsdefnotken@gmail.com>
Co-authored-by: azyges <aaaaaa@aaa.aaa>
Co-authored-by: choco <choco@patat.nl>
Co-authored-by: cake <admin@cakeandbanana.nl>
Co-authored-by: Minmoose <KennethBohr@outlook.com>
Reviewed-on: #92
This commit was merged in pull request #92.
This commit is contained in:
2025-12-21 17:19:34 +00:00
parent 906f401940
commit 835a0a637d
191 changed files with 32636 additions and 8841 deletions

View File

@@ -1,18 +1,21 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Gui.ContextMenu;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.Textures;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using System;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using LightlessSync.FileCache;
using LightlessSync.Interop.Ipc;
using LightlessSync.Interop.Ipc.Framework;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Localization;
@@ -24,8 +27,11 @@ using LightlessSync.Utils;
using LightlessSync.WebAPI;
using LightlessSync.WebAPI.SignalR;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
@@ -70,7 +76,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
private bool _isOneDrive = false;
private bool _isPenumbraDirectory = false;
private bool _moodlesExists = false;
private Dictionary<string, DateTime> _oauthTokenExpiry = new();
private readonly Dictionary<string, DateTime> _oauthTokenExpiry = [];
private bool _penumbraExists = false;
private bool _petNamesExists = false;
private int _serverSelectionIndex = -1;
@@ -178,15 +184,108 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
int i = 0;
double dblSByte = bytes;
while (dblSByte >= 1000 && i < suffix.Length - 1)
while (dblSByte >= 1024 && i < suffix.Length - 1)
{
dblSByte /= 1000.0;
dblSByte /= 1024.0;
i++;
}
return addSuffix ? $"{dblSByte:0.00} {suffix[i]}" : $"{dblSByte:0.00}";
}
public readonly struct TabOption<T>
{
public string Label { get; }
public T Value { get; }
public bool Enabled { get; }
public TabOption(string label, T value, bool enabled = true)
{
Label = label;
Value = value;
Enabled = enabled;
}
}
public static bool Tab<T>(string id, IReadOnlyList<TabOption<T>> options, ref T selectedValue) where T : struct
{
if (options.Count == 0)
return false;
var pushIdValue = string.IsNullOrEmpty(id)
? $"UiSharedTab_{RuntimeHelpers.GetHashCode(options):X}"
: id;
using var tabId = ImRaii.PushId(pushIdValue);
var selectedIndex = -1;
for (var i = 0; i < options.Count; i++)
{
if (!EqualityComparer<T>.Default.Equals(options[i].Value, selectedValue))
continue;
selectedIndex = i;
break;
}
if (selectedIndex == -1 || !options[selectedIndex].Enabled)
selectedIndex = GetFirstEnabledTabIndex(options);
if (selectedIndex == -1)
return false;
var changed = DrawTabsInternal(options, ref selectedIndex);
selectedValue = options[Math.Clamp(selectedIndex, 0, options.Count - 1)].Value;
return changed;
}
private static int GetFirstEnabledTabIndex<T>(IReadOnlyList<TabOption<T>> options)
{
for (var i = 0; i < options.Count; i++)
{
if (options[i].Enabled)
return i;
}
return -1;
}
private static bool DrawTabsInternal<T>(IReadOnlyList<TabOption<T>> options, ref int selectedIndex)
{
selectedIndex = Math.Clamp(selectedIndex, 0, Math.Max(0, options.Count - 1));
var style = ImGui.GetStyle();
var availableWidth = ImGui.GetContentRegionAvail().X;
var spacingX = style.ItemSpacing.X;
var buttonWidth = options.Count > 0 ? Math.Max(1f, (availableWidth - spacingX * (options.Count - 1)) / options.Count) : availableWidth;
var buttonHeight = Math.Max(ImGui.GetFrameHeight() + style.FramePadding.Y, 28f * ImGuiHelpers.GlobalScale);
var changed = false;
for (var i = 0; i < options.Count; i++)
{
if (i > 0)
ImGui.SameLine();
var tab = options[i];
var isSelected = i == selectedIndex;
using (ImRaii.Disabled(!tab.Enabled))
{
using var tabIndexId = ImRaii.PushId(i);
using var selectedButton = isSelected ? ImRaii.PushColor(ImGuiCol.Button, style.Colors[(int)ImGuiCol.TabActive]) : null;
using var selectedHover = isSelected ? ImRaii.PushColor(ImGuiCol.ButtonHovered, style.Colors[(int)ImGuiCol.TabHovered]) : null;
using var selectedActive = isSelected ? ImRaii.PushColor(ImGuiCol.ButtonActive, style.Colors[(int)ImGuiCol.TabActive]) : null;
if (ImGui.Button(tab.Label, new Vector2(buttonWidth, buttonHeight)))
{
selectedIndex = i;
changed = true;
}
}
}
return changed;
}
public static void CenterNextWindow(float width, float height, ImGuiCond cond = ImGuiCond.None)
{
var center = ImGui.GetMainViewport().GetCenter();
@@ -400,10 +499,21 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
public static bool ShiftPressed() => (GetKeyState(0xA1) & 0x8000) != 0 || (GetKeyState(0xA0) & 0x8000) != 0;
public static void TextWrapped(string text, float wrapPos = 0)
public static void TextWrapped(string text, float wrapPos = 0, Vector4? color = null)
{
ImGui.PushTextWrapPos(wrapPos);
if (color.HasValue)
{
ImGui.PushStyleColor(ImGuiCol.Text, color.Value);
}
ImGui.TextUnformatted(text);
if (color.HasValue)
{
ImGui.PopStyleColor();
}
ImGui.PopTextWrapPos();
}
@@ -475,7 +585,22 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
);
}
public void ColoredSeparator(Vector4? color = null, float thickness = 1f, float indent = 0f)
public static void AddContextMenuItem(IMenuOpenedArgs args, SeString name, char prefixChar, ushort colorMenuItem, Func<Task> onClick)
{
args.AddMenuItem(new MenuItem
{
Name = name,
PrefixChar = prefixChar,
UseDefaultPrefix = false,
PrefixColor = colorMenuItem,
OnClicked = _ =>
{
onClick();
},
});
}
public static void ColoredSeparator(Vector4? color = null, float thickness = 1f, float indent = 0f)
{
var drawList = ImGui.GetWindowDrawList();
var min = ImGui.GetCursorScreenPos();
@@ -519,8 +644,9 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
bool changed = ImGui.Checkbox(label, ref value);
var boxSize = ImGui.GetFrameHeight();
var min = pos;
var max = ImGui.GetItemRectMax();
var max = new Vector2(pos.X + boxSize, pos.Y + boxSize);
var col = ImGui.GetColorU32(borderColor ?? ImGuiColors.DalamudGrey);
ImGui.GetWindowDrawList().AddRect(min, max, col, rounding, ImDrawFlags.None, borderThickness);
@@ -945,36 +1071,36 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
ImGui.SameLine(150);
ColorText("Penumbra", GetBoolColor(_penumbraExists));
AttachToolTip($"Penumbra is " + (_penumbraExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("Penumbra", _penumbraExists, _ipcManager.Penumbra.State));
ImGui.SameLine();
ColorText("Glamourer", GetBoolColor(_glamourerExists));
AttachToolTip($"Glamourer is " + (_glamourerExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("Glamourer", _glamourerExists, _ipcManager.Glamourer.State));
ImGui.TextUnformatted("Optional Plugins:");
ImGui.SameLine(150);
ColorText("SimpleHeels", GetBoolColor(_heelsExists));
AttachToolTip($"SimpleHeels is " + (_heelsExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("SimpleHeels", _heelsExists, _ipcManager.Heels.State));
ImGui.SameLine();
ColorText("Customize+", GetBoolColor(_customizePlusExists));
AttachToolTip($"Customize+ is " + (_customizePlusExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("Customize+", _customizePlusExists, _ipcManager.CustomizePlus.State));
ImGui.SameLine();
ColorText("Honorific", GetBoolColor(_honorificExists));
AttachToolTip($"Honorific is " + (_honorificExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("Honorific", _honorificExists, _ipcManager.Honorific.State));
ImGui.SameLine();
ColorText("Moodles", GetBoolColor(_moodlesExists));
AttachToolTip($"Moodles is " + (_moodlesExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("Moodles", _moodlesExists, _ipcManager.Moodles.State));
ImGui.SameLine();
ColorText("PetNicknames", GetBoolColor(_petNamesExists));
AttachToolTip($"PetNicknames is " + (_petNamesExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("PetNicknames", _petNamesExists, _ipcManager.PetNames.State));
ImGui.SameLine();
ColorText("Brio", GetBoolColor(_brioExists));
AttachToolTip($"Brio is " + (_brioExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("Brio", _brioExists, _ipcManager.Brio.State));
if (!_penumbraExists || !_glamourerExists)
{
@@ -985,6 +1111,25 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
return true;
}
private static string BuildPluginTooltip(string pluginName, bool isAvailable, IpcConnectionState state)
{
var availability = isAvailable ? "available and up to date." : "unavailable or not up to date.";
return $"{pluginName} is {availability}{Environment.NewLine}IPC State: {DescribeIpcState(state)}";
}
private static string DescribeIpcState(IpcConnectionState state)
=> state switch
{
IpcConnectionState.Unknown => "Not evaluated yet",
IpcConnectionState.MissingPlugin => "Plugin not installed",
IpcConnectionState.VersionMismatch => "Installed version below required minimum",
IpcConnectionState.PluginDisabled => "Plugin installed but disabled",
IpcConnectionState.NotReady => "Plugin is not ready yet",
IpcConnectionState.Available => "Available",
IpcConnectionState.Error => "Error occurred while checking IPC",
_ => state.ToString()
};
public int DrawServiceSelection(bool selectOnChange = false, bool showConnect = true)
{
string[] comboEntries = _serverConfigurationManager.GetServerNames();
@@ -1067,7 +1212,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
{
using (ImRaii.Disabled(_discordOAuthUIDs == null))
{
var aliasPairs = _discordOAuthUIDs?.Result?.Select(t => new UIDAliasPair(t.Key, t.Value)).ToList() ?? [new UIDAliasPair(item.UID ?? null, null)];
var aliasPairs = _discordOAuthUIDs?.Result?.Select(t => new UidAliasPair(t.Key, t.Value)).ToList() ?? [new UidAliasPair(item.UID ?? null, null)];
var uidComboName = "UID###" + item.CharacterName + item.WorldId + serverUri + indexOffset + aliasPairs.Count;
DrawCombo(uidComboName, aliasPairs,
(v) =>
@@ -1220,6 +1365,100 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
return _textureProvider.CreateFromImageAsync(imageData).Result;
}
private static readonly (bool ItemHq, bool HiRes)[] IconLookupOrders =
[
(false, true),
(true, true),
(false, false),
(true, false)
];
public bool TryGetIcon(uint iconId, out IDalamudTextureWrap? wrap)
{
foreach (var (itemHq, hiRes) in IconLookupOrders)
{
if (TryGetIconWithLookup(iconId, itemHq, hiRes, out wrap))
return true;
}
foreach (var (itemHq, hiRes) in IconLookupOrders)
{
if (!_textureProvider.TryGetIconPath(new GameIconLookup(iconId, itemHq, hiRes), out var path) || string.IsNullOrEmpty(path))
continue;
try
{
var reference = _textureProvider.GetFromGame(path);
if (reference.TryGetWrap(out var texture, out _))
{
wrap = texture;
return true;
}
}
catch (Exception ex)
{
Logger.LogTrace(ex, "Failed to load icon {IconId} from path {Path}", iconId, path);
}
}
foreach (var hiRes in new[] { true, false })
{
var manualPath = BuildIconPath(iconId, hiRes);
if (TryLoadTextureFromPath(manualPath, iconId, out wrap))
return true;
}
wrap = null;
return false;
}
private bool TryLoadTextureFromPath(string path, uint iconId, out IDalamudTextureWrap? wrap)
{
try
{
var reference = _textureProvider.GetFromGame(path);
if (reference.TryGetWrap(out var texture, out _))
{
wrap = texture;
return true;
}
}
catch (Exception ex)
{
Logger.LogTrace(ex, "Failed to load icon {IconId} from manual path {Path}", iconId, path);
}
wrap = null;
return false;
}
private static string BuildIconPath(uint iconId, bool hiRes)
{
var folder = iconId - iconId % 1000;
var basePath = $"ui/icon/{folder:000000}/{iconId:000000}";
return hiRes ? $"{basePath}_hr1.tex" : $"{basePath}.tex";
}
private bool TryGetIconWithLookup(uint iconId, bool itemHq, bool hiRes, out IDalamudTextureWrap? wrap)
{
try
{
var icon = _textureProvider.GetFromGameIcon(new GameIconLookup(iconId, itemHq, hiRes));
if (icon.TryGetWrap(out var texture, out _))
{
wrap = texture;
return true;
}
}
catch (Exception ex)
{
Logger.LogTrace(ex, "Failed to load icon {IconId} (HQ:{ItemHq}, HR:{HiRes})", iconId, itemHq, hiRes);
}
wrap = null;
return false;
}
public void LoadLocalization(string languageCode)
{
_localization.SetupWithLangCode(languageCode);
@@ -1253,6 +1492,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
UidFont.Dispose();
GameFont.Dispose();
MediumFont.Dispose();
_discordOAuthGetCts.Dispose();
}
private static void CenterWindow(float width, float height, ImGuiCond cond = ImGuiCond.None)
@@ -1285,13 +1525,24 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
num++;
}
ImGui.PushID(text);
string displayText = text;
string idText = text;
int idSeparatorIndex = text.IndexOf("##", StringComparison.Ordinal);
if (idSeparatorIndex >= 0)
{
displayText = text[..idSeparatorIndex];
idText = text[(idSeparatorIndex + 2)..];
if (string.IsNullOrEmpty(idText))
idText = displayText;
}
ImGui.PushID(idText);
Vector2 vector;
using (IconFont.Push())
vector = ImGui.CalcTextSize(icon.ToIconString());
Vector2 vector2 = ImGui.CalcTextSize(text);
Vector2 vector2 = ImGui.CalcTextSize(displayText);
ImDrawListPtr windowDrawList = ImGui.GetWindowDrawList();
Vector2 cursorScreenPos = ImGui.GetCursorScreenPos();
float num2 = 3f * ImGuiHelpers.GlobalScale;
@@ -1316,7 +1567,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
windowDrawList.AddText(pos, ImGui.GetColorU32(ImGuiCol.Text), icon.ToIconString());
Vector2 pos2 = new Vector2(pos.X + vector.X + num2, cursorScreenPos.Y + ImGui.GetStyle().FramePadding.Y);
windowDrawList.AddText(pos2, ImGui.GetColorU32(ImGuiCol.Text), text);
windowDrawList.AddText(pos2, ImGui.GetColorU32(ImGuiCol.Text), displayText);
ImGui.PopID();
if (num > 0)
{
@@ -1325,6 +1576,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
return result;
}
public sealed record IconScaleData(Vector2 IconSize, Vector2 NormalizedIconScale, float OffsetX, float IconScaling);
private record UIDAliasPair(string? UID, string? Alias);
private sealed record UidAliasPair(string? UID, string? Alias);
}