using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; using LightlessSync.Interop.Ipc; using LightlessSync.LightlessConfiguration; using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; using LightlessSync.UI.Components; using LightlessSync.UI.Handlers; using LightlessSync.Utils; using LightlessSync.WebAPI; using LightlessSync.WebAPI.Files; using LightlessSync.WebAPI.Files.Models; using LightlessSync.WebAPI.SignalR.Utils; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; using System.Collections.Immutable; using System.Globalization; using System.Numerics; using System.Reflection; namespace LightlessSync.UI; public class CompactUi : WindowMediatorSubscriberBase { private readonly CharacterAnalyzer _characterAnalyzer; private readonly ApiController _apiController; private readonly LightlessConfigService _configService; private readonly LightlessMediator _lightlessMediator; private readonly ConcurrentDictionary> _currentDownloads = new(); private readonly DrawEntityFactory _drawEntityFactory; private readonly FileUploadManager _fileTransferManager; private readonly PlayerPerformanceConfigService _playerPerformanceConfig; private readonly PairManager _pairManager; private readonly SelectTagForPairUi _selectTagForPairUi; private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi; private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi; private readonly RenameSyncshellTagUi _renameSyncshellTagUi; private readonly SelectPairForTagUi _selectPairsForGroupUi; private readonly RenamePairTagUi _renamePairTagUi; private readonly IpcManager _ipcManager; private readonly ServerConfigurationManager _serverManager; private readonly TopTabMenu _tabMenu; private readonly TagHandler _tagHandler; private readonly UiSharedService _uiSharedService; private List _drawFolders; private Dictionary>? _cachedAnalysis; private Pair? _lastAddedUser; private string _lastAddedUserComment = string.Empty; private Vector2 _lastPosition = Vector2.One; private Vector2 _lastSize = Vector2.One; private bool _showModalForUserAddition; private float _transferPartHeight; private bool _wasOpen; private float _windowContentWidth; public CompactUi(ILogger logger, UiSharedService uiShared, LightlessConfigService configService, ApiController apiController, PairManager pairManager, ServerConfigurationManager serverManager, LightlessMediator mediator, FileUploadManager fileTransferManager, TagHandler tagHandler, DrawEntityFactory drawEntityFactory, SelectTagForPairUi selectTagForPairUi, SelectPairForTagUi selectPairForTagUi, RenamePairTagUi renameTagUi, SelectTagForSyncshellUi selectTagForSyncshellUi, SelectSyncshellForTagUi selectSyncshellForTagUi, RenameSyncshellTagUi renameSyncshellTagUi, PerformanceCollectorService performanceCollectorService, IpcManager ipcManager, CharacterAnalyzer characterAnalyzer, PlayerPerformanceConfigService playerPerformanceConfig, LightlessMediator lightlessMediator) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService) { _uiSharedService = uiShared; _configService = configService; _apiController = apiController; _pairManager = pairManager; _serverManager = serverManager; _fileTransferManager = fileTransferManager; _tagHandler = tagHandler; _drawEntityFactory = drawEntityFactory; _selectTagForPairUi = selectTagForPairUi; _selectTagForSyncshellUi = selectTagForSyncshellUi; _selectSyncshellForTagUi = selectSyncshellForTagUi; _renameSyncshellTagUi = renameSyncshellTagUi; _selectPairsForGroupUi = selectPairForTagUi; _renamePairTagUi = renameTagUi; _ipcManager = ipcManager; _tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService); AllowPinning = true; AllowClickthrough = false; TitleBarButtons = new() { new TitleBarButton() { Icon = FontAwesomeIcon.Cog, Click = (msg) => { Mediator.Publish(new UiToggleMessage(typeof(SettingsUi))); }, IconOffset = new(2,1), ShowTooltip = () => { ImGui.BeginTooltip(); ImGui.Text("Open Lightless Settings"); ImGui.EndTooltip(); } }, new TitleBarButton() { Icon = FontAwesomeIcon.Book, Click = (msg) => { Mediator.Publish(new UiToggleMessage(typeof(EventViewerUI))); }, IconOffset = new(2,1), ShowTooltip = () => { ImGui.BeginTooltip(); ImGui.Text("Open Lightless Event Viewer"); ImGui.EndTooltip(); } } }; _drawFolders = [.. GetDrawFolders()]; #if DEBUG string dev = "Dev Build"; var ver = Assembly.GetExecutingAssembly().GetName().Version!; WindowName = $"Lightless Sync {dev} ({ver.Major}.{ver.Minor}.{ver.Build})###LightlessSyncMainUI"; Toggle(); #else var ver = Assembly.GetExecutingAssembly().GetName().Version; WindowName = "Lightless Sync " + ver.Major + "." + ver.Minor + "." + ver.Build + "###LightlessSyncMainUI"; #endif Mediator.Subscribe(this, (_) => IsOpen = true); Mediator.Subscribe(this, (_) => IsOpen = false); Mediator.Subscribe(this, (_) => UiSharedService_GposeStart()); Mediator.Subscribe(this, (_) => UiSharedService_GposeEnd()); Mediator.Subscribe(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus); Mediator.Subscribe(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _)); Mediator.Subscribe(this, (msg) => _drawFolders = GetDrawFolders().ToList()); Flags |= ImGuiWindowFlags.NoDocking; SizeConstraints = new WindowSizeConstraints() { MinimumSize = new Vector2(375, 400), MaximumSize = new Vector2(375, 2000), }; _characterAnalyzer = characterAnalyzer; _playerPerformanceConfig = playerPerformanceConfig; _lightlessMediator = lightlessMediator; } protected override void DrawInternal() { _windowContentWidth = UiSharedService.GetWindowContentRegionWidth(); if (!_apiController.IsCurrentVersion) { var ver = _apiController.CurrentClientVersion; var unsupported = "UNSUPPORTED VERSION"; using (_uiSharedService.UidFont.Push()) { var uidTextSize = ImGui.CalcTextSize(unsupported); ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X + ImGui.GetWindowContentRegionMin().X) / 2 - uidTextSize.X / 2); ImGui.AlignTextToFramePadding(); ImGui.TextColored(UIColors.Get("DimRed"), unsupported); } UiSharedService.ColorTextWrapped($"Your Lightless Sync installation is out of date, the current version is {ver.Major}.{ver.Minor}.{ver.Build}. " + $"It is highly recommended to keep Lightless Sync up to date. Open /xlplugins and update the plugin.", UIColors.Get("DimRed")); } if (!_ipcManager.Initialized) { var unsupported = "MISSING ESSENTIAL PLUGINS"; using (_uiSharedService.UidFont.Push()) { var uidTextSize = ImGui.CalcTextSize(unsupported); ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X + ImGui.GetWindowContentRegionMin().X) / 2 - uidTextSize.X / 2); ImGui.AlignTextToFramePadding(); ImGui.TextColored(UIColors.Get("DimRed"), unsupported); } var penumAvailable = _ipcManager.Penumbra.APIAvailable; var glamAvailable = _ipcManager.Glamourer.APIAvailable; UiSharedService.ColorTextWrapped($"One or more Plugins essential for Lightless operation are unavailable. Enable or update following plugins:", UIColors.Get("DimRed")); using var indent = ImRaii.PushIndent(10f); if (!penumAvailable) { UiSharedService.TextWrapped("Penumbra"); _uiSharedService.BooleanToColoredIcon(penumAvailable, true); } if (!glamAvailable) { UiSharedService.TextWrapped("Glamourer"); _uiSharedService.BooleanToColoredIcon(glamAvailable, true); } ImGui.Separator(); } using (ImRaii.PushId("header")) DrawUIDHeader(); _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); using (ImRaii.PushId("serverstatus")) DrawServerStatus(); ImGui.Separator(); if (_apiController.ServerState is ServerState.Connected) { using (ImRaii.PushId("global-topmenu")) _tabMenu.Draw(); using (ImRaii.PushId("pairlist")) DrawPairs(); ImGui.Separator(); 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-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) { _lastAddedUser = _pairManager.LastAddedUser; _pairManager.LastAddedUser = null; ImGui.OpenPopup("Set Notes for New User"); _showModalForUserAddition = true; _lastAddedUserComment = string.Empty; } if (ImGui.BeginPopupModal("Set Notes for New User", ref _showModalForUserAddition, UiSharedService.PopupWindowFlags)) { if (_lastAddedUser == null) { _showModalForUserAddition = false; } else { UiSharedService.TextWrapped($"You have successfully added {_lastAddedUser.UserData.AliasOrUID}. Set a local note for the user in the field below:"); ImGui.InputTextWithHint("##noteforuser", $"Note for {_lastAddedUser.UserData.AliasOrUID}", ref _lastAddedUserComment, 100); if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Note")) { _serverManager.SetNoteForUid(_lastAddedUser.UserData.UID, _lastAddedUserComment); _lastAddedUser = null; _lastAddedUserComment = string.Empty; _showModalForUserAddition = false; } } UiSharedService.SetScaledWindowSize(275); ImGui.EndPopup(); } var pos = ImGui.GetWindowPos(); var size = ImGui.GetWindowSize(); if (_lastSize != size || _lastPosition != pos) { _lastSize = size; _lastPosition = pos; Mediator.Publish(new CompactUiChange(_lastSize, _lastPosition)); } } private void DrawPairs() { var ySize = _transferPartHeight == 0 ? 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); foreach (var item in _drawFolders) { item.Draw(); } ImGui.EndChild(); } private void DrawServerStatus() { var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Link); var userCount = _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture); var userSize = ImGui.CalcTextSize(userCount); var textSize = ImGui.CalcTextSize("Users Online"); #if DEBUG string shardConnection = $"Shard: {_apiController.ServerInfo.ShardName}"; #else string shardConnection = string.Equals(_apiController.ServerInfo.ShardName, "Main", StringComparison.OrdinalIgnoreCase) ? string.Empty : $"Shard: {_apiController.ServerInfo.ShardName}"; #endif var shardTextSize = ImGui.CalcTextSize(shardConnection); var printShard = !string.IsNullOrEmpty(_apiController.ServerInfo.ShardName) && shardConnection != string.Empty; if (_apiController.ServerState is ServerState.Connected) { ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth()) / 2 - (userSize.X + textSize.X) / 2 - ImGui.GetStyle().ItemSpacing.X / 2); if (!printShard) ImGui.AlignTextToFramePadding(); ImGui.TextColored(UIColors.Get("LightlessPurple"), userCount); ImGui.SameLine(); if (!printShard) ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted("Users Online"); } else { ImGui.AlignTextToFramePadding(); ImGui.TextColored(UIColors.Get("DimRed"), "Not connected to any server"); } if (printShard) { ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetStyle().ItemSpacing.Y); ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth()) / 2 - shardTextSize.X / 2); ImGui.TextUnformatted(shardConnection); } ImGui.SameLine(); if (printShard) { ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((userSize.Y + textSize.Y) / 2 + shardTextSize.Y) / 2 - ImGui.GetStyle().ItemSpacing.Y + buttonSize.Y / 2); } bool isConnectingOrConnected = _apiController.ServerState is ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting; var color = UiSharedService.GetBoolColor(!isConnectingOrConnected); var connectedIcon = isConnectingOrConnected ? FontAwesomeIcon.Unlink : FontAwesomeIcon.Link; ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X); if (printShard) { ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((userSize.Y + textSize.Y) / 2 + shardTextSize.Y) / 2 - ImGui.GetStyle().ItemSpacing.Y + buttonSize.Y / 2); } if (_apiController.ServerState is not (ServerState.Reconnecting or ServerState.Disconnecting)) { using (ImRaii.PushColor(ImGuiCol.Text, color)) { if (_uiSharedService.IconButton(connectedIcon)) { if (isConnectingOrConnected && !_serverManager.CurrentServer.FullPause) { _serverManager.CurrentServer.FullPause = true; _serverManager.Save(); } else if (!isConnectingOrConnected && _serverManager.CurrentServer.FullPause) { _serverManager.CurrentServer.FullPause = false; _serverManager.Save(); } _ = _apiController.CreateConnectionsAsync(); } } UiSharedService.AttachToolTip(isConnectingOrConnected ? "Disconnect from " + _serverManager.CurrentServer.ServerName : "Connect to " + _serverManager.CurrentServer.ServerName); } } private void DrawTransfers() { var currentUploads = _fileTransferManager.CurrentUploads.ToList(); ImGui.AlignTextToFramePadding(); _uiSharedService.IconText(FontAwesomeIcon.Upload); ImGui.SameLine(35 * ImGuiHelpers.GlobalScale); if (currentUploads.Any()) { var totalUploads = currentUploads.Count; var doneUploads = currentUploads.Count(c => c.IsTransferred); var totalUploaded = currentUploads.Sum(c => c.Transferred); var totalToUpload = currentUploads.Sum(c => c.Total); ImGui.TextUnformatted($"{doneUploads}/{totalUploads}"); var uploadText = $"({UiSharedService.ByteToString(totalUploaded)}/{UiSharedService.ByteToString(totalToUpload)})"; var textSize = ImGui.CalcTextSize(uploadText); ImGui.SameLine(_windowContentWidth - textSize.X); ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(uploadText); } else { ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted("No uploads in progress"); } var currentDownloads = _currentDownloads.SelectMany(d => d.Value.Values).ToList(); ImGui.AlignTextToFramePadding(); _uiSharedService.IconText(FontAwesomeIcon.Download); ImGui.SameLine(35 * ImGuiHelpers.GlobalScale); if (currentDownloads.Any()) { var totalDownloads = currentDownloads.Sum(c => c.TotalFiles); var doneDownloads = currentDownloads.Sum(c => c.TransferredFiles); var totalDownloaded = currentDownloads.Sum(c => c.TransferredBytes); var totalToDownload = currentDownloads.Sum(c => c.TotalBytes); ImGui.TextUnformatted($"{doneDownloads}/{totalDownloads}"); var downloadText = $"({UiSharedService.ByteToString(totalDownloaded)}/{UiSharedService.ByteToString(totalToDownload)})"; var textSize = ImGui.CalcTextSize(downloadText); ImGui.SameLine(_windowContentWidth - textSize.X); ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(downloadText); } else { ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted("No downloads in progress"); } } private void DrawUIDHeader() { var uidText = GetUidText(); //Getting information of character and triangles threshold to show overlimit status in UID bar. _cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone(); using (_uiSharedService.UidFont.Push()) { var uidTextSize = ImGui.CalcTextSize(uidText); ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) / 2 - (uidTextSize.X / 2)); ImGui.TextColored(GetUidColor(), uidText); } UiSharedService.AttachToolTip("Click to copy"); if (ImGui.IsItemClicked()) { ImGui.SetClipboardText(uidText); } if (_cachedAnalysis != null && _apiController.ServerState is ServerState.Connected) { var firstEntry = _cachedAnalysis.FirstOrDefault(); var valueDict = firstEntry.Value; if (valueDict != null && valueDict.Count > 0) { var groupedfiles = valueDict .Select(v => v.Value) .Where(v => v != null) .GroupBy(f => f.FileType, StringComparer.Ordinal) .OrderBy(k => k.Key, StringComparer.Ordinal) .ToList(); var actualTriCount = valueDict .Select(v => v.Value) .Where(v => v != null) .Sum(f => f.Triangles); if (groupedfiles != null) { //Checking of VRAM threshhold var texGroup = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal)); var actualVramUsage = texGroup != null ? texGroup.Sum(f => f.OriginalSize) : 0L; var isOverVRAMUsage = _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024 < actualVramUsage; var isOverTriHold = actualTriCount > (_playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000); if ((isOverTriHold || isOverVRAMUsage) && _playerPerformanceConfig.Current.WarnOnExceedingThresholds) { ImGui.SameLine(); _uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow")); string warningMessage = ""; if (isOverTriHold) { warningMessage += $"You exceed your own triangles threshold by " + $"{actualTriCount - _playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000} triangles."; warningMessage += Environment.NewLine; } if (isOverVRAMUsage) { warningMessage += $"You exceed your own VRAM threshold by " + $"{UiSharedService.ByteToString(actualVramUsage - (_playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024))}."; } UiSharedService.AttachToolTip(warningMessage); if (ImGui.IsItemClicked()) { _lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi))); } } } } } if (_apiController.ServerState is ServerState.Connected) { if (ImGui.IsItemClicked()) { ImGui.SetClipboardText(_apiController.DisplayName); } if (!string.Equals(_apiController.DisplayName, _apiController.UID, StringComparison.Ordinal)) { var origTextSize = ImGui.CalcTextSize(_apiController.UID); ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) / 2 - (origTextSize.X / 2)); ImGui.TextColored(GetUidColor(), _apiController.UID); UiSharedService.AttachToolTip("Click to copy"); if (ImGui.IsItemClicked()) { _lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi))); } } } else { UiSharedService.ColorTextWrapped(GetServerError(), GetUidColor()); } } private IEnumerable GetDrawFolders() { List drawFolders = []; var allPairs = _pairManager.PairsWithGroups .ToDictionary(k => k.Key, k => k.Value); var filteredPairs = allPairs .Where(p => { if (_tabMenu.Filter.IsNullOrEmpty()) return true; return p.Key.UserData.AliasOrUID.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) || (p.Key.GetNote()?.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) ?? false) || (p.Key.PlayerName?.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) ?? false); }) .ToDictionary(k => k.Key, k => k.Value); string? AlphabeticalSort(KeyValuePair> u) => (_configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(u.Key.PlayerName) ? (_configService.Current.PreferNotesOverNamesForVisible ? u.Key.GetNote() : u.Key.PlayerName) : (u.Key.GetNote() ?? u.Key.UserData.AliasOrUID)); bool FilterOnlineOrPausedSelf(KeyValuePair> u) => (u.Key.IsOnline || (!u.Key.IsOnline && !_configService.Current.ShowOfflineUsersSeparately) || u.Key.UserPair.OwnPermissions.IsPaused()); Dictionary> BasicSortedDictionary(IEnumerable>> u) => u.OrderByDescending(u => u.Key.IsVisible) .ThenByDescending(u => u.Key.IsOnline) .ThenBy(AlphabeticalSort, StringComparer.OrdinalIgnoreCase) .ToDictionary(u => u.Key, u => u.Value); ImmutableList ImmutablePairList(IEnumerable>> u) => u.Select(k => k.Key).ToImmutableList(); bool FilterVisibleUsers(KeyValuePair> u) => u.Key.IsVisible && (_configService.Current.ShowSyncshellUsersInVisible || !(!_configService.Current.ShowSyncshellUsersInVisible && !u.Key.IsDirectlyPaired)); bool FilterTagUsers(KeyValuePair> u, string tag) => u.Key.IsDirectlyPaired && !u.Key.IsOneSidedPair && _tagHandler.HasPairTag(u.Key.UserData.UID, tag); bool FilterGroupUsers(KeyValuePair> u, GroupFullInfoDto group) => u.Value.Exists(g => string.Equals(g.GID, group.GID, StringComparison.Ordinal)); bool FilterNotTaggedUsers(KeyValuePair> u) => u.Key.IsDirectlyPaired && !u.Key.IsOneSidedPair && !_tagHandler.HasAnyPairTag(u.Key.UserData.UID); bool FilterOfflineUsers(KeyValuePair> u) => ((u.Key.IsDirectlyPaired && _configService.Current.ShowSyncshellOfflineUsersSeparately) || !_configService.Current.ShowSyncshellOfflineUsersSeparately) && (!u.Key.IsOneSidedPair || u.Value.Any()) && !u.Key.IsOnline && !u.Key.UserPair.OwnPermissions.IsPaused(); bool FilterOfflineSyncshellUsers(KeyValuePair> u) => (!u.Key.IsDirectlyPaired && !u.Key.IsOnline && !u.Key.UserPair.OwnPermissions.IsPaused()); if (_configService.Current.ShowVisibleUsersSeparately) { var allVisiblePairs = ImmutablePairList(allPairs .Where(FilterVisibleUsers)); var filteredVisiblePairs = BasicSortedDictionary(filteredPairs .Where(FilterVisibleUsers)); drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomVisibleTag, filteredVisiblePairs, allVisiblePairs)); } List groupFolders = new(); foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase)) { GetGroups(allPairs, filteredPairs, group, out ImmutableList allGroupPairs, out Dictionary> filteredGroupPairs); groupFolders.Add(_drawEntityFactory.CreateDrawGroupFolder(group, filteredGroupPairs, allGroupPairs)); } if (_configService.Current.GroupUpSyncshells) drawFolders.Add(new DrawGroupedGroupFolder(groupFolders, _tagHandler, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, "")); else drawFolders.AddRange(groupFolders); var tags = _tagHandler.GetAllPairTagsSorted(); foreach (var tag in tags) { var allTagPairs = ImmutablePairList(allPairs .Where(u => FilterTagUsers(u, tag))); var filteredTagPairs = BasicSortedDictionary(filteredPairs .Where(u => FilterTagUsers(u, tag) && FilterOnlineOrPausedSelf(u))); drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(tag, filteredTagPairs, allTagPairs)); } var syncshellTags = _tagHandler.GetAllSyncshellTagsSorted(); foreach (var syncshelltag in syncshellTags) { List syncshellFolderTags = []; foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase)) { if (_tagHandler.HasSyncshellTag(group.GID, syncshelltag)) { GetGroups(allPairs, filteredPairs, group, out ImmutableList allGroupPairs, out Dictionary> filteredGroupPairs); syncshellFolderTags.Add(_drawEntityFactory.CreateDrawGroupFolder($"tag_{group.GID}", group, filteredGroupPairs, allGroupPairs)); } } if (syncshellFolderTags.Count > 0) { drawFolders.Add(new DrawGroupedGroupFolder(syncshellFolderTags, _tagHandler, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, syncshelltag)); } } var allOnlineNotTaggedPairs = ImmutablePairList(allPairs .Where(FilterNotTaggedUsers)); var onlineNotTaggedPairs = BasicSortedDictionary(filteredPairs .Where(u => FilterNotTaggedUsers(u) && FilterOnlineOrPausedSelf(u))); drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder((_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag), onlineNotTaggedPairs, allOnlineNotTaggedPairs)); if (_configService.Current.ShowOfflineUsersSeparately) { var allOfflinePairs = ImmutablePairList(allPairs .Where(FilterOfflineUsers)); var filteredOfflinePairs = BasicSortedDictionary(filteredPairs .Where(FilterOfflineUsers)); drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineTag, filteredOfflinePairs, allOfflinePairs)); if (_configService.Current.ShowSyncshellOfflineUsersSeparately) { var allOfflineSyncshellUsers = ImmutablePairList(allPairs .Where(FilterOfflineSyncshellUsers)); var filteredOfflineSyncshellUsers = BasicSortedDictionary(filteredPairs .Where(FilterOfflineSyncshellUsers)); drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineSyncshellTag, filteredOfflineSyncshellUsers, allOfflineSyncshellUsers)); } } drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomUnpairedTag, BasicSortedDictionary(filteredPairs.Where(u => u.Key.IsOneSidedPair)), ImmutablePairList(allPairs.Where(u => u.Key.IsOneSidedPair)))); return drawFolders; void GetGroups(Dictionary> allPairs, Dictionary> filteredPairs, GroupFullInfoDto group, out ImmutableList allGroupPairs, out Dictionary> filteredGroupPairs) { allGroupPairs = ImmutablePairList(allPairs .Where(u => FilterGroupUsers(u, group))); filteredGroupPairs = filteredPairs .Where(u => FilterGroupUsers(u, group) && FilterOnlineOrPausedSelf(u)) .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(AlphabeticalSort, StringComparer.OrdinalIgnoreCase) .ToDictionary(k => k.Key, k => k.Value); } } private string GetServerError() { return _apiController.ServerState switch { ServerState.Connecting => "Attempting to connect to the server.", ServerState.Reconnecting => "Connection to server interrupted, attempting to reconnect to the server.", ServerState.Disconnected => "You are currently disconnected from the Lightless Sync server.", ServerState.Disconnecting => "Disconnecting from the server", ServerState.Unauthorized => "Server Response: " + _apiController.AuthFailureMessage, ServerState.Offline => "Your selected Lightless Sync server is currently offline.", ServerState.VersionMisMatch => "Your plugin or the server you are connecting to is out of date. Please update your plugin now. If you already did so, contact the server provider to update their server to the latest version.", ServerState.RateLimited => "You are rate limited for (re)connecting too often. Disconnect, wait 10 minutes and try again.", ServerState.Connected => string.Empty, ServerState.NoSecretKey => "You have no secret key set for this current character. Open Settings -> Service Settings and set a secret key for the current character. You can reuse the same secret key for multiple characters.", ServerState.MultiChara => "Your Character Configuration has multiple characters configured with same name and world. You will not be able to connect until you fix this issue. Remove the duplicates from the configuration in Settings -> Service Settings -> Character Management and reconnect manually after.", ServerState.OAuthMisconfigured => "OAuth2 is enabled but not fully configured, verify in the Settings -> Service Settings that you have OAuth2 connected and, importantly, a UID assigned to your current character.", ServerState.OAuthLoginTokenStale => "Your OAuth2 login token is stale and cannot be used to renew. Go to the Settings -> Service Settings and unlink then relink your OAuth2 configuration.", ServerState.NoAutoLogon => "This character has automatic login into Lightless disabled. Press the connect button to connect to Lightless.", _ => string.Empty }; } private Vector4 GetUidColor() { return _apiController.ServerState switch { ServerState.Connecting => UIColors.Get("LightlessYellow"), ServerState.Reconnecting => UIColors.Get("DimRed"), ServerState.Connected => UIColors.Get("LightlessPurple"), ServerState.Disconnected => UIColors.Get("LightlessYellow"), ServerState.Disconnecting => UIColors.Get("LightlessYellow"), ServerState.Unauthorized => UIColors.Get("DimRed"), ServerState.VersionMisMatch => UIColors.Get("DimRed"), ServerState.Offline => UIColors.Get("DimRed"), ServerState.RateLimited => UIColors.Get("LightlessYellow"), ServerState.NoSecretKey => UIColors.Get("LightlessYellow"), ServerState.MultiChara => UIColors.Get("LightlessYellow"), ServerState.OAuthMisconfigured => UIColors.Get("DimRed"), ServerState.OAuthLoginTokenStale => UIColors.Get("DimRed"), ServerState.NoAutoLogon => UIColors.Get("LightlessYellow"), _ => UIColors.Get("DimRed") }; } private string GetUidText() { return _apiController.ServerState switch { ServerState.Reconnecting => "Reconnecting", ServerState.Connecting => "Connecting", ServerState.Disconnected => "Disconnected", ServerState.Disconnecting => "Disconnecting", ServerState.Unauthorized => "Unauthorized", ServerState.VersionMisMatch => "Version mismatch", ServerState.Offline => "Unavailable", ServerState.RateLimited => "Rate Limited", ServerState.NoSecretKey => "No Secret Key", ServerState.MultiChara => "Duplicate Characters", ServerState.OAuthMisconfigured => "Misconfigured OAuth2", ServerState.OAuthLoginTokenStale => "Stale OAuth2", ServerState.NoAutoLogon => "Auto Login disabled", ServerState.Connected => _apiController.DisplayName, _ => string.Empty }; } private void UiSharedService_GposeEnd() { IsOpen = _wasOpen; } private void UiSharedService_GposeStart() { _wasOpen = IsOpen; IsOpen = false; } }