1453 lines
55 KiB
C#
1453 lines
55 KiB
C#
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;
|
|
using LightlessSync.API.Data.Enum;
|
|
using LightlessSync.API.Data.Extensions;
|
|
using LightlessSync.API.Dto.Group;
|
|
using LightlessSync.PlayerData.Factories;
|
|
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 SharpDX.DirectWrite;
|
|
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<string> _oneTimeInvites = [];
|
|
private readonly LightlessProfileManager _lightlessProfileManager;
|
|
private readonly UiSharedService _uiSharedService;
|
|
private readonly PairUiService _pairUiService;
|
|
private List<BannedGroupUserDto> _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<int>? _pruneTestTask;
|
|
private Task<int>? _pruneTask;
|
|
private int _pruneDays = 14;
|
|
|
|
// Ban management fields
|
|
private Task<List<BannedGroupUserDto>>? _bannedUsersTask;
|
|
private bool _bannedUsersLoaded;
|
|
private string? _bannedUsersLoadError;
|
|
|
|
private string _newBanUid = string.Empty;
|
|
private string _newBanReason = string.Empty;
|
|
private Task? _newBanTask;
|
|
private string? _newBanError;
|
|
private DateTime _newBanBusyUntilUtc;
|
|
|
|
// Ban editing fields
|
|
private string? _editingBanUid;
|
|
private readonly Dictionary<string, string> _banReasonEdits = new(StringComparer.Ordinal);
|
|
|
|
private Task? _banEditTask;
|
|
private string? _banEditError;
|
|
|
|
private Task<GroupPruneSettingsDto>? _pruneSettingsTask;
|
|
private bool _pruneSettingsLoaded;
|
|
private bool _autoPruneEnabled;
|
|
private int _autoPruneDays = 14;
|
|
private readonly PairFactory _pairFactory;
|
|
|
|
public SyncshellAdminUI(ILogger<SyncshellAdminUI> logger, LightlessMediator mediator, ApiController apiController,
|
|
UiSharedService uiSharedService, PairUiService pairUiService, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, LightlessProfileManager lightlessProfileManager, PairFactory pairFactory)
|
|
: 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<ClearProfileGroupDataMessage>(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;
|
|
_pairFactory = pairFactory;
|
|
}
|
|
|
|
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;
|
|
|
|
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 tabbar = ImRaii.TabBar("syncshell_tab_" + GroupFullInfo.GID))
|
|
{
|
|
if (tabbar)
|
|
{
|
|
DrawInvites(perm);
|
|
DrawManagement();
|
|
DrawPermission(perm);
|
|
DrawProfile();
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
bool isChatDisabled = perm.IsDisableChat();
|
|
ImGui.AlignTextToFramePadding();
|
|
ImGui.TextUnformatted("Syncshell Chat");
|
|
_uiSharedService.BooleanToColoredIcon(!isChatDisabled);
|
|
ImGui.SameLine(230);
|
|
using (ImRaii.PushColor(ImGuiCol.Text, isChatDisabled ? UIColors.Get("PairBlue") : UIColors.Get("DimRed")))
|
|
{
|
|
if (_uiSharedService.IconTextButton(
|
|
isChatDisabled ? FontAwesomeIcon.Comment : FontAwesomeIcon.Ban,
|
|
isChatDisabled ? "Enable syncshell chat" : "Disable syncshell chat"))
|
|
{
|
|
perm.SetDisableChat(!isChatDisabled);
|
|
_ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm));
|
|
}
|
|
}
|
|
UiSharedService.AttachToolTip("Disables syncshell chat for all members.");
|
|
|
|
ImGuiHelpers.ScaledDummy(6f);
|
|
|
|
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.");
|
|
|
|
|
|
if (!_autoPruneEnabled)
|
|
{
|
|
ImGui.BeginDisabled();
|
|
}
|
|
ImGui.SameLine();
|
|
ImGui.SetNextItemWidth(150);
|
|
_uiSharedService.DrawCombo(
|
|
"Day(s) of inactivity (gets checked hourly)",
|
|
[0, 1, 3, 7, 14, 30, 90],
|
|
(count) => count == 0 ? "2 hours(s)" : count + " day(s)",
|
|
selected =>
|
|
{
|
|
_autoPruneDays = selected;
|
|
SavePruneSettings();
|
|
},
|
|
_autoPruneDays);
|
|
|
|
if (!_autoPruneEnabled)
|
|
{
|
|
ImGui.EndDisabled();
|
|
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("DimRed"), 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 ? "2 hours(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("DimRed"), 1.0f);
|
|
ImGuiHelpers.ScaledDummy(2f);
|
|
|
|
DrawAutoPruneSettings();
|
|
}
|
|
|
|
private void DrawUserBansSection()
|
|
{
|
|
_uiSharedService.MediumText("User Bans", UIColors.Get("LightlessYellow"));
|
|
ImGuiHelpers.ScaledDummy(3f);
|
|
|
|
EnsureBanListLoaded();
|
|
|
|
DrawNewBanEntryRow();
|
|
|
|
ImGuiHelpers.ScaledDummy(4f);
|
|
|
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist"))
|
|
{
|
|
QueueBanListRefresh(force: true);
|
|
}
|
|
|
|
ImGuiHelpers.ScaledDummy(2f);
|
|
|
|
if (!_bannedUsersLoaded)
|
|
{
|
|
UiSharedService.ColorTextWrapped("Loading banlist from server...", ImGuiColors.DalamudGrey);
|
|
return;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(_bannedUsersLoadError))
|
|
{
|
|
UiSharedService.ColorTextWrapped(_bannedUsersLoadError!, ImGuiColors.DalamudRed);
|
|
return;
|
|
}
|
|
|
|
ImGui.BeginChild("bannedListScroll#" + GroupFullInfo.GID, new Vector2(0, 0), true);
|
|
|
|
var style = ImGui.GetStyle();
|
|
float fullW = ImGui.GetContentRegionAvail().X;
|
|
float scale = ImGuiHelpers.GlobalScale;
|
|
|
|
float frame = ImGui.GetFrameHeight();
|
|
float actionIcons = 3;
|
|
float colActions = actionIcons * frame + (actionIcons - 1) * style.ItemSpacing.X + 10f * scale;
|
|
|
|
float colMeta = fullW * 0.35f;
|
|
|
|
float colIdentity = fullW - colMeta - colActions - style.ItemSpacing.X * 2.0f;
|
|
|
|
float minIdentity = fullW * 0.40f;
|
|
if (colIdentity < minIdentity)
|
|
{
|
|
colIdentity = minIdentity;
|
|
colMeta = fullW - colIdentity - colActions - style.ItemSpacing.X * 2.0f;
|
|
if (colMeta < 80f * scale) colMeta = 80f * scale;
|
|
}
|
|
|
|
DrawBannedListHeader(colIdentity, colMeta);
|
|
|
|
int rowIndex = 0;
|
|
foreach (var bannedUser in _bannedUsers.ToList())
|
|
{
|
|
DrawBannedRow(bannedUser, rowIndex++, colIdentity, colMeta, colActions);
|
|
}
|
|
|
|
ImGui.EndChild();
|
|
}
|
|
|
|
private void DrawNewBanEntryRow()
|
|
{
|
|
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"));
|
|
ImGui.TextUnformatted("Add new ban");
|
|
ImGui.PopStyleColor();
|
|
|
|
UiSharedService.TextWrapped("Enter a UID (Not Alias!) and optional reason. (Hold CTRL to enable the ban button.)");
|
|
|
|
var style = ImGui.GetStyle();
|
|
float fullW = ImGui.GetContentRegionAvail().X;
|
|
|
|
float uidW = fullW * 0.35f;
|
|
float reasonW = fullW * 0.50f;
|
|
float btnW = fullW - uidW - reasonW - style.ItemSpacing.X * 2f;
|
|
|
|
// UID
|
|
ImGui.SetNextItemWidth(uidW);
|
|
ImGui.InputTextWithHint("##newBanUid", "UID...", ref _newBanUid, 128);
|
|
|
|
// Reason
|
|
ImGui.SameLine(0f, style.ItemSpacing.X);
|
|
ImGui.SetNextItemWidth(reasonW);
|
|
ImGui.InputTextWithHint("##newBanReason", "Reason (optional)...", ref _newBanReason, 256);
|
|
|
|
// Ban button
|
|
ImGui.SameLine(0f, style.ItemSpacing.X);
|
|
|
|
var trimmedUid = (_newBanUid ?? string.Empty).Trim();
|
|
var now = DateTime.UtcNow;
|
|
bool taskRunning = _newBanTask != null && !_newBanTask.IsCompleted;
|
|
bool busyLatched = now < _newBanBusyUntilUtc;
|
|
bool busy = taskRunning || busyLatched;
|
|
|
|
bool canBan = UiSharedService.CtrlPressed()
|
|
&& !string.IsNullOrWhiteSpace(_newBanUid)
|
|
&& !busy;
|
|
|
|
using (ImRaii.Disabled(!canBan))
|
|
using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("DimRed")))
|
|
{
|
|
ImGui.SetNextItemWidth(btnW);
|
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, "Ban"))
|
|
{
|
|
_newBanError = null;
|
|
|
|
_newBanBusyUntilUtc = DateTime.UtcNow.AddMilliseconds(750);
|
|
|
|
_newBanTask = SubmitNewBanByUidAsync(trimmedUid, _newBanReason);
|
|
}
|
|
}
|
|
UiSharedService.AttachToolTip("Hold CTRL to enable banning by UID.");
|
|
|
|
if (busy)
|
|
{
|
|
UiSharedService.ColorTextWrapped("Banning user...", ImGuiColors.DalamudGrey);
|
|
}
|
|
|
|
if (_newBanTask != null && _newBanTask.IsCompleted && DateTime.UtcNow >= _newBanBusyUntilUtc)
|
|
{
|
|
if (_newBanTask.IsFaulted)
|
|
{
|
|
var _ = _newBanTask.Exception;
|
|
_newBanError ??= "Ban failed (see log).";
|
|
}
|
|
|
|
QueueBanListRefresh(force: true);
|
|
_newBanTask = null;
|
|
}
|
|
}
|
|
|
|
private async Task SubmitNewBanByUidAsync(string uidOrAlias, string reason)
|
|
{
|
|
try
|
|
{
|
|
await Task.Yield();
|
|
|
|
uidOrAlias = (uidOrAlias ?? string.Empty).Trim();
|
|
reason = (reason ?? string.Empty).Trim();
|
|
|
|
if (string.IsNullOrWhiteSpace(uidOrAlias))
|
|
{
|
|
_newBanError = "UID is empty.";
|
|
return;
|
|
}
|
|
|
|
string targetUid = uidOrAlias;
|
|
string? typedAlias = null;
|
|
|
|
var snap = _pairUiService.GetSnapshot();
|
|
if (snap.GroupPairs.TryGetValue(GroupFullInfo, out var pairs))
|
|
{
|
|
var match = pairs.FirstOrDefault(p =>
|
|
string.Equals(p.UserData.UID, uidOrAlias, StringComparison.Ordinal) ||
|
|
string.Equals(p.UserData.AliasOrUID, uidOrAlias, StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (match != null)
|
|
{
|
|
targetUid = match.UserData.UID;
|
|
typedAlias = match.UserData.Alias;
|
|
}
|
|
else
|
|
{
|
|
typedAlias = null;
|
|
}
|
|
}
|
|
|
|
var userData = new UserData(UID: targetUid, Alias: typedAlias);
|
|
|
|
await _apiController
|
|
.GroupBanUser(new GroupPairDto(GroupFullInfo.Group, userData), reason)
|
|
.ConfigureAwait(false);
|
|
|
|
_newBanUid = string.Empty;
|
|
_newBanReason = string.Empty;
|
|
_newBanError = null;
|
|
|
|
QueueBanListRefresh(force: true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to ban '{uidOrAlias}' in group {gid}", uidOrAlias, GroupFullInfo.Group.GID);
|
|
_newBanError = "Failed to ban user (see log).";
|
|
}
|
|
}
|
|
|
|
private async Task SaveBanReasonViaBanUserAsync(string uid)
|
|
{
|
|
try
|
|
{
|
|
if (!_banReasonEdits.TryGetValue(uid, out var newReason))
|
|
newReason = string.Empty;
|
|
|
|
newReason = (newReason ?? string.Empty).Trim();
|
|
|
|
var userData = new UserData(uid.Trim());
|
|
|
|
await _apiController
|
|
.GroupBanUser(new GroupPairDto(GroupFullInfo.Group, userData), newReason)
|
|
.ConfigureAwait(false);
|
|
|
|
_editingBanUid = null;
|
|
_banEditError = null;
|
|
|
|
await Task.Delay(450).ConfigureAwait(false);
|
|
|
|
QueueBanListRefresh(force: true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to edit ban reason for {uid} in group {gid}", uid, GroupFullInfo.Group.GID);
|
|
_banEditError = "Failed to update reason (see log).";
|
|
}
|
|
}
|
|
|
|
private void DrawBannedRow(BannedGroupUserDto bannedUser, int rowIndex, float colIdentity, float colMeta, float colActions)
|
|
{
|
|
using var id = ImRaii.PushId("banRow_" + bannedUser.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.6f;
|
|
var pMax = new Vector2(
|
|
pMin.X + colIdentity + colMeta + colActions + style.ItemSpacing.X * 2.0f,
|
|
pMin.Y + rowHeight);
|
|
|
|
var bgColor = UIColors.Get("FullBlack").WithAlpha(0.10f);
|
|
drawList.AddRectFilled(pMin, pMax, ImGui.ColorConvertFloat4ToU32(bgColor));
|
|
}
|
|
|
|
ImGui.SetCursorPosX(x0);
|
|
ImGui.AlignTextToFramePadding();
|
|
|
|
string alias = bannedUser.UserAlias ?? string.Empty;
|
|
string line1 = string.IsNullOrEmpty(alias)
|
|
? bannedUser.UID
|
|
: $"{alias} ({bannedUser.UID})";
|
|
|
|
ImGui.TextUnformatted(line1);
|
|
|
|
var fullReason = bannedUser.Reason ?? string.Empty;
|
|
|
|
if (string.Equals(_editingBanUid, bannedUser.UID, StringComparison.Ordinal))
|
|
{
|
|
_banReasonEdits.TryGetValue(bannedUser.UID, out var editReason);
|
|
editReason ??= StripAliasSuffix(fullReason);
|
|
|
|
ImGui.SetCursorPosX(x0);
|
|
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
|
|
ImGui.SetNextItemWidth(colIdentity);
|
|
ImGui.InputTextWithHint("##banReasonEdit", "Reason...", ref editReason, 255);
|
|
ImGui.PopStyleColor();
|
|
|
|
_banReasonEdits[bannedUser.UID] = editReason;
|
|
|
|
if (!string.IsNullOrWhiteSpace(_banEditError))
|
|
UiSharedService.ColorTextWrapped(_banEditError!, ImGuiColors.DalamudRed);
|
|
}
|
|
else
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(fullReason))
|
|
{
|
|
ImGui.SetCursorPosX(x0);
|
|
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
|
|
|
|
ImGui.PushTextWrapPos(x0 + colIdentity);
|
|
UiSharedService.TextWrapped(fullReason);
|
|
ImGui.PopTextWrapPos();
|
|
|
|
ImGui.PopStyleColor();
|
|
}
|
|
}
|
|
|
|
ImGui.SameLine();
|
|
ImGui.SetCursorPosX(x0 + colIdentity + style.ItemSpacing.X);
|
|
|
|
ImGui.AlignTextToFramePadding();
|
|
ImGui.TextUnformatted($"By: {bannedUser.BannedBy}");
|
|
|
|
var dateText = bannedUser.BannedOn.ToLocalTime().ToString(CultureInfo.CurrentCulture);
|
|
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
|
|
ImGui.TextUnformatted(dateText);
|
|
ImGui.PopStyleColor();
|
|
ImGui.SameLine();
|
|
|
|
float frame = ImGui.GetFrameHeight();
|
|
float actionsX0 = x0 + colIdentity + colMeta + style.ItemSpacing.X * 2.0f;
|
|
|
|
ImGui.SameLine();
|
|
ImGui.SetCursorPosX(actionsX0);
|
|
|
|
bool isEditing = string.Equals(_editingBanUid, bannedUser.UID, StringComparison.Ordinal);
|
|
int actionCount = 1 + (isEditing ? 2 : 1);
|
|
|
|
float totalW = actionCount * frame + (actionCount - 1) * style.ItemSpacing.X;
|
|
float startX = actionsX0 + MathF.Max(0, colActions - totalW) - 36f;
|
|
ImGui.SetCursorPosX(startX);
|
|
|
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Check))
|
|
{
|
|
_apiController.GroupUnbanUser(bannedUser);
|
|
_bannedUsers.RemoveAll(b => string.Equals(b.UID, bannedUser.UID, StringComparison.Ordinal));
|
|
}
|
|
UiSharedService.AttachToolTip("Unban");
|
|
|
|
ImGui.SameLine(0f, style.ItemSpacing.X);
|
|
|
|
if (!isEditing)
|
|
{
|
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Edit))
|
|
{
|
|
_banEditError = null;
|
|
_editingBanUid = bannedUser.UID;
|
|
_banReasonEdits[bannedUser.UID] = StripAliasSuffix(bannedUser.Reason ?? string.Empty);
|
|
}
|
|
UiSharedService.AttachToolTip("Edit reason");
|
|
}
|
|
else
|
|
{
|
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Save))
|
|
{
|
|
_banEditError = null;
|
|
_banEditTask = SaveBanReasonViaBanUserAsync(bannedUser.UID);
|
|
}
|
|
UiSharedService.AttachToolTip("Save");
|
|
|
|
ImGui.SameLine(0f, style.ItemSpacing.X);
|
|
|
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Times))
|
|
{
|
|
_banEditError = null;
|
|
_editingBanUid = null;
|
|
}
|
|
UiSharedService.AttachToolTip("Cancel");
|
|
}
|
|
}
|
|
|
|
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<Pair> pairs, GroupFullInfoDto GroupFullInfo)
|
|
{
|
|
// Search bar
|
|
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<Pair, GroupPairUserInfo?>(
|
|
pairs.Select(p => new KeyValuePair<Pair, GroupPairUserInfo?>(
|
|
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();
|
|
|
|
DrawUserListHeader();
|
|
|
|
ImGui.BeginChild("userListScroll#" + GroupFullInfo.Group.AliasOrGID, new Vector2(0, 0), true);
|
|
|
|
int rowIndex = 0;
|
|
foreach (var kv in orderedPairs)
|
|
{
|
|
var pair = kv.Key;
|
|
var userInfoOpt = kv.Value;
|
|
DrawUserRowCustom(pair, userInfoOpt, GroupFullInfo, rowIndex++);
|
|
}
|
|
|
|
ImGui.EndChild();
|
|
}
|
|
|
|
private static void DrawUserListHeader()
|
|
{
|
|
var style = ImGui.GetStyle();
|
|
float x0 = ImGui.GetCursorPosX();
|
|
float fullW = ImGui.GetContentRegionAvail().X;
|
|
|
|
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessPurple"));
|
|
|
|
ImGui.SetCursorPosX(x0);
|
|
ImGui.TextUnformatted("User");
|
|
|
|
const string actionsLabel = "Actions";
|
|
float labelWidth = ImGui.CalcTextSize(actionsLabel).X;
|
|
|
|
ImGui.SameLine();
|
|
ImGui.SetCursorPosX(x0 + fullW - labelWidth);
|
|
ImGui.TextUnformatted(actionsLabel);
|
|
|
|
ImGui.PopStyleColor();
|
|
|
|
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.0f);
|
|
}
|
|
|
|
private void DrawUserRowCustom(Pair pair, GroupPairUserInfo? userInfoOpt, GroupFullInfoDto GroupFullInfo, int rowIndex)
|
|
{
|
|
using var id = ImRaii.PushId("userRow_" + pair.UserData.UID);
|
|
|
|
var style = ImGui.GetStyle();
|
|
Vector2 rowStart = ImGui.GetCursorPos();
|
|
Vector2 rowStartScr = ImGui.GetCursorScreenPos();
|
|
float fullW = ImGui.GetContentRegionAvail().X;
|
|
|
|
float frameH = ImGui.GetFrameHeight();
|
|
float textH = ImGui.GetTextLineHeight();
|
|
float rowHeight = frameH;
|
|
|
|
if (rowIndex % 2 == 0)
|
|
{
|
|
var drawList = ImGui.GetWindowDrawList();
|
|
var pMin = rowStartScr;
|
|
var pMax = new Vector2(pMin.X + fullW, pMin.Y + rowHeight);
|
|
|
|
var bgColor = UIColors.Get("FullBlack").WithAlpha(0.05f);
|
|
drawList.AddRectFilled(pMin, pMax, ImGui.ColorConvertFloat4ToU32(bgColor));
|
|
}
|
|
|
|
var isUserOwner = string.Equals(pair.UserData.UID, GroupFullInfo.OwnerUID, StringComparison.Ordinal);
|
|
var userInfo = userInfoOpt ?? GroupPairUserInfo.None;
|
|
|
|
float baselineY = rowStart.Y + (rowHeight - textH) / 2f;
|
|
|
|
ImGui.SetCursorPos(new Vector2(rowStart.X, baselineY));
|
|
|
|
bool hasFlag = false;
|
|
if (userInfoOpt != null && (userInfo.IsModerator() || userInfo.IsPinned() || isUserOwner))
|
|
{
|
|
if (userInfo.IsModerator())
|
|
{
|
|
_uiSharedService.IconText(FontAwesomeIcon.UserShield, UIColors.Get("LightlessPurple"));
|
|
UiSharedService.AttachToolTip("Moderator");
|
|
hasFlag = true;
|
|
}
|
|
|
|
if (userInfo.IsPinned() && !isUserOwner)
|
|
{
|
|
if (hasFlag) ImGui.SameLine(0f, style.ItemSpacing.X);
|
|
_uiSharedService.IconText(FontAwesomeIcon.Thumbtack);
|
|
UiSharedService.AttachToolTip("Pinned");
|
|
hasFlag = true;
|
|
}
|
|
|
|
if (isUserOwner)
|
|
{
|
|
if (hasFlag) ImGui.SameLine(0f, style.ItemSpacing.X);
|
|
_uiSharedService.IconText(FontAwesomeIcon.Crown, UIColors.Get("LightlessYellow"));
|
|
UiSharedService.AttachToolTip("Owner");
|
|
hasFlag = true;
|
|
}
|
|
}
|
|
|
|
if (hasFlag)
|
|
ImGui.SameLine(0f, style.ItemSpacing.X);
|
|
|
|
ImGui.SetCursorPosY(baselineY);
|
|
|
|
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(pair.UserData.AliasOrUID);
|
|
|
|
if (!string.IsNullOrEmpty(pair.PlayerName))
|
|
UiSharedService.AttachToolTip(pair.PlayerName + $"{Environment.NewLine}Click to copy UID or Alias");
|
|
|
|
DrawUserActions(pair, GroupFullInfo, userInfo, isUserOwner, baselineY);
|
|
|
|
// Move cursor to next row
|
|
ImGui.SetCursorPos(new Vector2(rowStart.X, rowStart.Y + rowHeight + style.ItemSpacing.Y));
|
|
}
|
|
|
|
private void DrawUserActions(Pair pair, GroupFullInfoDto GroupFullInfo, GroupPairUserInfo userInfo, bool isUserOwner, float baselineY)
|
|
{
|
|
var style = ImGui.GetStyle();
|
|
float frameH = ImGui.GetFrameHeight();
|
|
|
|
int buttonCount = 0;
|
|
if (_isOwner)
|
|
buttonCount += 2; // Crown + Mod
|
|
if (userInfo == GroupPairUserInfo.None || (!userInfo.IsModerator() && !isUserOwner))
|
|
buttonCount += 3; // Pin + Trash + Ban
|
|
|
|
if (buttonCount == 0)
|
|
return;
|
|
|
|
float totalWidth = _isOwner
|
|
? buttonCount * frameH + buttonCount * style.ItemSpacing.X + 20f
|
|
: buttonCount * frameH + buttonCount * style.ItemSpacing.X;
|
|
|
|
float curX = ImGui.GetCursorPosX();
|
|
float avail = ImGui.GetContentRegionAvail().X;
|
|
|
|
float startX = curX + MathF.Max(0, avail - (totalWidth + 30f));
|
|
|
|
ImGui.SetCursorPos(new Vector2(startX, baselineY));
|
|
|
|
bool first = true;
|
|
|
|
if (_isOwner)
|
|
{
|
|
// Transfer ownership
|
|
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.");
|
|
|
|
first = false;
|
|
|
|
// Mod / Demod
|
|
using (ImRaii.PushColor(ImGuiCol.Text,
|
|
userInfo.IsModerator() ? UIColors.Get("DimRed") : UIColors.Get("PairBlue")))
|
|
{
|
|
if (!first) ImGui.SameLine(0f, style.ItemSpacing.X);
|
|
|
|
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}");
|
|
first = false;
|
|
}
|
|
|
|
if (userInfo == GroupPairUserInfo.None || (!userInfo.IsModerator() && !isUserOwner))
|
|
{
|
|
// Pin
|
|
using (ImRaii.PushColor(ImGuiCol.Text,
|
|
userInfo.IsPinned() ? UIColors.Get("DimRed") : UIColors.Get("PairBlue")))
|
|
{
|
|
if (!first) ImGui.SameLine(0f, style.ItemSpacing.X);
|
|
|
|
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}");
|
|
first = false;
|
|
|
|
// Trash
|
|
using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("DimRed")))
|
|
using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
|
|
{
|
|
if (!first) ImGui.SameLine(0f, style.ItemSpacing.X);
|
|
|
|
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");
|
|
first = false;
|
|
|
|
// Ban
|
|
using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("DimRed")))
|
|
using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
|
|
{
|
|
if (!first) ImGui.SameLine(0f, style.ItemSpacing.X);
|
|
|
|
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 static void DrawBannedListHeader(float colIdentity, float colMeta)
|
|
{
|
|
var style = ImGui.GetStyle();
|
|
float x0 = ImGui.GetCursorPosX();
|
|
|
|
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"));
|
|
|
|
// User and reason
|
|
ImGui.SetCursorPosX(x0);
|
|
ImGui.TextUnformatted("User / Reason");
|
|
|
|
// Moderator and Date
|
|
ImGui.SameLine();
|
|
ImGui.SetCursorPosX(x0 + colIdentity + style.ItemSpacing.X);
|
|
ImGui.TextUnformatted("Moderator / Date");
|
|
|
|
// Actions
|
|
ImGui.SameLine();
|
|
ImGui.SetCursorPosX(x0 + colIdentity + colMeta + style.ItemSpacing.X * 2.0f);
|
|
ImGui.TextUnformatted("Actions");
|
|
|
|
ImGui.PopStyleColor();
|
|
|
|
UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.0f);
|
|
}
|
|
|
|
private void QueueBanListRefresh(bool force = false)
|
|
{
|
|
if (!force)
|
|
{
|
|
if (_bannedUsersTask != null && !_bannedUsersTask.IsCompleted)
|
|
return;
|
|
}
|
|
|
|
_bannedUsersLoaded = false;
|
|
_bannedUsersLoadError = null;
|
|
|
|
_bannedUsersTask = _apiController.GroupGetBannedUsers(new GroupDto(GroupFullInfo.Group));
|
|
}
|
|
|
|
private void EnsureBanListLoaded()
|
|
{
|
|
_bannedUsersTask ??= _apiController.GroupGetBannedUsers(new GroupDto(GroupFullInfo.Group));
|
|
|
|
if (_bannedUsersLoaded || _bannedUsersTask == null)
|
|
return;
|
|
|
|
if (!_bannedUsersTask.IsCompleted)
|
|
return;
|
|
|
|
if (_bannedUsersTask.IsFaulted || _bannedUsersTask.IsCanceled)
|
|
{
|
|
_bannedUsersLoadError = "Failed to load banlist from server.";
|
|
_bannedUsers = [];
|
|
_bannedUsersLoaded = true;
|
|
return;
|
|
}
|
|
|
|
_bannedUsers = _bannedUsersTask.GetAwaiter().GetResult() ?? [];
|
|
_bannedUsersLoaded = true;
|
|
}
|
|
|
|
private void SavePruneSettings()
|
|
{
|
|
if (_autoPruneDays <= 0)
|
|
{
|
|
_autoPruneEnabled = false;
|
|
}
|
|
|
|
var dto = new GroupPruneSettingsDto(Group: GroupFullInfo.Group, AutoPruneEnabled: _autoPruneEnabled, AutoPruneDays: _autoPruneDays);
|
|
|
|
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 string StripAliasSuffix(string reason)
|
|
{
|
|
const string marker = " (Alias at time of ban:";
|
|
var idx = reason.IndexOf(marker, StringComparison.Ordinal);
|
|
return idx >= 0 ? reason[..idx] : reason;
|
|
}
|
|
|
|
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 OnOpen()
|
|
{
|
|
base.OnOpen();
|
|
QueueBanListRefresh(force: true);
|
|
}
|
|
public override void OnClose()
|
|
{
|
|
Mediator.Publish(new RemoveWindowMessage(this));
|
|
_pfpTextureWrap?.Dispose();
|
|
}
|
|
} |