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
1262 lines
52 KiB
C#
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();
|
|
}
|
|
} |