diff --git a/LightlessAPI b/LightlessAPI index 0170ac3..dfb0594 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 0170ac377d7d2341c0d0e206ab871af22ac4767b +Subproject commit dfb0594a5be49994cda6d95aa0d995bd93cdfbc0 diff --git a/LightlessSync/PlayerData/Pairs/PairManager.cs b/LightlessSync/PlayerData/Pairs/PairManager.cs index fc6844a..eb70a54 100644 --- a/LightlessSync/PlayerData/Pairs/PairManager.cs +++ b/LightlessSync/PlayerData/Pairs/PairManager.cs @@ -379,7 +379,8 @@ public sealed class PairManager dto.GroupPermissions, shell.GroupFullInfo.GroupUserPermissions, shell.GroupFullInfo.GroupUserInfo, - new Dictionary(shell.GroupFullInfo.GroupPairUserInfos, StringComparer.Ordinal)); + new Dictionary(shell.GroupFullInfo.GroupPairUserInfos, StringComparer.Ordinal), + 0); shell.Update(updated); return PairOperationResult.Ok(); @@ -514,7 +515,8 @@ public sealed class PairManager GroupPermissions.NoneSet, GroupUserPreferredPermissions.NoneSet, GroupPairUserInfo.None, - new Dictionary(StringComparer.Ordinal)); + new Dictionary(StringComparer.Ordinal), + 0); shell = new Syncshell(placeholder); _groups[group.GID] = shell; diff --git a/LightlessSync/PlayerData/Pairs/VisibleUserDataDistributor.cs b/LightlessSync/PlayerData/Pairs/VisibleUserDataDistributor.cs index a1c7587..f71080a 100644 --- a/LightlessSync/PlayerData/Pairs/VisibleUserDataDistributor.cs +++ b/LightlessSync/PlayerData/Pairs/VisibleUserDataDistributor.cs @@ -23,7 +23,7 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase private readonly List _previouslyVisiblePlayers = []; private Task? _fileUploadTask = null; private readonly HashSet _usersToPushDataTo = new(UserDataComparer.Instance); - private readonly SemaphoreSlim _pushDataSemaphore = new(1, 1); + private readonly SemaphoreSlim _pushLock = new(1, 1); private readonly CancellationTokenSource _runtimeCts = new(); public VisibleUserDataDistributor(ILogger logger, ApiController apiController, DalamudUtilService dalamudUtil, @@ -108,53 +108,49 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase private void PushCharacterData(bool forced = false) { if (_lastCreatedData == null || _usersToPushDataTo.Count == 0) return; + _ = PushCharacterDataAsync(forced); + } - _ = Task.Run(async () => + private async Task PushCharacterDataAsync(bool forced = false) + { + await _pushLock.WaitAsync(_runtimeCts.Token).ConfigureAwait(false); + try { - try - { - forced |= _uploadingCharacterData?.DataHash != _lastCreatedData.DataHash; + if (_lastCreatedData == null || _usersToPushDataTo.Count == 0) + return; - if (_fileUploadTask == null || (_fileUploadTask?.IsCompleted ?? false) || forced) + var hashChanged = _uploadingCharacterData?.DataHash != _lastCreatedData.DataHash; + forced |= hashChanged; + + if (_fileUploadTask == null || _fileUploadTask.IsCompleted || forced) { _uploadingCharacterData = _lastCreatedData.DeepClone(); + var uploadTargets = _usersToPushDataTo.ToList(); Logger.LogDebug("Starting UploadTask for {hash}, Reason: TaskIsNull: {task}, TaskIsCompleted: {taskCpl}, Forced: {frc}", - _lastCreatedData.DataHash, _fileUploadTask == null, _fileUploadTask?.IsCompleted ?? false, forced); - _fileUploadTask = _fileTransferManager.UploadFiles(_uploadingCharacterData, [.. _usersToPushDataTo]); + _lastCreatedData.DataHash, + _fileUploadTask == null, + _fileUploadTask?.IsCompleted ?? false, + forced); + + _fileUploadTask = _fileTransferManager.UploadFiles(_uploadingCharacterData, uploadTargets); } - if (_fileUploadTask != null) - { - var dataToSend = await _fileUploadTask.ConfigureAwait(false); - await _pushDataSemaphore.WaitAsync(_runtimeCts.Token).ConfigureAwait(false); - try - { - if (_usersToPushDataTo.Count == 0) return; - Logger.LogDebug("Pushing {data} to {users}", dataToSend.DataHash, string.Join(", ", _usersToPushDataTo.Select(k => k.AliasOrUID))); - await _apiController.PushCharacterData(dataToSend, [.. _usersToPushDataTo]).ConfigureAwait(false); - _usersToPushDataTo.Clear(); - } - finally - { - _pushDataSemaphore.Release(); - } - } - } - catch (OperationCanceledException) when (_runtimeCts.IsCancellationRequested) - { - Logger.LogDebug("PushCharacterData cancelled"); - } - catch (Exception ex) - { - Logger.LogError(ex, "Failed to push character data"); - } - }); + var dataToSend = await _fileUploadTask.ConfigureAwait(false); + + var users = _usersToPushDataTo.ToList(); + if (users.Count == 0) + return; + + Logger.LogDebug("Pushing {data} to {users}", dataToSend.DataHash, string.Join(", ", users.Select(k => k.AliasOrUID))); + + await _apiController.PushCharacterData(dataToSend, users).ConfigureAwait(false); + _usersToPushDataTo.Clear(); + } + finally + { + _pushLock.Release(); + } } - private List GetVisibleUsers() - { - return _pairLedger.GetVisiblePairs() - .Select(connection => connection.User) - .ToList(); - } + private List GetVisibleUsers() => [.. _pairLedger.GetVisiblePairs().Select(connection => connection.User)]; } diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index a40dd1b..65c6dfa 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -957,11 +957,10 @@ public class CompactUi : WindowMediatorSubscriberBase private ImmutableList SortEntries(IEnumerable entries) { - return entries + return [.. entries .OrderByDescending(e => e.IsVisible) .ThenByDescending(e => e.IsOnline) - .ThenBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase) - .ToImmutableList(); + .ThenBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)]; } private ImmutableList SortVisibleEntries(IEnumerable entries) @@ -972,9 +971,7 @@ public class CompactUi : WindowMediatorSubscriberBase VisiblePairSortMode.VramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateVramBytes), VisiblePairSortMode.EffectiveVramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateEffectiveVramBytes), VisiblePairSortMode.TriangleCount => SortVisibleByMetric(entryList, e => e.LastAppliedDataTris), - VisiblePairSortMode.Alphabetical => entryList - .OrderBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase) - .ToImmutableList(), + VisiblePairSortMode.Alphabetical => [.. entryList.OrderBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)], VisiblePairSortMode.PreferredDirectPairs => SortVisibleByPreferred(entryList), _ => SortEntries(entryList), }; @@ -982,31 +979,28 @@ public class CompactUi : WindowMediatorSubscriberBase private ImmutableList SortVisibleByMetric(IEnumerable entries, Func selector) { - return entries + return [.. entries .OrderByDescending(entry => selector(entry) >= 0) .ThenByDescending(selector) .ThenByDescending(entry => entry.IsOnline) - .ThenBy(entry => AlphabeticalSortKey(entry), StringComparer.OrdinalIgnoreCase) - .ToImmutableList(); + .ThenBy(entry => AlphabeticalSortKey(entry), StringComparer.OrdinalIgnoreCase)]; } private ImmutableList SortVisibleByPreferred(IEnumerable entries) { - return entries + return [.. entries .OrderByDescending(entry => entry.IsDirectlyPaired && entry.SelfPermissions.IsSticky()) .ThenByDescending(entry => entry.IsDirectlyPaired) .ThenByDescending(entry => entry.IsOnline) - .ThenBy(entry => AlphabeticalSortKey(entry), StringComparer.OrdinalIgnoreCase) - .ToImmutableList(); + .ThenBy(entry => AlphabeticalSortKey(entry), StringComparer.OrdinalIgnoreCase)]; } private ImmutableList SortGroupEntries(IEnumerable entries, GroupFullInfoDto group) { - return entries + return [.. entries .OrderByDescending(e => e.IsOnline) .ThenBy(e => GroupSortWeight(e, group)) - .ThenBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase) - .ToImmutableList(); + .ThenBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)]; } private int GroupSortWeight(PairUiEntry entry, GroupFullInfoDto group) diff --git a/LightlessSync/UI/DownloadUi.cs b/LightlessSync/UI/DownloadUi.cs index dac49c1..37119e2 100644 --- a/LightlessSync/UI/DownloadUi.cs +++ b/LightlessSync/UI/DownloadUi.cs @@ -103,7 +103,7 @@ public class DownloadUi : WindowMediatorSubscriberBase // Check if download notifications are enabled (not set to TextOverlay) var useNotifications = _configService.Current.UseLightlessNotifications - ? _configService.Current.LightlessDownloadNotification != NotificationLocation.TextOverlay + ? _configService.Current.LightlessDownloadNotification != NotificationLocation.LightlessUi : _configService.Current.UseNotificationsForDownloads; if (useNotifications) @@ -534,6 +534,7 @@ public class DownloadUi : WindowMediatorSubscriberBase if (lineSize.X > contentWidth) contentWidth = lineSize.X; } + } var lineHeight = ImGui.GetTextLineHeight(); @@ -635,32 +636,40 @@ public class DownloadUi : WindowMediatorSubscriberBase foreach (var p in orderedPlayers) { - var playerSpeedText = p.SpeedBytesPerSecond > 0 + var hasSpeed = p.SpeedBytesPerSecond > 0; + var playerSpeedText = hasSpeed ? $"{UiSharedService.ByteToString((long)p.SpeedBytesPerSecond)}/s" : "-"; + // Label line for the player var labelLine = $"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}] {p.TransferredFiles}/{p.TotalFiles}"; - if (!_configService.Current.ShowPlayerSpeedBarsTransferWindow || p.DlProg <= 0) - { - var fullLine = - $"{labelLine} " + - $"({UiSharedService.ByteToString(p.TransferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(p.TotalBytes)}) " + - $"@ {playerSpeedText}"; + // State flags + var isDownloading = p.DlProg > 0; + var isDecompressing = p.DlDecomp > 0 + || (!isDownloading && p.TotalBytes > 0 && p.TransferredBytes >= p.TotalBytes); + + var showBar = _configService.Current.ShowPlayerSpeedBarsTransferWindow + && (isDownloading || isDecompressing); + + if (!showBar) + { UiSharedService.DrawOutlinedFont( drawList, - fullLine, + labelLine, cursor, UiSharedService.Color(255, 255, 255, _transferBoxTransparency), UiSharedService.Color(0, 0, 0, _transferBoxTransparency), 1 ); + cursor.Y += lineHeight + spacingY; continue; } + // Top label line (only name + W/Q/P/D + files) UiSharedService.DrawOutlinedFont( drawList, labelLine, @@ -671,6 +680,7 @@ public class DownloadUi : WindowMediatorSubscriberBase ); cursor.Y += lineHeight + spacingY; + // Bar background var barBgMin = new Vector2(boxMin.X + padding, cursor.Y); var barBgMax = new Vector2(boxMax.X - padding, cursor.Y + perPlayerBarHeight); @@ -682,8 +692,14 @@ public class DownloadUi : WindowMediatorSubscriberBase ); float ratio = 0f; - if (maxSpeed > 0) - ratio = (float)(p.SpeedBytesPerSecond / maxSpeed); + if (isDownloading && p.TotalBytes > 0) + { + ratio = (float)p.TransferredBytes / p.TotalBytes; + } + else if (isDecompressing) + { + ratio = 1f; + } if (ratio < 0f) ratio = 0f; if (ratio > 1f) ratio = 1f; @@ -698,24 +714,43 @@ public class DownloadUi : WindowMediatorSubscriberBase 3f ); - var barText = - $"{UiSharedService.ByteToString(p.TransferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(p.TotalBytes)} @ {playerSpeedText}"; + string barText; - var barTextSize = ImGui.CalcTextSize(barText); + if (isDownloading) + { + var bytesInside = + $"{UiSharedService.ByteToString(p.TransferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(p.TotalBytes)}"; - var barTextPos = new Vector2( - barBgMin.X + ((barBgMax.X - barBgMin.X) - barTextSize.X) / 2f - 1, - barBgMin.Y + ((perPlayerBarHeight - barTextSize.Y) / 2f) - 1 - ); + barText = hasSpeed + ? $"{bytesInside} @ {playerSpeedText}" + : bytesInside; + } + else if (isDecompressing) + { + barText = "Decompressing..."; + } + else + { + barText = string.Empty; + } - UiSharedService.DrawOutlinedFont( - drawList, - barText, - barTextPos, - UiSharedService.Color(255, 255, 255, _transferBoxTransparency), - UiSharedService.Color(0, 0, 0, _transferBoxTransparency), - 1 - ); + if (!string.IsNullOrEmpty(barText)) + { + var barTextSize = ImGui.CalcTextSize(barText); + var barTextPos = new Vector2( + barBgMin.X + ((barBgMax.X - barBgMin.X) - barTextSize.X) / 2f - 1, + barBgMin.Y + ((perPlayerBarHeight - barTextSize.Y) / 2f) - 1 + ); + + UiSharedService.DrawOutlinedFont( + drawList, + barText, + barTextPos, + UiSharedService.Color(255, 255, 255, _transferBoxTransparency), + UiSharedService.Color(0, 0, 0, _transferBoxTransparency), + 1 + ); + } cursor.Y += perPlayerBarHeight + spacingY; } diff --git a/LightlessSync/UI/DtrEntry.cs b/LightlessSync/UI/DtrEntry.cs index 770331e..9cadb4c 100644 --- a/LightlessSync/UI/DtrEntry.cs +++ b/LightlessSync/UI/DtrEntry.cs @@ -2,7 +2,6 @@ using Dalamud.Game.Gui.Dtr; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Plugin.Services; -using Dalamud.Utility; using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration.Configurations; using LightlessSync.Services; @@ -13,11 +12,9 @@ using LightlessSync.WebAPI; using LightlessSync.WebAPI.SignalR.Utils; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; using System.Runtime.InteropServices; using System.Text; using LightlessSync.UI.Services; -using LightlessSync.PlayerData.Pairs; using static LightlessSync.Services.PairRequestService; using LightlessSync.Services.LightFinder; diff --git a/LightlessSync/UI/EditProfileUi.Group.cs b/LightlessSync/UI/EditProfileUi.Group.cs index 6e8f6d3..f93ba70 100644 --- a/LightlessSync/UI/EditProfileUi.Group.cs +++ b/LightlessSync/UI/EditProfileUi.Group.cs @@ -2,26 +2,17 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; -using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; -using LightlessSync.API.Data; using LightlessSync.API.Dto.Group; -using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Services.Profiles; using LightlessSync.UI.Tags; using LightlessSync.Utils; using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.PixelFormats; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Numerics; -using System.Threading.Tasks; namespace LightlessSync.UI; @@ -68,6 +59,7 @@ public partial class EditProfileUi _bannerTextureWrap = null; _showProfileImageError = false; _showBannerImageError = false; + _groupVisibilityInitialized = false; } private void DrawGroupEditor(float scale) @@ -376,6 +368,8 @@ public partial class EditProfileUi private void DrawGroupProfileVisibilityControls() { + EnsureGroupVisibilityStateInitialised(); + bool changedNsfw = DrawCheckboxRow("Profile is NSFW", _groupIsNsfw, out var newNsfw, "Flag this profile as not safe for work."); if (changedNsfw) _groupIsNsfw = newNsfw; @@ -504,33 +498,36 @@ public partial class EditProfileUi try { var fileContent = await File.ReadAllBytesAsync(filePath).ConfigureAwait(false); - await using var stream = new MemoryStream(fileContent); - var format = await Image.DetectFormatAsync(stream).ConfigureAwait(false); - if (!IsSupportedImageFormat(format)) + var stream = new MemoryStream(fileContent); + await using (stream.ConfigureAwait(false)) { - _showBannerImageError = true; - return; + var format = await Image.DetectFormatAsync(stream).ConfigureAwait(false); + if (!IsSupportedImageFormat(format)) + { + _showBannerImageError = true; + return; + } + + using var image = Image.Load(fileContent); + if (image.Width > 840 || image.Height > 260 || fileContent.Length > 2000 * 1024) + { + _showBannerImageError = true; + return; + } + + await _apiController.GroupSetProfile(new GroupProfileDto( + _groupInfo.Group, + Description: null, + Tags: null, + PictureBase64: null, + BannerBase64: Convert.ToBase64String(fileContent), + IsNsfw: null, + IsDisabled: null)).ConfigureAwait(false); + + _showBannerImageError = false; + _queuedBannerImage = fileContent; + Mediator.Publish(new ClearProfileGroupDataMessage(_groupInfo.Group)); } - - using var image = Image.Load(fileContent); - if (image.Width > 840 || image.Height > 260 || fileContent.Length > 2000 * 1024) - { - _showBannerImageError = true; - return; - } - - await _apiController.GroupSetProfile(new GroupProfileDto( - _groupInfo.Group, - Description: null, - Tags: null, - PictureBase64: null, - BannerBase64: Convert.ToBase64String(fileContent), - IsNsfw: null, - IsDisabled: null)).ConfigureAwait(false); - - _showBannerImageError = false; - _queuedBannerImage = fileContent; - Mediator.Publish(new ClearProfileGroupDataMessage(_groupInfo.Group)); } catch (Exception ex) { @@ -588,6 +585,16 @@ public partial class EditProfileUi } } + private void EnsureGroupVisibilityStateInitialised() + { + if (_groupInfo == null || _groupVisibilityInitialized) + return; + + _groupIsNsfw = _groupServerIsNsfw; + _groupIsDisabled = _groupServerIsDisabled; + _groupVisibilityInitialized = true; + } + private async Task SubmitGroupTagChanges(int[] payload) { if (_groupInfo is null) @@ -695,11 +702,15 @@ public partial class EditProfileUi } } - _groupIsNsfw = profile.IsNsfw; - _groupIsDisabled = profile.IsDisabled; _groupServerIsNsfw = profile.IsNsfw; _groupServerIsDisabled = profile.IsDisabled; - } + if (!_groupVisibilityInitialized) + { + _groupIsNsfw = _groupServerIsNsfw; + _groupIsDisabled = _groupServerIsDisabled; + _groupVisibilityInitialized = true; + } + } } diff --git a/LightlessSync/UI/EditProfileUi.cs b/LightlessSync/UI/EditProfileUi.cs index 78c38ed..4a5bd84 100644 --- a/LightlessSync/UI/EditProfileUi.cs +++ b/LightlessSync/UI/EditProfileUi.cs @@ -43,7 +43,7 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase " - toggle style flags.\n" + " - create clickable links."; - private static readonly HashSet SupportedImageExtensions = new(StringComparer.OrdinalIgnoreCase) + private static readonly HashSet _supportedImageExtensions = new(StringComparer.OrdinalIgnoreCase) { "png", "jpg", @@ -52,7 +52,7 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase "bmp" }; private const string _imageFileDialogFilter = "Images{.png,.jpg,.jpeg,.webp,.bmp}"; - private readonly List _tagEditorSelection = new(); + private readonly List _tagEditorSelection = []; private int[] _profileTagIds = []; private readonly List _tagPreviewSegments = new(); private enum ProfileEditorMode @@ -68,6 +68,7 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase private bool _groupIsDisabled; private bool _groupServerIsNsfw; private bool _groupServerIsDisabled; + private bool _groupVisibilityInitialized; private byte[]? _queuedProfileImage; private byte[]? _queuedBannerImage; private readonly Vector4 _tagBackgroundColor = new(0.18f, 0.18f, 0.18f, 0.95f); @@ -85,6 +86,9 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase private bool _showProfileImageError = false; private bool _showBannerImageError = false; private bool _wasOpen; + private bool _userServerIsNsfw; + private bool _isNsfwInitialized; + private bool _isNsfw; private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f); private bool _textEnabled; @@ -171,6 +175,8 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase return; } + _isNsfwInitialized = false; + var user = await EnsureSelfProfileUserDataAsync().ConfigureAwait(false); if (user is not null) { @@ -339,13 +345,12 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase } SyncProfileState(profile); - DrawSection("Profile Preview", scale, () => DrawProfileSnapshot(profile, scale)); DrawSection("Profile Image", scale, () => DrawProfileImageControls(profile, scale)); DrawSection("Profile Banner", scale, () => DrawProfileBannerControls(profile, scale)); DrawSection("Profile Description", scale, () => DrawProfileDescriptionEditor(profile, scale)); DrawSection("Profile Tags", scale, () => DrawProfileTagsEditor(profile, scale)); - DrawSection("Visibility", scale, () => DrawProfileVisibilityControls(profile)); + DrawSection("Visibility", scale, () => DrawProfileVisibilityControls()); } private void DrawProfileSnapshot(LightlessUserProfileData profile, float scale) @@ -877,21 +882,46 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase UiSharedService.AttachToolTip(saveTooltip); } - private void DrawProfileVisibilityControls(LightlessUserProfileData profile) + private void DrawProfileVisibilityControls() { - var isNsfw = profile.IsNSFW; - if (DrawCheckboxRow("Mark profile as NSFW", isNsfw, out var newValue, "Enable when your profile could be considered NSFW.")) + if (!_isNsfwInitialized) + ImGui.BeginDisabled(); + + bool changed = DrawCheckboxRow("Mark profile as NSFW", _isNsfw, out var newValue, "Enable when your profile could be considered NSFW."); + + if (changed) + _isNsfw = newValue; + + bool visibilityChanged = _isNsfwInitialized && (_isNsfw != _userServerIsNsfw); + + if (!_isNsfwInitialized) + ImGui.EndDisabled(); + + if (!_isNsfwInitialized || !visibilityChanged) + ImGui.BeginDisabled(); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Apply Visibility Changes")) { + _userServerIsNsfw = _isNsfw; + _ = _apiController.UserSetProfile(new UserProfileDto( new UserData(_apiController.UID), Disabled: false, - newValue, - ProfilePictureBase64: GetCurrentProfilePictureBase64(profile), + IsNSFW: _isNsfw, + ProfilePictureBase64: null, + BannerPictureBase64: null, Description: null, - BannerPictureBase64: GetCurrentProfileBannerBase64(profile), - Tags: GetServerTagPayload())); + Tags: null)); } + UiSharedService.AttachToolTip("Apply the visibility toggles above."); + + if (!_isNsfwInitialized || !visibilityChanged) + ImGui.EndDisabled(); + + ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.SyncAlt, "Reset") && _isNsfwInitialized) + _isNsfw = _userServerIsNsfw; } private string? GetCurrentProfilePictureBase64(LightlessUserProfileData profile) @@ -932,7 +962,7 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase foreach (var ext in format.FileExtensions) { - if (SupportedImageExtensions.Contains(ext)) + if (_supportedImageExtensions.Contains(ext)) return true; } @@ -1183,7 +1213,7 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase }); } - private void DrawSection(string title, float scale, Action body) + private static void DrawSection(string title, float scale, Action body) { ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(6f, 4f) * scale); @@ -1199,9 +1229,8 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase } } - private bool DrawCheckboxRow(string label, bool currentValue, out bool newValue, string? tooltip = null) + private static bool DrawCheckboxRow(string label, bool currentValue, out bool newValue, string? tooltip = null) { - bool value = currentValue; bool changed = UiSharedService.CheckboxWithBorder(label, ref value, UIColors.Get("LightlessPurple"), 1.5f); if (!string.IsNullOrEmpty(tooltip)) @@ -1214,7 +1243,17 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase private void SyncProfileState(LightlessUserProfileData profile) { if (string.Equals(profile.Description, LoadingProfileDescription, StringComparison.Ordinal)) + { + _isNsfwInitialized = false; return; + } + + if (!_isNsfwInitialized) + { + _userServerIsNsfw = profile.IsNSFW; + _isNsfw = profile.IsNSFW; + _isNsfwInitialized = true; + } var profileBytes = profile.ImageData.Value; if (_pfpTextureWrap == null || !_profileImage.SequenceEqual(profileBytes)) @@ -1239,11 +1278,11 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase _descriptionText = _profileDescription; } - var serverTags = profile.Tags ?? Array.Empty(); + var serverTags = profile.Tags ?? []; if (!TagsEqual(serverTags, _profileTagIds)) { var previous = _profileTagIds; - _profileTagIds = serverTags.Count == 0 ? Array.Empty() : serverTags.ToArray(); + _profileTagIds = serverTags.Count == 0 ? [] : [.. serverTags]; if (TagsEqual(_tagEditorSelection, previous)) { diff --git a/LightlessSync/UI/StandaloneProfileUi.cs b/LightlessSync/UI/StandaloneProfileUi.cs index f2e805e..eb694aa 100644 --- a/LightlessSync/UI/StandaloneProfileUi.cs +++ b/LightlessSync/UI/StandaloneProfileUi.cs @@ -184,7 +184,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase var profile = _lightlessProfileManager.GetLightlessProfile(userData); IReadOnlyList profileTags = profile.Tags.Count > 0 ? ProfileTagService.ResolveTags(profile.Tags) - : Array.Empty(); + : []; if (_textureWrap == null || !profile.ImageData.Value.SequenceEqual(_lastProfilePicture)) { @@ -225,7 +225,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase bool directPair = false; bool youPaused = false; bool theyPaused = false; - List syncshellLines = new(); + List syncshellLines = []; if (!_isLightfinderContext && Pair != null) { @@ -245,7 +245,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase theyPaused = pairInfo.OtherPermissions.IsPaused(); } - if (pairInfo.Groups.Any()) + if (pairInfo.Groups.Count != 0) { foreach (var gid in pairInfo.Groups) { @@ -276,8 +276,11 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase presenceTokens.Add(new PresenceToken("They paused syncing", true)); } + if (profile.IsNSFW) + presenceTokens.Add(new PresenceToken("NSFW", Emphasis: true)); + if (syncshellLines.Count > 0) - presenceTokens.Add(new PresenceToken($"Sharing Syncshells ({syncshellLines.Count})", false, syncshellLines, "Shared Syncshells")); + presenceTokens.Add(new PresenceToken($"Sharing Syncshells ({syncshellLines.Count})", Emphasis: false, syncshellLines, "Shared Syncshells")); var drawList = ImGui.GetWindowDrawList(); var style = ImGui.GetStyle(); @@ -780,7 +783,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase }; if (profile.IsNsfw) - presenceTokens.Add(new PresenceToken("NSFW", true)); + presenceTokens.Add(new PresenceToken("NSFW", Emphasis: true)); int memberCount = 0; List? groupMembers = null; diff --git a/LightlessSync/UI/SyncshellAdminUI.cs b/LightlessSync/UI/SyncshellAdminUI.cs index 45b97a6..66765d4 100644 --- a/LightlessSync/UI/SyncshellAdminUI.cs +++ b/LightlessSync/UI/SyncshellAdminUI.cs @@ -12,7 +12,6 @@ using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Services.Profiles; using LightlessSync.UI.Services; -using LightlessSync.UI.Style; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; @@ -42,6 +41,11 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase 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) @@ -89,36 +93,147 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase _profileData = _lightlessProfileManager.GetLightlessGroupProfile(GroupFullInfo.Group); using var id = ImRaii.PushId("syncshell_admin_" + GroupFullInfo.GID); - using (_uiSharedService.UidFont.Push()) - { - var headerText = $"{GroupFullInfo.GroupAliasOrGID} Administrative Panel"; - _uiSharedService.UnderlinedBigText(headerText, UIColors.Get("LightlessBlue")); - } + 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(text: GroupFullInfo.Group.CreatedAt?.ToString("yyyy-MM-dd HH:mm:ss 'UTC'")); + ImGui.Text(GroupFullInfo.Group.CreatedAt?.ToString("yyyy-MM-dd HH:mm:ss 'UTC'") ?? "Unknown"); ImGui.EndTooltip(); } - ImGui.Separator(); - var perm = GroupFullInfo.GroupPermissions; + var titlePos = new Vector2(pMin.X + 12f * scale, pMin.Y + 8f * scale); + ImGui.SetCursorScreenPos(titlePos); - using var tabbar = ImRaii.TabBar("syncshell_tab_" + GroupFullInfo.GID); - - if (tabbar) + float titleHeight; + using (_uiSharedService.UidFont.Push()) { - DrawInvites(perm); - - DrawManagement(); - - DrawPermission(perm); - - DrawProfile(); + 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) @@ -218,6 +333,70 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase } } + 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"); @@ -268,167 +447,230 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase private void DrawManagement() { var mgmtTab = ImRaii.TabItem("User Management"); - if (mgmtTab) + if (!mgmtTab) + return; + + ImGuiHelpers.ScaledDummy(3f); + + var style = ImGui.GetStyle(); + + 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); + + 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 innerTabBar = ImRaii.TabBar("user_mgmt_inner_tab_" + GroupFullInfo.GID)) { - if (_uiSharedService.MediumTreeNode("User List & Administration", UIColors.Get("LightlessPurple"))) + if (innerTabBar) { - var snapshot = _pairUiService.GetSnapshot(); - if (!snapshot.GroupPairs.TryGetValue(GroupFullInfo, out var pairs)) + // Users tab + var usersTab = ImRaii.TabItem("Users"); + if (usersTab) { - UiSharedService.ColorTextWrapped("No users found in this Syncshell", ImGuiColors.DalamudYellow); - } - else - { - DrawUserListCustom(pairs, GroupFullInfo); + DrawUserListSection(); } + usersTab.Dispose(); - ImGui.TreePop(); + // Cleanup tab + var cleanupTab = ImRaii.TabItem("Cleanup"); + if (cleanupTab) + { + DrawMassCleanupSection(); + } + cleanupTab.Dispose(); + + // Bans tab + var bansTab = ImRaii.TabItem("Bans"); + if (bansTab) + { + DrawUserBansSection(); + } + bansTab.Dispose(); } - ImGui.Separator(); - - if (_uiSharedService.MediumTreeNode("Mass Cleanup", UIColors.Get("DimRed"))) - { - 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); - ImGui.Separator(); - 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) => - { - return 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."); - } - } - UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 1.5f); - ImGui.TreePop(); - } - ImGui.Separator(); - - if (_uiSharedService.MediumTreeNode("User Bans", UIColors.Get("LightlessYellow"))) - { - 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(); - } - UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); - ImGui.TreePop(); - } - ImGui.Separator(); } mgmtTab.Dispose(); } + 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"); @@ -476,6 +718,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase private void DrawUserListCustom(IReadOnlyList pairs, GroupFullInfoDto GroupFullInfo) { + // Search bar (unchanged) ImGui.PushItemWidth(0); _uiSharedService.IconText(FontAwesomeIcon.Search, UIColors.Get("LightlessPurple")); ImGui.SameLine(); @@ -511,21 +754,20 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase if (p.Value.Value.IsPinned()) return 2; return 10; }) - .ThenBy(p => p.Key.GetNote() ?? p.Key.UserData.AliasOrUID, StringComparer.OrdinalIgnoreCase); + .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.5f; + float colActions = fullW - colUid - colFlags - style.ItemSpacing.X * 2.0f; DrawUserListHeader(colUid, colFlags); - bool useScroll = pairs.Count > 10; - float childHeight = useScroll ? 260f * ImGuiHelpers.GlobalScale : 0f; - - ImGui.BeginChild("userListScroll#" + GroupFullInfo.Group.AliasOrGID, new Vector2(0, childHeight), true); - int rowIndex = 0; foreach (var kv in orderedPairs) { @@ -533,8 +775,8 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase var userInfoOpt = kv.Value; DrawUserRowCustom(pair, userInfoOpt, GroupFullInfo, rowIndex++, colUid, colFlags, colActions); } + ImGui.EndChild(); - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.0f); } private static void DrawUserListHeader(float colUid, float colFlags) @@ -544,18 +786,18 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessPurple")); - // Alias/UID/Note + // Alias / UID / Note ImGui.SetCursorPosX(x0); ImGui.TextUnformatted("Alias / UID / Note"); - // User Flags + // Flags ImGui.SameLine(); ImGui.SetCursorPosX(x0 + colUid + style.ItemSpacing.X); ImGui.TextUnformatted("Flags"); - // User Actions + // Actions ImGui.SameLine(); - ImGui.SetCursorPosX(x0 + colUid + colFlags + style.ItemSpacing.X * 2.5f); + ImGui.SetCursorPosX(x0 + colUid + colFlags + style.ItemSpacing.X * 2.0f); ImGui.TextUnformatted("Actions"); ImGui.PopStyleColor(); @@ -724,6 +966,27 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase } } + 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; diff --git a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Groups.cs b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Groups.cs index d212f6c..88264b9 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Groups.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Groups.cs @@ -151,6 +151,20 @@ public partial class ApiController .ConfigureAwait(false); } + public async Task GroupGetPruneSettings(GroupDto dto) + { + CheckConnection(); + return await _lightlessHub!.InvokeAsync(nameof(GroupGetPruneSettings), dto) + .ConfigureAwait(false); + } + + public async Task GroupSetPruneSettings(GroupPruneSettingsDto dto) + { + CheckConnection(); + await _lightlessHub!.SendAsync(nameof(GroupSetPruneSettings), dto) + .ConfigureAwait(false); + } + private void CheckConnection() { if (ServerState is not (ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting)) throw new InvalidDataException("Not connected"); diff --git a/LightlessSync/WebAPI/SignalR/HubFactory.cs b/LightlessSync/WebAPI/SignalR/HubFactory.cs index 1d5a0c8..9b008f0 100644 --- a/LightlessSync/WebAPI/SignalR/HubFactory.cs +++ b/LightlessSync/WebAPI/SignalR/HubFactory.cs @@ -71,6 +71,7 @@ public class HubFactory : MediatorSubscriberBase }; Logger.LogDebug("Building new HubConnection using transport {transport}", transportType); + var msgpackOptions = MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.Lz4Block).WithResolver(ContractlessStandardResolver.Instance); _instance = new HubConnectionBuilder() .WithUrl(_serverConfigurationManager.CurrentApiUrl + ILightlessHub.Path, options => @@ -80,22 +81,7 @@ public class HubFactory : MediatorSubscriberBase }) .AddMessagePackProtocol(opt => { - var resolver = CompositeResolver.Create(StandardResolverAllowPrivate.Instance, - BuiltinResolver.Instance, - AttributeFormatterResolver.Instance, - // replace enum resolver - DynamicEnumAsStringResolver.Instance, - DynamicGenericResolver.Instance, - DynamicUnionResolver.Instance, - DynamicObjectResolver.Instance, - PrimitiveObjectResolver.Instance, - // final fallback(last priority) - StandardResolver.Instance); - - opt.SerializerOptions = - MessagePackSerializerOptions.Standard - .WithCompression(MessagePackCompression.Lz4Block) - .WithResolver(resolver); + opt.SerializerOptions = msgpackOptions; }) .WithAutomaticReconnect(new ForeverRetryPolicy(Mediator)) .ConfigureLogging(a =>