using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; 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.User; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.UI.Style; using LightlessSync.Utils; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using System.Numerics; namespace LightlessSync.UI; public class EditProfileUi : WindowMediatorSubscriberBase { private readonly ApiController _apiController; private readonly FileDialogManager _fileDialogManager; private readonly LightlessProfileManager _lightlessProfileManager; private readonly UiSharedService _uiSharedService; private bool _adjustedForScollBarsLocalProfile = false; private bool _adjustedForScollBarsOnlineProfile = false; private string _descriptionText = string.Empty; private IDalamudTextureWrap? _pfpTextureWrap; private string _profileDescription = string.Empty; private byte[] _profileImage = []; private bool _showFileDialogError = false; private bool _wasOpen; private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f); private bool vanityInitialized; // useless for now private bool textEnabled; private bool glowEnabled; private Vector4 textColor; private Vector4 glowColor; private record VanityState(bool TextEnabled, bool GlowEnabled, Vector4 TextColor, Vector4 GlowColor); private VanityState _savedVanity; public EditProfileUi(ILogger logger, LightlessMediator mediator, ApiController apiController, UiSharedService uiSharedService, FileDialogManager fileDialogManager, LightlessProfileManager lightlessProfileManager, PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "Lightless Sync Edit Profile###LightlessSyncEditProfileUI", performanceCollectorService) { IsOpen = false; this.SizeConstraints = new() { MinimumSize = new(850, 640), MaximumSize = new(850, 700) }; _apiController = apiController; _uiSharedService = uiSharedService; _fileDialogManager = fileDialogManager; _lightlessProfileManager = lightlessProfileManager; Mediator.Subscribe(this, (_) => { _wasOpen = IsOpen; IsOpen = false; }); Mediator.Subscribe(this, (_) => IsOpen = _wasOpen); Mediator.Subscribe(this, (_) => IsOpen = false); Mediator.Subscribe(this, (msg) => { if (msg.UserData == null || string.Equals(msg.UserData.UID, _apiController.UID, StringComparison.Ordinal)) { _pfpTextureWrap?.Dispose(); _pfpTextureWrap = null; } }); Mediator.Subscribe(this, msg => { LoadVanity(); }); } 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); vanityInitialized = true; } protected override void DrawInternal() { _uiSharedService.UnderlinedBigText("Notes and Rules for Profiles", UIColors.Get("LightlessYellow")); ImGui.Dummy(new Vector2(5)); 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 picture 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 profile settings."); ImGui.PopStyleVar(); ImGui.Dummy(new Vector2(3)); var profile = _lightlessProfileManager.GetLightlessUserProfile(new UserData(_apiController.UID)); _logger.LogInformation("Profile fetched for drawing: {profile}", profile); if (ImGui.BeginTabBar("##EditProfileTabs")) { if (ImGui.BeginTabItem("Current Profile")) { _uiSharedService.MediumText("Current Profile (as saved on server)", UIColors.Get("LightlessPurple")); ImGui.Dummy(new Vector2(5)); if (profile.IsFlagged) { UiSharedService.ColorTextWrapped(profile.Description, ImGuiColors.DalamudRed); return; } if (!_profileImage.SequenceEqual(profile.ImageData.Value)) { _profileImage = profile.ImageData.Value; _pfpTextureWrap?.Dispose(); _pfpTextureWrap = _uiSharedService.LoadImage(_profileImage); } if (!string.Equals(_profileDescription, profile.Description, StringComparison.OrdinalIgnoreCase)) { _profileDescription = profile.Description; _descriptionText = _profileDescription; } if (_pfpTextureWrap != null) { ImGui.Image(_pfpTextureWrap.Handle, ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height)); } var spacing = ImGui.GetStyle().ItemSpacing.X; ImGuiHelpers.ScaledRelativeSameLine(256, spacing); using (_uiSharedService.GameFont.Push()) { var descriptionTextSize = ImGui.CalcTextSize(profile.Description, wrapWidth: 256f); var childFrame = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 256); if (descriptionTextSize.Y > childFrame.Y) { _adjustedForScollBarsOnlineProfile = true; } else { _adjustedForScollBarsOnlineProfile = false; } childFrame = childFrame with { X = childFrame.X + (_adjustedForScollBarsOnlineProfile ? ImGui.GetStyle().ScrollbarSize : 0), }; if (ImGui.BeginChildFrame(101, childFrame)) { UiSharedService.TextWrapped(profile.Description); } ImGui.EndChildFrame(); } var nsfw = profile.IsNSFW; ImGui.BeginDisabled(); ImGui.Checkbox("Is NSFW", ref nsfw); ImGui.EndDisabled(); _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.EndTabItem(); } if (ImGui.BeginTabItem("Profile Settings")) { _uiSharedService.MediumText("Profile Settings", UIColors.Get("LightlessPurple")); ImGui.Dummy(new Vector2(5)); if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture")) { _fileDialogManager.OpenFileDialog("Select new Profile picture", ".png", (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 (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase)) { _showFileDialogError = true; return; } using var image = Image.Load(fileContent); if (image.Width > 256 || image.Height > 256 || (fileContent.Length > 250 * 1024)) { _showFileDialogError = true; return; } _showFileDialogError = false; await _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, Convert.ToBase64String(fileContent), Description: null, Tags: null)) .ConfigureAwait(false); }); }); } UiSharedService.AttachToolTip("Select and upload a new profile picture"); ImGui.SameLine(); if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture")) { _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, "", Description: null, Tags: null)); } UiSharedService.AttachToolTip("Clear your currently uploaded profile picture"); if (_showFileDialogError) { UiSharedService.ColorTextWrapped("The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size", ImGuiColors.DalamudRed); } var isNsfw = profile.IsNSFW; if (ImGui.Checkbox("Profile is NSFW", ref isNsfw)) { _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, isNsfw, ProfilePictureBase64: null, Description: null, Tags: null)); } _uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON"); var widthTextBox = 400; var posX = ImGui.GetCursorPosX(); ImGui.TextUnformatted($"Description {_descriptionText.Length}/1500"); ImGui.SetCursorPosX(posX); ImGuiHelpers.ScaledRelativeSameLine(widthTextBox, ImGui.GetStyle().ItemSpacing.X); ImGui.TextUnformatted("Preview (approximate)"); using (_uiSharedService.GameFont.Push()) ImGui.InputTextMultiline("##description", ref _descriptionText, 1500, ImGuiHelpers.ScaledVector2(widthTextBox, 200)); ImGui.SameLine(); using (_uiSharedService.GameFont.Push()) { var descriptionTextSizeLocal = ImGui.CalcTextSize(_descriptionText, wrapWidth: 256f); var childFrameLocal = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 200); if (descriptionTextSizeLocal.Y > childFrameLocal.Y) { _adjustedForScollBarsLocalProfile = true; } else { _adjustedForScollBarsLocalProfile = false; } childFrameLocal = childFrameLocal with { X = childFrameLocal.X + (_adjustedForScollBarsLocalProfile ? ImGui.GetStyle().ScrollbarSize : 0), }; if (ImGui.BeginChildFrame(102, childFrameLocal)) { UiSharedService.TextWrapped(_descriptionText); } ImGui.EndChildFrame(); } if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description")) { _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, _descriptionText, Tags: null)); } UiSharedService.AttachToolTip("Sets your profile description text"); ImGui.SameLine(); if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description")) { _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, "", Tags: null)); } UiSharedService.AttachToolTip("Clears your profile description text"); ImGui.EndTabItem(); } if (ImGui.BeginTabItem("Vanity Settings")) { _uiSharedService.MediumText("Supporter Vanity Settings", UIColors.Get("LightlessPurple")); ImGui.Dummy(new Vector2(4)); _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "Must be a supporter through Patreon/Ko-fi to access these settings."); var hasVanity = _apiController.HasVanity; if (!hasVanity) { UiSharedService.ColorTextWrapped("You do not currently have vanity access. Become a supporter to unlock these features.", UIColors.Get("DimRed")); ImGui.Dummy(new Vector2(8)); ImGui.BeginDisabled(); } _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); _uiSharedService.MediumText("Colored UID", UIColors.Get("LightlessPurple")); ImGui.Dummy(new Vector2(5)); var font = UiBuilder.MonoFont; var playerUID = _apiController.UID; var playerDisplay = _apiController.DisplayName; var previewTextColor = textEnabled ? textColor : Vector4.One; var previewGlowColor = glowEnabled ? glowColor : Vector4.Zero; var seString = SeStringUtils.BuildFormattedPlayerName(playerDisplay, previewTextColor, previewGlowColor); using (ImRaii.PushFont(font)) { var drawList = ImGui.GetWindowDrawList(); var textSize = ImGui.CalcTextSize(seString.TextValue); float minWidth = 150f * ImGuiHelpers.GlobalScale; float bgWidth = Math.Max(textSize.X + 20f, minWidth); float paddingY = 5f * ImGuiHelpers.GlobalScale; var cursor = ImGui.GetCursorScreenPos(); var rectMin = cursor; var rectMax = rectMin + new Vector2(bgWidth, textSize.Y + (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), 6.0f); drawList.AddRect(rectMin, rectMax, ImGui.GetColorU32(borderColor), 6.0f, ImDrawFlags.None, 1.5f); var textPos = new Vector2( rectMin.X + (bgWidth - textSize.X) * 0.5f, rectMin.Y + paddingY ); SeStringUtils.RenderSeStringWithHitbox(seString, textPos, font); ImGui.Dummy(new Vector2(5)); } const float colorPickAlign = 90f; _uiSharedService.DrawNoteLine("- ", UIColors.Get("LightlessPurple"), "Text Color"); ImGui.SameLine(colorPickAlign); ImGui.Checkbox("##toggleTextColor", ref textEnabled); ImGui.SameLine(); ImGui.BeginDisabled(!textEnabled); ImGui.ColorEdit4($"##color_text", ref textColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf); ImGui.EndDisabled(); _uiSharedService.DrawNoteLine("- ", UIColors.Get("LightlessPurple"), "Glow Color"); ImGui.SameLine(colorPickAlign); ImGui.Checkbox("##toggleGlowColor", ref glowEnabled); ImGui.SameLine(); ImGui.BeginDisabled(!glowEnabled); ImGui.ColorEdit4($"##color_glow", 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 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(); ImGui.Dummy(new Vector2(5)); _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); if (!hasVanity) ImGui.EndDisabled(); ImGui.EndTabItem(); } ImGui.EndTabBar(); } } protected override void Dispose(bool disposing) { base.Dispose(disposing); _pfpTextureWrap?.Dispose(); } }