Merge branch '2.0.0' into dotnet10-api14-migration

This commit is contained in:
defnotken
2025-11-26 16:20:38 -06:00
116 changed files with 20468 additions and 3311 deletions

View File

@@ -16,6 +16,8 @@ using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Components;
using LightlessSync.UI.Handlers;
using LightlessSync.UI.Models;
using LightlessSync.UI.Services;
using LightlessSync.UI.Style;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using LightlessSync.WebAPI.Files;
@@ -39,11 +41,12 @@ public class CompactUi : WindowMediatorSubscriberBase
private readonly ApiController _apiController;
private readonly LightlessConfigService _configService;
private readonly LightlessMediator _lightlessMediator;
private readonly PairLedger _pairLedger;
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
private readonly DrawEntityFactory _drawEntityFactory;
private readonly FileUploadManager _fileTransferManager;
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
private readonly PairManager _pairManager;
private readonly PairUiService _pairUiService;
private readonly SelectTagForPairUi _selectTagForPairUi;
private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi;
private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi;
@@ -66,13 +69,15 @@ public class CompactUi : WindowMediatorSubscriberBase
private float _transferPartHeight;
private bool _wasOpen;
private float _windowContentWidth;
private readonly SeluneBrush _seluneBrush = new();
private const float ConnectButtonHighlightThickness = 14f;
public CompactUi(
ILogger<CompactUi> logger,
UiSharedService uiShared,
LightlessConfigService configService,
ApiController apiController,
PairManager pairManager,
PairUiService pairUiService,
ServerConfigurationManager serverManager,
LightlessMediator mediator,
FileUploadManager fileTransferManager,
@@ -88,12 +93,12 @@ public class CompactUi : WindowMediatorSubscriberBase
IpcManager ipcManager,
BroadcastService broadcastService,
CharacterAnalyzer characterAnalyzer,
PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService, PairLedger pairLedger) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
{
_uiSharedService = uiShared;
_configService = configService;
_apiController = apiController;
_pairManager = pairManager;
_pairUiService = pairUiService;
_serverManager = serverManager;
_fileTransferManager = fileTransferManager;
_tagHandler = tagHandler;
@@ -106,7 +111,8 @@ public class CompactUi : WindowMediatorSubscriberBase
_renamePairTagUi = renameTagUi;
_ipcManager = ipcManager;
_broadcastService = broadcastService;
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService);
_pairLedger = pairLedger;
_tabMenu = new TopTabMenu(Mediator, _apiController, _uiSharedService, pairRequestService, dalamudUtilService, lightlessNotificationService);
AllowPinning = true;
AllowClickthrough = false;
@@ -177,6 +183,11 @@ public class CompactUi : WindowMediatorSubscriberBase
protected override void DrawInternal()
{
var drawList = ImGui.GetWindowDrawList();
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
using var selune = Selune.Begin(_seluneBrush, drawList, windowPos, windowSize);
_windowContentWidth = UiSharedService.GetWindowContentRegionWidth();
if (!_apiController.IsCurrentVersion)
{
@@ -223,30 +234,48 @@ public class CompactUi : WindowMediatorSubscriberBase
}
using (ImRaii.PushId("header")) DrawUIDHeader();
UiSharedService.RoundedSeparator(UIColors.Get("LightlessPurple"), 2.5f, 1f, 12f);
using (ImRaii.PushId("serverstatus")) DrawServerStatus();
_uiSharedService.RoundedSeparator(UIColors.Get("LightlessPurple"), 2.5f, 1f, 12f);
using (ImRaii.PushId("serverstatus"))
{
DrawServerStatus();
}
selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
var style = ImGui.GetStyle();
var contentMinY = windowPos.Y + ImGui.GetWindowContentRegionMin().Y;
var gradientInset = 4f * ImGuiHelpers.GlobalScale;
var gradientTop = MathF.Max(contentMinY, ImGui.GetCursorScreenPos().Y - style.ItemSpacing.Y + gradientInset);
ImGui.Separator();
if (_apiController.ServerState is ServerState.Connected)
{
using (ImRaii.PushId("global-topmenu")) _tabMenu.Draw();
var pairSnapshot = _pairUiService.GetSnapshot();
using (ImRaii.PushId("global-topmenu")) _tabMenu.Draw(pairSnapshot);
using (ImRaii.PushId("pairlist")) DrawPairs();
ImGui.Separator();
var transfersTop = ImGui.GetCursorScreenPos().Y;
var gradientBottom = MathF.Max(gradientTop, transfersTop - style.ItemSpacing.Y - gradientInset);
selune.DrawGradient(gradientTop, gradientBottom, ImGui.GetIO().DeltaTime);
float pairlistEnd = ImGui.GetCursorPosY();
using (ImRaii.PushId("transfers")) DrawTransfers();
_transferPartHeight = ImGui.GetCursorPosY() - pairlistEnd - ImGui.GetTextLineHeight();
using (ImRaii.PushId("group-pair-popup")) _selectPairsForGroupUi.Draw(_pairManager.DirectPairs);
using (ImRaii.PushId("group-syncshell-popup")) _selectSyncshellForTagUi.Draw([.. _pairManager.Groups.Values]);
using (ImRaii.PushId("group-pair-popup")) _selectPairsForGroupUi.Draw(pairSnapshot.DirectPairs);
using (ImRaii.PushId("group-syncshell-popup")) _selectSyncshellForTagUi.Draw(pairSnapshot.Groups);
using (ImRaii.PushId("group-pair-edit")) _renamePairTagUi.Draw();
using (ImRaii.PushId("group-syncshell-edit")) _renameSyncshellTagUi.Draw();
using (ImRaii.PushId("grouping-pair-popup")) _selectTagForPairUi.Draw();
using (ImRaii.PushId("grouping-syncshell-popup")) _selectTagForSyncshellUi.Draw();
}
if (_configService.Current.OpenPopupOnAdd && _pairManager.LastAddedUser != null)
else
{
_lastAddedUser = _pairManager.LastAddedUser;
_pairManager.LastAddedUser = null;
selune.Animate(ImGui.GetIO().DeltaTime);
}
var lastAddedPair = _pairUiService.GetLastAddedPair();
if (_configService.Current.OpenPopupOnAdd && lastAddedPair is not null)
{
_lastAddedUser = lastAddedPair;
_pairUiService.ClearLastAddedPair();
ImGui.OpenPopup("Set Notes for New User");
_showModalForUserAddition = true;
_lastAddedUserComment = string.Empty;
@@ -288,17 +317,20 @@ public class CompactUi : WindowMediatorSubscriberBase
{
float ySize = Math.Abs(_transferPartHeight) < 0.0001f
? 1
: ((ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y
+ ImGui.GetTextLineHeight() - ImGui.GetStyle().WindowPadding.Y - ImGui.GetStyle().WindowBorderSize) - _transferPartHeight - ImGui.GetCursorPosY());
ImGui.BeginChild("list", new Vector2(_windowContentWidth, ySize), border: false);
: (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y
+ ImGui.GetTextLineHeight() - ImGui.GetStyle().WindowPadding.Y - ImGui.GetStyle().WindowBorderSize) - _transferPartHeight - ImGui.GetCursorPosY();
foreach (var item in _drawFolders)
if (ImGui.BeginChild("list", new Vector2(_windowContentWidth, ySize), border: false))
{
item.Draw();
foreach (var item in _drawFolders)
{
item.Draw();
}
}
ImGui.EndChild();
}
private void DrawServerStatus()
{
var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Link);
@@ -371,6 +403,19 @@ public class CompactUi : WindowMediatorSubscriberBase
}
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(
ImGui.GetItemRectMin(),
ImGui.GetItemRectMax(),
SeluneHighlightMode.Both,
borderOnly: true,
borderThicknessOverride: ConnectButtonHighlightThickness,
exactSize: true,
clipToElement: true,
roundingOverride: ImGui.GetStyle().FrameRounding);
}
UiSharedService.AttachToolTip(isConnectingOrConnected ? "Disconnect from " + _serverManager.CurrentServer.ServerName : "Connect to " + _serverManager.CurrentServer.ServerName);
}
}
@@ -528,6 +573,17 @@ public class CompactUi : WindowMediatorSubscriberBase
if (ImGui.IsItemHovered())
{
var padding = new Vector2(10f * ImGuiHelpers.GlobalScale);
Selune.RegisterHighlight(
ImGui.GetItemRectMin() - padding,
ImGui.GetItemRectMax() + padding,
SeluneHighlightMode.Point,
exactSize: true,
clipToElement: true,
clipPadding: padding,
highlightColorOverride: UIColors.Get("LightlessGreen"),
highlightAlphaOverride: 0.2f);
ImGui.BeginTooltip();
ImGui.PushTextWrapPos(ImGui.GetFontSize() * 32f);
@@ -604,6 +660,20 @@ public class CompactUi : WindowMediatorSubscriberBase
}
}
if (ImGui.IsItemHovered())
{
var padding = new Vector2(35f * ImGuiHelpers.GlobalScale);
Selune.RegisterHighlight(
ImGui.GetItemRectMin() - padding,
ImGui.GetItemRectMax() + padding,
SeluneHighlightMode.Point,
exactSize: true,
clipToElement: true,
clipPadding: padding,
highlightColorOverride: vanityGlowColor,
highlightAlphaOverride: 0.05f);
}
headerItemClicked = ImGui.IsItemClicked();
if (headerItemClicked)
@@ -676,6 +746,20 @@ public class CompactUi : WindowMediatorSubscriberBase
ImGui.TextColored(GetUidColor(), _apiController.UID);
}
if (ImGui.IsItemHovered())
{
var padding = new Vector2(30f * ImGuiHelpers.GlobalScale);
Selune.RegisterHighlight(
ImGui.GetItemRectMin() - padding,
ImGui.GetItemRectMax() + padding,
SeluneHighlightMode.Point,
exactSize: true,
clipToElement: true,
clipPadding: padding,
highlightColorOverride: vanityGlowColor,
highlightAlphaOverride: 0.05f);
}
bool uidFooterClicked = ImGui.IsItemClicked();
UiSharedService.AttachToolTip("Click to copy");
if (uidFooterClicked)
@@ -697,28 +781,45 @@ public class CompactUi : WindowMediatorSubscriberBase
var drawFolders = new List<IDrawFolder>();
var filter = _tabMenu.Filter;
var allPairs = _pairManager.PairsWithGroups.ToDictionary(k => k.Key, k => k.Value);
var filteredPairs = allPairs.Where(p => PassesFilter(p.Key, filter)).ToDictionary(k => k.Key, k => k.Value);
var allEntries = _drawEntityFactory.GetAllEntries().ToList();
var filteredEntries = string.IsNullOrEmpty(filter)
? allEntries
: allEntries.Where(e => PassesFilter(e, filter)).ToList();
var syncshells = _pairLedger.GetAllSyncshells();
var groupInfos = syncshells.Values
.Select(s => s.GroupFullInfo)
.OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase)
.ToList();
var entryLookup = allEntries.ToDictionary(e => e.DisplayEntry.Ident.UserId, StringComparer.Ordinal);
var filteredEntryLookup = filteredEntries.ToDictionary(e => e.DisplayEntry.Ident.UserId, StringComparer.Ordinal);
//Filter of online/visible pairs
if (_configService.Current.ShowVisibleUsersSeparately)
{
var allVisiblePairs = ImmutablePairList(allPairs.Where(p => FilterVisibleUsers(p.Key)));
var filteredVisiblePairs = BasicSortedDictionary(filteredPairs.Where(p => FilterVisibleUsers(p.Key)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomVisibleTag, filteredVisiblePairs, allVisiblePairs));
var allVisiblePairs = SortVisibleEntries(allEntries.Where(FilterVisibleUsers));
if (allVisiblePairs.Count > 0)
{
var filteredVisiblePairs = SortVisibleEntries(filteredEntries.Where(FilterVisibleUsers));
drawFolders.Add(_drawEntityFactory.CreateTagFolder(TagHandler.CustomVisibleTag, filteredVisiblePairs, allVisiblePairs));
}
}
//Filter of not foldered syncshells
var groupFolders = new List<GroupFolder>();
foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase))
foreach (var group in groupInfos)
{
GetGroups(allPairs, filteredPairs, group, out ImmutableList<Pair> allGroupPairs, out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs);
if (FilterNotTaggedSyncshells(group))
if (!FilterNotTaggedSyncshells(group))
{
groupFolders.Add(new GroupFolder(group, _drawEntityFactory.CreateDrawGroupFolder(group, filteredGroupPairs, allGroupPairs)));
continue;
}
var allGroupEntries = ResolveGroupEntries(entryLookup, syncshells, group, applyFilters: false);
var filteredGroupEntries = ResolveGroupEntries(filteredEntryLookup, syncshells, group, applyFilters: true);
// Always create the folder so empty syncshells remain visible in the UI.
var drawGroupFolder = _drawEntityFactory.CreateGroupFolder(group.Group.GID, group, filteredGroupEntries, allGroupEntries);
groupFolders.Add(new GroupFolder(group, drawGroupFolder));
}
//Filter of grouped up syncshells (All Syncshells Folder)
@@ -731,123 +832,215 @@ public class CompactUi : WindowMediatorSubscriberBase
//Filter of grouped/foldered pairs
foreach (var tag in _tagHandler.GetAllPairTagsSorted())
{
var allTagPairs = ImmutablePairList(allPairs.Where(p => FilterTagUsers(p.Key, tag)));
var filteredTagPairs = BasicSortedDictionary(filteredPairs.Where(p => FilterTagUsers(p.Key, tag) && FilterOnlineOrPausedSelf(p.Key)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(tag, filteredTagPairs, allTagPairs));
var allTagPairs = SortEntries(allEntries.Where(e => FilterTagUsers(e, tag)));
if (allTagPairs.Count > 0)
{
var filteredTagPairs = SortEntries(filteredEntries.Where(e => FilterTagUsers(e, tag) && FilterOnlineOrPausedSelf(e)));
drawFolders.Add(_drawEntityFactory.CreateTagFolder(tag, filteredTagPairs, allTagPairs));
}
}
//Filter of grouped/foldered syncshells
foreach (var syncshellTag in _tagHandler.GetAllSyncshellTagsSorted())
{
var syncshellFolderTags = new List<GroupFolder>();
foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase))
foreach (var group in groupInfos)
{
if (_tagHandler.HasSyncshellTag(group.GID, syncshellTag))
if (!_tagHandler.HasSyncshellTag(group.Group.GID, syncshellTag))
{
GetGroups(allPairs, filteredPairs, group,
out ImmutableList<Pair> allGroupPairs,
out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs);
syncshellFolderTags.Add(new GroupFolder(group, _drawEntityFactory.CreateDrawGroupFolder($"tag_{group.GID}", group, filteredGroupPairs, allGroupPairs)));
continue;
}
var allGroupEntries = ResolveGroupEntries(entryLookup, syncshells, group, applyFilters: false);
var filteredGroupEntries = ResolveGroupEntries(filteredEntryLookup, syncshells, group, applyFilters: true);
// Keep tagged syncshells rendered regardless of whether membership info has loaded.
var taggedGroupFolder = _drawEntityFactory.CreateGroupFolder($"tag_{group.Group.GID}", group, filteredGroupEntries, allGroupEntries);
syncshellFolderTags.Add(new GroupFolder(group, taggedGroupFolder));
}
drawFolders.Add(new DrawGroupedGroupFolder(syncshellFolderTags, _tagHandler, _apiController, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, syncshellTag));
}
//Filter of not grouped/foldered and offline pairs
var allOnlineNotTaggedPairs = ImmutablePairList(allPairs.Where(p => FilterNotTaggedUsers(p.Key)));
var onlineNotTaggedPairs = BasicSortedDictionary(filteredPairs.Where(p => FilterNotTaggedUsers(p.Key) && FilterOnlineOrPausedSelf(p.Key)));
var allOnlineNotTaggedPairs = SortEntries(allEntries.Where(FilterNotTaggedUsers));
var onlineNotTaggedPairs = SortEntries(filteredEntries.Where(e => FilterNotTaggedUsers(e) && FilterOnlineOrPausedSelf(e)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder((_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag), onlineNotTaggedPairs, allOnlineNotTaggedPairs));
if (allOnlineNotTaggedPairs.Count > 0)
{
drawFolders.Add(_drawEntityFactory.CreateTagFolder(
_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag,
onlineNotTaggedPairs,
allOnlineNotTaggedPairs));
}
if (_configService.Current.ShowOfflineUsersSeparately)
{
var allOfflinePairs = ImmutablePairList(allPairs.Where(p => FilterOfflineUsers(p.Key, p.Value)));
var filteredOfflinePairs = BasicSortedDictionary(filteredPairs.Where(p => FilterOfflineUsers(p.Key, p.Value)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineTag, filteredOfflinePairs, allOfflinePairs));
var allOfflinePairs = SortEntries(allEntries.Where(FilterOfflineUsers));
if (allOfflinePairs.Count > 0)
{
var filteredOfflinePairs = SortEntries(filteredEntries.Where(FilterOfflineUsers));
drawFolders.Add(_drawEntityFactory.CreateTagFolder(TagHandler.CustomOfflineTag, filteredOfflinePairs, allOfflinePairs));
}
if (_configService.Current.ShowSyncshellOfflineUsersSeparately)
{
var allOfflineSyncshellUsers = ImmutablePairList(allPairs.Where(p => FilterOfflineSyncshellUsers(p.Key)));
var filteredOfflineSyncshellUsers = BasicSortedDictionary(filteredPairs.Where(p => FilterOfflineSyncshellUsers(p.Key)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineSyncshellTag, filteredOfflineSyncshellUsers, allOfflineSyncshellUsers));
var allOfflineSyncshellUsers = SortEntries(allEntries.Where(FilterOfflineSyncshellUsers));
if (allOfflineSyncshellUsers.Count > 0)
{
var filteredOfflineSyncshellUsers = SortEntries(filteredEntries.Where(FilterOfflineSyncshellUsers));
drawFolders.Add(_drawEntityFactory.CreateTagFolder(TagHandler.CustomOfflineSyncshellTag, filteredOfflineSyncshellUsers, allOfflineSyncshellUsers));
}
}
}
//Unpaired
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomUnpairedTag,
BasicSortedDictionary(filteredPairs.Where(p => p.Key.IsOneSidedPair)),
ImmutablePairList(allPairs.Where(p => p.Key.IsOneSidedPair))));
//Unpaired
var unpairedAllEntries = SortEntries(allEntries.Where(e => e.IsOneSided));
if (unpairedAllEntries.Count > 0)
{
var unpairedFiltered = SortEntries(filteredEntries.Where(e => e.IsOneSided));
drawFolders.Add(_drawEntityFactory.CreateTagFolder(TagHandler.CustomUnpairedTag, unpairedFiltered, unpairedAllEntries));
}
return drawFolders;
}
}
private static bool PassesFilter(Pair pair, string filter)
private bool PassesFilter(PairUiEntry entry, string filter)
{
if (string.IsNullOrEmpty(filter)) return true;
return pair.UserData.AliasOrUID.Contains(filter, StringComparison.OrdinalIgnoreCase) || (pair.GetNote()?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false) || (pair.PlayerName?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false);
return entry.AliasOrUid.Contains(filter, StringComparison.OrdinalIgnoreCase)
|| (!string.IsNullOrEmpty(entry.Note) && entry.Note.Contains(filter, StringComparison.OrdinalIgnoreCase))
|| (!string.IsNullOrEmpty(entry.DisplayName) && entry.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase));
}
private string AlphabeticalSortKey(Pair pair)
private string AlphabeticalSortKey(PairUiEntry entry)
{
if (_configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(pair.PlayerName))
if (_configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(entry.DisplayName))
{
return _configService.Current.PreferNotesOverNamesForVisible ? (pair.GetNote() ?? string.Empty) : pair.PlayerName;
return _configService.Current.PreferNotesOverNamesForVisible ? (entry.Note ?? string.Empty) : entry.DisplayName;
}
return pair.GetNote() ?? pair.UserData.AliasOrUID;
return !string.IsNullOrEmpty(entry.Note) ? entry.Note : entry.AliasOrUid;
}
private bool FilterOnlineOrPausedSelf(Pair pair) => pair.IsOnline || (!pair.IsOnline && !_configService.Current.ShowOfflineUsersSeparately) || pair.UserPair.OwnPermissions.IsPaused();
private bool FilterOnlineOrPausedSelf(PairUiEntry entry) => entry.IsOnline || (!entry.IsOnline && !_configService.Current.ShowOfflineUsersSeparately) || entry.SelfPermissions.IsPaused();
private bool FilterVisibleUsers(Pair pair) => pair.IsVisible && (_configService.Current.ShowSyncshellUsersInVisible || pair.IsDirectlyPaired);
private bool FilterVisibleUsers(PairUiEntry entry) => entry.IsVisible && entry.IsOnline && (_configService.Current.ShowSyncshellUsersInVisible || entry.IsDirectlyPaired);
private bool FilterTagUsers(Pair pair, string tag) => pair.IsDirectlyPaired && !pair.IsOneSidedPair && _tagHandler.HasPairTag(pair.UserData.UID, tag);
private bool FilterTagUsers(PairUiEntry entry, string tag) => entry.IsDirectlyPaired && !entry.IsOneSided && _tagHandler.HasPairTag(entry.DisplayEntry.Ident.UserId, tag);
private static bool FilterGroupUsers(List<GroupFullInfoDto> groups, GroupFullInfoDto group) => groups.Exists(g => string.Equals(g.GID, group.GID, StringComparison.Ordinal));
private bool FilterNotTaggedUsers(Pair pair) => pair.IsDirectlyPaired && !pair.IsOneSidedPair && !_tagHandler.HasAnyPairTag(pair.UserData.UID);
private bool FilterNotTaggedUsers(PairUiEntry entry) => entry.IsDirectlyPaired && !entry.IsOneSided && !_tagHandler.HasAnyPairTag(entry.DisplayEntry.Ident.UserId);
private bool FilterNotTaggedSyncshells(GroupFullInfoDto group) => !_tagHandler.HasAnySyncshellTag(group.GID) || _configService.Current.ShowGroupedSyncshellsInAll;
private bool FilterOfflineUsers(Pair pair, List<GroupFullInfoDto> groups) => ((pair.IsDirectlyPaired && _configService.Current.ShowSyncshellOfflineUsersSeparately) || !_configService.Current.ShowSyncshellOfflineUsersSeparately) && (!pair.IsOneSidedPair || groups.Count != 0) && !pair.IsOnline && !pair.UserPair.OwnPermissions.IsPaused();
private static bool FilterOfflineSyncshellUsers(Pair pair) => !pair.IsDirectlyPaired && !pair.IsOnline && !pair.UserPair.OwnPermissions.IsPaused();
private Dictionary<Pair, List<GroupFullInfoDto>> BasicSortedDictionary(IEnumerable<KeyValuePair<Pair, List<GroupFullInfoDto>>> pairs) => pairs.OrderByDescending(u => u.Key.IsVisible).ThenByDescending(u => u.Key.IsOnline).ThenBy(u => AlphabeticalSortKey(u.Key), StringComparer.OrdinalIgnoreCase).ToDictionary(u => u.Key, u => u.Value);
private static ImmutableList<Pair> ImmutablePairList(IEnumerable<KeyValuePair<Pair, List<GroupFullInfoDto>>> pairs) => [.. pairs.Select(k => k.Key)];
private void GetGroups(Dictionary<Pair, List<GroupFullInfoDto>> allPairs,
Dictionary<Pair, List<GroupFullInfoDto>> filteredPairs,
GroupFullInfoDto group,
out ImmutableList<Pair> allGroupPairs,
out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs)
private bool FilterOfflineUsers(PairUiEntry entry)
{
allGroupPairs = ImmutablePairList(allPairs
.Where(u => FilterGroupUsers(u.Value, group)));
var groups = entry.DisplayEntry.Groups;
var includeDirect = _configService.Current.ShowSyncshellOfflineUsersSeparately ? entry.IsDirectlyPaired : true;
var includeGroup = !entry.IsOneSided || groups.Count != 0;
return includeDirect && includeGroup && !entry.IsOnline && !entry.SelfPermissions.IsPaused();
}
filteredGroupPairs = filteredPairs
.Where(u => FilterGroupUsers(u.Value, group) && FilterOnlineOrPausedSelf(u.Key))
.OrderByDescending(u => u.Key.IsOnline)
.ThenBy(u =>
{
if (string.Equals(u.Key.UserData.UID, group.OwnerUID, StringComparison.Ordinal)) return 0;
if (group.GroupPairUserInfos.TryGetValue(u.Key.UserData.UID, out var info))
{
if (info.IsModerator()) return 1;
if (info.IsPinned()) return 2;
}
return u.Key.IsVisible ? 3 : 4;
})
.ThenBy(u => AlphabeticalSortKey(u.Key), StringComparer.OrdinalIgnoreCase)
.ToDictionary(k => k.Key, k => k.Value);
private static bool FilterOfflineSyncshellUsers(PairUiEntry entry) => !entry.IsDirectlyPaired && !entry.IsOnline && !entry.SelfPermissions.IsPaused();
private ImmutableList<PairUiEntry> SortEntries(IEnumerable<PairUiEntry> entries)
{
return entries
.OrderByDescending(e => e.IsVisible)
.ThenByDescending(e => e.IsOnline)
.ThenBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)
.ToImmutableList();
}
private ImmutableList<PairUiEntry> SortVisibleEntries(IEnumerable<PairUiEntry> entries)
{
var entryList = entries.ToList();
return _configService.Current.VisiblePairSortMode switch
{
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.PreferredDirectPairs => SortVisibleByPreferred(entryList),
_ => SortEntries(entryList),
};
}
private ImmutableList<PairUiEntry> SortVisibleByMetric(IEnumerable<PairUiEntry> entries, Func<PairUiEntry, long> selector)
{
return entries
.OrderByDescending(entry => selector(entry) >= 0)
.ThenByDescending(selector)
.ThenByDescending(entry => entry.IsOnline)
.ThenBy(entry => AlphabeticalSortKey(entry), StringComparer.OrdinalIgnoreCase)
.ToImmutableList();
}
private ImmutableList<PairUiEntry> SortVisibleByPreferred(IEnumerable<PairUiEntry> 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();
}
private ImmutableList<PairUiEntry> SortGroupEntries(IEnumerable<PairUiEntry> entries, GroupFullInfoDto group)
{
return entries
.OrderByDescending(e => e.IsOnline)
.ThenBy(e => GroupSortWeight(e, group))
.ThenBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)
.ToImmutableList();
}
private int GroupSortWeight(PairUiEntry entry, GroupFullInfoDto group)
{
if (string.Equals(entry.DisplayEntry.Ident.UserId, group.OwnerUID, StringComparison.Ordinal))
{
return 0;
}
if (group.GroupPairUserInfos.TryGetValue(entry.DisplayEntry.Ident.UserId, out var info))
{
if (info.IsModerator()) return 1;
if (info.IsPinned()) return 2;
}
return entry.IsVisible ? 3 : 4;
}
private ImmutableList<PairUiEntry> ResolveGroupEntries(
IReadOnlyDictionary<string, PairUiEntry> entryLookup,
IReadOnlyDictionary<string, Syncshell> syncshells,
GroupFullInfoDto group,
bool applyFilters)
{
if (!syncshells.TryGetValue(group.Group.GID, out var shell))
{
return ImmutableList<PairUiEntry>.Empty;
}
var entries = shell.Users.Keys
.Select(id => entryLookup.TryGetValue(id, out var entry) ? entry : null)
.Where(entry => entry is not null)
.Cast<PairUiEntry>();
if (applyFilters && _configService.Current.ShowOfflineUsersSeparately)
{
entries = entries.Where(entry => !FilterOfflineUsers(entry));
}
if (applyFilters && _configService.Current.ShowSyncshellOfflineUsersSeparately)
{
entries = entries.Where(entry => !FilterOfflineSyncshellUsers(entry));
}
return SortGroupEntries(entries, group);
}
private string GetServerError()