using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Services.Profiles; using LightlessSync.UI.Services; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; using System.Globalization; using System.Numerics; namespace LightlessSync.UI; public class SyncshellAdminUI : WindowMediatorSubscriberBase { private readonly ApiController _apiController; private readonly bool _isModerator = false; private readonly bool _isOwner = false; private readonly List _oneTimeInvites = []; private readonly LightlessProfileManager _lightlessProfileManager; private readonly UiSharedService _uiSharedService; private readonly PairUiService _pairUiService; private List _bannedUsers = []; private LightlessGroupProfileData? _profileData = null; private string _userSearchFilter = string.Empty; private IDalamudTextureWrap? _pfpTextureWrap; private string _profileDescription = string.Empty; private int _multiInvites; private string _newPassword; private bool _pwChangeSuccess; private Task? _pruneTestTask; private Task? _pruneTask; private int _pruneDays = 14; private Task? _pruneSettingsTask; private bool _pruneSettingsLoaded; private bool _autoPruneEnabled; private int _autoPruneDays = 14; public SyncshellAdminUI(ILogger logger, LightlessMediator mediator, ApiController apiController, UiSharedService uiSharedService, PairUiService pairUiService, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, LightlessProfileManager lightlessProfileManager) : base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService) { GroupFullInfo = groupFullInfo; _apiController = apiController; _uiSharedService = uiSharedService; _lightlessProfileManager = lightlessProfileManager; _pairUiService = pairUiService; _isOwner = string.Equals(GroupFullInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal); _isModerator = GroupFullInfo.GroupUserInfo.IsModerator(); _newPassword = string.Empty; _multiInvites = 30; _pwChangeSuccess = true; IsOpen = true; Mediator.Subscribe(this, (msg) => { if (msg.GroupData == null || string.Equals(msg.GroupData.AliasOrGID, GroupFullInfo.Group.AliasOrGID, StringComparison.Ordinal)) { _pfpTextureWrap?.Dispose(); _pfpTextureWrap = null; } }); SizeConstraints = new WindowSizeConstraints() { MinimumSize = new(700, 500), MaximumSize = new(700, 2000), }; _pairUiService = pairUiService; } public GroupFullInfoDto GroupFullInfo { get; private set; } protected override void DrawInternal() { if (!_isModerator && !_isOwner) return; _logger.LogTrace("Drawing Syncshell Admin UI for {group}", GroupFullInfo.GroupAliasOrGID); var snapshot = _pairUiService.GetSnapshot(); if (snapshot.GroupsByGid.TryGetValue(GroupFullInfo.Group.GID, out var updatedInfo)) { GroupFullInfo = updatedInfo; } _profileData = _lightlessProfileManager.GetLightlessGroupProfile(GroupFullInfo.Group); using var id = ImRaii.PushId("syncshell_admin_" + GroupFullInfo.GID); DrawAdminHeader(); ImGui.Separator(); var perm = GroupFullInfo.GroupPermissions; DrawAdminTopBar(perm); } private void DrawAdminHeader() { float scale = ImGuiHelpers.GlobalScale; var style = ImGui.GetStyle(); var cursorLocal = ImGui.GetCursorPos(); var pMin = ImGui.GetCursorScreenPos(); float width = ImGui.GetContentRegionAvail().X; float height = 64f * scale; var pMax = new Vector2(pMin.X + width, pMin.Y + height); var drawList = ImGui.GetWindowDrawList(); var purple = UIColors.Get("LightlessPurple"); var gradLeft = purple.WithAlpha(0.0f); var gradRight = purple.WithAlpha(0.85f); uint colTopLeft = ImGui.ColorConvertFloat4ToU32(gradLeft); uint colTopRight = ImGui.ColorConvertFloat4ToU32(gradRight); uint colBottomRight = ImGui.ColorConvertFloat4ToU32(gradRight); uint colBottomLeft = ImGui.ColorConvertFloat4ToU32(gradLeft); drawList.AddRectFilledMultiColor( pMin, pMax, colTopLeft, colTopRight, colBottomRight, colBottomLeft); float accentHeight = 3f * scale; var accentMin = new Vector2(pMin.X, pMax.Y - accentHeight); var accentMax = new Vector2(pMax.X, pMax.Y); var accentColor = UIColors.Get("LightlessBlue"); uint accentU32 = ImGui.ColorConvertFloat4ToU32(accentColor); drawList.AddRectFilled(accentMin, accentMax, accentU32); ImGui.InvisibleButton("##adminHeaderHitbox", new Vector2(width, height)); if (ImGui.IsItemHovered()) { ImGui.BeginTooltip(); ImGui.Text($"{GroupFullInfo.GroupAliasOrGID} is created at:"); ImGui.Separator(); ImGui.Text(GroupFullInfo.Group.CreatedAt?.ToString("yyyy-MM-dd HH:mm:ss 'UTC'") ?? "Unknown"); ImGui.EndTooltip(); } var titlePos = new Vector2(pMin.X + 12f * scale, pMin.Y + 8f * scale); ImGui.SetCursorScreenPos(titlePos); float titleHeight; using (_uiSharedService.UidFont.Push()) { ImGui.TextColored(UIColors.Get("LightlessBlue"), GroupFullInfo.GroupAliasOrGID); titleHeight = ImGui.GetTextLineHeightWithSpacing(); } var subtitlePos = new Vector2( pMin.X + 12f * scale, titlePos.Y + titleHeight - 2f * scale); ImGui.SetCursorScreenPos(subtitlePos); ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey); ImGui.TextUnformatted("Administrative Panel"); ImGui.PopStyleColor(); string roleLabel = _isOwner ? "Owner" : (_isModerator ? "Moderator" : string.Empty); if (!string.IsNullOrEmpty(roleLabel)) { float roleTextW = ImGui.CalcTextSize(roleLabel).X; float pillPaddingX = 8f * scale; float pillPaddingY = -1f * scale; float pillWidth = roleTextW + pillPaddingX * 2f; float pillHeight = ImGui.GetTextLineHeight() + pillPaddingY * 2f; var pillMin = new Vector2( pMax.X - pillWidth - style.WindowPadding.X, subtitlePos.Y - pillPaddingY); var pillMax = new Vector2(pillMin.X + pillWidth, pillMin.Y + pillHeight); var pillBg = _isOwner ? UIColors.Get("LightlessYellow") : UIColors.Get("LightlessOrange"); uint pillBgU = ImGui.ColorConvertFloat4ToU32(pillBg.WithAlpha(0.9f)); drawList.AddRectFilled(pillMin, pillMax, pillBgU, 8f * scale); ImGui.SetCursorScreenPos(new Vector2(pillMin.X + pillPaddingX, pillMin.Y + pillPaddingY)); ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("FullBlack")); ImGui.TextUnformatted(roleLabel); ImGui.PopStyleColor(); } ImGui.SetCursorPos(new Vector2(cursorLocal.X, cursorLocal.Y + height + 6f * scale)); } private void DrawAdminTopBar(GroupPermissions perm) { var style = ImGui.GetStyle(); ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(12f, 6f) * ImGuiHelpers.GlobalScale); ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(10f, style.ItemSpacing.Y)); ImGui.PushStyleVar(ImGuiStyleVar.TabRounding, 6f * ImGuiHelpers.GlobalScale); var baseTab = UIColors.Get("FullBlack").WithAlpha(0.0f); var baseTabDim = UIColors.Get("FullBlack").WithAlpha(0.1f); var accent = UIColors.Get("LightlessPurple"); var accentHover = accent.WithAlpha(0.90f); var accentActive = accent; ImGui.PushStyleColor(ImGuiCol.Tab, baseTab); ImGui.PushStyleColor(ImGuiCol.TabHovered, accentHover); ImGui.PushStyleColor(ImGuiCol.TabActive, accentActive); ImGui.PushStyleColor(ImGuiCol.TabUnfocused, baseTabDim); ImGui.PushStyleColor(ImGuiCol.TabUnfocusedActive, accentActive.WithAlpha(0.80f)); using (var tabbar = ImRaii.TabBar("syncshell_tab_" + GroupFullInfo.GID)) { if (tabbar) { DrawInvites(perm); DrawManagement(); DrawPermission(perm); DrawProfile(); } } ImGui.PopStyleColor(5); ImGui.PopStyleVar(3); ImGuiHelpers.ScaledDummy(2f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGuiHelpers.ScaledDummy(2f); } private void DrawPermission(GroupPermissions perm) { var permissionTab = ImRaii.TabItem("Permissions"); if (permissionTab) { bool isDisableAnimations = perm.IsPreferDisableAnimations(); bool isDisableSounds = perm.IsPreferDisableSounds(); bool isDisableVfx = perm.IsPreferDisableVFX(); ImGui.AlignTextToFramePadding(); ImGui.Text("Suggest Sound Sync"); _uiSharedService.BooleanToColoredIcon(!isDisableSounds); ImGui.SameLine(230); using (ImRaii.PushColor(ImGuiCol.Text, isDisableSounds ? UIColors.Get("PairBlue") : UIColors.Get("DimRed"))) { if (_uiSharedService.IconTextButton(isDisableSounds ? FontAwesomeIcon.VolumeUp : FontAwesomeIcon.VolumeMute, isDisableSounds ? "Suggest to enable sound sync" : "Suggest to disable sound sync")) { perm.SetPreferDisableSounds(!perm.IsPreferDisableSounds()); _ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm)); } } ImGui.AlignTextToFramePadding(); ImGui.Text("Suggest Animation Sync"); _uiSharedService.BooleanToColoredIcon(!isDisableAnimations); ImGui.SameLine(230); using (ImRaii.PushColor(ImGuiCol.Text, isDisableAnimations ? UIColors.Get("PairBlue") : UIColors.Get("DimRed"))) { if (_uiSharedService.IconTextButton(isDisableAnimations ? FontAwesomeIcon.Running : FontAwesomeIcon.Stop, isDisableAnimations ? "Suggest to enable animation sync" : "Suggest to disable animation sync")) { perm.SetPreferDisableAnimations(!perm.IsPreferDisableAnimations()); _ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm)); } } ImGui.AlignTextToFramePadding(); ImGui.Text("Suggest VFX Sync"); _uiSharedService.BooleanToColoredIcon(!isDisableVfx); ImGui.SameLine(230); using (ImRaii.PushColor(ImGuiCol.Text, isDisableVfx ? UIColors.Get("PairBlue") : UIColors.Get("DimRed"))) { if (_uiSharedService.IconTextButton(isDisableVfx ? FontAwesomeIcon.Sun : FontAwesomeIcon.Circle, isDisableVfx ? "Suggest to enable vfx sync" : "Suggest to disable vfx sync")) { perm.SetPreferDisableVFX(!perm.IsPreferDisableVFX()); _ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm)); } } UiSharedService.TextWrapped("Note: those suggested permissions will be shown to users on joining the Syncshell."); } permissionTab.Dispose(); if (_isOwner) { var ownerTab = ImRaii.TabItem("Owner Settings"); if (ownerTab) { ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted("New Password"); var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; var buttonSize = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.Passport, "Change Password"); var textSize = ImGui.CalcTextSize("New Password").X; var spacing = ImGui.GetStyle().ItemSpacing.X; ImGui.SameLine(); ImGui.SetNextItemWidth(availableWidth - buttonSize - textSize - spacing * 2); ImGui.InputTextWithHint("##changepw", "Min 10 characters", ref _newPassword, 50); ImGui.SameLine(); using (ImRaii.Disabled(_newPassword.Length < 10)) { if (_uiSharedService.IconTextButton(FontAwesomeIcon.Passport, "Change Password")) { _pwChangeSuccess = _apiController.GroupChangePassword(new GroupPasswordDto(GroupFullInfo.Group, _newPassword)).Result; _newPassword = string.Empty; } } UiSharedService.AttachToolTip("Password requires to be at least 10 characters long. This action is irreversible."); if (!_pwChangeSuccess) { UiSharedService.ColorTextWrapped("Failed to change the password. Password requires to be at least 10 characters long.", ImGuiColors.DalamudYellow); } if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Syncshell") && UiSharedService.CtrlPressed() && UiSharedService.ShiftPressed()) { IsOpen = false; _ = _apiController.GroupDelete(new(GroupFullInfo.Group)); } UiSharedService.AttachToolTip("Hold CTRL and Shift and click to delete this Syncshell." + Environment.NewLine + "WARNING: this action is irreversible."); } ownerTab.Dispose(); } } private void DrawAutoPruneSettings() { ImGuiHelpers.ScaledDummy(2f); UiSharedService.TextWrapped("Automatic prune (server-side scheduled cleanup of inactive users)."); _pruneSettingsTask ??= _apiController.GroupGetPruneSettings(new GroupDto(GroupFullInfo.Group)); if (!_pruneSettingsLoaded) { if (!_pruneSettingsTask!.IsCompleted) { UiSharedService.ColorTextWrapped("Loading prune settings from server...", ImGuiColors.DalamudGrey); return; } if (_pruneSettingsTask.IsFaulted || _pruneSettingsTask.IsCanceled) { UiSharedService.ColorTextWrapped("Failed to load auto-prune settings.", ImGuiColors.DalamudRed); _pruneSettingsTask = null; _pruneSettingsLoaded = false; return; } var dto = _pruneSettingsTask.GetAwaiter().GetResult(); _autoPruneEnabled = dto.AutoPruneEnabled && dto.AutoPruneDays > 0; _autoPruneDays = dto.AutoPruneDays > 0 ? dto.AutoPruneDays : 14; _pruneSettingsLoaded = true; } bool enabled = _autoPruneEnabled; if (ImGui.Checkbox("Enable automatic pruning", ref enabled)) { _autoPruneEnabled = enabled; SavePruneSettings(); } UiSharedService.AttachToolTip("When enabled, inactive non-pinned, non-moderator users will be pruned automatically on the server."); ImGui.SameLine(); ImGui.SetNextItemWidth(150); using (ImRaii.Disabled(!_autoPruneEnabled)) { _uiSharedService.DrawCombo( "Day(s) of inactivity", [1, 3, 7, 14, 30, 90], days => $"{days} day(s)", selected => { _autoPruneDays = selected; SavePruneSettings(); }, _autoPruneDays); } if (!_autoPruneEnabled) { UiSharedService.ColorTextWrapped( "Automatic prune is currently disabled. Enable it and choose an inactivity threshold to let the server clean up inactive users automatically.", ImGuiColors.DalamudGrey); } } private void DrawProfile() { var profileTab = ImRaii.TabItem("Profile"); if (!profileTab) return; if (_profileData != null) { if (!string.Equals(_profileDescription, _profileData.Description, StringComparison.Ordinal)) { _profileDescription = _profileData.Description; } UiSharedService.TextWrapped("Preview the Syncshell profile in a standalone window."); if (_uiSharedService.IconTextButton(FontAwesomeIcon.AddressCard, "Open Syncshell Profile")) { Mediator.Publish(new GroupProfileOpenStandaloneMessage(GroupFullInfo.Group)); } UiSharedService.AttachToolTip("Opens the standalone Syncshell profile window for this group."); ImGuiHelpers.ScaledDummy(2f); ImGui.TextDisabled("Profile Flags"); ImGui.BulletText(_profileData.IsNsfw ? "Marked as NSFW" : "Marked as SFW"); ImGui.BulletText(_profileData.IsDisabled ? "Profile disabled for viewers" : "Profile active"); ImGuiHelpers.ScaledDummy(2f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGuiHelpers.ScaledDummy(2f); UiSharedService.TextWrapped("Open the syncshell profile editor to update images, description, tags, and visibility settings."); ImGuiHelpers.ScaledDummy(2f); if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserEdit, "Open Syncshell Profile Editor")) { Mediator.Publish(new OpenGroupProfileEditorMessage(GroupFullInfo)); } UiSharedService.AttachToolTip("Launches the editor window and associated live preview for this syncshell."); } else { UiSharedService.TextWrapped("Profile information is loading..."); } profileTab.Dispose(); } private void DrawManagement() { var mgmtTab = ImRaii.TabItem("User Management"); if (!mgmtTab) return; ImGuiHelpers.ScaledDummy(3f); var style = ImGui.GetStyle(); var baseTab = UIColors.Get("FullBlack").WithAlpha(0.0f); var baseTabDim = UIColors.Get("FullBlack").WithAlpha(0.1f); var accent = UIColors.Get("LightlessPurple"); var accentHover = accent.WithAlpha(0.90f); var accentActive = accent; //Pushing style vars for inner tab bar ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(10f, 5f) * ImGuiHelpers.GlobalScale); ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(8f, style.ItemSpacing.Y)); ImGui.PushStyleVar(ImGuiStyleVar.TabRounding, 5f * ImGuiHelpers.GlobalScale); try { //Pushing color stack for inner tab bar using (ImRaii.PushColor(ImGuiCol.Tab, baseTab)) using (ImRaii.PushColor(ImGuiCol.TabHovered, accentHover)) using (ImRaii.PushColor(ImGuiCol.TabActive, accentActive)) using (ImRaii.PushColor(ImGuiCol.TabUnfocused, baseTabDim)) using (ImRaii.PushColor(ImGuiCol.TabUnfocusedActive, accentActive.WithAlpha(0.80f))) { using (var innerTabBar = ImRaii.TabBar("syncshell_tab_" + GroupFullInfo.GID)) { if (innerTabBar) { // Users tab var usersTab = ImRaii.TabItem("Users"); if (usersTab) { DrawUserListSection(); } usersTab.Dispose(); // Cleanup tab var cleanupTab = ImRaii.TabItem("Cleanup"); if (cleanupTab) { DrawMassCleanupSection(); } cleanupTab.Dispose(); // Bans tab var bansTab = ImRaii.TabItem("Bans"); if (bansTab) { DrawUserBansSection(); } bansTab.Dispose(); } } } mgmtTab.Dispose(); } finally { // Popping style vars (3) for inner tab bar ImGui.PopStyleVar(3); } } private void DrawUserListSection() { var snapshot = _pairUiService.GetSnapshot(); if (!snapshot.GroupPairs.TryGetValue(GroupFullInfo, out var pairs)) { UiSharedService.ColorTextWrapped("No users found in this Syncshell", ImGuiColors.DalamudYellow); return; } _uiSharedService.MediumText("User List & Administration", UIColors.Get("LightlessPurple")); ImGuiHelpers.ScaledDummy(2f); DrawUserListCustom(pairs, GroupFullInfo); } private void DrawMassCleanupSection() { _uiSharedService.MediumText("Mass Cleanup", UIColors.Get("DimRed")); UiSharedService.TextWrapped("Tools to bulk-clean inactive or unwanted users from this Syncshell."); ImGuiHelpers.ScaledDummy(3f); using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) { if (_uiSharedService.IconTextButton(FontAwesomeIcon.Broom, "Clear Syncshell")) { _ = _apiController.GroupClear(new(GroupFullInfo.Group)); } } UiSharedService.AttachToolTip("This will remove all non-pinned, non-moderator users from the Syncshell." + UiSharedService.TooltipSeparator + "Hold CTRL to enable this button"); ImGui.SameLine(); using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) { if (_uiSharedService.IconTextButton(FontAwesomeIcon.Brush, "Clear Lightfinder Users")) { _ = _apiController.GroupClearFinder(new(GroupFullInfo.Group)); } } UiSharedService.AttachToolTip("This will remove all users that joined through Lightfinder from the Syncshell." + UiSharedService.TooltipSeparator + "Hold CTRL to enable this button"); ImGuiHelpers.ScaledDummy(2f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.0f); ImGuiHelpers.ScaledDummy(2f); if (_uiSharedService.IconTextButton(FontAwesomeIcon.Unlink, "Check for Inactive Users")) { _pruneTestTask = _apiController.GroupPrune(new(GroupFullInfo.Group), _pruneDays, execute: false); _pruneTask = null; } UiSharedService.AttachToolTip($"This will start the prune process for this Syncshell of inactive Lightless users that have not logged in in the past {_pruneDays} day(s)." + Environment.NewLine + "You will be able to review the amount of inactive users before executing the prune." + UiSharedService.TooltipSeparator + "Note: this check excludes pinned users and moderators of this Syncshell."); ImGui.SameLine(); ImGui.SetNextItemWidth(150); _uiSharedService.DrawCombo( "Day(s) of inactivity", [0, 1, 3, 7, 14, 30, 90], (count) => count == 0 ? "15 minute(s)" : count + " day(s)", (selected) => { _pruneDays = selected; _pruneTestTask = null; _pruneTask = null; }, _pruneDays); if (_pruneTestTask != null) { if (!_pruneTestTask.IsCompleted) { UiSharedService.ColorTextWrapped("Calculating inactive users...", ImGuiColors.DalamudYellow); } else { ImGui.AlignTextToFramePadding(); UiSharedService.TextWrapped($"Found {_pruneTestTask.Result} user(s) that have not logged into Lightless in the past {_pruneDays} day(s)."); if (_pruneTestTask.Result > 0) { using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) { if (_uiSharedService.IconTextButton(FontAwesomeIcon.Broom, "Prune Inactive Users")) { _pruneTask = _apiController.GroupPrune(new(GroupFullInfo.Group), _pruneDays, execute: true); _pruneTestTask = null; } } UiSharedService.AttachToolTip($"Pruning will remove {_pruneTestTask?.Result ?? 0} inactive user(s)." + UiSharedService.TooltipSeparator + "Hold CTRL to enable this button"); } } } if (_pruneTask != null) { if (!_pruneTask.IsCompleted) { UiSharedService.ColorTextWrapped("Pruning Syncshell...", ImGuiColors.DalamudYellow); } else { UiSharedService.TextWrapped($"Syncshell was pruned and {_pruneTask.Result} inactive user(s) have been removed."); } } ImGuiHelpers.ScaledDummy(4f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.0f); ImGuiHelpers.ScaledDummy(2f); DrawAutoPruneSettings(); } private void DrawUserBansSection() { _uiSharedService.MediumText("User Bans", UIColors.Get("LightlessYellow")); ImGuiHelpers.ScaledDummy(3f); if (_uiSharedService.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist from Server")) { _bannedUsers = _apiController.GroupGetBannedUsers(new GroupDto(GroupFullInfo.Group)).Result; } var tableFlags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp; if (_bannedUsers.Count > 10) tableFlags |= ImGuiTableFlags.ScrollY; if (ImGui.BeginTable("bannedusertable" + GroupFullInfo.GID, 6, tableFlags)) { ImGui.TableSetupColumn("UID", ImGuiTableColumnFlags.None, 1); ImGui.TableSetupColumn("Alias", ImGuiTableColumnFlags.None, 1); ImGui.TableSetupColumn("By", ImGuiTableColumnFlags.None, 1); ImGui.TableSetupColumn("Date", ImGuiTableColumnFlags.None, 2); ImGui.TableSetupColumn("Reason", ImGuiTableColumnFlags.None, 3); ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 1); ImGui.TableHeadersRow(); foreach (var bannedUser in _bannedUsers.ToList()) { ImGui.TableNextColumn(); ImGui.TextUnformatted(bannedUser.UID); ImGui.TableNextColumn(); ImGui.TextUnformatted(bannedUser.UserAlias ?? string.Empty); ImGui.TableNextColumn(); ImGui.TextUnformatted(bannedUser.BannedBy); ImGui.TableNextColumn(); ImGui.TextUnformatted(bannedUser.BannedOn.ToLocalTime().ToString(CultureInfo.CurrentCulture)); ImGui.TableNextColumn(); UiSharedService.TextWrapped(bannedUser.Reason); ImGui.TableNextColumn(); using var _ = ImRaii.PushId(bannedUser.UID); if (_uiSharedService.IconTextButton(FontAwesomeIcon.Check, "Unban")) { _apiController.GroupUnbanUser(bannedUser); _bannedUsers.RemoveAll(b => string.Equals(b.UID, bannedUser.UID, StringComparison.Ordinal)); } } ImGui.EndTable(); } } private void DrawInvites(GroupPermissions perm) { var inviteTab = ImRaii.TabItem("Invites"); if (inviteTab) { bool isInvitesDisabled = perm.IsDisableInvites(); if (_uiSharedService.IconTextButton(isInvitesDisabled ? FontAwesomeIcon.Unlock : FontAwesomeIcon.Lock, isInvitesDisabled ? "Unlock Syncshell" : "Lock Syncshell")) { perm.SetDisableInvites(!isInvitesDisabled); _ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm)); } ImGuiHelpers.ScaledDummy(2f); UiSharedService.TextWrapped("One-time invites work as single-use passwords. Use those if you do not want to distribute your Syncshell password."); if (_uiSharedService.IconTextButton(FontAwesomeIcon.Envelope, "Single one-time invite")) { ImGui.SetClipboardText(_apiController.GroupCreateTempInvite(new(GroupFullInfo.Group), 1).Result.FirstOrDefault() ?? string.Empty); } UiSharedService.AttachToolTip("Creates a single-use password for joining the syncshell which is valid for 24h and copies it to the clipboard."); ImGui.InputInt("##amountofinvites", ref _multiInvites); ImGui.SameLine(); using (ImRaii.Disabled(_multiInvites <= 1 || _multiInvites > 100)) { if (_uiSharedService.IconTextButton(FontAwesomeIcon.Envelope, "Generate " + _multiInvites + " one-time invites")) { _oneTimeInvites.AddRange(_apiController.GroupCreateTempInvite(new(GroupFullInfo.Group), _multiInvites).Result); } } if (_oneTimeInvites.Any()) { var invites = string.Join(Environment.NewLine, _oneTimeInvites); ImGui.InputTextMultiline("Generated Multi Invites", ref invites, 5000, new(0, 0), ImGuiInputTextFlags.ReadOnly); if (_uiSharedService.IconTextButton(FontAwesomeIcon.Copy, "Copy Invites to clipboard")) { ImGui.SetClipboardText(invites); } } } inviteTab.Dispose(); } private void DrawUserListCustom(IReadOnlyList pairs, GroupFullInfoDto GroupFullInfo) { // Search bar (unchanged) ImGui.PushItemWidth(0); _uiSharedService.IconText(FontAwesomeIcon.Search, UIColors.Get("LightlessPurple")); ImGui.SameLine(); ImGui.InputTextWithHint( "##UserSearchFilter", "Search UID/alias or note...", ref _userSearchFilter, 64); ImGui.PopItemWidth(); ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale)); var groupedPairs = new Dictionary( pairs.Select(p => new KeyValuePair( p, GroupFullInfo.GroupPairUserInfos.TryGetValue(p.UserData.UID, out var value) ? value : null )) ); var filter = _userSearchFilter?.Trim(); bool hasFilter = !string.IsNullOrEmpty(filter); if (hasFilter) filter = filter!.ToLowerInvariant(); var orderedPairs = groupedPairs .Where(p => !hasFilter || MatchesUserFilter(p.Key, filter!)) .OrderBy(p => { if (p.Value == null) return 10; if (string.Equals(p.Key.UserData.UID, GroupFullInfo.OwnerUID, StringComparison.Ordinal)) return 0; if (p.Value.Value.IsModerator()) return 1; if (p.Value.Value.IsPinned()) return 2; return 10; }) .ThenBy(p => p.Key.GetNote() ?? p.Key.UserData.AliasOrUID, StringComparer.OrdinalIgnoreCase) .ToList(); ImGui.BeginChild("userListScroll#" + GroupFullInfo.Group.AliasOrGID, new Vector2(0, 0), true); var style = ImGui.GetStyle(); float fullW = ImGui.GetContentRegionAvail().X; float colUid = fullW * 0.50f; float colFlags = fullW * 0.10f; float colActions = fullW - colUid - colFlags - style.ItemSpacing.X * 2.0f; DrawUserListHeader(colUid, colFlags); int rowIndex = 0; foreach (var kv in orderedPairs) { var pair = kv.Key; var userInfoOpt = kv.Value; DrawUserRowCustom(pair, userInfoOpt, GroupFullInfo, rowIndex++, colUid, colFlags, colActions); } ImGui.EndChild(); } private static void DrawUserListHeader(float colUid, float colFlags) { var style = ImGui.GetStyle(); float x0 = ImGui.GetCursorPosX(); ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessPurple")); // Alias / UID / Note ImGui.SetCursorPosX(x0); ImGui.TextUnformatted("Alias / UID / Note"); // Flags ImGui.SameLine(); ImGui.SetCursorPosX(x0 + colUid + style.ItemSpacing.X); ImGui.TextUnformatted("Flags"); // Actions ImGui.SameLine(); ImGui.SetCursorPosX(x0 + colUid + colFlags + style.ItemSpacing.X * 2.0f); ImGui.TextUnformatted("Actions"); ImGui.PopStyleColor(); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.0f); } private void DrawUserRowCustom(Pair pair, GroupPairUserInfo? userInfoOpt, GroupFullInfoDto GroupFullInfo, int rowIndex, float colUid, float colFlags, float colActions) { using var id = ImRaii.PushId("userRow_" + pair.UserData.UID); var style = ImGui.GetStyle(); float x0 = ImGui.GetCursorPosX(); if (rowIndex % 2 == 0) { var drawList = ImGui.GetWindowDrawList(); var pMin = ImGui.GetCursorScreenPos(); var rowHeight = ImGui.GetTextLineHeightWithSpacing() * 2.5f; var pMax = new Vector2(pMin.X + colUid + colFlags + colActions + style.ItemSpacing.X * 2.0f, pMin.Y + rowHeight); var bgColor = UIColors.Get("FullBlack") with { W = 0.0f }; drawList.AddRectFilled(pMin, pMax, ImGui.ColorConvertFloat4ToU32(bgColor)); } var isUserOwner = string.Equals(pair.UserData.UID, GroupFullInfo.OwnerUID, StringComparison.Ordinal); var userInfo = userInfoOpt ?? GroupPairUserInfo.None; ImGui.SetCursorPosX(x0); ImGui.AlignTextToFramePadding(); var note = pair.GetNote(); var text = note == null ? pair.UserData.AliasOrUID : $"{note} ({pair.UserData.AliasOrUID})"; var boolcolor = UiSharedService.GetBoolColor(pair.IsOnline); UiSharedService.ColorText(text, boolcolor); if (ImGui.IsItemClicked()) { ImGui.SetClipboardText(text); } if (!string.IsNullOrEmpty(pair.PlayerName)) { UiSharedService.AttachToolTip(pair.PlayerName); } ImGui.SameLine(); ImGui.SetCursorPosX(x0 + colUid + style.ItemSpacing.X); if (userInfoOpt != null && (userInfo.IsModerator() || userInfo.IsPinned() || isUserOwner)) { if (userInfo.IsModerator()) { _uiSharedService.IconText(FontAwesomeIcon.UserShield, UIColors.Get("LightlessPurple")); UiSharedService.AttachToolTip("Moderator"); } if (userInfo.IsPinned() && !isUserOwner) { _uiSharedService.IconText(FontAwesomeIcon.Thumbtack); UiSharedService.AttachToolTip("Pinned"); } if (isUserOwner) { _uiSharedService.IconText(FontAwesomeIcon.Crown, UIColors.Get("LightlessYellow")); UiSharedService.AttachToolTip("Owner"); } } else { _uiSharedService.IconText(FontAwesomeIcon.None); } ImGui.SameLine(); ImGui.SetCursorPosX(x0 + colUid + colFlags + style.ItemSpacing.X * 2.0f); DrawUserActions(pair, GroupFullInfo, userInfo, isUserOwner); ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale)); } private void DrawUserActions(Pair pair, GroupFullInfoDto GroupFullInfo, GroupPairUserInfo userInfo, bool isUserOwner) { if (_isOwner) { // Transfer ownership to user using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"))) using (ImRaii.Disabled(!UiSharedService.ShiftPressed())) { if (_uiSharedService.IconButton(FontAwesomeIcon.Crown)) { _ = _apiController.GroupChangeOwnership(new(GroupFullInfo.Group, pair.UserData)); IsOpen = false; } } UiSharedService.AttachToolTip("Hold SHIFT and click to transfer ownership of this Syncshell to " + pair.UserData.AliasOrUID + Environment.NewLine + "WARNING: This action is irreversible and will close screen."); ImGui.SameLine(); // Mod / Demod user using (ImRaii.PushColor(ImGuiCol.Text, userInfo.IsModerator() ? UIColors.Get("DimRed") : UIColors.Get("PairBlue"))) { if (_uiSharedService.IconButton(FontAwesomeIcon.UserShield)) { userInfo.SetModerator(!userInfo.IsModerator()); _ = _apiController.GroupSetUserInfo( new GroupPairUserInfoDto(GroupFullInfo.Group, pair.UserData, userInfo)); } } UiSharedService.AttachToolTip( userInfo.IsModerator() ? $"Demod {pair.UserData.AliasOrUID}" : $"Mod {pair.UserData.AliasOrUID}"); ImGui.SameLine(); } if (userInfo == GroupPairUserInfo.None || (!userInfo.IsModerator() && !isUserOwner)) { // Pin user using (ImRaii.PushColor(ImGuiCol.Text, userInfo.IsPinned() ? UIColors.Get("DimRed") : UIColors.Get("PairBlue"))) { if (_uiSharedService.IconButton(FontAwesomeIcon.Thumbtack)) { userInfo.SetPinned(!userInfo.IsPinned()); _ = _apiController.GroupSetUserInfo( new GroupPairUserInfoDto(GroupFullInfo.Group, pair.UserData, userInfo)); } } UiSharedService.AttachToolTip( userInfo.IsPinned() ? $"Unpin {pair.UserData.AliasOrUID}" : $"Pin {pair.UserData.AliasOrUID}"); ImGui.SameLine(); // Remove user using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("DimRed"))) using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) { if (_uiSharedService.IconButton(FontAwesomeIcon.Trash)) { _ = _apiController.GroupRemoveUser(new GroupPairDto(GroupFullInfo.Group, pair.UserData)); } } UiSharedService.AttachToolTip($"Remove {pair.UserData.AliasOrUID} from Syncshell" + UiSharedService.TooltipSeparator + "Hold CTRL to enable this button"); ImGui.SameLine(); // Ban user using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("DimRed"))) using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) { if (_uiSharedService.IconButton(FontAwesomeIcon.Ban)) { Mediator.Publish(new OpenBanUserPopupMessage(pair, GroupFullInfo)); } } UiSharedService.AttachToolTip($"Ban {pair.UserData.AliasOrUID} from Syncshell" + UiSharedService.TooltipSeparator + "Hold CTRL to enable this button"); } } private void SavePruneSettings() { if (_autoPruneDays <= 0) { _autoPruneEnabled = false; } var enabled = _autoPruneEnabled && _autoPruneDays > 0; var dto = new GroupPruneSettingsDto(Group: GroupFullInfo.Group, AutoPruneEnabled: enabled, AutoPruneDays: enabled ? _autoPruneDays : 0); try { _apiController.GroupSetPruneSettings(dto).GetAwaiter().GetResult(); } catch (Exception ex) { _logger.LogError(ex, "Failed to save auto prune settings for group {GID}", GroupFullInfo.Group.GID); UiSharedService.ColorTextWrapped("Failed to save auto-prune settings.", ImGuiColors.DalamudRed); } } private static bool MatchesUserFilter(Pair pair, string filterLower) { var note = pair.GetNote() ?? string.Empty; var uid = pair.UserData.UID ?? string.Empty; var alias = pair.UserData.AliasOrUID ?? string.Empty; return note.Contains(filterLower, StringComparison.OrdinalIgnoreCase) || uid.Contains(filterLower, StringComparison.OrdinalIgnoreCase) || alias.Contains(filterLower, StringComparison.OrdinalIgnoreCase); } public override void OnClose() { Mediator.Publish(new RemoveWindowMessage(this)); _pfpTextureWrap?.Dispose(); } }