using System; using System.Numerics; using System.Text; using System.Threading.Tasks; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using MareSynchronos.API.Data; using MareSynchronos.API.Data.Enum; using MareSynchronos.API.Data.Extensions; using MareSynchronos.API.Dto.Group; using MareSynchronos.PlayerData.Pairs; using MareSynchronos.Services; using MareSynchronos.Services.AutoDetect; using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.UI.Handlers; using MareSynchronos.WebAPI; namespace MareSynchronos.UI.Components; public class DrawGroupPair : DrawPairBase { protected readonly MareMediator _mediator; private readonly GroupPairFullInfoDto _fullInfoDto; private readonly GroupFullInfoDto _group; private readonly CharaDataManager _charaDataManager; private readonly AutoDetectRequestService _autoDetectRequestService; private readonly ServerConfigurationManager _serverConfigurationManager; private const string ManualPairInvitePrefix = "[UmbraPairInvite|"; public DrawGroupPair(string id, Pair entry, ApiController apiController, MareMediator mareMediator, GroupFullInfoDto group, GroupPairFullInfoDto fullInfoDto, UidDisplayHandler handler, UiSharedService uiSharedService, CharaDataManager charaDataManager, AutoDetectRequestService autoDetectRequestService, ServerConfigurationManager serverConfigurationManager) : base(id, entry, apiController, handler, uiSharedService) { _group = group; _fullInfoDto = fullInfoDto; _mediator = mareMediator; _charaDataManager = charaDataManager; _autoDetectRequestService = autoDetectRequestService; _serverConfigurationManager = serverConfigurationManager; } protected override void DrawLeftSide(float textPosY, float originalY) { var entryUID = _pair.UserData.AliasOrUID; var entryIsMod = _fullInfoDto.GroupPairStatusInfo.IsModerator(); var entryIsOwner = string.Equals(_pair.UserData.UID, _group.OwnerUID, StringComparison.Ordinal); var entryIsPinned = _fullInfoDto.GroupPairStatusInfo.IsPinned(); var presenceIcon = _pair.IsVisible ? FontAwesomeIcon.Eye : FontAwesomeIcon.CloudMoon; var presenceColor = (_pair.IsOnline || _pair.IsVisible) ? new Vector4(0.63f, 0.25f, 1f, 1f) : ImGuiColors.DalamudGrey; var presenceText = entryUID + " is offline"; ImGui.SetCursorPosY(textPosY); bool drewPrefixIcon = false; if (_pair.IsPaused) { presenceText = entryUID + " online status is unknown (paused)"; ImGui.PushFont(UiBuilder.IconFont); UiSharedService.ColorText(FontAwesomeIcon.PauseCircle.ToIconString(), ImGuiColors.DalamudYellow); ImGui.PopFont(); UiSharedService.AttachToolTip("Pairing status with " + entryUID + " is paused"); drewPrefixIcon = true; } else { bool individuallyPaired = _pair.UserPair != null; var violet = new Vector4(0.63f, 0.25f, 1f, 1f); if (individuallyPaired && (_pair.IsOnline || _pair.IsVisible)) { ImGui.PushFont(UiBuilder.IconFont); UiSharedService.ColorText(FontAwesomeIcon.Moon.ToIconString(), violet); ImGui.PopFont(); UiSharedService.AttachToolTip("You are individually paired with " + entryUID); drewPrefixIcon = true; } } if (drewPrefixIcon) ImGui.SameLine(); ImGui.SetCursorPosY(textPosY); ImGui.PushFont(UiBuilder.IconFont); UiSharedService.ColorText(presenceIcon.ToIconString(), presenceColor); ImGui.PopFont(); if (_pair.IsOnline && !_pair.IsVisible) presenceText = entryUID + " is online"; else if (_pair.IsOnline && _pair.IsVisible) presenceText = entryUID + " is visible: " + _pair.PlayerName + Environment.NewLine + "Click to target this player"; if (_pair.IsVisible) { if (ImGui.IsItemClicked()) { _mediator.Publish(new TargetPairMessage(_pair)); } if (_pair.LastAppliedDataBytes >= 0) { presenceText += UiSharedService.TooltipSeparator; presenceText += ((!_pair.IsVisible) ? "(Last) " : string.Empty) + "Mods Info" + Environment.NewLine; presenceText += "Files Size: " + UiSharedService.ByteToString(_pair.LastAppliedDataBytes, true); if (_pair.LastAppliedApproximateVRAMBytes >= 0) { presenceText += Environment.NewLine + "Approx. VRAM Usage: " + UiSharedService.ByteToString(_pair.LastAppliedApproximateVRAMBytes, true); } if (_pair.LastAppliedDataTris >= 0) { presenceText += Environment.NewLine + "Triangle Count (excl. Vanilla): " + (_pair.LastAppliedDataTris > 1000 ? (_pair.LastAppliedDataTris / 1000d).ToString("0.0'k'") : _pair.LastAppliedDataTris); } } } UiSharedService.AttachToolTip(presenceText); if (entryIsOwner) { ImGui.SameLine(); ImGui.SetCursorPosY(textPosY); ImGui.PushFont(UiBuilder.IconFont); ImGui.TextUnformatted(FontAwesomeIcon.Crown.ToIconString()); ImGui.PopFont(); UiSharedService.AttachToolTip("User is owner of this Syncshell"); } else if (entryIsMod) { ImGui.SameLine(); ImGui.SetCursorPosY(textPosY); ImGui.PushFont(UiBuilder.IconFont); ImGui.TextUnformatted(FontAwesomeIcon.UserShield.ToIconString()); ImGui.PopFont(); UiSharedService.AttachToolTip("User is moderator of this Syncshell"); } else if (entryIsPinned) { ImGui.SameLine(); ImGui.SetCursorPosY(textPosY); ImGui.PushFont(UiBuilder.IconFont); ImGui.TextUnformatted(FontAwesomeIcon.Thumbtack.ToIconString()); ImGui.PopFont(); UiSharedService.AttachToolTip("User is pinned in this Syncshell"); } } protected override float DrawRightSide(float textPosY, float originalY) { var entryUID = _fullInfoDto.UserAliasOrUID; var entryIsMod = _fullInfoDto.GroupPairStatusInfo.IsModerator(); var entryIsOwner = string.Equals(_pair.UserData.UID, _group.OwnerUID, StringComparison.Ordinal); var entryIsPinned = _fullInfoDto.GroupPairStatusInfo.IsPinned(); var userIsOwner = string.Equals(_group.OwnerUID, _apiController.UID, StringComparison.OrdinalIgnoreCase); var userIsModerator = _group.GroupUserInfo.IsModerator(); var soundsDisabled = _fullInfoDto.GroupUserPermissions.IsDisableSounds(); var animDisabled = _fullInfoDto.GroupUserPermissions.IsDisableAnimations(); var vfxDisabled = _fullInfoDto.GroupUserPermissions.IsDisableVFX(); var individualSoundsDisabled = (_pair.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableSounds() ?? false); var individualAnimDisabled = (_pair.UserPair?.OwnPermissions.IsDisableAnimations() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableAnimations() ?? false); var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false); bool showShared = _charaDataManager.SharedWithYouData.TryGetValue(_pair.UserData, out var sharedData); bool showInfo = (individualAnimDisabled || individualSoundsDisabled || animDisabled || soundsDisabled); bool showPlus = _pair.UserPair == null && _pair.IsOnline; bool showBars = (userIsOwner || (userIsModerator && !entryIsMod && !entryIsOwner)) || !_pair.IsPaused; bool showPause = true; var spacing = ImGui.GetStyle().ItemSpacing.X; var permIcon = (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled) ? FontAwesomeIcon.ExclamationTriangle : ((soundsDisabled || animDisabled || vfxDisabled) ? FontAwesomeIcon.InfoCircle : FontAwesomeIcon.None); var runningIconWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Running).X; var infoIconWidth = UiSharedService.GetIconSize(permIcon).X; var plusButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus).X; var pauseIcon = _fullInfoDto.GroupUserPermissions.IsPaused() ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; var pauseButtonWidth = _uiSharedService.GetIconButtonSize(pauseIcon).X; var barButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X; var pos = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() + spacing - (showShared ? (runningIconWidth + spacing) : 0) - (showInfo ? (infoIconWidth + spacing) : 0) - (showPlus ? (plusButtonWidth + spacing) : 0) - (showPause ? (pauseButtonWidth + spacing) : 0) - (showBars ? (barButtonWidth + spacing) : 0); ImGui.SameLine(pos); if (showShared) { _uiSharedService.IconText(FontAwesomeIcon.Running); UiSharedService.AttachToolTip($"This user has shared {sharedData!.Count} Character Data Sets with you." + UiSharedService.TooltipSeparator + "Click to open the Character Data Hub and show the entries."); if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) { _mediator.Publish(new OpenCharaDataHubWithFilterMessage(_pair.UserData)); } ImGui.SameLine(); } if (individualAnimDisabled || individualSoundsDisabled) { ImGui.SetCursorPosY(textPosY); ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudYellow); _uiSharedService.IconText(permIcon); ImGui.PopStyleColor(); if (ImGui.IsItemHovered()) { ImGui.BeginTooltip(); ImGui.TextUnformatted("Individual User permissions"); if (individualSoundsDisabled) { var userSoundsText = "Sound sync disabled with " + _pair.UserData.AliasOrUID; _uiSharedService.IconText(FontAwesomeIcon.VolumeMute); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); ImGui.TextUnformatted(userSoundsText); ImGui.NewLine(); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); ImGui.TextUnformatted("You: " + (_pair.UserPair!.OwnPermissions.IsDisableSounds() ? "Disabled" : "Enabled") + ", They: " + (_pair.UserPair!.OtherPermissions.IsDisableSounds() ? "Disabled" : "Enabled")); } if (individualAnimDisabled) { var userAnimText = "Animation sync disabled with " + _pair.UserData.AliasOrUID; _uiSharedService.IconText(FontAwesomeIcon.WindowClose); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); ImGui.TextUnformatted(userAnimText); ImGui.NewLine(); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); ImGui.TextUnformatted("You: " + (_pair.UserPair!.OwnPermissions.IsDisableAnimations() ? "Disabled" : "Enabled") + ", They: " + (_pair.UserPair!.OtherPermissions.IsDisableAnimations() ? "Disabled" : "Enabled")); } if (individualVFXDisabled) { var userVFXText = "VFX sync disabled with " + _pair.UserData.AliasOrUID; _uiSharedService.IconText(FontAwesomeIcon.TimesCircle); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); ImGui.TextUnformatted(userVFXText); ImGui.NewLine(); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); ImGui.TextUnformatted("You: " + (_pair.UserPair!.OwnPermissions.IsDisableVFX() ? "Disabled" : "Enabled") + ", They: " + (_pair.UserPair!.OtherPermissions.IsDisableVFX() ? "Disabled" : "Enabled")); } ImGui.EndTooltip(); } ImGui.SameLine(); } else if ((animDisabled || soundsDisabled)) { ImGui.SetCursorPosY(textPosY); _uiSharedService.IconText(permIcon); if (ImGui.IsItemHovered()) { ImGui.BeginTooltip(); ImGui.TextUnformatted("Syncshell User permissions"); if (soundsDisabled) { var userSoundsText = "Sound sync disabled by " + _pair.UserData.AliasOrUID; _uiSharedService.IconText(FontAwesomeIcon.VolumeMute); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); ImGui.TextUnformatted(userSoundsText); } if (animDisabled) { var userAnimText = "Animation sync disabled by " + _pair.UserData.AliasOrUID; _uiSharedService.IconText(FontAwesomeIcon.WindowClose); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); ImGui.TextUnformatted(userAnimText); } if (vfxDisabled) { var userVFXText = "VFX sync disabled by " + _pair.UserData.AliasOrUID; _uiSharedService.IconText(FontAwesomeIcon.TimesCircle); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); ImGui.TextUnformatted(userVFXText); } ImGui.EndTooltip(); } ImGui.SameLine(); } if (showPlus) { ImGui.SetCursorPosY(originalY); if (_uiSharedService.IconButton(FontAwesomeIcon.Plus)) { var targetUid = _pair.UserData.UID; if (!string.IsNullOrEmpty(targetUid)) { _ = SendGroupPairInviteAsync(targetUid, entryUID); } } UiSharedService.AttachToolTip(AppendSeenInfo("Send pairing invite to " + entryUID)); ImGui.SameLine(); } if (showPause) { ImGui.SetCursorPosY(originalY); if (_uiSharedService.IconButton(pauseIcon)) { var newPermissions = _fullInfoDto.GroupUserPermissions ^ GroupUserPermissions.Paused; _fullInfoDto.GroupUserPermissions = newPermissions; _ = _apiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(_group.Group, _pair.UserData, newPermissions)); } UiSharedService.AttachToolTip(AppendSeenInfo((_fullInfoDto.GroupUserPermissions.IsPaused() ? "Resume" : "Pause") + " syncing with " + entryUID)); ImGui.SameLine(); } if (showBars) { ImGui.SetCursorPosY(originalY); if (_uiSharedService.IconButton(FontAwesomeIcon.Bars)) { ImGui.OpenPopup("Popup"); } } if (ImGui.BeginPopup("Popup")) { if ((userIsModerator || userIsOwner) && !(entryIsMod || entryIsOwner)) { var pinText = entryIsPinned ? "Unpin user" : "Pin user"; if (_uiSharedService.IconTextButton(FontAwesomeIcon.Thumbtack, pinText)) { ImGui.CloseCurrentPopup(); var userInfo = _fullInfoDto.GroupPairStatusInfo ^ GroupUserInfo.IsPinned; _ = _apiController.GroupSetUserInfo(new GroupPairUserInfoDto(_fullInfoDto.Group, _fullInfoDto.User, userInfo)); } UiSharedService.AttachToolTip("Pin this user to the Syncshell. Pinned users will not be deleted in case of a manually initiated Syncshell clean"); if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Remove user") && UiSharedService.CtrlPressed()) { ImGui.CloseCurrentPopup(); _ = _apiController.GroupRemoveUser(_fullInfoDto); } UiSharedService.AttachToolTip("Hold CTRL and click to remove user " + (_pair.UserData.AliasOrUID) + " from Syncshell"); if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, "Ban User")) { ImGui.CloseCurrentPopup(); _mediator.Publish(new OpenBanUserPopupMessage(_pair, _group)); } UiSharedService.AttachToolTip("Ban user from this Syncshell"); } if (userIsOwner) { string modText = entryIsMod ? "Demod user" : "Mod user"; if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserShield, modText) && UiSharedService.CtrlPressed()) { ImGui.CloseCurrentPopup(); var userInfo = _fullInfoDto.GroupPairStatusInfo ^ GroupUserInfo.IsModerator; _ = _apiController.GroupSetUserInfo(new GroupPairUserInfoDto(_fullInfoDto.Group, _fullInfoDto.User, userInfo)); } UiSharedService.AttachToolTip("Hold CTRL to change the moderator status for " + (_fullInfoDto.UserAliasOrUID) + Environment.NewLine + "Moderators can kick, ban/unban, pin/unpin users and clear the Syncshell."); if (_uiSharedService.IconTextButton(FontAwesomeIcon.Crown, "Transfer Ownership") && UiSharedService.CtrlPressed() && UiSharedService.ShiftPressed()) { ImGui.CloseCurrentPopup(); _ = _apiController.GroupChangeOwnership(_fullInfoDto); } UiSharedService.AttachToolTip("Hold CTRL and SHIFT and click to transfer ownership of this Syncshell to " + (_fullInfoDto.UserAliasOrUID) + Environment.NewLine + "WARNING: This action is irreversible."); } if (userIsOwner || (userIsModerator && !(entryIsMod || entryIsOwner))) ImGui.Separator(); if (_pair.IsVisible) { if (_uiSharedService.IconTextButton(FontAwesomeIcon.Eye, "Target player")) { _mediator.Publish(new TargetPairMessage(_pair)); ImGui.CloseCurrentPopup(); } } if (!_pair.IsPaused) { if (_uiSharedService.IconTextButton(FontAwesomeIcon.User, "Open Profile")) { _displayHandler.OpenProfile(_pair); ImGui.CloseCurrentPopup(); } } if (_pair.IsVisible) { #if DEBUG if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, "Open Analysis")) { _displayHandler.OpenAnalysis(_pair); ImGui.CloseCurrentPopup(); } #endif if (_uiSharedService.IconTextButton(FontAwesomeIcon.Sync, "Reload last data")) { _pair.ApplyLastReceivedData(forced: true); ImGui.CloseCurrentPopup(); } UiSharedService.AttachToolTip("This reapplies the last received character data to this character"); } ImGui.EndPopup(); } return pos - spacing; } private string AppendSeenInfo(string tooltip) { if (_pair.IsVisible) return tooltip; var lastSeen = _serverConfigurationManager.GetNameForUid(_pair.UserData.UID); if (string.IsNullOrWhiteSpace(lastSeen)) return tooltip; return tooltip + " (Vu sous : " + lastSeen + ")"; } private async Task SendGroupPairInviteAsync(string targetUid, string displayName) { try { var ok = await _autoDetectRequestService.SendDirectUidRequestAsync(targetUid, displayName).ConfigureAwait(false); if (!ok) return; await SendManualInviteSignalAsync(targetUid, displayName).ConfigureAwait(false); } catch { // errors are logged within the request service; ignore here } } private async Task SendManualInviteSignalAsync(string targetUid, string displayName) { if (string.IsNullOrEmpty(_apiController.UID)) return; var senderAliasRaw = string.IsNullOrEmpty(_apiController.DisplayName) ? _apiController.UID : _apiController.DisplayName; var senderAlias = EncodeInviteField(senderAliasRaw); var targetDisplay = EncodeInviteField(displayName); var inviteId = Guid.NewGuid().ToString("N"); var payloadText = new StringBuilder() .Append(ManualPairInvitePrefix) .Append(_apiController.UID) .Append('|') .Append(senderAlias) .Append('|') .Append(targetUid) .Append('|') .Append(targetDisplay) .Append('|') .Append(inviteId) .Append(']') .ToString(); var payload = new SeStringBuilder().AddText(payloadText).Build().Encode(); var chatMessage = new ChatMessage { SenderName = senderAlias, PayloadContent = payload }; try { await _apiController.GroupChatSendMsg(new GroupDto(_group.Group), chatMessage).ConfigureAwait(false); } catch { // ignore - invite remains tracked locally even if group chat signal fails } } private static string EncodeInviteField(string value) { var bytes = Encoding.UTF8.GetBytes(value ?? string.Empty); return Convert.ToBase64String(bytes); } }