Files
LightlessClient/LightlessSync/UI/EditProfileUi.cs
defnotken 835a0a637d
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m27s
2.0.0 (#92)
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
2025-12-21 17:19:34 +00:00

1262 lines
52 KiB
C#

using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data;
using LightlessSync.API.Dto.Group;
using LightlessSync.API.Dto.User;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.UI.Style;
using LightlessSync.UI.Tags;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.PixelFormats;
using System.Numerics;
using LightlessSync.Services.Profiles;
namespace LightlessSync.UI;
public partial class EditProfileUi : WindowMediatorSubscriberBase
{
private readonly ApiController _apiController;
private readonly FileDialogManager _fileDialogManager;
private readonly LightlessProfileManager _lightlessProfileManager;
private readonly UiSharedService _uiSharedService;
private readonly ProfileTagService _profileTagService;
private const string LoadingProfileDescription = "Loading Profile Data from server...";
private const string DescriptionMacroTooltip =
"Supported SeString markup:\n" +
"<br> - insert a line break (Enter already emits these).\n" +
"<-> - optional soft hyphen / word break.\n" +
"<icon(12345)> / <icon2(12345)> - show game icons by numeric id;" +
"<color=#RRGGBB>text</color> or <color(0xAARRGGBB)> - tint text; reset with </color> or <color(stackcolor)>.\n" +
"<colortype(n)> / <edgecolortype(n)> - use UI palette colours (0 restores defaults).\n" +
"<edgecolor(0xAARRGGBB)> / <edgecolor(stackcolor)> - change outline colour.\n" +
"<bold(1|0)> <italic(1|0)> <shadow(1|0)> <edge(1|0)> - toggle style flags.\n" +
"<link(0x0E,...)> - create clickable links.";
private static readonly HashSet<string> _supportedImageExtensions = new(StringComparer.OrdinalIgnoreCase)
{
"png",
"jpg",
"jpeg",
"webp",
"bmp"
};
private const string _imageFileDialogFilter = "Images{.png,.jpg,.jpeg,.webp,.bmp}";
private readonly List<int> _tagEditorSelection;
private int[] _profileTagIds = [];
private readonly List<SeStringUtils.SeStringSegment> _tagPreviewSegments = new();
private enum ProfileEditorMode
{
User,
Group
}
private ProfileEditorMode _mode = ProfileEditorMode.User;
private GroupFullInfoDto? _groupInfo;
private LightlessGroupProfileData? _groupProfileData;
private bool _groupIsNsfw;
private bool _groupIsDisabled;
private bool _groupServerIsNsfw;
private bool _groupServerIsDisabled;
private bool _groupVisibilityInitialized;
private byte[]? _queuedProfileImage;
private byte[]? _queuedBannerImage;
private readonly Vector4 _tagBackgroundColor = new(0.18f, 0.18f, 0.18f, 0.95f);
private readonly Vector4 _tagBorderColor = new(0.35f, 0.35f, 0.35f, 0.4f);
private const int _maxProfileTags = 12;
private const int _availableTagsPerPage = 6;
private int _availableTagPage;
private UserData? _selfProfileUserData;
private string _descriptionText = string.Empty;
private IDalamudTextureWrap? _pfpTextureWrap;
private IDalamudTextureWrap? _bannerTextureWrap;
private string _profileDescription = string.Empty;
private byte[] _profileImage = [];
private byte[] _bannerImage = [];
private bool _showProfileImageError = false;
private bool _showBannerImageError = false;
private bool _wasOpen;
private bool _userServerIsNsfw;
private bool _isNsfwInitialized;
private bool _isNsfw;
private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f);
private bool _textEnabled;
private bool _glowEnabled;
private Vector4 _textColor;
private Vector4 _glowColor;
private sealed record VanityState(bool TextEnabled, bool GlowEnabled, Vector4 TextColor, Vector4 GlowColor);
private VanityState? _savedVanity;
public EditProfileUi(ILogger<EditProfileUi> logger, LightlessMediator mediator,
ApiController apiController, UiSharedService uiSharedService, FileDialogManager fileDialogManager,
LightlessProfileManager lightlessProfileManager, ProfileTagService profileTagService, PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "Lightless Sync Edit Profile###LightlessSyncEditProfileUI", performanceCollectorService)
{
IsOpen = false;
var scale = ImGuiHelpers.GlobalScale;
var editorSize = ProfileEditorLayoutCoordinator.GetEditorSize(scale);
Size = editorSize;
SizeCondition = ImGuiCond.FirstUseEver;
SizeConstraints = new()
{
MinimumSize = editorSize,
MaximumSize = editorSize
};
Flags |= ImGuiWindowFlags.NoResize;
_apiController = apiController;
_uiSharedService = uiSharedService;
_fileDialogManager = fileDialogManager;
_lightlessProfileManager = lightlessProfileManager;
_profileTagService = profileTagService;
_tagEditorSelection = new List<int>(_maxProfileTags);
Mediator.Subscribe<GposeStartMessage>(this, (_) => { _wasOpen = IsOpen; IsOpen = false; });
Mediator.Subscribe<GposeEndMessage>(this, (_) => IsOpen = _wasOpen);
Mediator.Subscribe<DisconnectedMessage>(this, (_) =>
{
IsOpen = false;
_selfProfileUserData = null;
});
Mediator.Subscribe<ClearProfileUserDataMessage>(this, (msg) =>
{
if (msg.UserData == null || string.Equals(msg.UserData.UID, _apiController.UID, StringComparison.Ordinal))
{
_pfpTextureWrap?.Dispose();
_pfpTextureWrap = null;
}
});
Mediator.Subscribe<ConnectedMessage>(this, msg =>
{
_selfProfileUserData = msg.Connection.User with
{
IsAdmin = msg.Connection.IsAdmin,
IsModerator = msg.Connection.IsModerator,
HasVanity = msg.Connection.HasVanity,
TextColorHex = msg.Connection.TextColorHex,
TextGlowColorHex = msg.Connection.TextGlowColorHex
};
LoadVanity();
});
Mediator.Subscribe<OpenGroupProfileEditorMessage>(this, msg => OpenGroupEditor(msg.Group));
}
private void LoadVanity()
{
_textEnabled = !string.IsNullOrEmpty(_apiController.TextColorHex);
_glowEnabled = !string.IsNullOrEmpty(_apiController.TextGlowColorHex);
_textColor = _textEnabled ? UIColors.HexToRgba(_apiController.TextColorHex!) : Vector4.One;
_glowColor = _glowEnabled ? UIColors.HexToRgba(_apiController.TextGlowColorHex!) : Vector4.Zero;
_savedVanity = new VanityState(_textEnabled, _glowEnabled, _textColor, _glowColor);
}
public override async void OnOpen()
{
if (_mode == ProfileEditorMode.Group)
{
if (_groupInfo is not null)
{
var scale = ImGuiHelpers.GlobalScale;
var viewport = ImGui.GetMainViewport();
ProfileEditorLayoutCoordinator.EnsureAnchor(viewport.WorkPos, scale);
}
return;
}
_isNsfwInitialized = false;
var user = await EnsureSelfProfileUserDataAsync().ConfigureAwait(false);
if (user is not null)
{
ProfileEditorLayoutCoordinator.Enable(user.UID);
var scale = ImGuiHelpers.GlobalScale;
var viewport = ImGui.GetMainViewport();
ProfileEditorLayoutCoordinator.EnsureAnchor(viewport.WorkPos, scale);
Mediator.Publish(new OpenSelfProfilePreviewMessage(user));
}
}
public override void OnClose()
{
if (_mode == ProfileEditorMode.Group)
{
if (_groupInfo is not null)
{
ProfileEditorLayoutCoordinator.Disable(_groupInfo.Group.GID);
Mediator.Publish(new CloseGroupProfilePreviewMessage(_groupInfo));
}
ResetGroupEditorState();
_mode = ProfileEditorMode.User;
return;
}
if (_selfProfileUserData is not null)
{
ProfileEditorLayoutCoordinator.Disable(_selfProfileUserData.UID);
Mediator.Publish(new CloseSelfProfilePreviewMessage(_selfProfileUserData));
}
}
private async Task<UserData?> EnsureSelfProfileUserDataAsync()
{
if (_selfProfileUserData is not null)
return _selfProfileUserData;
try
{
var connection = await _apiController.GetConnectionDtoAsync(publishConnected: false).ConfigureAwait(false);
_selfProfileUserData = connection.User with
{
IsAdmin = connection.IsAdmin,
IsModerator = connection.IsModerator,
HasVanity = connection.HasVanity,
TextColorHex = connection.TextColorHex,
TextGlowColorHex = connection.TextGlowColorHex
};
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to acquire connection information for profile preview.");
}
return _selfProfileUserData;
}
protected override void DrawInternal()
{
var scale = ImGuiHelpers.GlobalScale;
if (_mode == ProfileEditorMode.Group)
{
DrawGroupEditor(scale);
return;
}
var viewport = ImGui.GetMainViewport();
var linked = _selfProfileUserData is not null && ProfileEditorLayoutCoordinator.IsActive(_selfProfileUserData.UID);
if (linked)
{
ProfileEditorLayoutCoordinator.EnsureAnchor(viewport.WorkPos, scale);
var desiredSize = ProfileEditorLayoutCoordinator.GetEditorSize(scale);
if (!ProfileEditorLayoutCoordinator.NearlyEquals(ImGui.GetWindowSize(), desiredSize))
ImGui.SetWindowSize(desiredSize, ImGuiCond.Always);
var currentPos = ImGui.GetWindowPos();
if (IsWindowBeingDragged())
ProfileEditorLayoutCoordinator.UpdateAnchorFromEditor(currentPos, scale);
var desiredPos = ProfileEditorLayoutCoordinator.GetEditorPosition(scale);
if (!ProfileEditorLayoutCoordinator.NearlyEquals(currentPos, desiredPos))
ImGui.SetWindowPos(desiredPos, ImGuiCond.Always);
}
else
{
var defaultProfilePos = viewport.WorkPos + new Vector2(50f, 70f) * scale;
var defaultEditorPos = defaultProfilePos + ProfileEditorLayoutCoordinator.GetEditorOffset(scale);
ImGui.SetWindowPos(defaultEditorPos, ImGuiCond.FirstUseEver);
}
var profile = _lightlessProfileManager.GetLightlessUserProfile(new UserData(_apiController.UID));
var accent = UIColors.Get("LightlessPurple");
var accentBg = new Vector4(accent.X, accent.Y, accent.Z, 0.015f);
var accentBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.07f);
using var panelBg = ImRaii.PushColor(ImGuiCol.ChildBg, accentBg);
using var panelBorder = ImRaii.PushColor(ImGuiCol.ChildBg, accentBorder);
ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 4f * scale);
if (ImGui.BeginChild("##ProfileEditorCanvas", -Vector2.One, true))
{
DrawGuidelinesSection(scale);
ImGui.Dummy(new Vector2(0f, 4f * scale));
DrawTabInterface(profile, scale);
}
ImGui.EndChild();
ImGui.PopStyleVar();
}
private void DrawGuidelinesSection(float scale)
{
DrawSection("Guidelines", scale, () =>
{
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, 1));
_uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "All users that are paired and unpaused with you will be able to see your profile pictures, tags and description.");
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Other users have the possibility to report your profile for breaking the rules.");
_uiSharedService.DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Anything as profile image that can be considered highly illegal or obscene (bestiality, anything that could be considered a sexual act with a minor (that includes Lalafells), etc.)");
_uiSharedService.DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Slurs of any kind in the description that can be considered highly offensive");
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "In case of valid reports from other users this can lead to disabling your profile forever or terminating your Lightless account indefinitely.");
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Judgement of your profile validity from reports through staff is not up to debate and the decisions to disable your profile/account permanent.");
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessBlue"), "If your profile picture or profile description could be considered NSFW, enable the toggle in visibility settings.");
ImGui.PopStyleVar();
});
}
private void DrawTabInterface(LightlessUserProfileData profile, float scale)
{
ImGui.PushStyleVar(ImGuiStyleVar.TabRounding, 4f * scale);
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(8f, 4f) * scale);
if (ImGui.BeginTabBar("##ProfileEditorTabs", ImGuiTabBarFlags.NoCloseWithMiddleMouseButton | ImGuiTabBarFlags.FittingPolicyResizeDown))
{
if (ImGui.BeginTabItem("Profile"))
{
DrawProfileTabContent(profile, scale);
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Vanity"))
{
DrawVanityTabContent(scale);
ImGui.EndTabItem();
}
ImGui.EndTabBar();
}
ImGui.PopStyleVar(2);
}
private void DrawProfileTabContent(LightlessUserProfileData profile, float scale)
{
if (profile.IsFlagged)
{
DrawSection("Moderation Status", scale, () =>
{
UiSharedService.ColorTextWrapped(profile.Description, ImGuiColors.DalamudRed);
});
return;
}
SyncProfileState(profile);
DrawSection("Profile Preview", scale, () => DrawProfileSnapshot(profile, scale));
DrawSection("Profile Image", scale, () => DrawProfileImageControls(profile));
DrawSection("Profile Banner", scale, () => DrawProfileBannerControls(profile));
DrawSection("Profile Description", scale, () => DrawProfileDescriptionEditor(profile, scale));
DrawSection("Profile Tags", scale, () => DrawProfileTagsEditor(profile, scale));
DrawSection("Visibility", scale, () => DrawProfileVisibilityControls());
}
private void DrawProfileSnapshot(LightlessUserProfileData profile, float scale)
{
var bannerHeight = 140f * scale;
ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 4f * scale);
if (ImGui.BeginChild("##ProfileBannerPreview", new Vector2(-1f, bannerHeight), true))
{
if (_bannerTextureWrap != null)
{
var childSize = ImGui.GetWindowSize();
var padding = ImGui.GetStyle().WindowPadding;
var contentSize = new Vector2(
MathF.Max(childSize.X - padding.X * 2f, 1f),
MathF.Max(childSize.Y - padding.Y * 2f, 1f));
var imageSize = ImGuiHelpers.ScaledVector2(_bannerTextureWrap.Width, _bannerTextureWrap.Height);
if (imageSize.X > contentSize.X || imageSize.Y > contentSize.Y)
{
var ratio = MathF.Min(contentSize.X / MathF.Max(imageSize.X, 1f), contentSize.Y / MathF.Max(imageSize.Y, 1f));
imageSize *= ratio;
}
var offset = new Vector2(
MathF.Max((contentSize.X - imageSize.X) * 0.5f, 0f),
MathF.Max((contentSize.Y - imageSize.Y) * 0.5f, 0f));
ImGui.SetCursorPos(padding + offset);
ImGui.Image(_bannerTextureWrap.Handle, imageSize);
}
else
{
ImGui.TextColored(UIColors.Get("LightlessPurple"), "No Profile Banner");
}
}
ImGui.EndChild();
ImGui.PopStyleVar();
ImGui.Dummy(new Vector2(0f, 6f * scale));
if (_pfpTextureWrap != null)
{
var size = ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height);
var maxEdge = 150f * scale;
if (size.X > maxEdge || size.Y > maxEdge)
{
var ratio = MathF.Min(maxEdge / MathF.Max(size.X, 1f), maxEdge / MathF.Max(size.Y, 1f));
size *= ratio;
}
ImGui.Image(_pfpTextureWrap.Handle, size);
}
else
{
ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 4f * scale);
if (ImGui.BeginChild("##ProfileImagePlaceholder", new Vector2(150f * scale, 150f * scale), true))
ImGui.TextColored(UIColors.Get("LightlessPurple"), "No Profile Picture");
ImGui.EndChild();
ImGui.PopStyleVar();
}
ImGui.Dummy(new Vector2(0f, 4f * scale));
using (_uiSharedService.GameFont.Push())
{
ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 4f * scale);
if (ImGui.BeginChild("##CurrentProfileDescription", new Vector2(-1f, 120f * scale), true))
{
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X);
if (string.IsNullOrWhiteSpace(profile.Description))
{
ImGui.TextDisabled("-- No description --");
}
else if (!SeStringUtils.TryRenderSeStringMarkupAtCursor(profile.Description!))
{
UiSharedService.TextWrapped(profile.Description);
}
ImGui.PopTextWrapPos();
}
ImGui.EndChild();
ImGui.PopStyleVar();
}
}
private void DrawProfileImageControls(LightlessUserProfileData profile)
{
_uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "Profile pictures must be 512x512 and under 2 MiB.");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture"))
{
var existingBanner = GetCurrentProfileBannerBase64(profile);
_fileDialogManager.OpenFileDialog("Select new Profile picture", _imageFileDialogFilter, (success, file) =>
{
if (!success) return;
_ = Task.Run(async () =>
{
var fileContent = File.ReadAllBytes(file);
using MemoryStream ms = new(fileContent);
var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false);
if (!IsSupportedImageFormat(format))
{
_showProfileImageError = true;
return;
}
using var image = Image.Load<Rgba32>(fileContent);
if (image.Width > 512 || image.Height > 512 || fileContent.Length > 2000 * 1024)
{
_showProfileImageError = true;
return;
}
_showProfileImageError = false;
var currentTags = GetServerTagPayload();
_queuedProfileImage = fileContent;
await _apiController.UserSetProfile(new UserProfileDto(
new UserData(_apiController.UID),
Disabled: false,
IsNSFW: null,
ProfilePictureBase64: Convert.ToBase64String(fileContent),
BannerPictureBase64: existingBanner,
Description: null,
Tags: currentTags)).ConfigureAwait(false);
});
});
}
UiSharedService.AttachToolTip("Select an image up to 512x512 pixels and <= 2 MiB (PNG/JPG/JPEG/WEBP/BMP).");
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear profile picture"))
{
_ = _apiController.UserSetProfile(new UserProfileDto(
new UserData(_apiController.UID),
Disabled: false,
IsNSFW: null,
ProfilePictureBase64: string.Empty,
BannerPictureBase64: GetCurrentProfileBannerBase64(profile),
Description: null,
Tags: GetServerTagPayload()));
}
UiSharedService.AttachToolTip("Remove your current profile picture.");
if (_showProfileImageError)
{
UiSharedService.ColorTextWrapped("Your profile picture must be no larger than 512x512 pixels and under 2 MiB.", ImGuiColors.DalamudRed);
}
}
private void DrawProfileBannerControls(LightlessUserProfileData profile)
{
_uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "Profile banners must be 840x260 and under 2 MiB.");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile banner"))
{
var existingProfile = GetCurrentProfilePictureBase64(profile);
_fileDialogManager.OpenFileDialog("Select new Profile banner", _imageFileDialogFilter, (success, file) =>
{
if (!success) return;
_ = Task.Run(async () =>
{
var fileContent = File.ReadAllBytes(file);
using MemoryStream ms = new(fileContent);
var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false);
if (!IsSupportedImageFormat(format))
{
_showBannerImageError = true;
return;
}
using var image = Image.Load<Rgba32>(fileContent);
if (image.Width > 840 || image.Height > 260 || fileContent.Length > 2000 * 1024)
{
_showBannerImageError = true;
return;
}
_showBannerImageError = false;
var currentTags = GetServerTagPayload();
await _apiController.UserSetProfile(new UserProfileDto(
new UserData(_apiController.UID),
Disabled: false,
IsNSFW: null,
ProfilePictureBase64: existingProfile,
BannerPictureBase64: Convert.ToBase64String(fileContent),
Description: null,
Tags: currentTags)).ConfigureAwait(false);
});
});
}
UiSharedService.AttachToolTip("Select an image up to 840x260 pixels and <= 2 MiB (PNG/JPG/JPEG/WEBP/BMP).");
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear profile banner"))
{
_ = _apiController.UserSetProfile(new UserProfileDto(
new UserData(_apiController.UID),
Disabled: false,
IsNSFW: null,
ProfilePictureBase64: GetCurrentProfilePictureBase64(profile),
BannerPictureBase64: string.Empty,
Description: null,
Tags: GetServerTagPayload()));
}
UiSharedService.AttachToolTip("Remove your current profile banner.");
if (_showBannerImageError)
{
UiSharedService.ColorTextWrapped("Your banner image must be no larger than 840x260 pixels and under 2 MiB.", ImGuiColors.DalamudRed);
}
}
private void DrawProfileDescriptionEditor(LightlessUserProfileData profile, float scale)
{
ImGui.TextUnformatted($"Description {_descriptionText.Length}/1500");
ImGui.SameLine();
ImGuiComponents.HelpMarker(DescriptionMacroTooltip);
using (_uiSharedService.GameFont.Push())
{
var inputSize = new Vector2(-1f, 160f * scale);
ImGui.InputTextMultiline("##profileDescriptionInput", ref _descriptionText, 1500, inputSize);
}
ImGui.Dummy(new Vector2(0f, 3f * scale));
ImGui.TextColored(UIColors.Get("LightlessBlue"), "Preview");
using (_uiSharedService.GameFont.Push())
{
ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 4f * scale);
if (ImGui.BeginChild("##profileDescriptionPreview", new Vector2(-1f, 140f * scale), true))
{
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X);
if (string.IsNullOrWhiteSpace(_descriptionText))
{
ImGui.TextDisabled("-- Description preview --");
}
else if (!SeStringUtils.TryRenderSeStringMarkupAtCursor(_descriptionText))
{
UiSharedService.TextWrapped(_descriptionText);
}
ImGui.PopTextWrapPos();
}
ImGui.EndChild();
ImGui.PopStyleVar();
}
ImGui.Dummy(new Vector2(0f, 4f * scale));
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description"))
{
_ = _apiController.UserSetProfile(new UserProfileDto(
new UserData(_apiController.UID),
Disabled: false,
IsNSFW: null,
ProfilePictureBase64: GetCurrentProfilePictureBase64(profile),
BannerPictureBase64: GetCurrentProfileBannerBase64(profile),
_descriptionText,
Tags: GetServerTagPayload()));
}
UiSharedService.AttachToolTip("Apply the text above to your profile.");
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description"))
{
_descriptionText = string.Empty;
_ = _apiController.UserSetProfile(new UserProfileDto(
new UserData(_apiController.UID),
Disabled: false,
IsNSFW: null,
ProfilePictureBase64: GetCurrentProfilePictureBase64(profile),
BannerPictureBase64: GetCurrentProfileBannerBase64(profile),
Description: string.Empty,
Tags: GetServerTagPayload()));
}
UiSharedService.AttachToolTip("Remove the description from your profile.");
}
private void DrawProfileTagsEditor(LightlessUserProfileData profile, float scale)
{
DrawTagEditor(
scale,
contextPrefix: "user",
saveTooltip: "Apply the selected tags to your profile.",
submitAction: payload => _apiController.UserSetProfile(new UserProfileDto(
new UserData(_apiController.UID),
Disabled: false,
IsNSFW: null,
ProfilePictureBase64: GetCurrentProfilePictureBase64(profile),
BannerPictureBase64: GetCurrentProfileBannerBase64(profile),
Description: null,
Tags: payload)),
allowReorder: true,
sortPayloadBeforeSubmit: false);
}
private void DrawTagEditor(
float scale,
string contextPrefix,
string saveTooltip,
Func<int[], Task> submitAction,
bool allowReorder,
bool sortPayloadBeforeSubmit,
Action<int[]>? onPayloadPrepared = null)
{
var tagLibrary = ProfileTagService.GetTagLibrary();
if (tagLibrary.Count == 0)
{
ImGui.TextDisabled("No profile tags are available.");
return;
}
var style = ImGui.GetStyle();
var defaultTextColorU32 = ImGui.GetColorU32(ImGuiCol.Text);
var selectedCount = _tagEditorSelection.Count;
ImGui.TextColored(UIColors.Get("LightlessBlue"), $"Selected Tags ({selectedCount}/{_maxProfileTags})");
int? tagToRemove = null;
int? moveUpRequest = null;
int? moveDownRequest = null;
if (selectedCount == 0)
{
ImGui.TextDisabled("-- No tags selected --");
}
else
{
var selectedFlags = ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.SizingStretchSame;
var selectedTableId = $"##{contextPrefix}SelectedTagsTable";
var columnCount = allowReorder ? 3 : 2;
if (ImGui.BeginTable(selectedTableId, columnCount, selectedFlags))
{
ImGui.TableSetupColumn("Preview", ImGuiTableColumnFlags.WidthStretch, allowReorder ? 0.55f : 0.75f);
if (allowReorder)
ImGui.TableSetupColumn("##order", ImGuiTableColumnFlags.WidthStretch, 0.1f);
ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, 90f);
ImGui.TableHeadersRow();
for (int i = 0; i < _tagEditorSelection.Count; i++)
{
var tagId = _tagEditorSelection[i];
if (!tagLibrary.TryGetValue(tagId, out var definition) || !definition.HasContent)
continue;
var displayName = GetTagDisplayName(definition, tagId);
var idLabel = $"ID {tagId}";
var previewSize = ProfileTagRenderer.MeasureTag(definition, scale, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _tagPreviewSegments, ResolveIconWrap, _logger);
var textHeight = ImGui.CalcTextSize(displayName).Y + style.ItemSpacing.Y + ImGui.CalcTextSize(idLabel).Y;
var rowHeight = MathF.Max(previewSize.Y + style.CellPadding.Y * 2f, textHeight + style.CellPadding.Y * 2f);
ImGui.TableNextRow(ImGuiTableRowFlags.None, rowHeight);
ImGui.TableNextColumn();
using (ImRaii.PushId($"{contextPrefix}-selected-tag-{tagId}-{i}"))
DrawCenteredTagCell(definition, scale, previewSize, rowHeight, defaultTextColorU32);
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
ImGui.TextUnformatted(displayName);
ImGui.TextDisabled(idLabel);
ImGui.EndTooltip();
}
if (allowReorder)
{
ImGui.TableNextColumn();
DrawReorderCell(contextPrefix, tagId, i, _tagEditorSelection.Count, rowHeight, scale, ref moveUpRequest, ref moveDownRequest);
}
ImGui.TableNextColumn();
DrawFullCellButton("Remove", UIColors.Get("DimRed"), ref tagToRemove, tagId, false, scale, rowHeight, $"{contextPrefix}-remove-{tagId}-{i}");
}
ImGui.EndTable();
}
}
if (allowReorder)
{
if (moveUpRequest.HasValue && moveUpRequest.Value > 0)
{
var idx = moveUpRequest.Value;
(_tagEditorSelection[idx - 1], _tagEditorSelection[idx]) = (_tagEditorSelection[idx], _tagEditorSelection[idx - 1]);
}
if (moveDownRequest.HasValue && moveDownRequest.Value < _tagEditorSelection.Count - 1)
{
var idx = moveDownRequest.Value;
(_tagEditorSelection[idx], _tagEditorSelection[idx + 1]) = (_tagEditorSelection[idx + 1], _tagEditorSelection[idx]);
}
}
if (tagToRemove.HasValue)
_tagEditorSelection.Remove(tagToRemove.Value);
bool limitReached = _tagEditorSelection.Count >= _maxProfileTags;
if (limitReached)
UiSharedService.ColorTextWrapped($"You have reached the maximum of {_maxProfileTags} tags. Remove one before adding more.", UIColors.Get("DimRed"));
ImGui.Dummy(new Vector2(0f, 6f * scale));
ImGui.TextColored(UIColors.Get("LightlessPurple"), "Available Tags");
var availableIds = new List<int>(tagLibrary.Count);
var seenDefinitions = new HashSet<ProfileTagDefinition>();
foreach (var kvp in tagLibrary)
{
var definition = kvp.Value;
if (!definition.HasContent)
continue;
if (!seenDefinitions.Add(definition))
continue;
if (_tagEditorSelection.Contains(kvp.Key))
continue;
availableIds.Add(kvp.Key);
}
availableIds.Sort();
int totalAvailable = availableIds.Count;
if (totalAvailable == 0)
{
ImGui.TextDisabled("-- No additional tags available --");
}
else
{
int pageCount = Math.Max(1, (totalAvailable + _availableTagsPerPage - 1) / _availableTagsPerPage);
_availableTagPage = Math.Clamp(_availableTagPage, 0, pageCount - 1);
int start = _availableTagPage * _availableTagsPerPage;
int end = Math.Min(totalAvailable, start + _availableTagsPerPage);
ImGui.SameLine();
ImGui.TextDisabled($"Page {_availableTagPage + 1}/{pageCount}");
ImGui.SameLine();
ImGui.BeginDisabled(_availableTagPage == 0);
if (ImGui.SmallButton($"<##{contextPrefix}TagPagePrev"))
_availableTagPage--;
ImGui.EndDisabled();
ImGui.SameLine();
ImGui.BeginDisabled(_availableTagPage >= pageCount - 1);
if (ImGui.SmallButton($">##{contextPrefix}TagPageNext"))
_availableTagPage++;
ImGui.EndDisabled();
var availableFlags = ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.SizingStretchSame;
int? tagToAdd = null;
if (ImGui.BeginTable($"##{contextPrefix}AvailableTagsTable", 2, availableFlags))
{
ImGui.TableSetupColumn("Preview", ImGuiTableColumnFlags.WidthStretch, 0.75f);
ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, 90f);
ImGui.TableHeadersRow();
for (int idx = start; idx < end; idx++)
{
var tagId = availableIds[idx];
if (!tagLibrary.TryGetValue(tagId, out var definition) || !definition.HasContent)
continue;
var previewSize = ProfileTagRenderer.MeasureTag(definition, scale, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _tagPreviewSegments, ResolveIconWrap, _logger);
var rowHeight = previewSize.Y + style.CellPadding.Y * 2f;
ImGui.TableNextRow(ImGuiTableRowFlags.None, rowHeight);
ImGui.TableNextColumn();
using (ImRaii.PushId($"{contextPrefix}-available-tag-{tagId}"))
DrawCenteredTagCell(definition, scale, previewSize, rowHeight, defaultTextColorU32);
if (ImGui.IsItemHovered())
{
var name = GetTagDisplayName(definition, tagId);
ImGui.BeginTooltip();
ImGui.TextUnformatted(name);
ImGui.TextDisabled($"ID {tagId}");
ImGui.EndTooltip();
}
ImGui.TableNextColumn();
DrawFullCellButton("Add", UIColors.Get("LightlessGreen"), ref tagToAdd, tagId, limitReached, scale, rowHeight, $"{contextPrefix}-add-{tagId}");
}
ImGui.EndTable();
}
if (tagToAdd.HasValue)
{
_tagEditorSelection.Add(tagToAdd.Value);
if (_availableTagPage > 0 && (totalAvailable - 1) <= start)
_availableTagPage = Math.Max(0, _availableTagPage - 1);
}
}
bool hasChanges = !TagsEqual(_tagEditorSelection, _profileTagIds);
ImGui.Dummy(new Vector2(0f, 6f * scale));
if (!hasChanges)
ImGui.BeginDisabled();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, $"Save Tags##{contextPrefix}"))
{
var payload = _tagEditorSelection.Count == 0 ? Array.Empty<int>() : _tagEditorSelection.ToArray();
if (sortPayloadBeforeSubmit && payload.Length > 1)
Array.Sort(payload);
onPayloadPrepared?.Invoke(payload);
_ = submitAction(payload);
}
if (!hasChanges)
ImGui.EndDisabled();
UiSharedService.AttachToolTip(saveTooltip);
}
private void DrawProfileVisibilityControls()
{
if (!_isNsfwInitialized)
ImGui.BeginDisabled();
bool changed = DrawCheckboxRow("Mark profile as NSFW", _isNsfw, out var newValue, "Enable when your profile could be considered NSFW.");
if (changed)
_isNsfw = newValue;
bool visibilityChanged = _isNsfwInitialized && (_isNsfw != _userServerIsNsfw);
if (!_isNsfwInitialized)
ImGui.EndDisabled();
if (!_isNsfwInitialized || !visibilityChanged)
ImGui.BeginDisabled();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Apply Visibility Changes"))
{
_userServerIsNsfw = _isNsfw;
_ = _apiController.UserSetProfile(new UserProfileDto(
new UserData(_apiController.UID),
Disabled: false,
IsNSFW: _isNsfw,
ProfilePictureBase64: null,
BannerPictureBase64: null,
Description: null,
Tags: null));
}
UiSharedService.AttachToolTip("Apply the visibility toggles above.");
if (!_isNsfwInitialized || !visibilityChanged)
ImGui.EndDisabled();
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.SyncAlt, "Reset") && _isNsfwInitialized)
_isNsfw = _userServerIsNsfw;
}
private string? GetCurrentProfilePictureBase64(LightlessUserProfileData profile)
{
if (_queuedProfileImage is { Length: > 0 })
return Convert.ToBase64String(_queuedProfileImage);
if (!string.IsNullOrWhiteSpace(profile.Base64ProfilePicture) && !string.Equals(profile.Description, LoadingProfileDescription, StringComparison.Ordinal))
return profile.Base64ProfilePicture;
return _profileImage.Length > 0 ? Convert.ToBase64String(_profileImage) : null;
}
private string? GetCurrentProfileBannerBase64(LightlessUserProfileData profile)
{
if (!string.IsNullOrWhiteSpace(profile.Base64BannerPicture) && !string.Equals(profile.Description, LoadingProfileDescription, StringComparison.Ordinal))
return profile.Base64BannerPicture;
return _bannerImage.Length > 0 ? Convert.ToBase64String(_bannerImage) : null;
}
private static bool IsSupportedImageFormat(IImageFormat? format)
{
if (format is null)
return false;
foreach (var ext in format.FileExtensions)
{
if (_supportedImageExtensions.Contains(ext))
return true;
}
return false;
}
private void DrawCenteredTagCell(ProfileTagDefinition tag, float scale, Vector2 tagSize, float rowHeight, uint defaultTextColorU32)
{
var style = ImGui.GetStyle();
var cellStart = ImGui.GetCursorPos();
var available = ImGui.GetContentRegionAvail();
var innerHeight = MathF.Max(0f, rowHeight - style.CellPadding.Y * 2f);
var offsetX = MathF.Max(0f, (available.X - tagSize.X) * 0.5f);
var offsetY = MathF.Max(0f, innerHeight - tagSize.Y) * 0.5f;
ImGui.SetCursorPos(new Vector2(cellStart.X + offsetX, cellStart.Y + style.CellPadding.Y + offsetY));
ImGui.InvisibleButton("##tagPreview", tagSize);
var rectMin = ImGui.GetItemRectMin();
var drawList = ImGui.GetWindowDrawList();
ProfileTagRenderer.RenderTag(tag, rectMin, scale, drawList, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _tagPreviewSegments, ResolveIconWrap, _logger);
}
private void DrawReorderCell(
string contextPrefix,
int tagId,
int index,
int count,
float rowHeight,
float scale,
ref int? moveUpTarget,
ref int? moveDownTarget)
{
var style = ImGui.GetStyle();
var cellStart = ImGui.GetCursorPos();
var availableWidth = ImGui.GetContentRegionAvail().X;
var innerHeight = MathF.Max(0f, rowHeight - style.CellPadding.Y * 2f);
var spacing = MathF.Min(style.ItemSpacing.Y * 0.5f, innerHeight * 0.15f);
var buttonHeight = MathF.Max(1f, (innerHeight - spacing) * 0.5f);
var width = MathF.Max(1f, availableWidth);
var upColor = UIColors.Get("LightlessBlue");
using (ImRaii.PushId($"{contextPrefix}-order-{tagId}-{index}"))
{
ImGui.SetCursorPos(new Vector2(cellStart.X, cellStart.Y + style.CellPadding.Y));
if (ColoredButton("\u2191##tagMoveUp", upColor, new Vector2(width, buttonHeight), scale, index == 0))
moveUpTarget = index;
ImGui.SetCursorPos(new Vector2(cellStart.X, cellStart.Y + style.CellPadding.Y + buttonHeight + spacing));
if (ColoredButton("\u2193##tagMoveDown", upColor, new Vector2(width, buttonHeight), scale, index >= count - 1))
moveDownTarget = index;
}
}
private void DrawFullCellButton(string label, Vector4 baseColor, ref int? target, int tagId, bool disabled, float scale, float rowHeight, string idSuffix)
{
var style = ImGui.GetStyle();
var cellStart = ImGui.GetCursorPos();
var available = ImGui.GetContentRegionAvail();
var buttonHeight = MathF.Max(1f, rowHeight - style.CellPadding.Y * 2f);
ImGui.SetCursorPos(new Vector2(cellStart.X, cellStart.Y + style.CellPadding.Y));
using (ImRaii.PushId(idSuffix))
{
if (ColoredButton(label, baseColor, new Vector2(MathF.Max(available.X, 1f), buttonHeight), scale, disabled))
target = tagId;
}
}
private static bool ColoredButton(string label, Vector4 baseColor, Vector2 size, float scale, bool disabled)
{
var style = ImGui.GetStyle();
var hovered = BlendTowardsWhite(baseColor, 0.15f);
var active = BlendTowardsWhite(baseColor, 0.3f);
ImGui.PushStyleColor(ImGuiCol.Button, baseColor);
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, hovered);
ImGui.PushStyleColor(ImGuiCol.ButtonActive, active);
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, style.FrameRounding);
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(MathF.Max(2f, style.FramePadding.X), MathF.Max(1f, style.FramePadding.Y * 0.3f)) * scale);
ImGui.BeginDisabled(disabled);
bool clicked = ImGui.Button(label, size);
ImGui.EndDisabled();
ImGui.PopStyleVar(2);
ImGui.PopStyleColor(3);
return clicked;
}
private static float Clamp01(float value)
=> value < 0f ? 0f : value > 1f ? 1f : value;
private static Vector4 BlendTowardsWhite(Vector4 color, float amount)
{
var result = Vector4.Lerp(color, Vector4.One, Clamp01(amount));
result.W = color.W;
return result;
}
private static string GetTagDisplayName(ProfileTagDefinition tag, int tagId)
{
if (!string.IsNullOrWhiteSpace(tag.Text))
return tag.Text!;
if (!string.IsNullOrWhiteSpace(tag.SeStringPayload))
{
var stripped = SeStringUtils.StripMarkup(tag.SeStringPayload!);
if (!string.IsNullOrWhiteSpace(stripped))
return stripped;
}
return $"Tag {tagId}";
}
private IDalamudTextureWrap? ResolveIconWrap(uint iconId)
{
if (_uiSharedService.TryGetIcon(iconId, out var wrap) && wrap != null)
return wrap;
return null;
}
private int[] GetServerTagPayload()
{
if (_profileTagIds.Length == 0)
return Array.Empty<int>();
var copy = new int[_profileTagIds.Length];
Array.Copy(_profileTagIds, copy, _profileTagIds.Length);
return copy;
}
private static bool TagsEqual(IReadOnlyList<int>? current, IReadOnlyList<int>? reference)
{
if (ReferenceEquals(current, reference))
return true;
if (current is null || reference is null)
return false;
if (current.Count != reference.Count)
return false;
for (int i = 0; i < current.Count; i++)
{
if (current[i] != reference[i])
return false;
}
return true;
}
private void DrawVanityTabContent(float scale)
{
DrawSection("Colored UID", scale, () =>
{
var hasVanity = _apiController.HasVanity;
if (!hasVanity)
{
UiSharedService.ColorTextWrapped("You do not currently have vanity access. Become a supporter to unlock these features. (If you already are, interact with the bot to update)", UIColors.Get("DimRed"));
}
var monoFont = UiBuilder.MonoFont;
using (ImRaii.PushFont(monoFont))
{
var previewTextColor = _textEnabled ? _textColor : Vector4.One;
var previewGlowColor = _glowEnabled ? _glowColor : Vector4.Zero;
var seString = SeStringUtils.BuildFormattedPlayerName(_apiController.DisplayName, previewTextColor, previewGlowColor);
var drawList = ImGui.GetWindowDrawList();
var textSize = ImGui.CalcTextSize(seString.TextValue);
float minWidth = 160f * ImGuiHelpers.GlobalScale;
float paddingY = 5f * ImGuiHelpers.GlobalScale;
float paddingX = 10f * ImGuiHelpers.GlobalScale;
float bgWidth = Math.Max(textSize.X + paddingX * 2f, minWidth);
var style = ImGui.GetStyle();
var fontHeight = monoFont.FontSize > 0f ? monoFont.FontSize : ImGui.GetFontSize();
float frameHeight = fontHeight + style.FramePadding.Y * 2f;
float textBlockHeight = MathF.Max(frameHeight, textSize.Y);
var cursor = ImGui.GetCursorScreenPos();
var rectMin = cursor;
var rectMax = rectMin + new Vector2(bgWidth, textBlockHeight + paddingY * 2f);
float boost = Luminance.ComputeHighlight(previewTextColor, previewGlowColor);
var baseBg = new Vector4(0.15f + boost, 0.15f + boost, 0.15f + boost, 1f);
var bgColor = Luminance.BackgroundContrast(previewTextColor, previewGlowColor, baseBg, ref _currentBg);
var borderColor = UIColors.Get("LightlessPurple");
drawList.AddRectFilled(rectMin, rectMax, ImGui.GetColorU32(bgColor), 5f);
drawList.AddRect(rectMin, rectMax, ImGui.GetColorU32(borderColor), 5f, ImDrawFlags.None, 1.2f);
var textOrigin = new Vector2(rectMin.X + (bgWidth - textSize.X) * 0.5f, rectMin.Y + paddingY);
SeStringUtils.RenderSeStringWithHitbox(seString, textOrigin, monoFont);
ImGui.Dummy(new Vector2(0f, 1.5f));
}
ImGui.TextColored(UIColors.Get("LightlessPurple"), "Colors");
if (!hasVanity)
ImGui.BeginDisabled();
if (DrawCheckboxRow("Enable custom text color", _textEnabled, out var newTextEnabled))
_textEnabled = newTextEnabled;
ImGui.SameLine();
ImGui.BeginDisabled(!_textEnabled);
ImGui.ColorEdit4("Text Color##vanityTextColor", ref _textColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf);
ImGui.EndDisabled();
if (DrawCheckboxRow("Enable glow color", _glowEnabled, out var newGlowEnabled))
_glowEnabled = newGlowEnabled;
ImGui.SameLine();
ImGui.BeginDisabled(!_glowEnabled);
ImGui.ColorEdit4("Glow Color##vanityGlowColor", ref _glowColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf);
ImGui.EndDisabled();
bool changed = !Equals(_savedVanity, new VanityState(_textEnabled, _glowEnabled, _textColor, _glowColor));
if (!changed)
ImGui.BeginDisabled();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Vanity Changes"))
{
string? newText = _textEnabled ? UIColors.RgbaToHex(_textColor) : string.Empty;
string? newGlow = _glowEnabled ? UIColors.RgbaToHex(_glowColor) : string.Empty;
_ = _apiController.UserUpdateVanityColors(new UserVanityColorsDto(newText, newGlow));
_savedVanity = new VanityState(_textEnabled, _glowEnabled, _textColor, _glowColor);
}
if (!changed)
ImGui.EndDisabled();
if (!hasVanity)
ImGui.EndDisabled();
});
}
private static void DrawSection(string title, float scale, Action body)
{
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(6f, 4f) * scale);
var flags = ImGuiTreeNodeFlags.SpanFullWidth | ImGuiTreeNodeFlags.Framed | ImGuiTreeNodeFlags.AllowItemOverlap | ImGuiTreeNodeFlags.DefaultOpen;
var open = ImGui.CollapsingHeader(title, flags);
ImGui.PopStyleVar();
if (open)
{
ImGui.Dummy(new Vector2(0f, 3f * scale));
body();
ImGui.Dummy(new Vector2(0f, 2f * scale));
}
}
private static bool DrawCheckboxRow(string label, bool currentValue, out bool newValue, string? tooltip = null)
{
bool value = currentValue;
bool changed = UiSharedService.CheckboxWithBorder(label, ref value, UIColors.Get("LightlessPurple"), 1.5f);
if (!string.IsNullOrEmpty(tooltip))
UiSharedService.AttachToolTip(tooltip);
newValue = value;
return changed;
}
private void SyncProfileState(LightlessUserProfileData profile)
{
if (string.Equals(profile.Description, LoadingProfileDescription, StringComparison.Ordinal))
{
_isNsfwInitialized = false;
return;
}
if (!_isNsfwInitialized)
{
_userServerIsNsfw = profile.IsNSFW;
_isNsfw = profile.IsNSFW;
_isNsfwInitialized = true;
}
var profileBytes = profile.ImageData.Value;
if (_pfpTextureWrap == null || !_profileImage.SequenceEqual(profileBytes))
{
_profileImage = profileBytes;
_pfpTextureWrap?.Dispose();
_pfpTextureWrap = _profileImage.Length > 0 ? _uiSharedService.LoadImage(_profileImage) : null;
_queuedProfileImage = null;
}
var bannerBytes = profile.BannerImageData.Value;
if (_bannerTextureWrap == null || !_bannerImage.SequenceEqual(bannerBytes))
{
_bannerImage = bannerBytes;
_bannerTextureWrap?.Dispose();
_bannerTextureWrap = _bannerImage.Length > 0 ? _uiSharedService.LoadImage(_bannerImage) : null;
}
if (!string.Equals(_profileDescription, profile.Description, StringComparison.Ordinal))
{
_profileDescription = profile.Description;
_descriptionText = _profileDescription;
}
var serverTags = profile.Tags ?? [];
if (!TagsEqual(serverTags, _profileTagIds))
{
var previous = _profileTagIds;
_profileTagIds = serverTags.Count == 0 ? [] : [.. serverTags];
if (TagsEqual(_tagEditorSelection, previous))
{
_tagEditorSelection.Clear();
if (_profileTagIds.Length > 0)
_tagEditorSelection.AddRange(_profileTagIds);
}
}
}
private static bool IsWindowBeingDragged()
{
return ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows) && ImGui.GetIO().MouseDown[0];
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_pfpTextureWrap?.Dispose();
_bannerTextureWrap?.Dispose();
}
}