2.0.0 #92

Merged
defnotken merged 171 commits from 2.0.0 into master 2025-12-21 17:19:36 +00:00
12 changed files with 684 additions and 344 deletions
Showing only changes of commit 675918624d - Show all commits

View File

@@ -379,7 +379,8 @@ public sealed class PairManager
dto.GroupPermissions,
shell.GroupFullInfo.GroupUserPermissions,
shell.GroupFullInfo.GroupUserInfo,
new Dictionary<string, GroupPairUserInfo>(shell.GroupFullInfo.GroupPairUserInfos, StringComparer.Ordinal));
new Dictionary<string, GroupPairUserInfo>(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<string, GroupPairUserInfo>(StringComparer.Ordinal));
new Dictionary<string, GroupPairUserInfo>(StringComparer.Ordinal),
0);
shell = new Syncshell(placeholder);
_groups[group.GID] = shell;

View File

@@ -23,7 +23,7 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
private readonly List<UserData> _previouslyVisiblePlayers = [];
private Task<CharacterData>? _fileUploadTask = null;
private readonly HashSet<UserData> _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<VisibleUserDataDistributor> 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<UserData> GetVisibleUsers()
{
return _pairLedger.GetVisiblePairs()
.Select(connection => connection.User)
.ToList();
}
private List<UserData> GetVisibleUsers() => [.. _pairLedger.GetVisiblePairs().Select(connection => connection.User)];
}

View File

@@ -957,11 +957,10 @@ public class CompactUi : WindowMediatorSubscriberBase
private ImmutableList<PairUiEntry> SortEntries(IEnumerable<PairUiEntry> 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<PairUiEntry> SortVisibleEntries(IEnumerable<PairUiEntry> 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<PairUiEntry> SortVisibleByMetric(IEnumerable<PairUiEntry> entries, Func<PairUiEntry, long> 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<PairUiEntry> SortVisibleByPreferred(IEnumerable<PairUiEntry> 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<PairUiEntry> SortGroupEntries(IEnumerable<PairUiEntry> 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)

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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<Rgba32>(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<Rgba32>(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;
}
}
}

View File

@@ -43,7 +43,7 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase
"<bold(1|0)> <italic(1|0)> <shadow(1|0)> <edge(1|0)> - toggle style flags.\n" +
"<link(0x0E,...)> - create clickable links.";
private static readonly HashSet<string> SupportedImageExtensions = new(StringComparer.OrdinalIgnoreCase)
private static readonly HashSet<string> _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<int> _tagEditorSelection = new();
private readonly List<int> _tagEditorSelection = [];
private int[] _profileTagIds = [];
private readonly List<SeStringUtils.SeStringSegment> _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<int>();
var serverTags = profile.Tags ?? [];
if (!TagsEqual(serverTags, _profileTagIds))
{
var previous = _profileTagIds;
_profileTagIds = serverTags.Count == 0 ? Array.Empty<int>() : serverTags.ToArray();
_profileTagIds = serverTags.Count == 0 ? [] : [.. serverTags];
if (TagsEqual(_tagEditorSelection, previous))
{

View File

@@ -184,7 +184,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
var profile = _lightlessProfileManager.GetLightlessProfile(userData);
IReadOnlyList<ProfileTagDefinition> profileTags = profile.Tags.Count > 0
? ProfileTagService.ResolveTags(profile.Tags)
: Array.Empty<ProfileTagDefinition>();
: [];
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<string> syncshellLines = new();
List<string> 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<Pair>? groupMembers = null;

View File

@@ -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<int>? _pruneTask;
private int _pruneDays = 14;
private Task<GroupPruneSettingsDto>? _pruneSettingsTask;
private bool _pruneSettingsLoaded;
private bool _autoPruneEnabled;
private int _autoPruneDays = 14;
public SyncshellAdminUI(ILogger<SyncshellAdminUI> 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<Pair> 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;

View File

@@ -151,6 +151,20 @@ public partial class ApiController
.ConfigureAwait(false);
}
public async Task<GroupPruneSettingsDto> GroupGetPruneSettings(GroupDto dto)
{
CheckConnection();
return await _lightlessHub!.InvokeAsync<GroupPruneSettingsDto>(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");

View File

@@ -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 =>