1160 lines
50 KiB
C#
1160 lines
50 KiB
C#
using Dalamud.Bindings.ImGui;
|
|
using Dalamud.Interface;
|
|
using Dalamud.Interface.Utility;
|
|
using Dalamud.Interface.Utility.Raii;
|
|
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.LightFinder;
|
|
using LightlessSync.Services.Mediator;
|
|
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;
|
|
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;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace LightlessSync.UI;
|
|
|
|
public class CompactUi : WindowMediatorSubscriberBase
|
|
{
|
|
#region Constants
|
|
|
|
private const float ConnectButtonHighlightThickness = 14f;
|
|
|
|
#endregion
|
|
|
|
#region Services
|
|
|
|
private readonly ApiController _apiController;
|
|
private readonly CharacterAnalyzer _characterAnalyzer;
|
|
private readonly DalamudUtilService _dalamudUtilService;
|
|
private readonly DrawEntityFactory _drawEntityFactory;
|
|
private readonly FileUploadManager _fileTransferManager;
|
|
private readonly IpcManager _ipcManager;
|
|
private readonly LightFinderService _broadcastService;
|
|
private readonly LightlessConfigService _configService;
|
|
private readonly LightlessMediator _lightlessMediator;
|
|
private readonly PairLedger _pairLedger;
|
|
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
|
|
private readonly PairUiService _pairUiService;
|
|
private readonly ServerConfigurationManager _serverManager;
|
|
private readonly TagHandler _tagHandler;
|
|
private readonly UiSharedService _uiSharedService;
|
|
|
|
#endregion
|
|
|
|
#region UI Components
|
|
|
|
private readonly AnimatedHeader _animatedHeader = new();
|
|
private readonly RenamePairTagUi _renamePairTagUi;
|
|
private readonly RenameSyncshellTagUi _renameSyncshellTagUi;
|
|
private readonly SelectPairForTagUi _selectPairsForGroupUi;
|
|
private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi;
|
|
private readonly SelectTagForPairUi _selectTagForPairUi;
|
|
private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi;
|
|
private readonly SeluneBrush _seluneBrush = new();
|
|
private readonly TopTabMenu _tabMenu;
|
|
|
|
#endregion
|
|
|
|
#region State
|
|
|
|
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
|
private List<IDrawFolder> _drawFolders;
|
|
private Pair? _focusedPair;
|
|
private Pair? _lastAddedUser;
|
|
private string _lastAddedUserComment = string.Empty;
|
|
private Vector2 _lastPosition = Vector2.One;
|
|
private Vector2 _lastSize = Vector2.One;
|
|
private int _pendingFocusFrame = -1;
|
|
private Pair? _pendingFocusPair;
|
|
private bool _showModalForUserAddition;
|
|
private float _transferPartHeight;
|
|
private bool _wasOpen;
|
|
private float _windowContentWidth;
|
|
|
|
#endregion
|
|
|
|
#region Constructor
|
|
|
|
public CompactUi(
|
|
ILogger<CompactUi> logger,
|
|
UiSharedService uiShared,
|
|
LightlessConfigService configService,
|
|
ApiController apiController,
|
|
PairUiService pairUiService,
|
|
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,
|
|
LightFinderService broadcastService,
|
|
CharacterAnalyzer characterAnalyzer,
|
|
PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService, PairLedger pairLedger, LightFinderScannerService lightFinderScannerService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
|
|
{
|
|
_uiSharedService = uiShared;
|
|
_configService = configService;
|
|
_apiController = apiController;
|
|
_pairUiService = pairUiService;
|
|
_serverManager = serverManager;
|
|
_fileTransferManager = fileTransferManager;
|
|
_tagHandler = tagHandler;
|
|
_drawEntityFactory = drawEntityFactory;
|
|
_selectTagForPairUi = selectTagForPairUi;
|
|
_selectTagForSyncshellUi = selectTagForSyncshellUi;
|
|
_selectSyncshellForTagUi = selectSyncshellForTagUi;
|
|
_renameSyncshellTagUi = renameSyncshellTagUi;
|
|
_selectPairsForGroupUi = selectPairForTagUi;
|
|
_renamePairTagUi = renameTagUi;
|
|
_ipcManager = ipcManager;
|
|
_broadcastService = broadcastService;
|
|
_pairLedger = pairLedger;
|
|
_dalamudUtilService = dalamudUtilService;
|
|
_tabMenu = new TopTabMenu(Mediator, _apiController, _uiSharedService, pairRequestService, dalamudUtilService, lightlessNotificationService, broadcastService, lightFinderScannerService);
|
|
Mediator.Subscribe<PairFocusCharacterMessage>(this, msg => RegisterFocusCharacter(msg.Pair));
|
|
|
|
WindowBuilder.For(this)
|
|
.AllowPinning(true)
|
|
.AllowClickthrough(false)
|
|
.SetSizeConstraints(new Vector2(375, 400), new Vector2(375, 2000))
|
|
.AddFlags(ImGuiWindowFlags.NoDocking)
|
|
.AddTitleBarButton(FontAwesomeIcon.Cog, "Open Lightless Settings", () => Mediator.Publish(new UiToggleMessage(typeof(SettingsUi))))
|
|
.AddTitleBarButton(FontAwesomeIcon.Book, "Open Lightless Event Viewer", () => Mediator.Publish(new UiToggleMessage(typeof(EventViewerUI))))
|
|
.Apply();
|
|
|
|
_drawFolders = [.. DrawFolders];
|
|
|
|
_animatedHeader.Height = 120f;
|
|
_animatedHeader.EnableBottomGradient = true;
|
|
_animatedHeader.GradientHeight = 250f;
|
|
_animatedHeader.EnableParticles = _configService.Current.EnableParticleEffects;
|
|
|
|
#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<SwitchToMainUiMessage>(this, (_) => IsOpen = true);
|
|
Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) => IsOpen = false);
|
|
Mediator.Subscribe<CutsceneStartMessage>(this, (_) => UiSharedService_GposeStart());
|
|
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd());
|
|
Mediator.Subscribe<DownloadStartedMessage>(this, msg =>
|
|
{
|
|
_currentDownloads[msg.DownloadId] = new Dictionary<string, FileDownloadStatus>(msg.DownloadStatus, StringComparer.Ordinal);
|
|
});
|
|
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
|
|
Mediator.Subscribe<RefreshUiMessage>(this, (msg) => _drawFolders = [.. DrawFolders]);
|
|
|
|
_characterAnalyzer = characterAnalyzer;
|
|
_playerPerformanceConfig = playerPerformanceConfig;
|
|
_lightlessMediator = mediator;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Lifecycle
|
|
|
|
public override void OnClose()
|
|
{
|
|
ForceReleaseFocus();
|
|
_animatedHeader.ClearParticles();
|
|
base.OnClose();
|
|
}
|
|
|
|
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();
|
|
|
|
// Draw animated header background (just the gradient/particles, content drawn by existing methods)
|
|
var startCursorY = ImGui.GetCursorPosY();
|
|
_animatedHeader.Draw(_windowContentWidth, (_, _) => { });
|
|
// Reset cursor to draw content on top of the header background
|
|
ImGui.SetCursorPosY(startCursorY);
|
|
|
|
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();
|
|
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);
|
|
|
|
if (_apiController.ServerState is ServerState.Connected)
|
|
{
|
|
var pairSnapshot = _pairUiService.GetSnapshot();
|
|
|
|
using (ImRaii.PushId("global-topmenu")) _tabMenu.Draw(pairSnapshot);
|
|
using (ImRaii.PushId("pairlist")) DrawPairs();
|
|
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(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();
|
|
}
|
|
else
|
|
{
|
|
selune.Animate(ImGui.GetIO().DeltaTime);
|
|
}
|
|
|
|
ProcessFocusTracker();
|
|
|
|
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;
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Content Drawing
|
|
|
|
private void DrawPairs()
|
|
{
|
|
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();
|
|
|
|
if (ImGui.BeginChild("list", new Vector2(_windowContentWidth, ySize), border: false))
|
|
{
|
|
foreach (var item in _drawFolders)
|
|
{
|
|
item.Draw();
|
|
}
|
|
}
|
|
|
|
ImGui.EndChild();
|
|
}
|
|
|
|
private void DrawTransfers()
|
|
{
|
|
var currentUploads = _fileTransferManager.GetCurrentUploadsSnapshot();
|
|
ImGui.AlignTextToFramePadding();
|
|
_uiSharedService.IconText(FontAwesomeIcon.Upload);
|
|
ImGui.SameLine(35 * ImGuiHelpers.GlobalScale);
|
|
|
|
if (currentUploads.Count > 0)
|
|
{
|
|
int totalUploads = currentUploads.Count;
|
|
int doneUploads = 0;
|
|
long totalUploaded = 0;
|
|
long totalToUpload = 0;
|
|
|
|
foreach (var upload in currentUploads)
|
|
{
|
|
if (upload.IsTransferred)
|
|
{
|
|
doneUploads++;
|
|
}
|
|
|
|
totalUploaded += upload.Transferred;
|
|
totalToUpload += upload.Total;
|
|
}
|
|
|
|
int activeUploads = totalUploads - doneUploads;
|
|
var uploadSlotLimit = Math.Clamp(_configService.Current.ParallelUploads, 1, 8);
|
|
|
|
ImGui.TextUnformatted($"{doneUploads}/{totalUploads} (slots {activeUploads}/{uploadSlotLimit})");
|
|
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 downloadSummary = GetDownloadSummary();
|
|
ImGui.AlignTextToFramePadding();
|
|
_uiSharedService.IconText(FontAwesomeIcon.Download);
|
|
ImGui.SameLine(35 * ImGuiHelpers.GlobalScale);
|
|
|
|
if (downloadSummary.HasDownloads)
|
|
{
|
|
var totalDownloads = downloadSummary.TotalFiles;
|
|
var doneDownloads = downloadSummary.TransferredFiles;
|
|
var totalDownloaded = downloadSummary.TransferredBytes;
|
|
var totalToDownload = downloadSummary.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 DownloadSummary GetDownloadSummary()
|
|
{
|
|
long totalBytes = 0;
|
|
long transferredBytes = 0;
|
|
int totalFiles = 0;
|
|
int transferredFiles = 0;
|
|
|
|
foreach (var kvp in _currentDownloads.ToArray())
|
|
{
|
|
if (kvp.Value is not { Count: > 0 } statuses)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var status in statuses.Values)
|
|
{
|
|
totalBytes += status.TotalBytes;
|
|
transferredBytes += status.TransferredBytes;
|
|
totalFiles += status.TotalFiles;
|
|
transferredFiles += status.TransferredFiles;
|
|
}
|
|
}
|
|
|
|
return new DownloadSummary(totalFiles, transferredFiles, transferredBytes, totalBytes);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Header Drawing
|
|
|
|
private void DrawUIDHeader()
|
|
{
|
|
var uidText = _apiController.ServerState.GetUidText(_apiController.DisplayName);
|
|
var uidColor = _apiController.ServerState.GetUidColor();
|
|
|
|
Vector4? vanityTextColor = null;
|
|
Vector4? vanityGlowColor = null;
|
|
bool useVanityColors = false;
|
|
|
|
if (_configService.Current.useColoredUIDs && _apiController.HasVanity)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(_apiController.TextColorHex))
|
|
{
|
|
vanityTextColor = UIColors.HexToRgba(_apiController.TextColorHex);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(_apiController.TextGlowColorHex))
|
|
{
|
|
vanityGlowColor = UIColors.HexToRgba(_apiController.TextGlowColorHex);
|
|
}
|
|
|
|
useVanityColors = vanityTextColor is not null || vanityGlowColor is not null;
|
|
}
|
|
|
|
//Getting information of character and triangles threshold to show overlimit status in UID bar.
|
|
var analysisSummary = _characterAnalyzer.LatestSummary;
|
|
|
|
Vector2 uidTextSize, iconSize;
|
|
using (_uiSharedService.UidFont.Push())
|
|
uidTextSize = ImGui.CalcTextSize(uidText);
|
|
|
|
using (_uiSharedService.IconFont.Push())
|
|
iconSize = ImGui.CalcTextSize(FontAwesomeIcon.PersonCirclePlus.ToIconString());
|
|
|
|
float uidStartX = 25f;
|
|
float cursorY = ImGui.GetCursorPosY();
|
|
|
|
ImGui.SetCursorPosY(cursorY);
|
|
ImGui.SetCursorPosX(uidStartX);
|
|
|
|
bool headerItemClicked;
|
|
using (_uiSharedService.UidFont.Push())
|
|
{
|
|
if (useVanityColors)
|
|
{
|
|
var seString = SeStringUtils.BuildFormattedPlayerName(uidText, vanityTextColor, vanityGlowColor);
|
|
var cursorPos = ImGui.GetCursorScreenPos();
|
|
var targetFontSize = ImGui.GetFontSize();
|
|
var font = ImGui.GetFont();
|
|
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, targetFontSize ,font , "uid-header");
|
|
}
|
|
else
|
|
{
|
|
ImGui.TextColored(uidColor, uidText);
|
|
}
|
|
}
|
|
|
|
// Get the actual rendered text rect for proper icon alignment
|
|
var uidTextRect = ImGui.GetItemRectMax() - ImGui.GetItemRectMin();
|
|
var uidTextRectMin = ImGui.GetItemRectMin();
|
|
var uidTextHovered = ImGui.IsItemHovered();
|
|
headerItemClicked = ImGui.IsItemClicked();
|
|
|
|
// Track position for icons next to UID text
|
|
// Use uidTextSize.Y (actual font height) for vertical centering, not hitbox height
|
|
float nextIconX = uidTextRectMin.X + uidTextRect.X + 10f;
|
|
float iconYOffset = (uidTextSize.Y - iconSize.Y) * 0.5f;
|
|
float textVerticalOffset = (uidTextRect.Y - uidTextSize.Y) * 0.5f;
|
|
var buttonSize = new Vector2(iconSize.X, uidTextSize.Y);
|
|
|
|
if (_configService.Current.BroadcastEnabled && _apiController.IsConnected)
|
|
{
|
|
ImGui.SetCursorScreenPos(new Vector2(nextIconX, uidTextRectMin.Y + textVerticalOffset));
|
|
|
|
ImGui.InvisibleButton("BroadcastIcon", buttonSize);
|
|
var iconPos = ImGui.GetItemRectMin() + new Vector2(0f, iconYOffset);
|
|
using (_uiSharedService.IconFont.Push())
|
|
ImGui.GetWindowDrawList().AddText(iconPos, ImGui.GetColorU32(UIColors.Get("LightlessGreen")), FontAwesomeIcon.Wifi.ToIconString());
|
|
|
|
nextIconX = ImGui.GetItemRectMax().X + 6f;
|
|
|
|
|
|
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);
|
|
|
|
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("PairBlue"));
|
|
ImGui.Text("Lightfinder");
|
|
ImGui.PopStyleColor();
|
|
|
|
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
|
ImGui.TextWrapped("Use Lightfinder when you're okay with being visible to other users and understand that you are responsible for your own experience.");
|
|
ImGui.PopStyleColor();
|
|
|
|
ImGuiHelpers.ScaledDummy(0.2f);
|
|
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
|
|
|
|
if (_configService.Current.BroadcastEnabled)
|
|
{
|
|
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessGreen"));
|
|
ImGui.Text("The Lightfinder calls, and somewhere, a soul may answer."); // cringe..
|
|
ImGui.PopStyleColor();
|
|
|
|
var ttl = _broadcastService.RemainingTtl;
|
|
if (ttl is { } remaining && remaining > TimeSpan.Zero)
|
|
{
|
|
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"));
|
|
ImGui.Text($"Still shining, for {remaining:hh\\:mm\\:ss}");
|
|
ImGui.PopStyleColor();
|
|
}
|
|
else
|
|
{
|
|
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
|
ImGui.Text("The Lightfinder's light wanes, but not in vain."); // cringe..
|
|
ImGui.PopStyleColor();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
|
ImGui.Text("The Lightfinder rests, waiting to shine again."); // cringe..
|
|
ImGui.PopStyleColor();
|
|
}
|
|
|
|
var cooldown = _broadcastService.RemainingCooldown;
|
|
if (cooldown is { } cd)
|
|
{
|
|
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
|
ImGui.Text($"The Lightfinder gathers its strength... ({Math.Ceiling(cd.TotalSeconds)}s)");
|
|
ImGui.PopStyleColor();
|
|
}
|
|
|
|
ImGui.PopTextWrapPos();
|
|
ImGui.EndTooltip();
|
|
}
|
|
|
|
if (ImGui.IsItemClicked())
|
|
_lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
|
}
|
|
|
|
// Warning threshold icon (next to lightfinder or UID text)
|
|
if (_apiController.ServerState is ServerState.Connected && analysisSummary.HasData)
|
|
{
|
|
var objectSummary = analysisSummary.Objects.Values.FirstOrDefault(summary => summary.HasEntries);
|
|
if (objectSummary.HasEntries)
|
|
{
|
|
var actualVramUsage = objectSummary.TexOriginalBytes;
|
|
var actualTriCount = objectSummary.TotalTriangles;
|
|
|
|
var isOverVRAMUsage = _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024 < actualVramUsage;
|
|
var isOverTriHold = actualTriCount > (_playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000);
|
|
|
|
if ((isOverTriHold || isOverVRAMUsage) && _playerPerformanceConfig.Current.WarnOnExceedingThresholds)
|
|
{
|
|
ImGui.SetCursorScreenPos(new Vector2(nextIconX, uidTextRectMin.Y + textVerticalOffset));
|
|
|
|
ImGui.InvisibleButton("WarningThresholdIcon", buttonSize);
|
|
var warningIconPos = ImGui.GetItemRectMin() + new Vector2(0f, iconYOffset);
|
|
using (_uiSharedService.IconFont.Push())
|
|
ImGui.GetWindowDrawList().AddText(warningIconPos, ImGui.GetColorU32(UIColors.Get("LightlessYellow")), FontAwesomeIcon.ExclamationTriangle.ToIconString());
|
|
|
|
if (ImGui.IsItemHovered())
|
|
{
|
|
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 (uidTextHovered)
|
|
{
|
|
var padding = new Vector2(35f * ImGuiHelpers.GlobalScale);
|
|
Selune.RegisterHighlight(
|
|
uidTextRectMin - padding,
|
|
uidTextRectMin + uidTextRect + padding,
|
|
SeluneHighlightMode.Point,
|
|
exactSize: true,
|
|
clipToElement: true,
|
|
clipPadding: padding,
|
|
highlightColorOverride: vanityGlowColor,
|
|
highlightAlphaOverride: 0.05f);
|
|
|
|
ImGui.SetTooltip("Click to copy");
|
|
}
|
|
|
|
if (headerItemClicked)
|
|
{
|
|
ImGui.SetClipboardText(uidText);
|
|
}
|
|
|
|
// Connect/Disconnect button next to big UID (use screen pos to avoid affecting layout)
|
|
DrawConnectButton(uidTextRectMin.Y + textVerticalOffset, uidTextSize.Y);
|
|
|
|
// Add spacing below the big UID
|
|
ImGuiHelpers.ScaledDummy(5f);
|
|
|
|
if (_apiController.ServerState is ServerState.Connected)
|
|
{
|
|
if (headerItemClicked)
|
|
{
|
|
ImGui.SetClipboardText(_apiController.DisplayName);
|
|
}
|
|
|
|
// Only show smaller UID line if DisplayName differs from UID (custom vanity name)
|
|
bool hasCustomName = !string.Equals(_apiController.DisplayName, _apiController.UID, StringComparison.OrdinalIgnoreCase);
|
|
|
|
if (hasCustomName)
|
|
{
|
|
ImGui.SetCursorPosX(uidStartX);
|
|
|
|
if (useVanityColors)
|
|
{
|
|
var seString = SeStringUtils.BuildFormattedPlayerName(_apiController.UID, vanityTextColor, vanityGlowColor);
|
|
var cursorPos = ImGui.GetCursorScreenPos();
|
|
var targetFontSize = ImGui.GetFontSize();
|
|
var font = ImGui.GetFont();
|
|
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, targetFontSize, font, "uid-footer");
|
|
}
|
|
else
|
|
{
|
|
ImGui.TextColored(uidColor, _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)
|
|
{
|
|
ImGui.SetClipboardText(_apiController.UID);
|
|
}
|
|
|
|
// Users Online on same line as smaller UID (with separator)
|
|
ImGui.SameLine();
|
|
ImGui.AlignTextToFramePadding();
|
|
ImGui.TextUnformatted("|");
|
|
ImGui.SameLine();
|
|
ImGui.TextColored(UIColors.Get("LightlessGreen"), _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture));
|
|
ImGui.SameLine();
|
|
ImGui.TextUnformatted("Users Online");
|
|
}
|
|
else
|
|
{
|
|
// No custom name - just show Users Online aligned to uidStartX
|
|
ImGui.SetCursorPosX(uidStartX);
|
|
ImGui.TextColored(UIColors.Get("LightlessGreen"), _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture));
|
|
ImGui.SameLine();
|
|
ImGui.TextUnformatted("Users Online");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ImGui.SetCursorPosX(uidStartX);
|
|
UiSharedService.ColorTextWrapped(_apiController.ServerState.GetServerError(_apiController.AuthFailureMessage), uidColor);
|
|
}
|
|
}
|
|
|
|
private void DrawConnectButton(float screenY, float textHeight)
|
|
{
|
|
var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Link);
|
|
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;
|
|
|
|
// Position on right side, vertically centered with text
|
|
if (_apiController.ServerState is not (ServerState.Reconnecting or ServerState.Disconnecting))
|
|
{
|
|
var windowPos = ImGui.GetWindowPos();
|
|
var screenX = windowPos.X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X - 13f;
|
|
var yOffset = (textHeight - buttonSize.Y) * 0.5f;
|
|
ImGui.SetCursorScreenPos(new Vector2(screenX, screenY + yOffset));
|
|
|
|
using (ImRaii.PushColor(ImGuiCol.Text, color))
|
|
using (ImRaii.PushColor(ImGuiCol.Button, ImGui.ColorConvertFloat4ToU32(new(0, 0, 0, 0))))
|
|
{
|
|
if (_uiSharedService.IconButton(connectedIcon, buttonSize.Y))
|
|
{
|
|
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();
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Folder Building
|
|
|
|
private IEnumerable<IDrawFolder> DrawFolders
|
|
{
|
|
get
|
|
{
|
|
var drawFolders = new List<IDrawFolder>();
|
|
var filter = _tabMenu.Filter;
|
|
|
|
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 = 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 groupInfos)
|
|
{
|
|
if (!FilterNotTaggedSyncshells(group))
|
|
{
|
|
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)
|
|
if (_configService.Current.GroupUpSyncshells)
|
|
drawFolders.Add(new DrawGroupedGroupFolder(groupFolders, _tagHandler, _apiController, _uiSharedService,
|
|
_selectSyncshellForTagUi, _renameSyncshellTagUi, ""));
|
|
else
|
|
drawFolders.AddRange(groupFolders.Select(v => v.GroupDrawFolder));
|
|
|
|
//Filter of grouped/foldered pairs
|
|
foreach (var tag in _tagHandler.GetAllPairTagsSorted())
|
|
{
|
|
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 groupInfos)
|
|
{
|
|
if (!_tagHandler.HasSyncshellTag(group.Group.GID, syncshellTag))
|
|
{
|
|
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 = SortEntries(allEntries.Where(FilterNotTaggedUsers));
|
|
if (allOnlineNotTaggedPairs.Count > 0 && _configService.Current.ShowOfflineUsersSeparately) {
|
|
var filteredOnlineEntries = SortOnlineEntries(filteredEntries.Where(e => FilterNotTaggedUsers(e) && FilterOnlineOrPausedSelf(e)));
|
|
drawFolders.Add(_drawEntityFactory.CreateTagFolder(
|
|
TagHandler.CustomOnlineTag,
|
|
filteredOnlineEntries,
|
|
allOnlineNotTaggedPairs));
|
|
} else if (allOnlineNotTaggedPairs.Count > 0 && !_configService.Current.ShowOfflineUsersSeparately) {
|
|
var onlineNotTaggedPairs = SortEntries(filteredEntries.Where(FilterNotTaggedUsers));
|
|
drawFolders.Add(_drawEntityFactory.CreateTagFolder(
|
|
TagHandler.CustomAllTag,
|
|
onlineNotTaggedPairs,
|
|
allOnlineNotTaggedPairs));
|
|
}
|
|
|
|
if (_configService.Current.ShowOfflineUsersSeparately)
|
|
{
|
|
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 = SortEntries(allEntries.Where(FilterOfflineSyncshellUsers));
|
|
if (allOfflineSyncshellUsers.Count > 0)
|
|
{
|
|
var filteredOfflineSyncshellUsers = SortEntries(filteredEntries.Where(FilterOfflineSyncshellUsers));
|
|
drawFolders.Add(_drawEntityFactory.CreateTagFolder(TagHandler.CustomOfflineSyncshellTag, filteredOfflineSyncshellUsers, allOfflineSyncshellUsers));
|
|
}
|
|
}
|
|
}
|
|
|
|
//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;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Filtering & Sorting
|
|
|
|
private static bool PassesFilter(PairUiEntry entry, string filter)
|
|
{
|
|
if (string.IsNullOrEmpty(filter)) return true;
|
|
|
|
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(PairUiEntry entry)
|
|
{
|
|
if (_configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(entry.DisplayName))
|
|
{
|
|
return _configService.Current.PreferNotesOverNamesForVisible ? (entry.Note ?? string.Empty) : entry.DisplayName;
|
|
}
|
|
|
|
return !string.IsNullOrEmpty(entry.Note) ? entry.Note : entry.AliasOrUid;
|
|
}
|
|
|
|
private bool FilterOnlineOrPausedSelf(PairUiEntry entry) => entry.IsOnline || (!entry.IsOnline && !_configService.Current.ShowOfflineUsersSeparately) || entry.SelfPermissions.IsPaused();
|
|
|
|
private bool FilterVisibleUsers(PairUiEntry entry) => entry.IsVisible && entry.IsOnline && (_configService.Current.ShowSyncshellUsersInVisible || entry.IsDirectlyPaired);
|
|
|
|
private bool FilterTagUsers(PairUiEntry entry, string tag) => entry.IsDirectlyPaired && !entry.IsOneSided && _tagHandler.HasPairTag(entry.DisplayEntry.Ident.UserId, tag);
|
|
|
|
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(PairUiEntry entry)
|
|
{
|
|
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();
|
|
}
|
|
|
|
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)];
|
|
}
|
|
|
|
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.EffectiveTriangleCount => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateEffectiveTris),
|
|
VisiblePairSortMode.Alphabetical => [.. entryList.OrderBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)],
|
|
VisiblePairSortMode.PreferredDirectPairs => SortVisibleByPreferred(entryList),
|
|
_ => SortEntries(entryList),
|
|
};
|
|
}
|
|
|
|
private ImmutableList<PairUiEntry> SortOnlineEntries(IEnumerable<PairUiEntry> entries)
|
|
{
|
|
var entryList = entries.ToList();
|
|
return _configService.Current.OnlinePairSortMode switch
|
|
{
|
|
OnlinePairSortMode.Alphabetical => [.. entryList.OrderBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)],
|
|
OnlinePairSortMode.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)];
|
|
}
|
|
|
|
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)];
|
|
}
|
|
|
|
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)];
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GPose Handlers
|
|
|
|
private void UiSharedService_GposeEnd() => IsOpen = _wasOpen;
|
|
|
|
private void UiSharedService_GposeStart()
|
|
{
|
|
_wasOpen = IsOpen;
|
|
IsOpen = false;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Focus Tracking
|
|
|
|
private void RegisterFocusCharacter(Pair pair)
|
|
{
|
|
_pendingFocusPair = pair;
|
|
_pendingFocusFrame = ImGui.GetFrameCount();
|
|
}
|
|
|
|
private void ProcessFocusTracker()
|
|
{
|
|
var frame = ImGui.GetFrameCount();
|
|
Pair? character = _pendingFocusFrame == frame ? _pendingFocusPair : null;
|
|
if (!ReferenceEquals(character, _focusedPair))
|
|
{
|
|
if (character is null)
|
|
{
|
|
_dalamudUtilService.ReleaseVisiblePairFocus();
|
|
}
|
|
else
|
|
{
|
|
_dalamudUtilService.FocusVisiblePair(character);
|
|
}
|
|
|
|
_focusedPair = character;
|
|
}
|
|
|
|
if (_pendingFocusFrame == frame)
|
|
{
|
|
_pendingFocusPair = null;
|
|
_pendingFocusFrame = -1;
|
|
}
|
|
}
|
|
|
|
private void ForceReleaseFocus()
|
|
{
|
|
if (_focusedPair is null)
|
|
{
|
|
_pendingFocusPair = null;
|
|
_pendingFocusFrame = -1;
|
|
return;
|
|
}
|
|
|
|
_dalamudUtilService.ReleaseVisiblePairFocus();
|
|
_focusedPair = null;
|
|
_pendingFocusPair = null;
|
|
_pendingFocusFrame = -1;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Types
|
|
|
|
[StructLayout(LayoutKind.Auto)]
|
|
private readonly record struct DownloadSummary(int TotalFiles, int TransferredFiles, long TransferredBytes, long TotalBytes)
|
|
{
|
|
public bool HasDownloads => TotalFiles > 0 || TotalBytes > 0;
|
|
}
|
|
|
|
#endregion
|
|
}
|