1290 lines
56 KiB
C#
1290 lines
56 KiB
C#
using Dalamud.Bindings.ImGui;
|
|
using Dalamud.Interface.Textures.TextureWraps;
|
|
using Dalamud.Interface.Utility;
|
|
using LightlessSync.API.Data;
|
|
using LightlessSync.API.Data.Extensions;
|
|
using LightlessSync.API.Dto.Group;
|
|
using LightlessSync.PlayerData.Pairs;
|
|
using LightlessSync.Services;
|
|
using LightlessSync.Services.Mediator;
|
|
using LightlessSync.Services.ServerConfiguration;
|
|
using LightlessSync.UI.Services;
|
|
using LightlessSync.UI.Tags;
|
|
using LightlessSync.Utils;
|
|
using LightlessSync.WebAPI;
|
|
using Microsoft.Extensions.Logging;
|
|
using System.Numerics;
|
|
|
|
namespace LightlessSync.UI;
|
|
|
|
public class StandaloneProfileUi : WindowMediatorSubscriberBase
|
|
{
|
|
private readonly LightlessProfileManager _lightlessProfileManager;
|
|
private readonly PairUiService _pairUiService;
|
|
private readonly ServerConfigurationManager _serverManager;
|
|
private readonly ProfileTagService _profileTagService;
|
|
private readonly ApiController _apiController;
|
|
private readonly UiSharedService _uiSharedService;
|
|
private readonly UserData? _userData;
|
|
private readonly GroupData? _groupData;
|
|
private readonly bool _isGroupProfile;
|
|
private readonly bool _isLightfinderContext;
|
|
private readonly string? _lightfinderCid;
|
|
private byte[] _lastProfilePicture = [];
|
|
private byte[] _lastSupporterPicture = [];
|
|
private byte[] _lastBannerPicture = [];
|
|
private IDalamudTextureWrap? _supporterTextureWrap;
|
|
private IDalamudTextureWrap? _textureWrap;
|
|
private IDalamudTextureWrap? _bannerTextureWrap;
|
|
private bool _bannerTextureLoaded;
|
|
private Vector4 _tagBackgroundColor = new(0.18f, 0.18f, 0.18f, 0.95f);
|
|
private Vector4 _tagBorderColor = new(0.35f, 0.35f, 0.35f, 0.4f);
|
|
private readonly List<SeStringUtils.SeStringSegment> _seResolvedSegments = new();
|
|
private const float MaxHeightMultiplier = 2.5f;
|
|
private const float DescriptionMaxVisibleLines = 12f;
|
|
private const string UserDescriptionPlaceholder = "-- User has no description set --";
|
|
private const string GroupDescriptionPlaceholder = "-- Syncshell has no description set --";
|
|
private const string LightfinderDisplayName = "Lightfinder User";
|
|
private readonly string _lightfinderDisplayName = LightfinderDisplayName;
|
|
private float _lastComputedWindowHeight = -1f;
|
|
|
|
public StandaloneProfileUi(
|
|
ILogger<StandaloneProfileUi> logger,
|
|
LightlessMediator mediator,
|
|
UiSharedService uiBuilder,
|
|
ServerConfigurationManager serverManager,
|
|
ProfileTagService profileTagService,
|
|
DalamudUtilService dalamudUtilService,
|
|
LightlessProfileManager lightlessProfileManager,
|
|
PairUiService pairUiService,
|
|
Pair? pair,
|
|
UserData? userData,
|
|
GroupData? groupData,
|
|
bool isLightfinderContext,
|
|
string? lightfinderCid,
|
|
PerformanceCollectorService performanceCollector,
|
|
ApiController apiController)
|
|
: base(logger, mediator, BuildWindowTitle(
|
|
userData,
|
|
groupData,
|
|
isLightfinderContext,
|
|
isLightfinderContext ? ResolveLightfinderDisplayName(dalamudUtilService, lightfinderCid) : null),
|
|
performanceCollector)
|
|
{
|
|
_uiSharedService = uiBuilder;
|
|
_serverManager = serverManager;
|
|
_profileTagService = profileTagService;
|
|
_lightlessProfileManager = lightlessProfileManager;
|
|
Pair = pair;
|
|
_pairUiService = pairUiService;
|
|
_userData = userData;
|
|
_groupData = groupData;
|
|
_isGroupProfile = groupData is not null;
|
|
_isLightfinderContext = isLightfinderContext;
|
|
_lightfinderCid = lightfinderCid;
|
|
if (_isLightfinderContext)
|
|
_lightfinderDisplayName = ResolveLightfinderDisplayName(dalamudUtilService, lightfinderCid);
|
|
|
|
Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoResize;
|
|
|
|
var fixedSize = new Vector2(840f, 525f) * ImGuiHelpers.GlobalScale;
|
|
Size = fixedSize;
|
|
SizeCondition = ImGuiCond.Always;
|
|
WindowBuilder.For(this)
|
|
.SetSizeConstraints(
|
|
fixedSize,
|
|
new Vector2(fixedSize.X, fixedSize.Y * MaxHeightMultiplier))
|
|
.Apply();
|
|
|
|
IsOpen = true;
|
|
_apiController = apiController;
|
|
}
|
|
|
|
public Pair? Pair { get; }
|
|
public bool IsGroupProfile => _isGroupProfile;
|
|
public GroupData? ProfileGroupData => _groupData;
|
|
public bool IsLightfinderContext => _isLightfinderContext;
|
|
public string? LightfinderCid => _lightfinderCid;
|
|
public UserData ProfileUserData => _userData ?? throw new InvalidOperationException("ProfileUserData is only available for user profiles.");
|
|
|
|
public void SetTagColorTheme(Vector4? background, Vector4? border)
|
|
{
|
|
if (background.HasValue)
|
|
_tagBackgroundColor = background.Value;
|
|
if (border.HasValue)
|
|
_tagBorderColor = border.Value;
|
|
}
|
|
|
|
private static Vector4 ResolveThemeColor(string colorName, Vector4 fallback)
|
|
{
|
|
try
|
|
{
|
|
return UIColors.Get(colorName);
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
// fallback when the color key is not registered
|
|
}
|
|
|
|
return fallback;
|
|
}
|
|
|
|
private static string BuildWindowTitle(UserData? userData, GroupData? groupData, bool isLightfinderContext, string? lightfinderDisplayName)
|
|
{
|
|
if (groupData is not null)
|
|
{
|
|
var alias = groupData.AliasOrGID;
|
|
return $"Syncshell Profile of {alias}##LightlessSyncStandaloneGroupProfileUI{groupData.GID}";
|
|
}
|
|
|
|
if (userData is null)
|
|
return "Lightless Profile##LightlessSyncStandaloneProfileUI";
|
|
|
|
var name = isLightfinderContext ? lightfinderDisplayName ?? LightfinderDisplayName : userData.AliasOrUID;
|
|
var suffix = isLightfinderContext ? " (Lightfinder)" : string.Empty;
|
|
return $"Lightless Profile of {name}{suffix}##LightlessSyncStandaloneProfileUI{name}";
|
|
}
|
|
|
|
private static string ResolveLightfinderDisplayName(DalamudUtilService dalamudUtilService, string? hashedCid)
|
|
{
|
|
if (string.IsNullOrEmpty(hashedCid))
|
|
return LightfinderDisplayName;
|
|
|
|
try
|
|
{
|
|
var (name, address) = dalamudUtilService.FindPlayerByNameHash(hashedCid);
|
|
if (string.IsNullOrEmpty(name))
|
|
return LightfinderDisplayName;
|
|
|
|
var world = dalamudUtilService.GetWorldNameFromPlayerAddress(address);
|
|
return string.IsNullOrEmpty(world) ? name : $"{name} ({world})";
|
|
}
|
|
catch
|
|
{
|
|
return LightfinderDisplayName;
|
|
}
|
|
}
|
|
|
|
protected override void DrawInternal()
|
|
{
|
|
try
|
|
{
|
|
if (_isGroupProfile)
|
|
{
|
|
DrawGroupProfileWindow();
|
|
return;
|
|
}
|
|
|
|
if (_userData is null)
|
|
return;
|
|
|
|
var userData = _userData;
|
|
var scale = ImGuiHelpers.GlobalScale;
|
|
var viewport = ImGui.GetMainViewport();
|
|
var linked = !_isLightfinderContext
|
|
&& Pair is null
|
|
&& ProfileEditorLayoutCoordinator.IsActive(userData.UID);
|
|
var baseSize = ProfileEditorLayoutCoordinator.GetProfileSize(scale);
|
|
var baseWidth = baseSize.X;
|
|
var minHeight = baseSize.Y;
|
|
var maxAllowedHeight = minHeight * MaxHeightMultiplier;
|
|
var targetHeight = _lastComputedWindowHeight > 0f
|
|
? Math.Clamp(_lastComputedWindowHeight, minHeight, maxAllowedHeight)
|
|
: minHeight;
|
|
var desiredSize = new Vector2(baseWidth, targetHeight);
|
|
Size = desiredSize;
|
|
if (linked)
|
|
{
|
|
ProfileEditorLayoutCoordinator.EnsureAnchor(viewport.WorkPos, scale);
|
|
|
|
var currentPos = ImGui.GetWindowPos();
|
|
if (IsWindowBeingDragged())
|
|
ProfileEditorLayoutCoordinator.UpdateAnchorFromProfile(currentPos);
|
|
|
|
var desiredPos = ProfileEditorLayoutCoordinator.GetProfilePosition(scale);
|
|
if (!ProfileEditorLayoutCoordinator.NearlyEquals(currentPos, desiredPos))
|
|
ImGui.SetWindowPos(desiredPos, ImGuiCond.Always);
|
|
|
|
if (!ProfileEditorLayoutCoordinator.NearlyEquals(ImGui.GetWindowSize(), desiredSize))
|
|
ImGui.SetWindowSize(desiredSize, ImGuiCond.Always);
|
|
}
|
|
else
|
|
{
|
|
var defaultPosition = viewport.WorkPos + (new Vector2(50f, 70f) * scale);
|
|
ImGui.SetWindowPos(defaultPosition, ImGuiCond.FirstUseEver);
|
|
ImGui.SetWindowSize(desiredSize, ImGuiCond.Always);
|
|
}
|
|
|
|
var profile = _lightlessProfileManager.GetLightlessProfile(userData);
|
|
IReadOnlyList<ProfileTagDefinition> profileTags = profile.Tags.Count > 0
|
|
? ProfileTagService.ResolveTags(profile.Tags)
|
|
: [];
|
|
|
|
if (_textureWrap == null || !profile.ImageData.Value.SequenceEqual(_lastProfilePicture))
|
|
{
|
|
_textureWrap?.Dispose();
|
|
_textureWrap = null;
|
|
_lastProfilePicture = profile.ImageData.Value;
|
|
ResetBannerTexture();
|
|
if (_lastProfilePicture.Length > 0)
|
|
{
|
|
_textureWrap = _uiSharedService.LoadImage(_lastProfilePicture);
|
|
}
|
|
}
|
|
|
|
if (_supporterTextureWrap == null || !profile.SupporterImageData.Value.SequenceEqual(_lastSupporterPicture))
|
|
{
|
|
_supporterTextureWrap?.Dispose();
|
|
_supporterTextureWrap = null;
|
|
if (!string.IsNullOrEmpty(profile.Base64SupporterPicture))
|
|
{
|
|
_lastSupporterPicture = profile.SupporterImageData.Value;
|
|
if (_lastSupporterPicture.Length > 0)
|
|
{
|
|
_supporterTextureWrap = _uiSharedService.LoadImage(_lastSupporterPicture);
|
|
}
|
|
}
|
|
}
|
|
|
|
var bannerBytes = profile.BannerImageData.Value;
|
|
if (!_lastBannerPicture.SequenceEqual(bannerBytes))
|
|
{
|
|
ResetBannerTexture();
|
|
_lastBannerPicture = bannerBytes;
|
|
}
|
|
string? noteText = null;
|
|
|
|
var isSelfProfile = !_isLightfinderContext
|
|
&& _userData is not null
|
|
&& !string.IsNullOrEmpty(_apiController.UID)
|
|
&& string.Equals(_userData.UID, _apiController.UID, StringComparison.Ordinal);
|
|
|
|
string statusLabel = _isLightfinderContext
|
|
? "Exploring"
|
|
: isSelfProfile ? "Online" : "Offline";
|
|
|
|
string? visiblePlayerName = null;
|
|
bool directPair = false;
|
|
bool youPaused = false;
|
|
bool theyPaused = false;
|
|
List<string> syncshellLines = [];
|
|
|
|
if (!_isLightfinderContext)
|
|
{
|
|
noteText = _serverManager.GetNoteForUid(_userData!.UID);
|
|
}
|
|
|
|
if (!_isLightfinderContext && Pair != null)
|
|
{
|
|
var snapshot = _pairUiService.GetSnapshot();
|
|
noteText = _serverManager.GetNoteForUid(Pair.UserData.UID);
|
|
|
|
statusLabel = Pair.IsVisible ? "Visible" : (Pair.IsOnline ? "Online" : "Offline");
|
|
visiblePlayerName = Pair.IsVisible ? Pair.PlayerName : null;
|
|
|
|
directPair = Pair.IsDirectlyPaired;
|
|
|
|
var pairInfo = Pair.UserPair;
|
|
if (pairInfo != null)
|
|
{
|
|
if (directPair)
|
|
{
|
|
youPaused = pairInfo.OwnPermissions.IsPaused();
|
|
theyPaused = pairInfo.OtherPermissions.IsPaused();
|
|
}
|
|
|
|
if (pairInfo.Groups.Count != 0)
|
|
{
|
|
foreach (var gid in pairInfo.Groups)
|
|
{
|
|
var groupLabel = snapshot.GroupsByGid.TryGetValue(gid, out var groupInfo)
|
|
? groupInfo.GroupAliasOrGID
|
|
: gid;
|
|
|
|
var groupNote = _serverManager.GetNoteForGid(gid);
|
|
syncshellLines.Add(string.IsNullOrEmpty(groupNote) ? groupLabel : $"{groupNote} ({groupLabel})");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isSelfProfile)
|
|
statusLabel = "Online";
|
|
}
|
|
|
|
var presenceTokens = new List<PresenceToken>
|
|
{
|
|
new(statusLabel, string.Equals(statusLabel, "Offline", StringComparison.OrdinalIgnoreCase))
|
|
};
|
|
|
|
if (!string.IsNullOrEmpty(visiblePlayerName))
|
|
presenceTokens.Add(new PresenceToken(visiblePlayerName, false));
|
|
|
|
if (directPair)
|
|
{
|
|
presenceTokens.Add(new PresenceToken("Direct Pair", true));
|
|
if (youPaused)
|
|
presenceTokens.Add(new PresenceToken("You paused syncing", true));
|
|
if (theyPaused)
|
|
presenceTokens.Add(new PresenceToken("They paused syncing", true));
|
|
}
|
|
|
|
if (profile.IsNSFW)
|
|
presenceTokens.Add(new PresenceToken("NSFW", Emphasis: true));
|
|
|
|
if (syncshellLines.Count > 0)
|
|
presenceTokens.Add(new PresenceToken($"Sharing Syncshells ({syncshellLines.Count})", Emphasis: false, syncshellLines, "Shared Syncshells"));
|
|
|
|
var drawList = ImGui.GetWindowDrawList();
|
|
var style = ImGui.GetStyle();
|
|
var windowPos = ImGui.GetWindowPos();
|
|
var windowSize = ImGui.GetWindowSize();
|
|
var bannerHeight = 260f * scale;
|
|
var portraitSize = new Vector2(180f, 180f) * scale;
|
|
var portraitBorder = 1.5f * scale;
|
|
var portraitRounding = 1f * scale;
|
|
var portraitFrameSize = portraitSize + new Vector2(portraitBorder * 2f);
|
|
var portraitOverlap = portraitSize.Y * 0.35f;
|
|
var portraitOffsetX = 35f * scale;
|
|
var infoOffsetX = portraitOffsetX + portraitFrameSize.X + style.ItemSpacing.X * 2f;
|
|
var bannerTexture = GetBannerTexture(_lastBannerPicture) ?? _textureWrap ?? _supporterTextureWrap;
|
|
|
|
string defaultSubtitle = !_isLightfinderContext && Pair != null && !string.IsNullOrEmpty(Pair.UserData.Alias)
|
|
? Pair.UserData.Alias!
|
|
: _isLightfinderContext ? "Lightfinder Session" : noteText ?? string.Empty;
|
|
|
|
bool hasVanityAlias = !_isLightfinderContext && userData.HasVanity && !string.IsNullOrWhiteSpace(userData.Alias);
|
|
Vector4? vanityTextColor = null;
|
|
Vector4? vanityGlowColor = null;
|
|
|
|
if (hasVanityAlias)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(userData.TextColorHex))
|
|
vanityTextColor = UIColors.HexToRgba(userData.TextColorHex);
|
|
|
|
if (!string.IsNullOrWhiteSpace(userData.TextGlowColorHex))
|
|
vanityGlowColor = UIColors.HexToRgba(userData.TextGlowColorHex);
|
|
}
|
|
|
|
bool useVanityColors = vanityTextColor.HasValue || vanityGlowColor.HasValue;
|
|
string primaryHeaderText = _isLightfinderContext
|
|
? _lightfinderDisplayName
|
|
: hasVanityAlias ? userData.Alias! : userData.UID;
|
|
|
|
List<(string Text, bool UseVanityColor, bool Disabled)> secondaryHeaderLines = new();
|
|
if (!_isLightfinderContext && hasVanityAlias)
|
|
{
|
|
secondaryHeaderLines.Add((userData.UID, useVanityColors, false));
|
|
|
|
if (!string.IsNullOrEmpty(defaultSubtitle)
|
|
&& !string.Equals(defaultSubtitle, userData.UID, StringComparison.OrdinalIgnoreCase)
|
|
&& !string.Equals(defaultSubtitle, userData.Alias, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
secondaryHeaderLines.Add((defaultSubtitle, false, true));
|
|
}
|
|
}
|
|
else if (!string.IsNullOrEmpty(defaultSubtitle)
|
|
&& !string.Equals(defaultSubtitle, userData.UID, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
secondaryHeaderLines.Add((defaultSubtitle, false, true));
|
|
}
|
|
|
|
var bannerScrollOffset = new Vector2(ImGui.GetScrollX(), ImGui.GetScrollY());
|
|
var bannerMin = windowPos - bannerScrollOffset;
|
|
var bannerMax = bannerMin + new Vector2(windowSize.X, bannerHeight);
|
|
|
|
if (bannerTexture != null)
|
|
{
|
|
drawList.AddImage(
|
|
bannerTexture.Handle,
|
|
bannerMin,
|
|
bannerMax);
|
|
}
|
|
else
|
|
{
|
|
var headerBase = ImGui.ColorConvertU32ToFloat4(ImGui.GetColorU32(ImGuiCol.Header));
|
|
var topColor = ResolveThemeColor("ProfileBodyGradientTop", Vector4.Lerp(headerBase, Vector4.One, 0.25f));
|
|
topColor.W = 1f;
|
|
var bottomColor = ResolveThemeColor("ProfileBodyGradientBottom", Vector4.Lerp(headerBase, Vector4.Zero, 0.35f));
|
|
bottomColor.W = 1f;
|
|
|
|
drawList.AddRectFilledMultiColor(
|
|
bannerMin,
|
|
bannerMax,
|
|
ImGui.ColorConvertFloat4ToU32(topColor),
|
|
ImGui.ColorConvertFloat4ToU32(topColor),
|
|
ImGui.ColorConvertFloat4ToU32(bottomColor),
|
|
ImGui.ColorConvertFloat4ToU32(bottomColor));
|
|
}
|
|
|
|
if (_supporterTextureWrap != null)
|
|
{
|
|
const float iconBaseSize = 40f;
|
|
var iconPadding = new Vector2(style.WindowPadding.X + 18f * scale, style.WindowPadding.Y + 18f * scale);
|
|
var textureWidth = MathF.Max(1f, _supporterTextureWrap.Width);
|
|
var textureHeight = MathF.Max(1f, _supporterTextureWrap.Height);
|
|
var textureMaxEdge = MathF.Max(textureWidth, textureHeight);
|
|
var iconScale = (iconBaseSize * scale) / textureMaxEdge;
|
|
var iconSize = new Vector2(textureWidth * iconScale, textureHeight * iconScale);
|
|
var iconMax = bannerMax - iconPadding;
|
|
var iconMin = iconMax - iconSize;
|
|
var backgroundPadding = 6f * scale;
|
|
var iconBackgroundMin = iconMin - new Vector2(backgroundPadding);
|
|
var iconBackgroundMax = iconMax + new Vector2(backgroundPadding);
|
|
var backgroundColor = new Vector4(0f, 0f, 0f, 0.65f);
|
|
var cornerRadius = MathF.Max(4f * scale, iconSize.Y * 0.25f);
|
|
|
|
drawList.AddRectFilled(iconBackgroundMin, iconBackgroundMax, ImGui.GetColorU32(backgroundColor), cornerRadius);
|
|
drawList.AddImage(_supporterTextureWrap.Handle, iconMin, iconMax);
|
|
}
|
|
|
|
var contentStartY = MathF.Max(style.WindowPadding.Y, bannerHeight - portraitOverlap);
|
|
var topAreaStart = ImGui.GetCursorPos();
|
|
|
|
var portraitBackgroundPadding = 12f * scale;
|
|
var portraitAreaSize = portraitFrameSize + new Vector2(portraitBackgroundPadding * 2f);
|
|
var portraitAreaPos = new Vector2(style.WindowPadding.X + portraitOffsetX - portraitBackgroundPadding, contentStartY - portraitBackgroundPadding - 24f * scale);
|
|
|
|
ImGui.SetCursorPos(portraitAreaPos);
|
|
var portraitAreaScreenPos = ImGui.GetCursorScreenPos();
|
|
ImGui.Dummy(portraitAreaSize);
|
|
|
|
var portraitAreaMin = portraitAreaScreenPos;
|
|
var portraitAreaMax = portraitAreaMin + portraitAreaSize;
|
|
var portraitFrameMin = portraitAreaMin + new Vector2(portraitBackgroundPadding);
|
|
var portraitFrameMax = portraitFrameMin + portraitFrameSize;
|
|
|
|
var portraitAreaColor = style.Colors[(int)ImGuiCol.WindowBg];
|
|
portraitAreaColor.W = MathF.Min(1f, portraitAreaColor.W + 0.2f);
|
|
drawList.AddRectFilled(portraitAreaMin, portraitAreaMax, ImGui.GetColorU32(portraitAreaColor), portraitRounding + portraitBorder + portraitBackgroundPadding);
|
|
|
|
var portraitFrameBorder = style.Colors[(int)ImGuiCol.Border];
|
|
|
|
if (_textureWrap != null)
|
|
{
|
|
drawList.AddImageRounded(
|
|
_textureWrap.Handle,
|
|
portraitFrameMin + new Vector2(portraitBorder, portraitBorder),
|
|
portraitFrameMax - new Vector2(portraitBorder, portraitBorder),
|
|
Vector2.Zero,
|
|
Vector2.One,
|
|
0xFFFFFFFF,
|
|
portraitRounding);
|
|
}
|
|
else
|
|
{
|
|
drawList.AddRect(
|
|
portraitFrameMin + new Vector2(portraitBorder, portraitBorder),
|
|
portraitFrameMax - new Vector2(portraitBorder, portraitBorder),
|
|
ImGui.GetColorU32(portraitFrameBorder),
|
|
portraitRounding);
|
|
}
|
|
|
|
var portraitAreaLocalMin = portraitAreaMin - windowPos;
|
|
var portraitAreaLocalMax = portraitAreaMax - windowPos;
|
|
var portraitFrameLocalMin = portraitFrameMin - windowPos;
|
|
var portraitFrameLocalMax = portraitFrameMax - windowPos;
|
|
var portraitBlockBottom = windowPos.Y + portraitAreaLocalMax.Y;
|
|
|
|
var infoStartY = MathF.Max(contentStartY, bannerHeight + style.WindowPadding.Y);
|
|
var aliasColumnX = infoOffsetX + 18f * scale;
|
|
ImGui.SetCursorPos(new Vector2(aliasColumnX, infoStartY));
|
|
|
|
ImGui.BeginGroup();
|
|
using (_uiSharedService.UidFont.Push())
|
|
{
|
|
if (useVanityColors)
|
|
{
|
|
var seString = SeStringUtils.BuildFormattedPlayerName(primaryHeaderText, vanityTextColor, vanityGlowColor);
|
|
SeStringUtils.RenderSeStringWithHitbox(seString, ImGui.GetCursorScreenPos(), ImGui.GetFont());
|
|
}
|
|
else
|
|
{
|
|
ImGui.TextUnformatted(primaryHeaderText);
|
|
}
|
|
}
|
|
|
|
foreach (var (text, useColor, disabled) in secondaryHeaderLines)
|
|
{
|
|
if (useColor && useVanityColors)
|
|
{
|
|
var seString = SeStringUtils.BuildFormattedPlayerName(text, vanityTextColor, vanityGlowColor);
|
|
SeStringUtils.RenderSeStringWithHitbox(seString, ImGui.GetCursorScreenPos(), ImGui.GetFont());
|
|
}
|
|
else
|
|
{
|
|
if (disabled)
|
|
ImGui.TextDisabled(text);
|
|
else
|
|
ImGui.TextUnformatted(text);
|
|
}
|
|
}
|
|
ImGui.EndGroup();
|
|
var namesEnd = ImGui.GetCursorPos();
|
|
|
|
var namesBlockBottom = windowPos.Y + namesEnd.Y;
|
|
var aliasGroupRectMin = ImGui.GetItemRectMin();
|
|
var aliasGroupRectMax = ImGui.GetItemRectMax();
|
|
var aliasGroupLocalMin = aliasGroupRectMin - windowPos;
|
|
var aliasGroupLocalMax = aliasGroupRectMax - windowPos;
|
|
|
|
var tagsStartLocal = new Vector2(aliasGroupLocalMax.X + style.ItemSpacing.X + 25f * scale, aliasGroupLocalMin.Y + style.FramePadding.Y + 2f * scale);
|
|
ImGui.SetCursorPos(tagsStartLocal);
|
|
RenderProfileTags(profileTags, scale);
|
|
var tagsEndLocal = ImGui.GetCursorPos();
|
|
var tagsBlockBottom = windowPos.Y + tagsEndLocal.Y;
|
|
var aliasBlockBottom = windowPos.Y + aliasGroupLocalMax.Y;
|
|
var aliasAndTagsBottomLocal = MathF.Max(aliasGroupLocalMax.Y, tagsEndLocal.Y);
|
|
var aliasAndTagsBlockBottom = MathF.Max(aliasBlockBottom, tagsBlockBottom);
|
|
|
|
var descriptionPreSpacing = style.ItemSpacing.Y * 1.35f;
|
|
var descriptionStartLocal = new Vector2(aliasColumnX, aliasAndTagsBottomLocal + descriptionPreSpacing);
|
|
var horizontalInset = style.ItemSpacing.X * 0.5f;
|
|
var descriptionSeparatorSpacing = style.ItemSpacing.Y * 0.5f;
|
|
var descriptionSeparatorThickness = MathF.Max(1f, scale);
|
|
var descriptionSeparatorStart = windowPos + new Vector2(aliasColumnX - horizontalInset, descriptionStartLocal.Y);
|
|
var descriptionSeparatorEnd = new Vector2(windowPos.X + windowSize.X - style.WindowPadding.X + horizontalInset, descriptionSeparatorStart.Y);
|
|
drawList.AddLine(descriptionSeparatorStart, descriptionSeparatorEnd, ImGui.GetColorU32(portraitFrameBorder), descriptionSeparatorThickness);
|
|
|
|
var descriptionContentStartLocal = descriptionStartLocal + new Vector2(0f, descriptionSeparatorThickness + descriptionSeparatorSpacing + style.FramePadding.Y * 0.75f);
|
|
ImGui.SetCursorPos(descriptionContentStartLocal);
|
|
ImGui.TextDisabled("Description");
|
|
ImGui.SetCursorPosX(aliasColumnX);
|
|
var descriptionRegionWidth = ImGui.GetContentRegionAvail().X;
|
|
if (descriptionRegionWidth <= 0f)
|
|
descriptionRegionWidth = 1f;
|
|
var measurementWrapWidth = MathF.Max(1f, descriptionRegionWidth - style.WindowPadding.X * 2f);
|
|
var hasDescription = !string.IsNullOrWhiteSpace(profile.Description);
|
|
float descriptionContentHeight;
|
|
float lineHeightWithSpacing;
|
|
using (_uiSharedService.GameFont.Push())
|
|
{
|
|
lineHeightWithSpacing = ImGui.GetTextLineHeightWithSpacing();
|
|
var measurementText = hasDescription
|
|
? NormalizeDescriptionForMeasurement(profile.Description!)
|
|
: UserDescriptionPlaceholder;
|
|
if (string.IsNullOrWhiteSpace(measurementText))
|
|
measurementText = UserDescriptionPlaceholder;
|
|
|
|
descriptionContentHeight = ImGui.CalcTextSize(measurementText, wrapWidth: measurementWrapWidth).Y;
|
|
if (descriptionContentHeight <= 0f)
|
|
descriptionContentHeight = lineHeightWithSpacing;
|
|
}
|
|
|
|
var maxDescriptionHeight = lineHeightWithSpacing * DescriptionMaxVisibleLines;
|
|
var descriptionChildHeight = Math.Clamp(descriptionContentHeight, lineHeightWithSpacing, maxDescriptionHeight);
|
|
|
|
RenderDescriptionChild(
|
|
"##StandaloneProfileDescription",
|
|
new Vector2(descriptionRegionWidth, descriptionChildHeight),
|
|
hasDescription ? profile.Description : null,
|
|
UserDescriptionPlaceholder);
|
|
|
|
var descriptionEndLocal = ImGui.GetCursorPos();
|
|
var descriptionBlockBottom = windowPos.Y + descriptionEndLocal.Y;
|
|
aliasAndTagsBottomLocal = MathF.Max(aliasAndTagsBottomLocal, descriptionEndLocal.Y);
|
|
aliasAndTagsBlockBottom = MathF.Max(aliasAndTagsBlockBottom, descriptionBlockBottom);
|
|
|
|
var presenceLabelSpacing = style.ItemSpacing.Y * 0.35f;
|
|
var presenceAnchorY = MathF.Max(portraitFrameLocalMax.Y, aliasGroupLocalMax.Y);
|
|
var presenceStartLocal = new Vector2(
|
|
portraitFrameLocalMin.X,
|
|
presenceAnchorY + presenceLabelSpacing);
|
|
ImGui.SetCursorPos(presenceStartLocal);
|
|
ImGui.TextDisabled("Presence");
|
|
ImGui.SetCursorPosX(portraitFrameLocalMin.X);
|
|
if (presenceTokens.Count > 0)
|
|
{
|
|
var presenceColumnWidth = MathF.Max(1f, aliasColumnX - portraitFrameLocalMin.X - style.ItemSpacing.X);
|
|
RenderPresenceTokens(presenceTokens, scale, presenceColumnWidth);
|
|
}
|
|
else
|
|
{
|
|
ImGui.SetCursorPosX(portraitFrameLocalMin.X);
|
|
ImGui.TextDisabled("-- No presence information --");
|
|
ImGui.SetCursorPosX(portraitFrameLocalMin.X);
|
|
ImGui.Dummy(new Vector2(0f, style.ItemSpacing.Y * 0.25f));
|
|
}
|
|
|
|
var presenceContentEnd = ImGui.GetCursorPos();
|
|
var separatorSpacing = style.ItemSpacing.Y * 0.2f;
|
|
var separatorThickness = MathF.Max(1f, scale);
|
|
var separatorStartLocal = new Vector2(portraitFrameLocalMin.X, presenceContentEnd.Y + separatorSpacing);
|
|
var separatorStart = windowPos + separatorStartLocal;
|
|
var separatorEnd = new Vector2(portraitFrameMax.X, separatorStart.Y);
|
|
drawList.AddLine(separatorStart, separatorEnd, ImGui.GetColorU32(portraitFrameBorder), separatorThickness);
|
|
var afterSeparatorLocal = separatorStartLocal + new Vector2(0f, separatorThickness + separatorSpacing * 0.75f);
|
|
|
|
var columnStartLocalY = afterSeparatorLocal.Y;
|
|
var leftColumnX = portraitFrameLocalMin.X;
|
|
var leftWrapPos = windowPos.X + aliasColumnX - style.ItemSpacing.X;
|
|
|
|
ImGui.SetCursorPos(new Vector2(leftColumnX, columnStartLocalY));
|
|
float leftColumnEndY = columnStartLocalY;
|
|
|
|
if (!string.IsNullOrEmpty(noteText))
|
|
{
|
|
ImGui.TextDisabled("Notes");
|
|
ImGui.SetCursorPosX(leftColumnX);
|
|
ImGui.PushTextWrapPos(leftWrapPos);
|
|
ImGui.TextUnformatted(noteText);
|
|
ImGui.PopTextWrapPos();
|
|
ImGui.SetCursorPos(new Vector2(leftColumnX, ImGui.GetCursorPosY() + style.ItemSpacing.Y * 0.5f));
|
|
leftColumnEndY = ImGui.GetCursorPosY();
|
|
}
|
|
|
|
leftColumnEndY = MathF.Max(leftColumnEndY, ImGui.GetCursorPosY());
|
|
|
|
var columnsBottomLocal = leftColumnEndY;
|
|
var columnsBottom = windowPos.Y + columnsBottomLocal;
|
|
var topAreaBase = windowPos.Y + topAreaStart.Y;
|
|
var contentBlockBottom = MathF.Max(columnsBottom, aliasAndTagsBlockBottom);
|
|
var leftBlockBottom = MathF.Max(portraitBlockBottom, contentBlockBottom);
|
|
var topAreaHeight = leftBlockBottom - topAreaBase;
|
|
if (topAreaHeight < 0f)
|
|
topAreaHeight = 0f;
|
|
|
|
ImGui.SetCursorPos(new Vector2(leftColumnX, topAreaStart.Y + topAreaHeight + style.ItemSpacing.Y));
|
|
|
|
var finalCursorY = ImGui.GetCursorPosY();
|
|
var paddingY = ImGui.GetStyle().WindowPadding.Y;
|
|
var computedHeight = finalCursorY + paddingY;
|
|
var adjustedHeight = Math.Clamp(computedHeight, minHeight, maxAllowedHeight);
|
|
_lastComputedWindowHeight = adjustedHeight;
|
|
|
|
var finalSize = new Vector2(baseWidth, adjustedHeight);
|
|
Size = finalSize;
|
|
ImGui.SetWindowSize(finalSize, ImGuiCond.Always);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Error during standalone profile draw");
|
|
}
|
|
}
|
|
|
|
private IDalamudTextureWrap? GetIconWrap(uint iconId)
|
|
{
|
|
try
|
|
{
|
|
if (_uiSharedService.TryGetIcon(iconId, out var wrap) && wrap != null)
|
|
return wrap;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogDebug(ex, "Failed to resolve icon {IconId} for profile tags", iconId);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void RenderDescriptionChild(
|
|
string childId,
|
|
Vector2 childSize,
|
|
string? description,
|
|
string placeholderText)
|
|
{
|
|
ImGui.PushStyleVar(ImGuiStyleVar.ChildBorderSize, 0f);
|
|
if (ImGui.BeginChild(childId, childSize, false))
|
|
{
|
|
using (_uiSharedService.GameFont.Push())
|
|
{
|
|
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X);
|
|
if (string.IsNullOrWhiteSpace(description))
|
|
{
|
|
ImGui.TextUnformatted(placeholderText);
|
|
}
|
|
else if (!SeStringUtils.TryRenderSeStringMarkupAtCursor(description))
|
|
{
|
|
ImGui.TextUnformatted(description);
|
|
}
|
|
ImGui.PopTextWrapPos();
|
|
}
|
|
}
|
|
ImGui.EndChild();
|
|
ImGui.PopStyleVar();
|
|
}
|
|
|
|
private void RenderProfileTags(IReadOnlyList<ProfileTagDefinition> tags, float scale)
|
|
{
|
|
if (tags.Count == 0)
|
|
{
|
|
ImGui.TextDisabled("-- No tags set --");
|
|
return;
|
|
}
|
|
|
|
var drawList = ImGui.GetWindowDrawList();
|
|
var style = ImGui.GetStyle();
|
|
var defaultTextColorU32 = ImGui.GetColorU32(ImGuiCol.Text);
|
|
|
|
var startLocal = ImGui.GetCursorPos();
|
|
var startScreen = ImGui.GetCursorScreenPos();
|
|
float availableWidth = ImGui.GetContentRegionAvail().X;
|
|
if (availableWidth <= 0f)
|
|
availableWidth = 1f;
|
|
|
|
float cursorX = startScreen.X;
|
|
float cursorY = startScreen.Y;
|
|
float rowHeight = 0f;
|
|
|
|
for (int i = 0; i < tags.Count; i++)
|
|
{
|
|
var tag = tags[i];
|
|
if (!tag.HasContent)
|
|
continue;
|
|
|
|
var tagSize = ProfileTagRenderer.MeasureTag(tag, scale, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _seResolvedSegments, GetIconWrap, _logger);
|
|
var tagWidth = tagSize.X;
|
|
var tagHeight = tagSize.Y;
|
|
|
|
if (cursorX > startScreen.X && cursorX + tagWidth > startScreen.X + availableWidth)
|
|
{
|
|
cursorX = startScreen.X;
|
|
cursorY += rowHeight + style.ItemSpacing.Y;
|
|
rowHeight = 0f;
|
|
}
|
|
|
|
var tagPos = new Vector2(cursorX, cursorY);
|
|
ImGui.SetCursorScreenPos(tagPos);
|
|
ImGui.InvisibleButton($"##profileTag_{i}", tagSize);
|
|
ProfileTagRenderer.RenderTag(tag, tagPos, scale, drawList, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _seResolvedSegments, GetIconWrap, _logger);
|
|
|
|
cursorX += tagWidth + style.ItemSpacing.X;
|
|
rowHeight = MathF.Max(rowHeight, tagHeight);
|
|
}
|
|
|
|
var totalHeight = (cursorY + rowHeight) - startScreen.Y;
|
|
if (totalHeight < 0f)
|
|
totalHeight = 0f;
|
|
|
|
ImGui.SetCursorPos(new Vector2(startLocal.X, startLocal.Y + totalHeight));
|
|
}
|
|
|
|
private void DrawGroupProfileWindow()
|
|
{
|
|
if (_groupData is null)
|
|
return;
|
|
|
|
var scale = ImGuiHelpers.GlobalScale;
|
|
var viewport = ImGui.GetMainViewport();
|
|
var linked = ProfileEditorLayoutCoordinator.IsActive(_groupData.GID);
|
|
var baseSize = ProfileEditorLayoutCoordinator.GetProfileSize(scale);
|
|
var baseWidth = baseSize.X;
|
|
var minHeight = baseSize.Y;
|
|
var maxAllowedHeight = minHeight * MaxHeightMultiplier;
|
|
var targetHeight = _lastComputedWindowHeight > 0f
|
|
? Math.Clamp(_lastComputedWindowHeight, minHeight, maxAllowedHeight)
|
|
: minHeight;
|
|
var desiredSize = new Vector2(baseWidth, targetHeight);
|
|
Size = desiredSize;
|
|
|
|
if (linked)
|
|
{
|
|
ProfileEditorLayoutCoordinator.EnsureAnchor(viewport.WorkPos, scale);
|
|
|
|
var currentPos = ImGui.GetWindowPos();
|
|
if (IsWindowBeingDragged())
|
|
ProfileEditorLayoutCoordinator.UpdateAnchorFromProfile(currentPos);
|
|
|
|
var desiredPos = ProfileEditorLayoutCoordinator.GetProfilePosition(scale);
|
|
if (!ProfileEditorLayoutCoordinator.NearlyEquals(currentPos, desiredPos))
|
|
ImGui.SetWindowPos(desiredPos, ImGuiCond.Always);
|
|
|
|
if (!ProfileEditorLayoutCoordinator.NearlyEquals(ImGui.GetWindowSize(), desiredSize))
|
|
ImGui.SetWindowSize(desiredSize, ImGuiCond.Always);
|
|
}
|
|
else
|
|
{
|
|
var defaultPosition = viewport.WorkPos + (new Vector2(50f, 70f) * scale);
|
|
ImGui.SetWindowPos(defaultPosition, ImGuiCond.FirstUseEver);
|
|
ImGui.SetWindowSize(desiredSize, ImGuiCond.Always);
|
|
}
|
|
|
|
var profile = _lightlessProfileManager.GetLightlessGroupProfile(_groupData);
|
|
IReadOnlyList<ProfileTagDefinition> profileTags = profile.Tags.Count > 0
|
|
? ProfileTagService.ResolveTags(profile.Tags)
|
|
: Array.Empty<ProfileTagDefinition>();
|
|
|
|
if (_textureWrap == null || !profile.ProfileImageData.Value.SequenceEqual(_lastProfilePicture))
|
|
{
|
|
_textureWrap?.Dispose();
|
|
_textureWrap = null;
|
|
_lastProfilePicture = profile.ProfileImageData.Value;
|
|
ResetBannerTexture();
|
|
if (_lastProfilePicture.Length > 0)
|
|
{
|
|
_textureWrap = _uiSharedService.LoadImage(_lastProfilePicture);
|
|
}
|
|
}
|
|
|
|
if (_supporterTextureWrap != null)
|
|
{
|
|
_supporterTextureWrap.Dispose();
|
|
_supporterTextureWrap = null;
|
|
}
|
|
_lastSupporterPicture = Array.Empty<byte>();
|
|
|
|
var bannerBytes = profile.BannerImageData.Value;
|
|
if (!_lastBannerPicture.SequenceEqual(bannerBytes))
|
|
{
|
|
ResetBannerTexture();
|
|
_lastBannerPicture = bannerBytes;
|
|
}
|
|
|
|
var noteText = _serverManager.GetNoteForGid(_groupData.GID);
|
|
|
|
var presenceTokens = new List<PresenceToken>
|
|
{
|
|
new(profile.IsDisabled ? "Disabled" : "Active", profile.IsDisabled)
|
|
};
|
|
|
|
if (profile.IsNsfw)
|
|
presenceTokens.Add(new PresenceToken("NSFW", Emphasis: true));
|
|
|
|
int memberCount = 0;
|
|
List<Pair>? groupMembers = null;
|
|
var snapshot = _pairUiService.GetSnapshot();
|
|
GroupFullInfoDto? groupInfo = null;
|
|
if (_groupData is not null && snapshot.GroupsByGid.TryGetValue(_groupData.GID, out var refreshedGroupInfo))
|
|
{
|
|
groupInfo = refreshedGroupInfo;
|
|
}
|
|
if (groupInfo is not null && snapshot.GroupPairs.TryGetValue(groupInfo, out var pairsForGroup))
|
|
{
|
|
groupMembers = pairsForGroup.ToList();
|
|
memberCount = groupMembers.Count;
|
|
}
|
|
else if (groupInfo?.GroupPairUserInfos is { Count: > 0 })
|
|
{
|
|
memberCount = groupInfo.GroupPairUserInfos.Count;
|
|
}
|
|
|
|
string memberLabel = memberCount == 1 ? "1 Member" : $"{memberCount} Members";
|
|
presenceTokens.Add(new PresenceToken(memberLabel, false));
|
|
|
|
if (groupInfo?.GroupPermissions.IsDisableInvites() ?? false)
|
|
{
|
|
presenceTokens.Add(new PresenceToken(
|
|
"Invites Locked",
|
|
true,
|
|
new[]
|
|
{
|
|
"New members cannot join while this lock is active."
|
|
},
|
|
"Syncshell Status"));
|
|
}
|
|
|
|
var drawList = ImGui.GetWindowDrawList();
|
|
var style = ImGui.GetStyle();
|
|
var windowPos = ImGui.GetWindowPos();
|
|
var windowSize = ImGui.GetWindowSize();
|
|
var bannerHeight = 260f * scale;
|
|
var portraitSize = new Vector2(180f, 180f) * scale;
|
|
var portraitBorder = 1.5f * scale;
|
|
var portraitRounding = 1f * scale;
|
|
var portraitFrameSize = portraitSize + new Vector2(portraitBorder * 2f);
|
|
var portraitOverlap = portraitSize.Y * 0.35f;
|
|
var portraitOffsetX = 35f * scale;
|
|
var infoOffsetX = portraitOffsetX + portraitFrameSize.X + style.ItemSpacing.X * 2f;
|
|
var bannerTexture = GetBannerTexture(_lastBannerPicture) ?? _textureWrap;
|
|
|
|
var bannerScrollOffset = new Vector2(ImGui.GetScrollX(), ImGui.GetScrollY());
|
|
var bannerMin = windowPos - bannerScrollOffset;
|
|
var bannerMax = bannerMin + new Vector2(windowSize.X, bannerHeight);
|
|
|
|
if (bannerTexture != null)
|
|
{
|
|
drawList.AddImage(
|
|
bannerTexture.Handle,
|
|
bannerMin,
|
|
bannerMax);
|
|
}
|
|
else
|
|
{
|
|
var headerBase = ImGui.ColorConvertU32ToFloat4(ImGui.GetColorU32(ImGuiCol.Header));
|
|
var topColor = ResolveThemeColor("ProfileBodyGradientTop", Vector4.Lerp(headerBase, Vector4.One, 0.25f));
|
|
topColor.W = 1f;
|
|
var bottomColor = ResolveThemeColor("ProfileBodyGradientBottom", Vector4.Lerp(headerBase, Vector4.Zero, 0.35f));
|
|
bottomColor.W = 1f;
|
|
|
|
drawList.AddRectFilledMultiColor(
|
|
bannerMin,
|
|
bannerMax,
|
|
ImGui.ColorConvertFloat4ToU32(topColor),
|
|
ImGui.ColorConvertFloat4ToU32(topColor),
|
|
ImGui.ColorConvertFloat4ToU32(bottomColor),
|
|
ImGui.ColorConvertFloat4ToU32(bottomColor));
|
|
}
|
|
|
|
var contentStartY = MathF.Max(style.WindowPadding.Y, bannerHeight - portraitOverlap);
|
|
var topAreaStart = ImGui.GetCursorPos();
|
|
|
|
var portraitBackgroundPadding = 12f * scale;
|
|
var portraitAreaSize = portraitFrameSize + new Vector2(portraitBackgroundPadding * 2f);
|
|
var portraitAreaPos = new Vector2(style.WindowPadding.X + portraitOffsetX - portraitBackgroundPadding, contentStartY - portraitBackgroundPadding - 24f * scale);
|
|
|
|
ImGui.SetCursorPos(portraitAreaPos);
|
|
var portraitAreaScreenPos = ImGui.GetCursorScreenPos();
|
|
ImGui.Dummy(portraitAreaSize);
|
|
var contentStart = ImGui.GetCursorPos();
|
|
|
|
var portraitAreaMin = portraitAreaScreenPos;
|
|
var portraitAreaMax = portraitAreaMin + portraitAreaSize;
|
|
var portraitFrameMin = portraitAreaMin + new Vector2(portraitBackgroundPadding);
|
|
var portraitFrameMax = portraitFrameMin + portraitFrameSize;
|
|
|
|
var portraitAreaColor = style.Colors[(int)ImGuiCol.WindowBg];
|
|
portraitAreaColor.W = MathF.Min(1f, portraitAreaColor.W + 0.2f);
|
|
drawList.AddRectFilled(portraitAreaMin, portraitAreaMax, ImGui.GetColorU32(portraitAreaColor), portraitRounding + portraitBorder + portraitBackgroundPadding);
|
|
var portraitFrameBorder = style.Colors[(int)ImGuiCol.Border];
|
|
|
|
if (_textureWrap != null)
|
|
{
|
|
drawList.AddImageRounded(
|
|
_textureWrap.Handle,
|
|
portraitFrameMin + new Vector2(portraitBorder, portraitBorder),
|
|
portraitFrameMax - new Vector2(portraitBorder, portraitBorder),
|
|
Vector2.Zero,
|
|
Vector2.One,
|
|
0xFFFFFFFF,
|
|
portraitRounding);
|
|
}
|
|
else
|
|
{
|
|
drawList.AddRect(
|
|
portraitFrameMin + new Vector2(portraitBorder, portraitBorder),
|
|
portraitFrameMax - new Vector2(portraitBorder, portraitBorder),
|
|
ImGui.GetColorU32(portraitFrameBorder),
|
|
portraitRounding);
|
|
}
|
|
|
|
drawList.AddRect(portraitFrameMin, portraitFrameMax, ImGui.GetColorU32(portraitFrameBorder), portraitRounding);
|
|
var portraitAreaLocalMax = portraitAreaMax - windowPos;
|
|
var portraitFrameLocalMin = portraitFrameMin - windowPos;
|
|
var portraitFrameLocalMax = portraitFrameMax - windowPos;
|
|
var portraitBlockBottom = windowPos.Y + portraitAreaLocalMax.Y;
|
|
|
|
ImGui.SetCursorPos(contentStart);
|
|
|
|
bool useVanityColors = false;
|
|
Vector4? vanityTextColor = null;
|
|
Vector4? vanityGlowColor = null;
|
|
string primaryHeaderText = _groupData.AliasOrGID;
|
|
|
|
List<(string Text, bool UseVanityColor, bool Disabled)> secondaryHeaderLines =
|
|
[
|
|
(_groupData.GID, false, true)
|
|
];
|
|
|
|
if (groupInfo is not null)
|
|
secondaryHeaderLines.Add(($"Owner: {groupInfo.Owner.AliasOrUID}", false, true));
|
|
else
|
|
secondaryHeaderLines.Add(($"Unknown Owner", false, true));
|
|
|
|
var infoStartY = MathF.Max(contentStartY, bannerHeight + style.WindowPadding.Y);
|
|
var aliasColumnX = infoOffsetX + 18f * scale;
|
|
ImGui.SetCursorPos(new Vector2(aliasColumnX, infoStartY));
|
|
|
|
ImGui.BeginGroup();
|
|
using (_uiSharedService.UidFont.Push())
|
|
{
|
|
ImGui.TextUnformatted(primaryHeaderText);
|
|
}
|
|
|
|
foreach (var (text, useColor, disabled) in secondaryHeaderLines)
|
|
{
|
|
if (useColor && useVanityColors)
|
|
{
|
|
var seString = SeStringUtils.BuildFormattedPlayerName(text, vanityTextColor, vanityGlowColor);
|
|
SeStringUtils.RenderSeStringWithHitbox(seString, ImGui.GetCursorScreenPos(), ImGui.GetFont());
|
|
}
|
|
else
|
|
{
|
|
if (disabled)
|
|
ImGui.TextDisabled(text);
|
|
else
|
|
ImGui.TextUnformatted(text);
|
|
}
|
|
}
|
|
ImGui.EndGroup();
|
|
var namesEnd = ImGui.GetCursorPos();
|
|
|
|
var aliasGroupRectMin = ImGui.GetItemRectMin();
|
|
var aliasGroupRectMax = ImGui.GetItemRectMax();
|
|
var aliasGroupLocalMin = aliasGroupRectMin - windowPos;
|
|
var aliasGroupLocalMax = aliasGroupRectMax - windowPos;
|
|
|
|
var tagsStartLocal = new Vector2(aliasGroupLocalMax.X + style.ItemSpacing.X + 25f * scale, aliasGroupLocalMin.Y + style.FramePadding.Y + 2f * scale);
|
|
ImGui.SetCursorPos(tagsStartLocal);
|
|
if (profileTags.Count > 0)
|
|
RenderProfileTags(profileTags, scale);
|
|
else
|
|
ImGui.TextDisabled("-- No tags set --");
|
|
var tagsEndLocal = ImGui.GetCursorPos();
|
|
var tagsBlockBottom = windowPos.Y + tagsEndLocal.Y;
|
|
var aliasBlockBottom = windowPos.Y + aliasGroupLocalMax.Y;
|
|
var aliasAndTagsBottomLocal = MathF.Max(aliasGroupLocalMax.Y, tagsEndLocal.Y);
|
|
var aliasAndTagsBlockBottom = MathF.Max(aliasBlockBottom, tagsBlockBottom);
|
|
|
|
var descriptionSeparatorSpacing = style.ItemSpacing.Y * 0.35f;
|
|
var descriptionSeparatorThickness = MathF.Max(1f, scale);
|
|
var descriptionExtraOffset = 0f;
|
|
if (groupInfo?.Owner is not null)
|
|
descriptionExtraOffset = style.ItemSpacing.Y * 0.6f;
|
|
var descriptionStartLocal = new Vector2(aliasColumnX, aliasAndTagsBottomLocal + descriptionSeparatorSpacing + descriptionExtraOffset);
|
|
var horizontalInset = style.ItemSpacing.X * 0.5f;
|
|
var descriptionSeparatorStart = windowPos + new Vector2(aliasColumnX - horizontalInset, descriptionStartLocal.Y);
|
|
var descriptionSeparatorEnd = new Vector2(windowPos.X + windowSize.X - style.WindowPadding.X + horizontalInset, descriptionSeparatorStart.Y);
|
|
drawList.AddLine(descriptionSeparatorStart, descriptionSeparatorEnd, ImGui.GetColorU32(portraitFrameBorder), descriptionSeparatorThickness);
|
|
|
|
var descriptionContentStartLocal = new Vector2(aliasColumnX, descriptionStartLocal.Y + descriptionSeparatorThickness + descriptionSeparatorSpacing + style.FramePadding.Y * 0.75f);
|
|
ImGui.SetCursorPos(descriptionContentStartLocal);
|
|
ImGui.TextDisabled("Description");
|
|
ImGui.SetCursorPosX(aliasColumnX);
|
|
var descriptionRegionWidth = ImGui.GetContentRegionAvail().X;
|
|
if (descriptionRegionWidth <= 0f)
|
|
descriptionRegionWidth = 1f;
|
|
var measurementWrapWidth = MathF.Max(1f, descriptionRegionWidth - style.WindowPadding.X * 2f);
|
|
var hasDescription = !string.IsNullOrWhiteSpace(profile.Description);
|
|
float descriptionContentHeight;
|
|
float lineHeightWithSpacing;
|
|
using (_uiSharedService.GameFont.Push())
|
|
{
|
|
lineHeightWithSpacing = ImGui.GetTextLineHeightWithSpacing();
|
|
var measurementText = hasDescription
|
|
? NormalizeDescriptionForMeasurement(profile.Description!)
|
|
: GroupDescriptionPlaceholder;
|
|
if (string.IsNullOrWhiteSpace(measurementText))
|
|
measurementText = GroupDescriptionPlaceholder;
|
|
|
|
descriptionContentHeight = ImGui.CalcTextSize(measurementText, wrapWidth: measurementWrapWidth).Y;
|
|
if (descriptionContentHeight <= 0f)
|
|
descriptionContentHeight = lineHeightWithSpacing;
|
|
}
|
|
|
|
var maxDescriptionHeight = lineHeightWithSpacing * DescriptionMaxVisibleLines;
|
|
var descriptionChildHeight = Math.Clamp(descriptionContentHeight, lineHeightWithSpacing, maxDescriptionHeight);
|
|
|
|
RenderDescriptionChild(
|
|
"##StandaloneGroupDescription",
|
|
new Vector2(descriptionRegionWidth, descriptionChildHeight),
|
|
hasDescription ? profile.Description : null,
|
|
GroupDescriptionPlaceholder);
|
|
var descriptionEndLocal = ImGui.GetCursorPos();
|
|
var descriptionBlockBottom = windowPos.Y + descriptionEndLocal.Y;
|
|
aliasAndTagsBottomLocal = MathF.Max(aliasAndTagsBottomLocal, descriptionEndLocal.Y);
|
|
aliasAndTagsBlockBottom = MathF.Max(aliasAndTagsBlockBottom, descriptionBlockBottom);
|
|
|
|
var presenceLabelSpacing = style.ItemSpacing.Y * 0.35f;
|
|
var presenceAnchorY = MathF.Max(portraitFrameLocalMax.Y, aliasGroupLocalMax.Y);
|
|
var presenceStartLocal = new Vector2(portraitFrameLocalMin.X, presenceAnchorY + presenceLabelSpacing);
|
|
ImGui.SetCursorPos(presenceStartLocal);
|
|
ImGui.TextDisabled("Presence");
|
|
ImGui.SetCursorPosX(portraitFrameLocalMin.X);
|
|
if (presenceTokens.Count > 0)
|
|
{
|
|
var presenceColumnWidth = MathF.Max(1f, aliasColumnX - portraitFrameLocalMin.X - style.ItemSpacing.X);
|
|
RenderPresenceTokens(presenceTokens, scale, presenceColumnWidth);
|
|
}
|
|
else
|
|
{
|
|
ImGui.TextDisabled("-- No status flags --");
|
|
ImGui.Dummy(new Vector2(0f, style.ItemSpacing.Y * 0.25f));
|
|
}
|
|
|
|
var presenceContentEnd = ImGui.GetCursorPos();
|
|
var separatorSpacing = style.ItemSpacing.Y * 0.2f;
|
|
var separatorThickness = MathF.Max(1f, scale);
|
|
var separatorStartLocal = new Vector2(portraitFrameLocalMin.X, presenceContentEnd.Y + separatorSpacing);
|
|
var separatorStart = windowPos + separatorStartLocal;
|
|
var separatorEnd = new Vector2(portraitFrameMax.X, separatorStart.Y);
|
|
drawList.AddLine(separatorStart, separatorEnd, ImGui.GetColorU32(portraitFrameBorder), separatorThickness);
|
|
var afterSeparatorLocal = separatorStartLocal + new Vector2(0f, separatorThickness + separatorSpacing * 0.75f);
|
|
|
|
var columnStartLocalY = afterSeparatorLocal.Y;
|
|
var leftColumnX = portraitFrameLocalMin.X;
|
|
ImGui.SetCursorPos(new Vector2(leftColumnX, columnStartLocalY));
|
|
float leftColumnEndY = columnStartLocalY;
|
|
|
|
if (!string.IsNullOrEmpty(noteText))
|
|
{
|
|
ImGui.TextDisabled("Notes");
|
|
ImGui.SetCursorPosX(leftColumnX);
|
|
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X);
|
|
ImGui.TextUnformatted(noteText);
|
|
ImGui.PopTextWrapPos();
|
|
ImGui.SetCursorPos(new Vector2(leftColumnX, ImGui.GetCursorPosY() + style.ItemSpacing.Y * 0.5f));
|
|
leftColumnEndY = ImGui.GetCursorPosY();
|
|
}
|
|
|
|
leftColumnEndY = MathF.Max(leftColumnEndY, ImGui.GetCursorPosY());
|
|
|
|
var columnsBottomLocal = leftColumnEndY;
|
|
var columnsBottom = windowPos.Y + columnsBottomLocal;
|
|
var topAreaBase = windowPos.Y + topAreaStart.Y;
|
|
var contentBlockBottom = MathF.Max(columnsBottom, aliasAndTagsBlockBottom);
|
|
var leftBlockBottom = MathF.Max(portraitBlockBottom, contentBlockBottom);
|
|
var topAreaHeight = leftBlockBottom - topAreaBase;
|
|
if (topAreaHeight < 0f)
|
|
topAreaHeight = 0f;
|
|
|
|
ImGui.SetCursorPos(new Vector2(leftColumnX, topAreaStart.Y + topAreaHeight + style.ItemSpacing.Y));
|
|
|
|
var finalCursorY = ImGui.GetCursorPosY();
|
|
var paddingY = ImGui.GetStyle().WindowPadding.Y;
|
|
var computedHeight = finalCursorY + paddingY;
|
|
var adjustedHeight = Math.Clamp(computedHeight, minHeight, maxAllowedHeight);
|
|
_lastComputedWindowHeight = adjustedHeight;
|
|
|
|
var finalSize = new Vector2(baseWidth, adjustedHeight);
|
|
Size = finalSize;
|
|
ImGui.SetWindowSize(finalSize, ImGuiCond.Always);
|
|
}
|
|
|
|
private IDalamudTextureWrap? GetBannerTexture(byte[] bannerBytes)
|
|
{
|
|
if (_bannerTextureLoaded)
|
|
return _bannerTextureWrap;
|
|
|
|
_bannerTextureLoaded = true;
|
|
|
|
if (bannerBytes.Length == 0)
|
|
return null;
|
|
|
|
_bannerTextureWrap = _uiSharedService.LoadImage(bannerBytes);
|
|
return _bannerTextureWrap;
|
|
}
|
|
|
|
private void ResetBannerTexture()
|
|
{
|
|
_bannerTextureWrap?.Dispose();
|
|
_bannerTextureWrap = null;
|
|
_lastBannerPicture = [];
|
|
_bannerTextureLoaded = false;
|
|
}
|
|
|
|
private static bool IsWindowBeingDragged()
|
|
{
|
|
return ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows) && ImGui.GetIO().MouseDown[0];
|
|
}
|
|
|
|
private static string NormalizeDescriptionForMeasurement(string description)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(description))
|
|
return string.Empty;
|
|
|
|
var normalized = description.ReplaceLineEndings("\n");
|
|
normalized = normalized
|
|
.Replace("<br>", "\n", StringComparison.OrdinalIgnoreCase)
|
|
.Replace("<br/>", "\n", StringComparison.OrdinalIgnoreCase)
|
|
.Replace("<br />", "\n", StringComparison.OrdinalIgnoreCase)
|
|
.Replace("</br>", "\n", StringComparison.OrdinalIgnoreCase);
|
|
|
|
return SeStringUtils.StripMarkup(normalized);
|
|
}
|
|
private static void RenderPresenceTokens(IReadOnlyList<PresenceToken> tokens, float scale, float? maxWidth = null)
|
|
{
|
|
if (tokens.Count == 0)
|
|
return;
|
|
|
|
var drawList = ImGui.GetWindowDrawList();
|
|
var style = ImGui.GetStyle();
|
|
|
|
var startPos = ImGui.GetCursorPos();
|
|
float startX = startPos.X;
|
|
float cursorX = startX;
|
|
float cursorY = startPos.Y;
|
|
float availWidth = maxWidth ?? ImGui.GetContentRegionAvail().X;
|
|
if (availWidth <= 0f)
|
|
availWidth = ImGui.GetContentRegionAvail().X;
|
|
if (availWidth <= 0f)
|
|
availWidth = 1f;
|
|
float spacingX = style.ItemSpacing.X;
|
|
float spacingY = style.ItemSpacing.Y;
|
|
float rounding = style.FrameRounding > 0f ? style.FrameRounding : 6f * scale;
|
|
|
|
var padding = new Vector2(8f * scale, 4f * scale);
|
|
var baseColor = new Vector4(0.16f, 0.16f, 0.16f, 0.95f);
|
|
var baseBorder = style.Colors[(int)ImGuiCol.Border];
|
|
baseBorder.W *= 0.35f;
|
|
var alertColor = new Vector4(0.32f, 0.2f, 0.2f, 0.95f);
|
|
var alertBorder = Vector4.Lerp(baseBorder, new Vector4(0.9f, 0.5f, 0.5f, baseBorder.W), 0.6f);
|
|
var textColor = style.Colors[(int)ImGuiCol.Text];
|
|
|
|
float rowHeight = 0f;
|
|
|
|
for (int i = 0; i < tokens.Count; i++)
|
|
{
|
|
var token = tokens[i];
|
|
var textSize = ImGui.CalcTextSize(token.Text);
|
|
var tagSize = textSize + padding * 2f;
|
|
|
|
if (cursorX > startX && cursorX + tagSize.X > startX + availWidth)
|
|
{
|
|
cursorX = startX;
|
|
cursorY += rowHeight + spacingY;
|
|
rowHeight = 0f;
|
|
}
|
|
|
|
ImGui.SetCursorPos(new Vector2(cursorX, cursorY));
|
|
ImGui.InvisibleButton($"##presenceTag_{i}", tagSize);
|
|
|
|
var tagMin = ImGui.GetItemRectMin();
|
|
var tagMax = ImGui.GetItemRectMax();
|
|
|
|
var fillColor = token.Emphasis ? alertColor : baseColor;
|
|
var borderColor = token.Emphasis ? alertBorder : baseBorder;
|
|
|
|
drawList.AddRectFilled(tagMin, tagMax, ImGui.GetColorU32(fillColor), rounding);
|
|
drawList.AddRect(tagMin, tagMax, ImGui.GetColorU32(borderColor), rounding);
|
|
drawList.AddText(tagMin + padding, ImGui.GetColorU32(textColor), token.Text);
|
|
|
|
if (token.Tooltip is { Count: > 0 })
|
|
{
|
|
if (ImGui.IsItemHovered())
|
|
{
|
|
ImGui.BeginTooltip();
|
|
if (!string.IsNullOrEmpty(token.TooltipTitle))
|
|
{
|
|
ImGui.TextUnformatted(token.TooltipTitle);
|
|
ImGui.Separator();
|
|
}
|
|
|
|
foreach (var line in token.Tooltip)
|
|
ImGui.TextUnformatted(line);
|
|
ImGui.EndTooltip();
|
|
}
|
|
}
|
|
|
|
cursorX += tagSize.X + spacingX;
|
|
rowHeight = MathF.Max(rowHeight, tagSize.Y);
|
|
}
|
|
|
|
ImGui.SetCursorPos(new Vector2(startX, cursorY + rowHeight));
|
|
ImGui.Dummy(new Vector2(0f, spacingY * 0.25f));
|
|
}
|
|
|
|
public override void OnClose()
|
|
{
|
|
if (!_isGroupProfile
|
|
&& !_isLightfinderContext
|
|
&& Pair is null
|
|
&& _userData is not null
|
|
&& ProfileEditorLayoutCoordinator.IsActive(_userData.UID))
|
|
{
|
|
ProfileEditorLayoutCoordinator.Disable(_userData.UID);
|
|
}
|
|
else if (_isGroupProfile
|
|
&& _groupData is not null
|
|
&& ProfileEditorLayoutCoordinator.IsActive(_groupData.GID))
|
|
{
|
|
ProfileEditorLayoutCoordinator.Disable(_groupData.GID);
|
|
}
|
|
Mediator.Publish(new RemoveWindowMessage(this));
|
|
}
|
|
|
|
private readonly record struct PresenceToken(
|
|
string Text,
|
|
bool Emphasis,
|
|
IReadOnlyList<string>? Tooltip = null,
|
|
string? TooltipTitle = null);
|
|
}
|