All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m9s
# Patchnotes 2.1.0 The changes in this update are more than just "patches". With a new UI, a new feature, and a bunch of bug fixes, improvements and a new member on the dev team, we thought this was more of a minor update. We would like to introduce @tsubasahane of MareCN to the team! We’re happy to work with them to bring Lightless and its features to the CN client as well as having another talented dev bring features and ideas to us. Speaking of which: # Location Sharing (Big shout out to @tsubasahane for bringing this feature) - Are you TIRED of scrambling to find the address of the venue you're in to share with your friends? We are introducing Location Sharing! An optional feature where you can share your location with direct pairs temporarily [30 minutes, 1 hour, 3 hours] minutes or until you turn it off for them. That's up to you! [#125](<#125>) [#49](<Lightless-Sync/LightlessServer#49>) - To share your location with a pair, click the three dots beside the pair and choose a duration to share with them. [#125](<#125>) [#49](<Lightless-Sync/LightlessServer#49>) - To view the location of someone who's shared with you, simply hover over the globe icon! [#125](<#125>) [#49](<Lightless-Sync/LightlessServer#49>) [1] # Model Optimization (Mesh Decimating) - This new option can automatically “simplify” incoming character meshes to help performance by reducing triangle counts. You choose how strong the reduction is (default/recommended is 80%). [#131](<#131>) - Decimation only kicks in when a mesh is above a certain triangle threshold, and only for the items that qualify for it and you selected for. [#131](<#131>) - Hair meshes is always excluded, since simplifying hair meshes is very prone to breaking. - You can find everything under Settings → Performance → Model Optimization. [#131](<#131>) + ** IF YOU HAVE USED DECIMATION IN TESTING, PLEASE CLEAR YOUR CACHE ❗ ** [2] # Animation (PAP) Validation (Safer animations) - Lightless now checks your currently animations to see if they work with your local skeleton/bone mod. If an animation matches, it’s included in what gets sent to other players. If it doesn’t, Lightless will skip it and write a warning to your log showing how many were skipped due to skeleton changes. Its defaulted to Unsafe (off). turn it on if you experience crashes from others users. [#131](<#131>) - Lightless also does the same kind of check for incoming animation files, to make sure they match the body/skeleton they were sent with. [#131](<#131>) - Because these checks can sometimes be a little picky, you can adjust how strict they are in Settings -> General -> Animation & Bones to reduce false positives. [#131](<#131>) # UI Changes (Thanks to @kyuwu for UI Changes) - The top part of the main screen has gotten a makeover. You can adjust the colors of the gradiant in the Color settings of Lightless. [#127](<#127>) [3] - Settings have gotten some changes as well to make this change more universal, and will use the same color settings. [#127](<#127>) - The particle effects of the gradient are toggleable in 'Settings -> UI -> Behavior' [#127](<#127>) - Instead of showing download/upload on bottom of Main UI, it will show VRAM usage and triangles with their optimization options next to it [#138](<#138>) # LightFinder / ShellFinder - UI Changes that follow our new design follow the color codes for the Gradient top as the main screen does. [#127](<#127>) [4] Co-authored-by: defnotken <itsdefnotken@gmail.com> Co-authored-by: azyges <aaaaaa@aaa.aaa> Co-authored-by: cake <admin@cakeandbanana.nl> Co-authored-by: Tsubasa <tsubasa@noreply.git.lightless-sync.org> Co-authored-by: choco <choco@patat.nl> Co-authored-by: celine <aaa@aaa.aaa> Co-authored-by: celine <celine@noreply.git.lightless-sync.org> Co-authored-by: Tsubasahane <wozaiha@gmail.com> Co-authored-by: cake <cake@noreply.git.lightless-sync.org> Reviewed-on: #123
1062 lines
46 KiB
C#
1062 lines
46 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;
|
|
|
|
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;
|
|
private readonly OptimizationSummaryCard _optimizationSummaryCard;
|
|
|
|
#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 _footerPartHeight;
|
|
private bool _hasFooterPartHeight;
|
|
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;
|
|
_optimizationSummaryCard = new OptimizationSummaryCard(_uiSharedService, _pairUiService, _playerPerformanceConfig, _fileTransferManager, _lightlessMediator);
|
|
}
|
|
|
|
#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 footerTop = ImGui.GetCursorScreenPos().Y;
|
|
var gradientBottom = MathF.Max(gradientTop, footerTop - style.ItemSpacing.Y - gradientInset);
|
|
selune.DrawGradient(gradientTop, gradientBottom, ImGui.GetIO().DeltaTime);
|
|
float pairlistEnd = ImGui.GetCursorPosY();
|
|
bool drewFooter;
|
|
using (ImRaii.PushId("optimization-summary"))
|
|
{
|
|
drewFooter = _optimizationSummaryCard.Draw(_currentDownloads.Count);
|
|
}
|
|
_footerPartHeight = drewFooter ? ImGui.GetCursorPosY() - pairlistEnd : 0f;
|
|
_hasFooterPartHeight = true;
|
|
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 = !_hasFooterPartHeight
|
|
? 1
|
|
: MathF.Max(1f, ImGui.GetContentRegionAvail().Y - _footerPartHeight);
|
|
|
|
if (ImGui.BeginChild("list", new Vector2(_windowContentWidth, ySize), border: false))
|
|
{
|
|
foreach (var item in _drawFolders)
|
|
{
|
|
item.Draw();
|
|
}
|
|
}
|
|
|
|
ImGui.EndChild();
|
|
}
|
|
|
|
#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
|
|
|
|
}
|