2.1.0 (#123)
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m9s
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
This commit was merged in pull request #123.
This commit is contained in:
@@ -28,50 +28,72 @@ using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace LightlessSync.UI;
|
||||
|
||||
public class CompactUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
private readonly CharacterAnalyzer _characterAnalyzer;
|
||||
#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 ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
||||
private readonly DrawEntityFactory _drawEntityFactory;
|
||||
private readonly FileUploadManager _fileTransferManager;
|
||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
|
||||
private readonly PairUiService _pairUiService;
|
||||
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 readonly LightFinderService _broadcastService;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
|
||||
|
||||
#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 _transferPartHeight;
|
||||
private float _footerPartHeight;
|
||||
private bool _hasFooterPartHeight;
|
||||
private bool _wasOpen;
|
||||
private float _windowContentWidth;
|
||||
private readonly SeluneBrush _seluneBrush = new();
|
||||
private const float _connectButtonHighlightThickness = 14f;
|
||||
private Pair? _focusedPair;
|
||||
private Pair? _pendingFocusPair;
|
||||
private int _pendingFocusFrame = -1;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
||||
public CompactUi(
|
||||
ILogger<CompactUi> logger,
|
||||
@@ -127,6 +149,11 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
.Apply();
|
||||
|
||||
_drawFolders = [.. DrawFolders];
|
||||
|
||||
_animatedHeader.Height = 120f;
|
||||
_animatedHeader.EnableBottomGradient = true;
|
||||
_animatedHeader.GradientHeight = 250f;
|
||||
_animatedHeader.EnableParticles = _configService.Current.EnableParticleEffects;
|
||||
|
||||
#if DEBUG
|
||||
string dev = "Dev Build";
|
||||
@@ -141,18 +168,27 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
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] = msg.DownloadStatus);
|
||||
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.ToList());
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -164,6 +200,13 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
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;
|
||||
@@ -209,17 +252,11 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
|
||||
using (ImRaii.PushId("header")) DrawUIDHeader();
|
||||
_uiSharedService.RoundedSeparator(UIColors.Get("LightlessPurple"), 2.5f, 1f, 12f);
|
||||
using (ImRaii.PushId("serverstatus"))
|
||||
{
|
||||
DrawServerStatus();
|
||||
}
|
||||
selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
|
||||
var style = ImGui.GetStyle();
|
||||
var contentMinY = windowPos.Y + ImGui.GetWindowContentRegionMin().Y;
|
||||
var gradientInset = 4f * ImGuiHelpers.GlobalScale;
|
||||
var gradientTop = MathF.Max(contentMinY, ImGui.GetCursorScreenPos().Y - style.ItemSpacing.Y + gradientInset);
|
||||
ImGui.Separator();
|
||||
|
||||
if (_apiController.ServerState is ServerState.Connected)
|
||||
{
|
||||
@@ -227,13 +264,17 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
|
||||
using (ImRaii.PushId("global-topmenu")) _tabMenu.Draw(pairSnapshot);
|
||||
using (ImRaii.PushId("pairlist")) DrawPairs();
|
||||
ImGui.Separator();
|
||||
var transfersTop = ImGui.GetCursorScreenPos().Y;
|
||||
var gradientBottom = MathF.Max(gradientTop, transfersTop - style.ItemSpacing.Y - gradientInset);
|
||||
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();
|
||||
using (ImRaii.PushId("transfers")) DrawTransfers();
|
||||
_transferPartHeight = ImGui.GetCursorPosY() - pairlistEnd - ImGui.GetTextLineHeight();
|
||||
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();
|
||||
@@ -290,12 +331,15 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Content Drawing
|
||||
|
||||
private void DrawPairs()
|
||||
{
|
||||
float ySize = Math.Abs(_transferPartHeight) < 0.0001f
|
||||
float ySize = !_hasFooterPartHeight
|
||||
? 1
|
||||
: (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y
|
||||
+ ImGui.GetTextLineHeight() - ImGui.GetStyle().WindowPadding.Y - ImGui.GetStyle().WindowBorderSize) - _transferPartHeight - ImGui.GetCursorPosY();
|
||||
: MathF.Max(1f, ImGui.GetContentRegionAvail().Y - _footerPartHeight);
|
||||
|
||||
if (ImGui.BeginChild("list", new Vector2(_windowContentWidth, ySize), border: false))
|
||||
{
|
||||
@@ -308,195 +352,9 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
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;
|
||||
#endregion
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private readonly record struct DownloadSummary(int TotalFiles, int TransferredFiles, long TransferredBytes, long TotalBytes)
|
||||
{
|
||||
public bool HasDownloads => TotalFiles > 0 || TotalBytes > 0;
|
||||
}
|
||||
#region Header Drawing
|
||||
|
||||
private void DrawUIDHeader()
|
||||
{
|
||||
@@ -532,21 +390,52 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
using (_uiSharedService.IconFont.Push())
|
||||
iconSize = ImGui.CalcTextSize(FontAwesomeIcon.PersonCirclePlus.ToIconString());
|
||||
|
||||
float contentWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
||||
float uidStartX = (contentWidth - uidTextSize.X) / 2f;
|
||||
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)
|
||||
{
|
||||
float iconYOffset = (uidTextSize.Y - iconSize.Y) * 0.5f;
|
||||
var buttonSize = new Vector2(iconSize.X, uidTextSize.Y);
|
||||
|
||||
ImGui.SetCursorPos(new Vector2(ImGui.GetStyle().ItemSpacing.X + 5f, cursorY));
|
||||
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.PersonCirclePlus.ToIconString());
|
||||
ImGui.GetWindowDrawList().AddText(iconPos, ImGui.GetColorU32(UIColors.Get("LightlessGreen")), FontAwesomeIcon.Wifi.ToIconString());
|
||||
|
||||
nextIconX = ImGui.GetItemRectMax().X + 6f;
|
||||
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
@@ -618,50 +507,8 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
if (ImGui.IsItemClicked())
|
||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
var padding = new Vector2(35f * ImGuiHelpers.GlobalScale);
|
||||
Selune.RegisterHighlight(
|
||||
ImGui.GetItemRectMin() - padding,
|
||||
ImGui.GetItemRectMax() + padding,
|
||||
SeluneHighlightMode.Point,
|
||||
exactSize: true,
|
||||
clipToElement: true,
|
||||
clipPadding: padding,
|
||||
highlightColorOverride: vanityGlowColor,
|
||||
highlightAlphaOverride: 0.05f);
|
||||
}
|
||||
|
||||
headerItemClicked = ImGui.IsItemClicked();
|
||||
|
||||
if (headerItemClicked)
|
||||
{
|
||||
ImGui.SetClipboardText(uidText);
|
||||
}
|
||||
|
||||
UiSharedService.AttachToolTip("Click to copy");
|
||||
|
||||
|
||||
// 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);
|
||||
@@ -675,24 +522,30 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
|
||||
if ((isOverTriHold || isOverVRAMUsage) && _playerPerformanceConfig.Current.WarnOnExceedingThresholds)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosY(cursorY + 15f);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow"));
|
||||
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());
|
||||
|
||||
string warningMessage = "";
|
||||
if (isOverTriHold)
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
warningMessage += $"You exceed your own triangles threshold by " +
|
||||
$"{actualTriCount - _playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000} triangles.";
|
||||
warningMessage += Environment.NewLine;
|
||||
|
||||
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 (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)));
|
||||
@@ -701,6 +554,34 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
@@ -708,10 +589,12 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
ImGui.SetClipboardText(_apiController.DisplayName);
|
||||
}
|
||||
|
||||
if (!string.Equals(_apiController.DisplayName, _apiController.UID, StringComparison.Ordinal))
|
||||
// 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)
|
||||
{
|
||||
var origTextSize = ImGui.CalcTextSize(_apiController.UID);
|
||||
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) / 2 - (origTextSize.X / 2));
|
||||
ImGui.SetCursorPosX(uidStartX);
|
||||
|
||||
if (useVanityColors)
|
||||
{
|
||||
@@ -746,14 +629,88 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
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
|
||||
@@ -889,6 +846,10 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Filtering & Sorting
|
||||
|
||||
private static bool PassesFilter(PairUiEntry entry, string filter)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filter)) return true;
|
||||
@@ -944,6 +905,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
VisiblePairSortMode.VramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateVramBytes),
|
||||
VisiblePairSortMode.EffectiveVramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateEffectiveVramBytes),
|
||||
VisiblePairSortMode.TriangleCount => SortVisibleByMetric(entryList, e => e.LastAppliedDataTris),
|
||||
VisiblePairSortMode.EffectiveTriangleCount => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateEffectiveTris),
|
||||
VisiblePairSortMode.Alphabetical => [.. entryList.OrderBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)],
|
||||
VisiblePairSortMode.PreferredDirectPairs => SortVisibleByPreferred(entryList),
|
||||
_ => SortEntries(entryList),
|
||||
@@ -1032,10 +994,11 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
return SortGroupEntries(entries, group);
|
||||
}
|
||||
|
||||
private void UiSharedService_GposeEnd()
|
||||
{
|
||||
IsOpen = _wasOpen;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region GPose Handlers
|
||||
|
||||
private void UiSharedService_GposeEnd() => IsOpen = _wasOpen;
|
||||
|
||||
private void UiSharedService_GposeStart()
|
||||
{
|
||||
@@ -1043,6 +1006,10 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
IsOpen = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Focus Tracking
|
||||
|
||||
private void RegisterFocusCharacter(Pair pair)
|
||||
{
|
||||
_pendingFocusPair = pair;
|
||||
@@ -1088,4 +1055,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
_pendingFocusPair = null;
|
||||
_pendingFocusFrame = -1;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
@@ -39,7 +39,8 @@ public abstract class DrawFolderBase : IDrawFolder
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
if (!RenderIfEmpty && !DrawPairs.Any()) return;
|
||||
var drawPairCount = DrawPairs.Count;
|
||||
if (!RenderIfEmpty && drawPairCount == 0) return;
|
||||
|
||||
_suppressNextRowToggle = false;
|
||||
|
||||
@@ -111,9 +112,9 @@ public abstract class DrawFolderBase : IDrawFolder
|
||||
if (_tagHandler.IsTagOpen(_id))
|
||||
{
|
||||
using var indent = ImRaii.PushIndent(_uiSharedService.GetIconSize(FontAwesomeIcon.EllipsisV).X + ImGui.GetStyle().ItemSpacing.X, false);
|
||||
if (DrawPairs.Any())
|
||||
if (drawPairCount > 0)
|
||||
{
|
||||
using var clipper = ImUtf8.ListClipper(DrawPairs.Count, ImGui.GetFrameHeightWithSpacing());
|
||||
using var clipper = ImUtf8.ListClipper(drawPairCount, ImGui.GetFrameHeightWithSpacing());
|
||||
while (clipper.Step())
|
||||
{
|
||||
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
|
||||
|
||||
@@ -326,6 +326,7 @@ public class DrawFolderTag : DrawFolderBase
|
||||
VisiblePairSortMode.VramUsage => "VRAM usage (descending)",
|
||||
VisiblePairSortMode.EffectiveVramUsage => "Effective VRAM usage (descending)",
|
||||
VisiblePairSortMode.TriangleCount => "Triangle count (descending)",
|
||||
VisiblePairSortMode.EffectiveTriangleCount => "Effective triangle count (descending)",
|
||||
VisiblePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
|
||||
_ => "Default",
|
||||
};
|
||||
|
||||
@@ -22,13 +22,16 @@ public class DrawGroupedGroupFolder : IDrawFolder
|
||||
private readonly ApiController _apiController;
|
||||
private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi;
|
||||
private readonly RenameSyncshellTagUi _renameSyncshellTagUi;
|
||||
private readonly HashSet<string> _onlinePairBuffer = new(StringComparer.Ordinal);
|
||||
private IImmutableList<DrawUserPair>? _drawPairsCache;
|
||||
private int? _totalPairsCache;
|
||||
private bool _wasHovered = false;
|
||||
private float _menuWidth;
|
||||
private bool _rowClickArmed;
|
||||
|
||||
public IImmutableList<DrawUserPair> DrawPairs => _groups.SelectMany(g => g.GroupDrawFolder.DrawPairs).ToImmutableList();
|
||||
public int OnlinePairs => _groups.SelectMany(g => g.GroupDrawFolder.DrawPairs).Where(g => g.Pair.IsOnline).DistinctBy(g => g.Pair.UserData.UID).Count();
|
||||
public int TotalPairs => _groups.Sum(g => g.GroupDrawFolder.TotalPairs);
|
||||
public IImmutableList<DrawUserPair> DrawPairs => _drawPairsCache ??= _groups.SelectMany(g => g.GroupDrawFolder.DrawPairs).ToImmutableList();
|
||||
public int OnlinePairs => CountOnlinePairs(DrawPairs);
|
||||
public int TotalPairs => _totalPairsCache ??= _groups.Sum(g => g.GroupDrawFolder.TotalPairs);
|
||||
|
||||
public DrawGroupedGroupFolder(IEnumerable<GroupFolder> groups, TagHandler tagHandler, ApiController apiController, UiSharedService uiSharedService, SelectSyncshellForTagUi selectSyncshellForTagUi, RenameSyncshellTagUi renameSyncshellTagUi, string tag)
|
||||
{
|
||||
@@ -50,6 +53,10 @@ public class DrawGroupedGroupFolder : IDrawFolder
|
||||
}
|
||||
|
||||
using var id = ImRaii.PushId(_id);
|
||||
var drawPairs = DrawPairs;
|
||||
var onlinePairs = CountOnlinePairs(drawPairs);
|
||||
var totalPairs = TotalPairs;
|
||||
var hasPairs = drawPairs.Count > 0;
|
||||
var color = ImRaii.PushColor(ImGuiCol.ChildBg, ImGui.GetColorU32(ImGuiCol.FrameBgHovered), _wasHovered);
|
||||
var allowRowClick = string.IsNullOrEmpty(_tag);
|
||||
var suppressRowToggle = false;
|
||||
@@ -85,10 +92,10 @@ public class DrawGroupedGroupFolder : IDrawFolder
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted("[" + OnlinePairs.ToString() + "]");
|
||||
ImGui.TextUnformatted("[" + onlinePairs.ToString() + "]");
|
||||
}
|
||||
UiSharedService.AttachToolTip(OnlinePairs + " online in all of your joined syncshells" + Environment.NewLine +
|
||||
TotalPairs + " pairs combined in all of your joined syncshells");
|
||||
UiSharedService.AttachToolTip(onlinePairs + " online in all of your joined syncshells" + Environment.NewLine +
|
||||
totalPairs + " pairs combined in all of your joined syncshells");
|
||||
ImGui.SameLine();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
if (_tag != "")
|
||||
@@ -96,7 +103,7 @@ public class DrawGroupedGroupFolder : IDrawFolder
|
||||
ImGui.TextUnformatted(_tag);
|
||||
|
||||
ImGui.SameLine();
|
||||
DrawPauseButton();
|
||||
DrawPauseButton(hasPairs);
|
||||
ImGui.SameLine();
|
||||
DrawMenu(ref suppressRowToggle);
|
||||
} else
|
||||
@@ -104,7 +111,7 @@ public class DrawGroupedGroupFolder : IDrawFolder
|
||||
ImGui.TextUnformatted("All Syncshells");
|
||||
|
||||
ImGui.SameLine();
|
||||
DrawPauseButton();
|
||||
DrawPauseButton(hasPairs);
|
||||
}
|
||||
}
|
||||
color.Dispose();
|
||||
@@ -151,9 +158,9 @@ public class DrawGroupedGroupFolder : IDrawFolder
|
||||
}
|
||||
}
|
||||
|
||||
protected void DrawPauseButton()
|
||||
protected void DrawPauseButton(bool hasPairs)
|
||||
{
|
||||
if (DrawPairs.Count > 0)
|
||||
if (hasPairs)
|
||||
{
|
||||
var isPaused = _groups.Select(g => g.GroupFullInfo).All(g => g.GroupUserPermissions.IsPaused());
|
||||
FontAwesomeIcon pauseIcon = isPaused ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
|
||||
@@ -179,6 +186,27 @@ public class DrawGroupedGroupFolder : IDrawFolder
|
||||
}
|
||||
}
|
||||
|
||||
private int CountOnlinePairs(IImmutableList<DrawUserPair> drawPairs)
|
||||
{
|
||||
if (drawPairs.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
_onlinePairBuffer.Clear();
|
||||
foreach (var pair in drawPairs)
|
||||
{
|
||||
if (!pair.Pair.IsOnline)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_onlinePairBuffer.Add(pair.Pair.UserData.UID);
|
||||
}
|
||||
|
||||
return _onlinePairBuffer.Count;
|
||||
}
|
||||
|
||||
protected void ChangePauseStateGroups()
|
||||
{
|
||||
foreach(var group in _groups)
|
||||
|
||||
@@ -4,8 +4,10 @@ using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services;
|
||||
@@ -37,8 +39,10 @@ public class DrawUserPair
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private readonly PlayerPerformanceConfigService _performanceConfigService;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly LocationShareService _locationShareService;
|
||||
private readonly CharaDataManager _charaDataManager;
|
||||
private readonly PairLedger _pairLedger;
|
||||
private readonly IpcCallerLifestream _lifestreamIpc;
|
||||
private float _menuWidth = -1;
|
||||
private bool _wasHovered = false;
|
||||
private TooltipSnapshot _tooltipSnapshot = TooltipSnapshot.Empty;
|
||||
@@ -57,8 +61,10 @@ public class DrawUserPair
|
||||
UiSharedService uiSharedService,
|
||||
PlayerPerformanceConfigService performanceConfigService,
|
||||
LightlessConfigService configService,
|
||||
LocationShareService locationShareService,
|
||||
CharaDataManager charaDataManager,
|
||||
PairLedger pairLedger)
|
||||
PairLedger pairLedger,
|
||||
IpcCallerLifestream lifestreamIpc)
|
||||
{
|
||||
_id = id;
|
||||
_uiEntry = uiEntry;
|
||||
@@ -74,8 +80,10 @@ public class DrawUserPair
|
||||
_uiSharedService = uiSharedService;
|
||||
_performanceConfigService = performanceConfigService;
|
||||
_configService = configService;
|
||||
_locationShareService = locationShareService;
|
||||
_charaDataManager = charaDataManager;
|
||||
_pairLedger = pairLedger;
|
||||
_lifestreamIpc = lifestreamIpc;
|
||||
}
|
||||
|
||||
public PairDisplayEntry DisplayEntry => _displayEntry;
|
||||
@@ -216,6 +224,48 @@ public class DrawUserPair
|
||||
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions));
|
||||
}
|
||||
UiSharedService.AttachToolTip("Changes VFX sync permissions with this user." + (individual ? individualText : string.Empty));
|
||||
|
||||
ImGui.SetCursorPosX(10f);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Globe);
|
||||
ImGui.SameLine();
|
||||
if (ImGui.BeginMenu("Toggle Location sharing"))
|
||||
{
|
||||
if (ImGui.MenuItem("Share for 30 Mins"))
|
||||
{
|
||||
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddMinutes(30));
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Share for 1 Hour"))
|
||||
{
|
||||
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddHours(1));
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Share for 3 Hours"))
|
||||
{
|
||||
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddHours(3));
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Share until manually stop"))
|
||||
{
|
||||
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.MaxValue);
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
if (ImGui.MenuItem("Stop Sharing"))
|
||||
{
|
||||
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.MinValue);
|
||||
}
|
||||
ImGui.EndMenu();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ToggleLocationSharing(List<string> users, DateTimeOffset expireAt)
|
||||
{
|
||||
var updated = await _apiController.ToggleLocationSharing(new LocationSharingToggleDto(users, expireAt)).ConfigureAwait(false);
|
||||
if (updated)
|
||||
{
|
||||
_locationShareService.UpdateSharingStatus(users, expireAt);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawIndividualMenu()
|
||||
@@ -295,7 +345,10 @@ public class DrawUserPair
|
||||
? FontAwesomeIcon.User : FontAwesomeIcon.Users);
|
||||
}
|
||||
|
||||
UiSharedService.AttachToolTip(GetUserTooltip());
|
||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||
{
|
||||
UiSharedService.AttachToolTip(GetUserTooltip());
|
||||
}
|
||||
|
||||
if (_performanceConfigService.Current.ShowPerformanceIndicator
|
||||
&& !_performanceConfigService.Current.UIDsToIgnore
|
||||
@@ -309,22 +362,25 @@ public class DrawUserPair
|
||||
|
||||
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow"));
|
||||
|
||||
string userWarningText = "WARNING: This user exceeds one or more of your defined thresholds:" + UiSharedService.TooltipSeparator;
|
||||
bool shownVram = false;
|
||||
if (_performanceConfigService.Current.VRAMSizeWarningThresholdMiB > 0
|
||||
&& _performanceConfigService.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024 < _pair.LastAppliedApproximateVRAMBytes)
|
||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||
{
|
||||
shownVram = true;
|
||||
userWarningText += $"Approx. VRAM Usage: Used: {UiSharedService.ByteToString(_pair.LastAppliedApproximateVRAMBytes)}, Threshold: {_performanceConfigService.Current.VRAMSizeWarningThresholdMiB} MiB";
|
||||
}
|
||||
if (_performanceConfigService.Current.TrisWarningThresholdThousands > 0
|
||||
&& _performanceConfigService.Current.TrisWarningThresholdThousands * 1024 < _pair.LastAppliedDataTris)
|
||||
{
|
||||
if (shownVram) userWarningText += Environment.NewLine;
|
||||
userWarningText += $"Approx. Triangle count: Used: {_pair.LastAppliedDataTris}, Threshold: {_performanceConfigService.Current.TrisWarningThresholdThousands * 1000}";
|
||||
}
|
||||
string userWarningText = "WARNING: This user exceeds one or more of your defined thresholds:" + UiSharedService.TooltipSeparator;
|
||||
bool shownVram = false;
|
||||
if (_performanceConfigService.Current.VRAMSizeWarningThresholdMiB > 0
|
||||
&& _performanceConfigService.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024 < _pair.LastAppliedApproximateVRAMBytes)
|
||||
{
|
||||
shownVram = true;
|
||||
userWarningText += $"Approx. VRAM Usage: Used: {UiSharedService.ByteToString(_pair.LastAppliedApproximateVRAMBytes)}, Threshold: {_performanceConfigService.Current.VRAMSizeWarningThresholdMiB} MiB";
|
||||
}
|
||||
if (_performanceConfigService.Current.TrisWarningThresholdThousands > 0
|
||||
&& _performanceConfigService.Current.TrisWarningThresholdThousands * 1024 < _pair.LastAppliedDataTris)
|
||||
{
|
||||
if (shownVram) userWarningText += Environment.NewLine;
|
||||
userWarningText += $"Approx. Triangle count: Used: {_pair.LastAppliedDataTris}, Threshold: {_performanceConfigService.Current.TrisWarningThresholdThousands * 1000}";
|
||||
}
|
||||
|
||||
UiSharedService.AttachToolTip(userWarningText);
|
||||
UiSharedService.AttachToolTip(userWarningText);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
@@ -384,6 +440,7 @@ public class DrawUserPair
|
||||
_pair.LastAppliedApproximateVRAMBytes,
|
||||
_pair.LastAppliedApproximateEffectiveVRAMBytes,
|
||||
_pair.LastAppliedDataTris,
|
||||
_pair.LastAppliedApproximateEffectiveTris,
|
||||
_pair.IsPaired,
|
||||
groupDisplays is null ? ImmutableArray<string>.Empty : ImmutableArray.CreateRange(groupDisplays));
|
||||
|
||||
@@ -399,6 +456,8 @@ public class DrawUserPair
|
||||
private static string BuildTooltip(in TooltipSnapshot snapshot)
|
||||
{
|
||||
var builder = new StringBuilder(256);
|
||||
static string FormatTriangles(long count) =>
|
||||
count > 1000 ? (count / 1000d).ToString("0.0'k'") : count.ToString();
|
||||
|
||||
if (snapshot.IsPaused)
|
||||
{
|
||||
@@ -465,9 +524,13 @@ public class DrawUserPair
|
||||
{
|
||||
builder.Append(Environment.NewLine);
|
||||
builder.Append("Approx. Triangle Count (excl. Vanilla): ");
|
||||
builder.Append(snapshot.LastAppliedDataTris > 1000
|
||||
? (snapshot.LastAppliedDataTris / 1000d).ToString("0.0'k'")
|
||||
: snapshot.LastAppliedDataTris);
|
||||
builder.Append(FormatTriangles(snapshot.LastAppliedDataTris));
|
||||
if (snapshot.LastAppliedApproximateEffectiveTris >= 0)
|
||||
{
|
||||
builder.Append(" (Effective: ");
|
||||
builder.Append(FormatTriangles(snapshot.LastAppliedApproximateEffectiveTris));
|
||||
builder.Append(')');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,11 +562,12 @@ public class DrawUserPair
|
||||
long LastAppliedApproximateVRAMBytes,
|
||||
long LastAppliedApproximateEffectiveVRAMBytes,
|
||||
long LastAppliedDataTris,
|
||||
long LastAppliedApproximateEffectiveTris,
|
||||
bool IsPaired,
|
||||
ImmutableArray<string> GroupDisplays)
|
||||
{
|
||||
public static TooltipSnapshot Empty { get; } =
|
||||
new(false, false, false, IndividualPairStatus.None, string.Empty, string.Empty, -1, -1, -1, -1, false, ImmutableArray<string>.Empty);
|
||||
new(false, false, false, IndividualPairStatus.None, string.Empty, string.Empty, -1, -1, -1, -1, -1, false, ImmutableArray<string>.Empty);
|
||||
}
|
||||
|
||||
private void DrawPairedClientMenu()
|
||||
@@ -560,12 +624,15 @@ public class DrawUserPair
|
||||
perm.SetPaused(!perm.IsPaused());
|
||||
_ = _apiController.UserSetPairPermissions(new(_pair.UserData, perm));
|
||||
}
|
||||
UiSharedService.AttachToolTip(!_pair.UserPair!.OwnPermissions.IsPaused()
|
||||
? ("Pause pairing with " + _pair.UserData.AliasOrUID
|
||||
+ (_pair.UserPair!.OwnPermissions.IsSticky()
|
||||
? string.Empty
|
||||
: UiSharedService.TooltipSeparator + "Hold CTRL to enable preferred permissions while pausing." + Environment.NewLine + "This will leave this pair paused even if unpausing syncshells including this pair."))
|
||||
: "Resume pairing with " + _pair.UserData.AliasOrUID);
|
||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||
{
|
||||
UiSharedService.AttachToolTip(!_pair.UserPair!.OwnPermissions.IsPaused()
|
||||
? ("Pause pairing with " + _pair.UserData.AliasOrUID
|
||||
+ (_pair.UserPair!.OwnPermissions.IsSticky()
|
||||
? string.Empty
|
||||
: UiSharedService.TooltipSeparator + "Hold CTRL to enable preferred permissions while pausing." + Environment.NewLine + "This will leave this pair paused even if unpausing syncshells including this pair."))
|
||||
: "Resume pairing with " + _pair.UserData.AliasOrUID);
|
||||
}
|
||||
|
||||
if (_pair.IsPaired)
|
||||
{
|
||||
@@ -574,6 +641,136 @@ public class DrawUserPair
|
||||
var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false);
|
||||
var individualIsSticky = _pair.UserPair!.OwnPermissions.IsSticky();
|
||||
var individualIcon = individualIsSticky ? FontAwesomeIcon.ArrowCircleUp : FontAwesomeIcon.InfoCircle;
|
||||
|
||||
var shareLocationIcon = FontAwesomeIcon.Globe;
|
||||
var location = _locationShareService.GetUserLocation(_pair.UserPair!.User.UID);
|
||||
var shareLocation = !string.IsNullOrEmpty(location);
|
||||
var expireAt = _locationShareService.GetSharingStatus(_pair.UserPair!.User.UID);
|
||||
var shareLocationToOther = expireAt > DateTimeOffset.UtcNow;
|
||||
var shareColor = shareLocation switch
|
||||
{
|
||||
true when shareLocationToOther => UIColors.Get("LightlessGreen"),
|
||||
true when !shareLocationToOther => UIColors.Get("LightlessBlue"),
|
||||
_ => UIColors.Get("LightlessYellow"),
|
||||
};
|
||||
|
||||
if (shareLocation || shareLocationToOther)
|
||||
{
|
||||
currentRightSide -= (_uiSharedService.GetIconSize(shareLocationIcon).X + spacingX);
|
||||
ImGui.SameLine(currentRightSide);
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, shareColor, shareLocation || shareLocationToOther))
|
||||
_uiSharedService.IconText(shareLocationIcon);
|
||||
|
||||
var popupId = $"LocationPopup_{_pair.UserData.UID}";
|
||||
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left) && shareLocation && !string.IsNullOrEmpty(location))
|
||||
{
|
||||
ImGui.OpenPopup(popupId);
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
|
||||
if (_pair.IsOnline)
|
||||
{
|
||||
if (shareLocation)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(location))
|
||||
{
|
||||
_uiSharedService.IconText(FontAwesomeIcon.LocationArrow);
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(location);
|
||||
ImGui.Separator();
|
||||
ImGui.TextUnformatted("Click to teleport to this location");
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted("Location info not updated, reconnect or wait for update.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted("NOT Sharing location with you. o(TヘTo)");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted("User not online. (´・ω・`)?");
|
||||
}
|
||||
ImGui.Separator();
|
||||
|
||||
if (shareLocationToOther)
|
||||
{
|
||||
ImGui.TextUnformatted("Sharing your location. ヾ(•ω•`)o");
|
||||
if (expireAt != DateTimeOffset.MaxValue)
|
||||
{
|
||||
ImGui.TextUnformatted("Expires at " + expireAt.ToLocalTime().ToString("g"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted("NOT sharing your location.  ̄へ ̄");
|
||||
}
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
|
||||
if (ImGui.BeginPopup(popupId))
|
||||
{
|
||||
|
||||
var locationInfo = _locationShareService.GetLocationForLifestreamByUid(_pair.UserData.UID);
|
||||
if (locationInfo != null)
|
||||
{
|
||||
var locationLi = locationInfo.Value;
|
||||
var housingAddress = _locationShareService.GetAddressBookEntryByLocation(locationLi);
|
||||
var mapAddress = _locationShareService.GetMapAddressByLocation(locationLi);
|
||||
ImGui.TextUnformatted("Teleport to user?");
|
||||
ImGui.Separator();
|
||||
if (!_lifestreamIpc.APIAvailable)
|
||||
{
|
||||
ImGui.TextUnformatted("Lifestream IPC is not available. Please ensure Lifestream is enabled");
|
||||
}
|
||||
else if (housingAddress != null || mapAddress != null)
|
||||
{
|
||||
ImGui.TextUnformatted($"Go to {location}?");
|
||||
ImGui.TextUnformatted($"NOTE: Teleporting to maps with multiple aetherytes or instances may not be accurate currently. (ie. Thavnair, Yanxia)");
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted("Lifestream cannot teleport here. If you are in a residential area, please make sure you're inside a plot.");
|
||||
}
|
||||
ImGui.Separator();
|
||||
if (_lifestreamIpc.APIAvailable && (housingAddress != null || mapAddress != null))
|
||||
{
|
||||
if (locationLi.HouseId is not 0 && housingAddress != null)
|
||||
{
|
||||
if (ImGui.Button("Navigate"))
|
||||
{
|
||||
_lifestreamIpc.GoToHousingAddress(housingAddress.Value);
|
||||
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
else if (mapAddress != null && locationLi.HouseId is 0)
|
||||
{
|
||||
if (ImGui.Button("Navigate"))
|
||||
{
|
||||
_lifestreamIpc.ExecuteLifestreamCommand(mapAddress);
|
||||
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
}
|
||||
if (ImGui.Button("Close"))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky)
|
||||
{
|
||||
@@ -663,8 +860,11 @@ public class DrawUserPair
|
||||
currentRightSide -= (_uiSharedService.GetIconSize(FontAwesomeIcon.Running).X + (spacingX / 2f));
|
||||
ImGui.SameLine(currentRightSide);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Running);
|
||||
UiSharedService.AttachToolTip($"This user has shared {sharedData.Count} Character Data Sets with you." + UiSharedService.TooltipSeparator
|
||||
+ "Click to open the Character Data Hub and show the entries.");
|
||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||
{
|
||||
UiSharedService.AttachToolTip($"This user has shared {sharedData.Count} Character Data Sets with you." + UiSharedService.TooltipSeparator
|
||||
+ "Click to open the Character Data Hub and show the entries.");
|
||||
}
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
||||
{
|
||||
_mediator.Publish(new OpenCharaDataHubWithFilterMessage(_pair.UserData));
|
||||
|
||||
1064
LightlessSync/UI/Components/OptimizationSettingsPanel.cs
Normal file
1064
LightlessSync/UI/Components/OptimizationSettingsPanel.cs
Normal file
File diff suppressed because it is too large
Load Diff
790
LightlessSync/UI/Components/OptimizationSummaryCard.cs
Normal file
790
LightlessSync/UI/Components/OptimizationSummaryCard.cs
Normal file
@@ -0,0 +1,790 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI.Services;
|
||||
using LightlessSync.UI.Style;
|
||||
using LightlessSync.WebAPI.Files;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace LightlessSync.UI.Components;
|
||||
|
||||
public sealed class OptimizationSummaryCard
|
||||
{
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
|
||||
private readonly FileUploadManager _fileTransferManager;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly OptimizationSettingsPanel _optimizationSettingsPanel;
|
||||
private readonly SeluneBrush _optimizationBrush = new();
|
||||
private const string OptimizationPopupId = "Optimization Settings##LightlessOptimization";
|
||||
private bool _optimizationPopupOpen;
|
||||
private bool _optimizationPopupRequest;
|
||||
private OptimizationPanelSection _optimizationPopupSection = OptimizationPanelSection.Texture;
|
||||
|
||||
public OptimizationSummaryCard(
|
||||
UiSharedService uiSharedService,
|
||||
PairUiService pairUiService,
|
||||
PlayerPerformanceConfigService playerPerformanceConfig,
|
||||
FileUploadManager fileTransferManager,
|
||||
LightlessMediator lightlessMediator)
|
||||
{
|
||||
_uiSharedService = uiSharedService;
|
||||
_pairUiService = pairUiService;
|
||||
_playerPerformanceConfig = playerPerformanceConfig;
|
||||
_fileTransferManager = fileTransferManager;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
_optimizationSettingsPanel = new OptimizationSettingsPanel(uiSharedService, playerPerformanceConfig, pairUiService);
|
||||
}
|
||||
|
||||
public bool Draw(int activeDownloads)
|
||||
{
|
||||
var totals = GetPerformanceTotals();
|
||||
var scale = ImGuiHelpers.GlobalScale;
|
||||
var accent = UIColors.Get("LightlessPurple");
|
||||
var accentBg = new Vector4(accent.X, accent.Y, accent.Z, 0.04f);
|
||||
var accentBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.16f);
|
||||
var summaryPadding = new Vector2(12f * scale, 6f * scale);
|
||||
var summaryItemSpacing = new Vector2(12f * scale, 4f * scale);
|
||||
var cellPadding = new Vector2(6f * scale, 2f * scale);
|
||||
var lineHeight = ImGui.GetFrameHeight();
|
||||
var lineSpacing = summaryItemSpacing.Y;
|
||||
var statsContentHeight = (lineHeight * 2f) + lineSpacing;
|
||||
var summaryHeight = MathF.Max(56f * scale, statsContentHeight + (summaryPadding.Y * 2f) + (cellPadding.Y * 2f));
|
||||
var activeUploads = _fileTransferManager.GetCurrentUploadsSnapshot().Count(upload => !upload.IsTransferred);
|
||||
|
||||
var textureButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Images);
|
||||
var modelButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.ProjectDiagram);
|
||||
var buttonWidth = MathF.Max(textureButtonSize.X, modelButtonSize.X);
|
||||
var performanceConfig = _playerPerformanceConfig.Current;
|
||||
var textureStatus = GetTextureOptimizationStatus(performanceConfig);
|
||||
var modelStatus = GetModelOptimizationStatus(performanceConfig);
|
||||
var textureStatusVisual = GetOptimizationStatusVisual(textureStatus);
|
||||
var modelStatusVisual = GetOptimizationStatusVisual(modelStatus);
|
||||
var textureStatusLines = BuildTextureOptimizationStatusLines(performanceConfig);
|
||||
var modelStatusLines = BuildModelOptimizationStatusLines(performanceConfig);
|
||||
var statusIconSpacing = 6f * scale;
|
||||
var statusIconWidth = MathF.Max(GetIconWidth(textureStatusVisual.Icon), GetIconWidth(modelStatusVisual.Icon));
|
||||
var buttonRowWidth = buttonWidth + statusIconWidth + statusIconSpacing;
|
||||
var vramValue = totals.HasVramData
|
||||
? UiSharedService.ByteToString(totals.DisplayVramBytes, addSuffix: true)
|
||||
: "n/a";
|
||||
var vramTooltip = BuildVramTooltipData(totals, UIColors.Get("LightlessBlue"));
|
||||
var triangleValue = totals.HasTriangleData
|
||||
? FormatTriangleCount(totals.DisplayTriangleCount)
|
||||
: "n/a";
|
||||
var triangleTooltip = BuildTriangleTooltipData(totals, UIColors.Get("LightlessPurple"));
|
||||
|
||||
var windowPos = ImGui.GetWindowPos();
|
||||
var windowSize = ImGui.GetWindowSize();
|
||||
var footerTop = ImGui.GetCursorScreenPos().Y;
|
||||
var gradientTop = MathF.Max(windowPos.Y, footerTop - (12f * scale));
|
||||
var gradientBottom = windowPos.Y + windowSize.Y;
|
||||
var footerSettings = new SeluneGradientSettings
|
||||
{
|
||||
GradientColor = UIColors.Get("LightlessPurple"),
|
||||
GradientPeakOpacity = 0.08f,
|
||||
GradientPeakPosition = 0.18f,
|
||||
BackgroundMode = SeluneGradientMode.Vertical,
|
||||
};
|
||||
using var footerSelune = Selune.Begin(_optimizationBrush, ImGui.GetWindowDrawList(), windowPos, windowSize, footerSettings);
|
||||
footerSelune.DrawGradient(gradientTop, gradientBottom, ImGui.GetIO().DeltaTime);
|
||||
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f * scale))
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize)))
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, summaryPadding))
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, summaryItemSpacing))
|
||||
using (ImRaii.PushColor(ImGuiCol.ChildBg, UiSharedService.Color(accentBg)))
|
||||
using (ImRaii.PushColor(ImGuiCol.Border, UiSharedService.Color(accentBorder)))
|
||||
using (var child = ImRaii.Child("optimizationSummary", new Vector2(-1f, summaryHeight), true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse))
|
||||
{
|
||||
if (child)
|
||||
{
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, cellPadding))
|
||||
{
|
||||
if (ImGui.BeginTable("optimizationSummaryTable", 2, ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.NoBordersInBody))
|
||||
{
|
||||
ImGui.TableSetupColumn("Stats", ImGuiTableColumnFlags.WidthStretch, 1f);
|
||||
ImGui.TableSetupColumn("Button", ImGuiTableColumnFlags.WidthFixed, buttonRowWidth + 12f * scale);
|
||||
|
||||
ImGui.TableNextRow();
|
||||
ImGui.TableNextColumn();
|
||||
var availableHeight = summaryHeight - (summaryPadding.Y * 2f) - (cellPadding.Y * 2f);
|
||||
var verticalPad = MathF.Max(0f, (availableHeight - statsContentHeight) * 0.5f);
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(summaryItemSpacing.X, 0f)))
|
||||
{
|
||||
if (verticalPad > 0f)
|
||||
{
|
||||
ImGui.Dummy(new Vector2(0f, verticalPad));
|
||||
}
|
||||
DrawOptimizationStatLine(FontAwesomeIcon.Memory, UIColors.Get("LightlessBlue"), "VRAM usage", vramValue, vramTooltip, scale);
|
||||
if (lineSpacing > 0f)
|
||||
{
|
||||
ImGui.Dummy(new Vector2(0f, lineSpacing));
|
||||
}
|
||||
DrawOptimizationStatLine(FontAwesomeIcon.ProjectDiagram, UIColors.Get("LightlessPurple"), "Triangles", triangleValue, triangleTooltip, scale);
|
||||
if (verticalPad > 0f)
|
||||
{
|
||||
ImGui.Dummy(new Vector2(0f, verticalPad));
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
var separatorX = ImGui.GetCursorScreenPos().X - cellPadding.X;
|
||||
var separatorTop = ImGui.GetWindowPos().Y + summaryPadding.Y;
|
||||
var separatorBottom = ImGui.GetWindowPos().Y + summaryHeight - summaryPadding.Y;
|
||||
ImGui.GetWindowDrawList().AddLine(
|
||||
new Vector2(separatorX, separatorTop),
|
||||
new Vector2(separatorX, separatorBottom),
|
||||
ImGui.ColorConvertFloat4ToU32(accentBorder),
|
||||
MathF.Max(1f, 1f * scale));
|
||||
float cellWidth = ImGui.GetContentRegionAvail().X;
|
||||
float offsetX = MathF.Max(0f, cellWidth - buttonRowWidth);
|
||||
float alignedX = ImGui.GetCursorPosX() + offsetX;
|
||||
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 6f * scale))
|
||||
using (ImRaii.PushColor(ImGuiCol.Button, ImGui.ColorConvertFloat4ToU32(new Vector4(0f, 0f, 0f, 0f))))
|
||||
{
|
||||
var buttonBorderThickness = 10f * scale;
|
||||
var buttonRounding = ImGui.GetStyle().FrameRounding;
|
||||
|
||||
DrawOptimizationStatusButtonRow(
|
||||
"Texture Optimization",
|
||||
textureStatusVisual.Icon,
|
||||
textureStatusVisual.Color,
|
||||
textureStatusVisual.Label,
|
||||
textureStatusLines,
|
||||
FontAwesomeIcon.Images,
|
||||
textureButtonSize,
|
||||
"Texture Optimization",
|
||||
activeUploads,
|
||||
activeDownloads,
|
||||
() => OpenOptimizationPopup(OptimizationPanelSection.Texture),
|
||||
alignedX,
|
||||
statusIconSpacing,
|
||||
buttonBorderThickness,
|
||||
buttonRounding);
|
||||
|
||||
DrawOptimizationStatusButtonRow(
|
||||
"Model Optimization",
|
||||
modelStatusVisual.Icon,
|
||||
modelStatusVisual.Color,
|
||||
modelStatusVisual.Label,
|
||||
modelStatusLines,
|
||||
FontAwesomeIcon.ProjectDiagram,
|
||||
modelButtonSize,
|
||||
"Model Optimization",
|
||||
activeUploads,
|
||||
activeDownloads,
|
||||
() => OpenOptimizationPopup(OptimizationPanelSection.Model),
|
||||
alignedX,
|
||||
statusIconSpacing,
|
||||
buttonBorderThickness,
|
||||
buttonRounding);
|
||||
}
|
||||
|
||||
ImGui.EndTable();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footerSelune.DrawHighlightOnly(gradientTop, gradientBottom, ImGui.GetIO().DeltaTime);
|
||||
DrawOptimizationPopup();
|
||||
return true;
|
||||
}
|
||||
|
||||
private PerformanceTotals GetPerformanceTotals()
|
||||
{
|
||||
HashSet<Pair> trackedPairs = new();
|
||||
|
||||
var snapshot = _pairUiService.GetSnapshot();
|
||||
|
||||
foreach (var pair in snapshot.DirectPairs)
|
||||
{
|
||||
trackedPairs.Add(pair);
|
||||
}
|
||||
|
||||
foreach (var group in snapshot.GroupPairs.Values)
|
||||
{
|
||||
foreach (var pair in group)
|
||||
{
|
||||
trackedPairs.Add(pair);
|
||||
}
|
||||
}
|
||||
|
||||
long displayVramBytes = 0;
|
||||
long originalVramBytes = 0;
|
||||
long effectiveVramBytes = 0;
|
||||
bool hasVramData = false;
|
||||
bool hasOriginalVram = false;
|
||||
bool hasEffectiveVram = false;
|
||||
|
||||
long displayTriangles = 0;
|
||||
long originalTriangles = 0;
|
||||
long effectiveTriangles = 0;
|
||||
bool hasTriangleData = false;
|
||||
bool hasOriginalTriangles = false;
|
||||
bool hasEffectiveTriangles = false;
|
||||
|
||||
foreach (var pair in trackedPairs)
|
||||
{
|
||||
if (!pair.IsVisible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var originalVram = pair.LastAppliedApproximateVRAMBytes;
|
||||
var effectiveVram = pair.LastAppliedApproximateEffectiveVRAMBytes;
|
||||
|
||||
if (originalVram >= 0)
|
||||
{
|
||||
originalVramBytes += originalVram;
|
||||
hasOriginalVram = true;
|
||||
}
|
||||
|
||||
if (effectiveVram >= 0)
|
||||
{
|
||||
effectiveVramBytes += effectiveVram;
|
||||
hasEffectiveVram = true;
|
||||
}
|
||||
|
||||
if (effectiveVram >= 0)
|
||||
{
|
||||
displayVramBytes += effectiveVram;
|
||||
hasVramData = true;
|
||||
}
|
||||
else if (originalVram >= 0)
|
||||
{
|
||||
displayVramBytes += originalVram;
|
||||
hasVramData = true;
|
||||
}
|
||||
|
||||
var originalTris = pair.LastAppliedDataTris;
|
||||
var effectiveTris = pair.LastAppliedApproximateEffectiveTris;
|
||||
|
||||
if (originalTris >= 0)
|
||||
{
|
||||
originalTriangles += originalTris;
|
||||
hasOriginalTriangles = true;
|
||||
}
|
||||
|
||||
if (effectiveTris >= 0)
|
||||
{
|
||||
effectiveTriangles += effectiveTris;
|
||||
hasEffectiveTriangles = true;
|
||||
}
|
||||
|
||||
if (effectiveTris >= 0)
|
||||
{
|
||||
displayTriangles += effectiveTris;
|
||||
hasTriangleData = true;
|
||||
}
|
||||
else if (originalTris >= 0)
|
||||
{
|
||||
displayTriangles += originalTris;
|
||||
hasTriangleData = true;
|
||||
}
|
||||
}
|
||||
|
||||
return new PerformanceTotals(
|
||||
displayVramBytes,
|
||||
originalVramBytes,
|
||||
effectiveVramBytes,
|
||||
displayTriangles,
|
||||
originalTriangles,
|
||||
effectiveTriangles,
|
||||
hasVramData,
|
||||
hasOriginalVram,
|
||||
hasEffectiveVram,
|
||||
hasTriangleData,
|
||||
hasOriginalTriangles,
|
||||
hasEffectiveTriangles);
|
||||
}
|
||||
|
||||
private void DrawOptimizationStatLine(FontAwesomeIcon icon, Vector4 iconColor, string label, string value, OptimizationStatTooltip? tooltip, float scale)
|
||||
{
|
||||
ImGui.AlignTextToFramePadding();
|
||||
_uiSharedService.IconText(icon, iconColor);
|
||||
var hovered = ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem);
|
||||
ImGui.SameLine(0f, 6f * scale);
|
||||
ImGui.TextUnformatted($"{label}: {value}");
|
||||
hovered |= ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem);
|
||||
if (hovered && tooltip.HasValue)
|
||||
{
|
||||
DrawOptimizationStatTooltip(tooltip.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private static OptimizationStatTooltip? BuildVramTooltipData(PerformanceTotals totals, Vector4 titleColor)
|
||||
{
|
||||
if (!totals.HasOriginalVram && !totals.HasEffectiveVram)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var lines = new List<OptimizationTooltipLine>();
|
||||
|
||||
if (totals.HasOriginalVram)
|
||||
{
|
||||
lines.Add(new OptimizationTooltipLine(
|
||||
"Original",
|
||||
UiSharedService.ByteToString(totals.OriginalVramBytes, addSuffix: true),
|
||||
UIColors.Get("LightlessYellow")));
|
||||
}
|
||||
|
||||
if (totals.HasEffectiveVram)
|
||||
{
|
||||
lines.Add(new OptimizationTooltipLine(
|
||||
"Effective",
|
||||
UiSharedService.ByteToString(totals.EffectiveVramBytes, addSuffix: true),
|
||||
UIColors.Get("LightlessGreen")));
|
||||
}
|
||||
|
||||
if (totals.HasOriginalVram && totals.HasEffectiveVram)
|
||||
{
|
||||
var savedBytes = Math.Max(0L, totals.OriginalVramBytes - totals.EffectiveVramBytes);
|
||||
if (savedBytes > 0)
|
||||
{
|
||||
lines.Add(new OptimizationTooltipLine(
|
||||
"Saved",
|
||||
UiSharedService.ByteToString(savedBytes, addSuffix: true),
|
||||
titleColor));
|
||||
}
|
||||
}
|
||||
|
||||
return new OptimizationStatTooltip(
|
||||
"Total VRAM usage",
|
||||
"Approximate texture memory across visible users.",
|
||||
titleColor,
|
||||
lines);
|
||||
}
|
||||
|
||||
private static OptimizationStatTooltip? BuildTriangleTooltipData(PerformanceTotals totals, Vector4 titleColor)
|
||||
{
|
||||
if (!totals.HasOriginalTriangles && !totals.HasEffectiveTriangles)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var lines = new List<OptimizationTooltipLine>();
|
||||
|
||||
if (totals.HasOriginalTriangles)
|
||||
{
|
||||
lines.Add(new OptimizationTooltipLine(
|
||||
"Original",
|
||||
$"{FormatTriangleCount(totals.OriginalTriangleCount)} tris",
|
||||
UIColors.Get("LightlessYellow")));
|
||||
}
|
||||
|
||||
if (totals.HasEffectiveTriangles)
|
||||
{
|
||||
lines.Add(new OptimizationTooltipLine(
|
||||
"Effective",
|
||||
$"{FormatTriangleCount(totals.EffectiveTriangleCount)} tris",
|
||||
UIColors.Get("LightlessGreen")));
|
||||
}
|
||||
|
||||
if (totals.HasOriginalTriangles && totals.HasEffectiveTriangles)
|
||||
{
|
||||
var savedTris = Math.Max(0L, totals.OriginalTriangleCount - totals.EffectiveTriangleCount);
|
||||
if (savedTris > 0)
|
||||
{
|
||||
lines.Add(new OptimizationTooltipLine(
|
||||
"Saved",
|
||||
$"{FormatTriangleCount(savedTris)} tris",
|
||||
titleColor));
|
||||
}
|
||||
}
|
||||
|
||||
return new OptimizationStatTooltip(
|
||||
"Total triangles",
|
||||
"Approximate triangle count across visible users.",
|
||||
titleColor,
|
||||
lines);
|
||||
}
|
||||
|
||||
private static string FormatTriangleCount(long triangleCount)
|
||||
{
|
||||
if (triangleCount < 0)
|
||||
{
|
||||
return "n/a";
|
||||
}
|
||||
|
||||
if (triangleCount >= 1_000_000)
|
||||
{
|
||||
return FormattableString.Invariant($"{triangleCount / 1_000_000d:0.#}m");
|
||||
}
|
||||
|
||||
if (triangleCount >= 1_000)
|
||||
{
|
||||
return FormattableString.Invariant($"{triangleCount / 1_000d:0.#}k");
|
||||
}
|
||||
|
||||
return triangleCount.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private enum OptimizationStatus
|
||||
{
|
||||
Off,
|
||||
Partial,
|
||||
On,
|
||||
}
|
||||
|
||||
private static OptimizationStatus GetTextureOptimizationStatus(PlayerPerformanceConfig config)
|
||||
{
|
||||
bool trimEnabled = config.EnableNonIndexTextureMipTrim;
|
||||
bool downscaleEnabled = config.EnableIndexTextureDownscale;
|
||||
|
||||
if (!trimEnabled && !downscaleEnabled)
|
||||
{
|
||||
return OptimizationStatus.Off;
|
||||
}
|
||||
|
||||
return trimEnabled && downscaleEnabled
|
||||
? OptimizationStatus.On
|
||||
: OptimizationStatus.Partial;
|
||||
}
|
||||
|
||||
private static OptimizationStatus GetModelOptimizationStatus(PlayerPerformanceConfig config)
|
||||
{
|
||||
if (!config.EnableModelDecimation)
|
||||
{
|
||||
return OptimizationStatus.Off;
|
||||
}
|
||||
|
||||
bool hasTargets = config.ModelDecimationAllowBody
|
||||
|| config.ModelDecimationAllowFaceHead
|
||||
|| config.ModelDecimationAllowTail
|
||||
|| config.ModelDecimationAllowClothing
|
||||
|| config.ModelDecimationAllowAccessories;
|
||||
|
||||
return hasTargets
|
||||
? OptimizationStatus.On
|
||||
: OptimizationStatus.Partial;
|
||||
}
|
||||
|
||||
private static (FontAwesomeIcon Icon, Vector4 Color, string Label) GetOptimizationStatusVisual(OptimizationStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OptimizationStatus.On => (FontAwesomeIcon.Check, UIColors.Get("LightlessGreen"), "Enabled"),
|
||||
OptimizationStatus.Partial => (FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow"), "Partial"),
|
||||
_ => (FontAwesomeIcon.Times, UIColors.Get("DimRed"), "Disabled"),
|
||||
};
|
||||
}
|
||||
|
||||
private static OptimizationTooltipLine[] BuildTextureOptimizationStatusLines(PlayerPerformanceConfig config)
|
||||
{
|
||||
return
|
||||
[
|
||||
new OptimizationTooltipLine("Trim mip levels", FormatOnOff(config.EnableNonIndexTextureMipTrim), GetOnOffColor(config.EnableNonIndexTextureMipTrim)),
|
||||
new OptimizationTooltipLine("Downscale index textures", FormatOnOff(config.EnableIndexTextureDownscale), GetOnOffColor(config.EnableIndexTextureDownscale)),
|
||||
new OptimizationTooltipLine("Max dimension", config.TextureDownscaleMaxDimension.ToString(CultureInfo.InvariantCulture)),
|
||||
new OptimizationTooltipLine("Only downscale uncompressed", FormatOnOff(config.OnlyDownscaleUncompressedTextures), GetOnOffColor(config.OnlyDownscaleUncompressedTextures)),
|
||||
new OptimizationTooltipLine("Compress uncompressed textures", FormatOnOff(config.EnableUncompressedTextureCompression), GetOnOffColor(config.EnableUncompressedTextureCompression)),
|
||||
new OptimizationTooltipLine("Skip auto-compress mipmaps", FormatOnOff(config.SkipUncompressedTextureCompressionMipMaps), GetOnOffColor(config.SkipUncompressedTextureCompressionMipMaps)),
|
||||
new OptimizationTooltipLine("Keep original textures", FormatOnOff(config.KeepOriginalTextureFiles), GetOnOffColor(config.KeepOriginalTextureFiles)),
|
||||
new OptimizationTooltipLine("Skip preferred pairs", FormatOnOff(config.SkipTextureDownscaleForPreferredPairs), GetOnOffColor(config.SkipTextureDownscaleForPreferredPairs)),
|
||||
];
|
||||
}
|
||||
|
||||
private static OptimizationTooltipLine[] BuildModelOptimizationStatusLines(PlayerPerformanceConfig config)
|
||||
{
|
||||
var targets = new List<string>();
|
||||
if (config.ModelDecimationAllowBody)
|
||||
{
|
||||
targets.Add("Body");
|
||||
}
|
||||
|
||||
if (config.ModelDecimationAllowFaceHead)
|
||||
{
|
||||
targets.Add("Face/head");
|
||||
}
|
||||
|
||||
if (config.ModelDecimationAllowTail)
|
||||
{
|
||||
targets.Add("Tails/Ears");
|
||||
}
|
||||
|
||||
if (config.ModelDecimationAllowClothing)
|
||||
{
|
||||
targets.Add("Clothing");
|
||||
}
|
||||
|
||||
if (config.ModelDecimationAllowAccessories)
|
||||
{
|
||||
targets.Add("Accessories");
|
||||
}
|
||||
|
||||
var targetLabel = targets.Count > 0 ? string.Join(", ", targets) : "None";
|
||||
var targetColor = targets.Count > 0 ? UIColors.Get("LightlessGreen") : UIColors.Get("DimRed");
|
||||
var threshold = config.ModelDecimationTriangleThreshold.ToString("N0", CultureInfo.InvariantCulture);
|
||||
var targetRatio = FormatPercent(config.ModelDecimationTargetRatio);
|
||||
|
||||
return
|
||||
[
|
||||
new OptimizationTooltipLine("Decimation enabled", FormatOnOff(config.EnableModelDecimation), GetOnOffColor(config.EnableModelDecimation)),
|
||||
new OptimizationTooltipLine("Triangle threshold", threshold),
|
||||
new OptimizationTooltipLine("Target ratio", targetRatio),
|
||||
new OptimizationTooltipLine("Normalize tangents", FormatOnOff(config.ModelDecimationNormalizeTangents), GetOnOffColor(config.ModelDecimationNormalizeTangents)),
|
||||
new OptimizationTooltipLine("Avoid body intersection", FormatOnOff(config.ModelDecimationAvoidBodyIntersection), GetOnOffColor(config.ModelDecimationAvoidBodyIntersection)),
|
||||
new OptimizationTooltipLine("Keep original models", FormatOnOff(config.KeepOriginalModelFiles), GetOnOffColor(config.KeepOriginalModelFiles)),
|
||||
new OptimizationTooltipLine("Skip preferred pairs", FormatOnOff(config.SkipModelDecimationForPreferredPairs), GetOnOffColor(config.SkipModelDecimationForPreferredPairs)),
|
||||
new OptimizationTooltipLine("Targets", targetLabel, targetColor),
|
||||
];
|
||||
}
|
||||
|
||||
private static string FormatOnOff(bool value)
|
||||
=> value ? "On" : "Off";
|
||||
|
||||
private static string FormatPercent(double value)
|
||||
=> FormattableString.Invariant($"{value * 100d:0.#}%");
|
||||
|
||||
private static Vector4 GetOnOffColor(bool value)
|
||||
=> value ? UIColors.Get("LightlessGreen") : UIColors.Get("DimRed");
|
||||
|
||||
private static float GetIconWidth(FontAwesomeIcon icon)
|
||||
{
|
||||
using var iconFont = ImRaii.PushFont(UiBuilder.IconFont);
|
||||
return ImGui.CalcTextSize(icon.ToIconString()).X;
|
||||
}
|
||||
|
||||
private readonly record struct OptimizationStatTooltip(string Title, string Description, Vector4 TitleColor, IReadOnlyList<OptimizationTooltipLine> Lines);
|
||||
|
||||
private static void DrawOptimizationStatTooltip(OptimizationStatTooltip tooltip)
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
ImGui.PushTextWrapPos(ImGui.GetFontSize() * 32f);
|
||||
|
||||
ImGui.TextColored(tooltip.TitleColor, tooltip.Title);
|
||||
ImGui.TextColored(UIColors.Get("LightlessGrey"), tooltip.Description);
|
||||
|
||||
foreach (var line in tooltip.Lines)
|
||||
{
|
||||
ImGui.TextUnformatted($"{line.Label}:");
|
||||
ImGui.SameLine();
|
||||
if (line.ValueColor.HasValue)
|
||||
{
|
||||
ImGui.TextColored(line.ValueColor.Value, line.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted(line.Value);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.PopTextWrapPos();
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
|
||||
private static void DrawOptimizationButtonTooltip(string title, int activeUploads, int activeDownloads)
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
ImGui.PushTextWrapPos(ImGui.GetFontSize() * 32f);
|
||||
|
||||
ImGui.TextColored(UIColors.Get("LightlessPurple"), title);
|
||||
ImGui.TextColored(UIColors.Get("LightlessGrey"), "Open optimization settings.");
|
||||
|
||||
if (activeUploads > 0 || activeDownloads > 0)
|
||||
{
|
||||
ImGui.Separator();
|
||||
ImGui.TextUnformatted($"Active uploads: {activeUploads}");
|
||||
ImGui.TextUnformatted($"Active downloads: {activeDownloads}");
|
||||
}
|
||||
|
||||
ImGui.PopTextWrapPos();
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
|
||||
private readonly record struct OptimizationTooltipLine(string Label, string Value, Vector4? ValueColor = null);
|
||||
|
||||
private static void DrawOptimizationStatusTooltip(string title, string statusLabel, Vector4 statusColor, IReadOnlyList<OptimizationTooltipLine> lines)
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
ImGui.PushTextWrapPos(ImGui.GetFontSize() * 32f);
|
||||
|
||||
ImGui.TextColored(UIColors.Get("LightlessPurple"), title);
|
||||
ImGui.TextUnformatted("Status:");
|
||||
ImGui.SameLine();
|
||||
ImGui.TextColored(statusColor, statusLabel);
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
ImGui.TextUnformatted($"{line.Label}:");
|
||||
ImGui.SameLine();
|
||||
if (line.ValueColor.HasValue)
|
||||
{
|
||||
ImGui.TextColored(line.ValueColor.Value, line.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted(line.Value);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.PopTextWrapPos();
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
|
||||
private void DrawOptimizationStatusButtonRow(
|
||||
string statusTitle,
|
||||
FontAwesomeIcon statusIcon,
|
||||
Vector4 statusColor,
|
||||
string statusLabel,
|
||||
IReadOnlyList<OptimizationTooltipLine> statusLines,
|
||||
FontAwesomeIcon buttonIcon,
|
||||
Vector2 buttonSize,
|
||||
string tooltipTitle,
|
||||
int activeUploads,
|
||||
int activeDownloads,
|
||||
Action openPopup,
|
||||
float alignedX,
|
||||
float iconSpacing,
|
||||
float buttonBorderThickness,
|
||||
float buttonRounding)
|
||||
{
|
||||
ImGui.SetCursorPosX(alignedX);
|
||||
ImGui.AlignTextToFramePadding();
|
||||
_uiSharedService.IconText(statusIcon, statusColor);
|
||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem))
|
||||
{
|
||||
DrawOptimizationStatusTooltip(statusTitle, statusLabel, statusColor, statusLines);
|
||||
}
|
||||
|
||||
ImGui.SameLine(0f, iconSpacing);
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
{
|
||||
if (ImGui.Button(buttonIcon.ToIconString(), buttonSize))
|
||||
{
|
||||
openPopup();
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
|
||||
{
|
||||
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem))
|
||||
{
|
||||
DrawOptimizationButtonTooltip(tooltipTitle, activeUploads, activeDownloads);
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenOptimizationPopup(OptimizationPanelSection section)
|
||||
{
|
||||
_optimizationPopupSection = section;
|
||||
_optimizationPopupOpen = true;
|
||||
_optimizationPopupRequest = true;
|
||||
}
|
||||
|
||||
private void DrawOptimizationPopup()
|
||||
{
|
||||
if (!_optimizationPopupOpen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_optimizationPopupRequest)
|
||||
{
|
||||
ImGui.OpenPopup(OptimizationPopupId);
|
||||
_optimizationPopupRequest = false;
|
||||
}
|
||||
|
||||
var scale = ImGuiHelpers.GlobalScale;
|
||||
ImGui.SetNextWindowSize(new Vector2(680f * scale, 640f * scale), ImGuiCond.Appearing);
|
||||
|
||||
if (ImGui.BeginPopupModal(OptimizationPopupId, ref _optimizationPopupOpen, UiSharedService.PopupWindowFlags))
|
||||
{
|
||||
DrawOptimizationPopupHeader();
|
||||
ImGui.Separator();
|
||||
ImGui.Dummy(new Vector2(0f, 4f * scale));
|
||||
using (var child = ImRaii.Child("optimization-popup-body", new Vector2(0f, 0f), false, ImGuiWindowFlags.AlwaysVerticalScrollbar))
|
||||
{
|
||||
if (child)
|
||||
{
|
||||
_optimizationSettingsPanel.DrawPopup(_optimizationPopupSection);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawOptimizationPopupHeader()
|
||||
{
|
||||
var scale = ImGuiHelpers.GlobalScale;
|
||||
var (title, icon, color, section) = GetPopupHeaderData(_optimizationPopupSection);
|
||||
var settingsButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Cog);
|
||||
using (var table = ImRaii.Table("optimization-popup-header", 2, ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.NoBordersInBody))
|
||||
{
|
||||
if (!table)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui.TableSetupColumn("Title", ImGuiTableColumnFlags.WidthStretch);
|
||||
ImGui.TableSetupColumn("Settings", ImGuiTableColumnFlags.WidthFixed, settingsButtonSize.X);
|
||||
|
||||
ImGui.TableNextRow();
|
||||
ImGui.TableNextColumn();
|
||||
using (_uiSharedService.MediumFont.Push())
|
||||
{
|
||||
_uiSharedService.IconText(icon, color);
|
||||
ImGui.SameLine(0f, 6f * scale);
|
||||
ImGui.TextColored(color, title);
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
{
|
||||
if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString(), settingsButtonSize))
|
||||
{
|
||||
OpenOptimizationSettings(section);
|
||||
}
|
||||
}
|
||||
|
||||
UiSharedService.AttachToolTip("Open this section in Settings.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenOptimizationSettings(OptimizationPanelSection section)
|
||||
{
|
||||
var target = section == OptimizationPanelSection.Texture
|
||||
? PerformanceSettingsSection.TextureOptimization
|
||||
: PerformanceSettingsSection.ModelOptimization;
|
||||
_lightlessMediator.Publish(new OpenPerformanceSettingsMessage(target));
|
||||
_optimizationPopupOpen = false;
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
|
||||
private static (string Title, FontAwesomeIcon Icon, Vector4 Color, OptimizationPanelSection Section) GetPopupHeaderData(OptimizationPanelSection section)
|
||||
{
|
||||
return section == OptimizationPanelSection.Texture
|
||||
? ("Texture Optimization", FontAwesomeIcon.Images, UIColors.Get("LightlessYellow"), OptimizationPanelSection.Texture)
|
||||
: ("Model Optimization", FontAwesomeIcon.ProjectDiagram, UIColors.Get("LightlessOrange"), OptimizationPanelSection.Model);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private readonly record struct PerformanceTotals(
|
||||
long DisplayVramBytes,
|
||||
long OriginalVramBytes,
|
||||
long EffectiveVramBytes,
|
||||
long DisplayTriangleCount,
|
||||
long OriginalTriangleCount,
|
||||
long EffectiveTriangleCount,
|
||||
bool HasVramData,
|
||||
bool HasOriginalVram,
|
||||
bool HasEffectiveVram,
|
||||
bool HasTriangleData,
|
||||
bool HasOriginalTriangles,
|
||||
bool HasEffectiveTriangles);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@ namespace LightlessSync.UI;
|
||||
public class DownloadUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
||||
private readonly ConcurrentDictionary<GameObjectHandler, IReadOnlyDictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly FileUploadManager _fileTransferManager;
|
||||
private readonly UiSharedService _uiShared;
|
||||
@@ -25,6 +25,8 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
|
||||
private readonly Dictionary<GameObjectHandler, Vector2> _smoothed = [];
|
||||
private readonly Dictionary<GameObjectHandler, DownloadSpeedTracker> _downloadSpeeds = [];
|
||||
private readonly Dictionary<GameObjectHandler, (int TotalFiles, long TotalBytes)> _downloadInitialTotals = [];
|
||||
|
||||
|
||||
private byte _transferBoxTransparency = 100;
|
||||
private bool _notificationDismissed = true;
|
||||
@@ -63,9 +65,15 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
|
||||
IsOpen = true;
|
||||
|
||||
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) =>
|
||||
Mediator.Subscribe<DownloadStartedMessage>(this, msg =>
|
||||
{
|
||||
_currentDownloads[msg.DownloadId] = msg.DownloadStatus;
|
||||
|
||||
var snap = msg.DownloadStatus.ToArray();
|
||||
var totalFiles = snap.Sum(kv => kv.Value?.TotalFiles ?? 0);
|
||||
var totalBytes = snap.Sum(kv => kv.Value?.TotalBytes ?? 0);
|
||||
|
||||
_downloadInitialTotals[msg.DownloadId] = (totalFiles, totalBytes);
|
||||
_notificationDismissed = false;
|
||||
});
|
||||
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) =>
|
||||
@@ -73,7 +81,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
_currentDownloads.TryRemove(msg.DownloadId, out _);
|
||||
|
||||
// Dismiss notification if all downloads are complete
|
||||
if (!_currentDownloads.Any() && !_notificationDismissed)
|
||||
if (_currentDownloads.IsEmpty && !_notificationDismissed)
|
||||
{
|
||||
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
|
||||
_notificationDismissed = true;
|
||||
@@ -164,10 +172,10 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
const float rounding = 6f;
|
||||
var shadowOffset = new Vector2(2, 2);
|
||||
|
||||
List<KeyValuePair<GameObjectHandler, Dictionary<string, FileDownloadStatus>>> transfers;
|
||||
List<KeyValuePair<GameObjectHandler, IReadOnlyDictionary<string, FileDownloadStatus>>> transfers;
|
||||
try
|
||||
{
|
||||
transfers = _currentDownloads.ToList();
|
||||
transfers = [.. _currentDownloads];
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
@@ -206,12 +214,16 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
var dlQueue = 0;
|
||||
var dlProg = 0;
|
||||
var dlDecomp = 0;
|
||||
var dlComplete = 0;
|
||||
|
||||
foreach (var entry in transfer.Value)
|
||||
{
|
||||
var fileStatus = entry.Value;
|
||||
switch (fileStatus.DownloadStatus)
|
||||
{
|
||||
case DownloadStatus.Initializing:
|
||||
dlQueue++;
|
||||
break;
|
||||
case DownloadStatus.WaitingForSlot:
|
||||
dlSlot++;
|
||||
break;
|
||||
@@ -224,15 +236,20 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
case DownloadStatus.Decompressing:
|
||||
dlDecomp++;
|
||||
break;
|
||||
case DownloadStatus.Completed:
|
||||
dlComplete++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var isAllComplete = dlComplete > 0 && dlProg == 0 && dlDecomp == 0 && dlQueue == 0 && dlSlot == 0;
|
||||
|
||||
string statusText;
|
||||
if (dlProg > 0)
|
||||
{
|
||||
statusText = "Downloading";
|
||||
}
|
||||
else if (dlDecomp > 0 || (totalBytes > 0 && transferredBytes >= totalBytes))
|
||||
else if (dlDecomp > 0)
|
||||
{
|
||||
statusText = "Decompressing";
|
||||
}
|
||||
@@ -244,6 +261,10 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
statusText = "Waiting for slot";
|
||||
}
|
||||
else if (isAllComplete)
|
||||
{
|
||||
statusText = "Completed";
|
||||
}
|
||||
else
|
||||
{
|
||||
statusText = "Waiting";
|
||||
@@ -304,16 +325,13 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
|
||||
if (hasValidSize)
|
||||
{
|
||||
if (dlProg > 0)
|
||||
{
|
||||
fillPercent = transferredBytes / (double)totalBytes;
|
||||
showFill = true;
|
||||
}
|
||||
else if (dlDecomp > 0 || transferredBytes >= totalBytes)
|
||||
fillPercent = totalBytes > 0 ? transferredBytes / (double)totalBytes : 0.0;
|
||||
if (isAllComplete && totalBytes > 0)
|
||||
{
|
||||
fillPercent = 1.0;
|
||||
showFill = true;
|
||||
}
|
||||
|
||||
showFill = transferredBytes > 0 || isAllComplete;
|
||||
}
|
||||
|
||||
if (showFill)
|
||||
@@ -341,10 +359,14 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
downloadText =
|
||||
$"{statusText} {UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
|
||||
}
|
||||
else if ((dlDecomp > 0 || transferredBytes >= totalBytes) && hasValidSize)
|
||||
else if (dlDecomp > 0)
|
||||
{
|
||||
downloadText = "Decompressing";
|
||||
}
|
||||
else if (isAllComplete)
|
||||
{
|
||||
downloadText = "Completed";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Waiting states
|
||||
@@ -417,6 +439,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
var totalDlQueue = 0;
|
||||
var totalDlProg = 0;
|
||||
var totalDlDecomp = 0;
|
||||
var totalDlComplete = 0;
|
||||
|
||||
var perPlayer = new List<(
|
||||
string Name,
|
||||
@@ -428,16 +451,21 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
int DlSlot,
|
||||
int DlQueue,
|
||||
int DlProg,
|
||||
int DlDecomp)>();
|
||||
int DlDecomp,
|
||||
int DlComplete)>();
|
||||
|
||||
foreach (var transfer in _currentDownloads)
|
||||
{
|
||||
var handler = transfer.Key;
|
||||
var statuses = transfer.Value.Values;
|
||||
|
||||
var playerTotalFiles = statuses.Sum(s => s.TotalFiles);
|
||||
var playerTransferredFiles = statuses.Sum(s => s.TransferredFiles);
|
||||
var playerTotalBytes = statuses.Sum(s => s.TotalBytes);
|
||||
var (playerTotalFiles, playerTotalBytes) = _downloadInitialTotals.TryGetValue(handler, out var totals)
|
||||
? totals
|
||||
: (statuses.Sum(s => s.TotalFiles), statuses.Sum(s => s.TotalBytes));
|
||||
|
||||
var playerTransferredFiles = statuses.Count(s =>
|
||||
s.DownloadStatus == DownloadStatus.Decompressing ||
|
||||
s.TransferredBytes >= s.TotalBytes);
|
||||
var playerTransferredBytes = statuses.Sum(s => s.TransferredBytes);
|
||||
|
||||
totalFiles += playerTotalFiles;
|
||||
@@ -445,25 +473,27 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
totalBytes += playerTotalBytes;
|
||||
transferredBytes += playerTransferredBytes;
|
||||
|
||||
// per-player W/Q/P/D
|
||||
// per-player W/Q/P/D/C
|
||||
var playerDlSlot = 0;
|
||||
var playerDlQueue = 0;
|
||||
var playerDlProg = 0;
|
||||
var playerDlDecomp = 0;
|
||||
var playerDlComplete = 0;
|
||||
|
||||
foreach (var entry in transfer.Value)
|
||||
{
|
||||
var fileStatus = entry.Value;
|
||||
switch (fileStatus.DownloadStatus)
|
||||
{
|
||||
case DownloadStatus.WaitingForSlot:
|
||||
playerDlSlot++;
|
||||
totalDlSlot++;
|
||||
break;
|
||||
case DownloadStatus.Initializing:
|
||||
case DownloadStatus.WaitingForQueue:
|
||||
playerDlQueue++;
|
||||
totalDlQueue++;
|
||||
break;
|
||||
case DownloadStatus.WaitingForSlot:
|
||||
playerDlSlot++;
|
||||
totalDlSlot++;
|
||||
break;
|
||||
case DownloadStatus.Downloading:
|
||||
playerDlProg++;
|
||||
totalDlProg++;
|
||||
@@ -472,6 +502,10 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
playerDlDecomp++;
|
||||
totalDlDecomp++;
|
||||
break;
|
||||
case DownloadStatus.Completed:
|
||||
playerDlComplete++;
|
||||
totalDlComplete++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,7 +531,8 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
playerDlSlot,
|
||||
playerDlQueue,
|
||||
playerDlProg,
|
||||
playerDlDecomp
|
||||
playerDlDecomp,
|
||||
playerDlComplete
|
||||
));
|
||||
}
|
||||
|
||||
@@ -511,17 +546,12 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
if (totalFiles == 0 || totalBytes == 0)
|
||||
return;
|
||||
|
||||
// max speed for per-player bar scale (clamped)
|
||||
double maxSpeed = perPlayer.Count > 0 ? perPlayer.Max(p => p.SpeedBytesPerSecond) : 0;
|
||||
if (maxSpeed <= 0)
|
||||
maxSpeed = 1;
|
||||
|
||||
var drawList = ImGui.GetBackgroundDrawList();
|
||||
var windowPos = ImGui.GetWindowPos();
|
||||
|
||||
// Overall texts
|
||||
var headerText =
|
||||
$"Downloading {transferredFiles}/{totalFiles} files [W:{totalDlSlot}/Q:{totalDlQueue}/P:{totalDlProg}/D:{totalDlDecomp}]";
|
||||
$"Downloading {transferredFiles}/{totalFiles} files [W:{totalDlSlot}/Q:{totalDlQueue}/P:{totalDlProg}/D:{totalDlDecomp}/C:{totalDlComplete}]";
|
||||
|
||||
var bytesText =
|
||||
$"{UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
|
||||
@@ -544,7 +574,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
foreach (var p in perPlayer)
|
||||
{
|
||||
var line =
|
||||
$"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}] {p.TransferredFiles}/{p.TotalFiles}";
|
||||
$"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}/C:{p.DlComplete}] {p.TransferredFiles}/{p.TotalFiles}";
|
||||
|
||||
var lineSize = ImGui.CalcTextSize(line);
|
||||
if (lineSize.X > contentWidth)
|
||||
@@ -662,7 +692,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
&& p.TransferredBytes > 0;
|
||||
|
||||
var labelLine =
|
||||
$"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}] {p.TransferredFiles}/{p.TotalFiles}";
|
||||
$"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}/C:{p.DlComplete}] {p.TransferredFiles}/{p.TotalFiles}";
|
||||
|
||||
if (!showBar)
|
||||
{
|
||||
@@ -721,13 +751,18 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
// Text inside bar: downloading vs decompressing
|
||||
string barText;
|
||||
|
||||
var isDecompressing = p.DlDecomp > 0 && p.TransferredBytes >= p.TotalBytes && p.TotalBytes > 0;
|
||||
var isDecompressing = p.DlDecomp > 0;
|
||||
var isAllComplete = p.DlComplete > 0 && p.DlProg == 0 && p.DlDecomp == 0 && p.DlQueue == 0 && p.DlSlot == 0;
|
||||
|
||||
if (isDecompressing)
|
||||
{
|
||||
// Keep bar full, static text showing decompressing
|
||||
barText = "Decompressing...";
|
||||
}
|
||||
else if (isAllComplete)
|
||||
{
|
||||
barText = "Completed";
|
||||
}
|
||||
else
|
||||
{
|
||||
var bytesInside =
|
||||
@@ -808,6 +843,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
var dlQueue = 0;
|
||||
var dlProg = 0;
|
||||
var dlDecomp = 0;
|
||||
var dlComplete = 0;
|
||||
long totalBytes = 0;
|
||||
long transferredBytes = 0;
|
||||
|
||||
@@ -817,22 +853,29 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
var fileStatus = entry.Value;
|
||||
switch (fileStatus.DownloadStatus)
|
||||
{
|
||||
case DownloadStatus.Initializing: dlQueue++; break;
|
||||
case DownloadStatus.WaitingForSlot: dlSlot++; break;
|
||||
case DownloadStatus.WaitingForQueue: dlQueue++; break;
|
||||
case DownloadStatus.Downloading: dlProg++; break;
|
||||
case DownloadStatus.Decompressing: dlDecomp++; break;
|
||||
case DownloadStatus.Completed: dlComplete++; break;
|
||||
}
|
||||
totalBytes += fileStatus.TotalBytes;
|
||||
transferredBytes += fileStatus.TransferredBytes;
|
||||
}
|
||||
|
||||
var progress = totalBytes > 0 ? (float)transferredBytes / totalBytes : 0f;
|
||||
if (dlComplete > 0 && dlProg == 0 && dlDecomp == 0 && dlQueue == 0 && dlSlot == 0)
|
||||
{
|
||||
progress = 1f;
|
||||
}
|
||||
|
||||
string status;
|
||||
if (dlDecomp > 0) status = "decompressing";
|
||||
else if (dlProg > 0) status = "downloading";
|
||||
else if (dlQueue > 0) status = "queued";
|
||||
else if (dlSlot > 0) status = "waiting";
|
||||
else if (dlComplete > 0) status = "completed";
|
||||
else status = "completed";
|
||||
|
||||
downloadStatus.Add((item.Key.Name, progress, status));
|
||||
|
||||
@@ -16,6 +16,7 @@ using LightlessSync.UI.Handlers;
|
||||
using LightlessSync.UI.Models;
|
||||
using LightlessSync.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
|
||||
namespace LightlessSync.UI;
|
||||
|
||||
@@ -29,6 +30,7 @@ public class DrawEntityFactory
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||
private readonly LocationShareService _locationShareService;
|
||||
private readonly CharaDataManager _charaDataManager;
|
||||
private readonly SelectTagForPairUi _selectTagForPairUi;
|
||||
private readonly RenamePairTagUi _renamePairTagUi;
|
||||
@@ -39,6 +41,7 @@ public class DrawEntityFactory
|
||||
private readonly IdDisplayHandler _uidDisplayHandler;
|
||||
private readonly PairLedger _pairLedger;
|
||||
private readonly PairFactory _pairFactory;
|
||||
private readonly IpcCallerLifestream _lifestreamIpc;
|
||||
|
||||
public DrawEntityFactory(
|
||||
ILogger<DrawEntityFactory> logger,
|
||||
@@ -53,12 +56,14 @@ public class DrawEntityFactory
|
||||
LightlessConfigService configService,
|
||||
UiSharedService uiSharedService,
|
||||
PlayerPerformanceConfigService playerPerformanceConfigService,
|
||||
LocationShareService locationShareService,
|
||||
CharaDataManager charaDataManager,
|
||||
SelectTagForSyncshellUi selectTagForSyncshellUi,
|
||||
RenameSyncshellTagUi renameSyncshellTagUi,
|
||||
SelectSyncshellForTagUi selectSyncshellForTagUi,
|
||||
PairLedger pairLedger,
|
||||
PairFactory pairFactory)
|
||||
PairFactory pairFactory,
|
||||
IpcCallerLifestream lifestreamIpc)
|
||||
{
|
||||
_logger = logger;
|
||||
_apiController = apiController;
|
||||
@@ -72,12 +77,14 @@ public class DrawEntityFactory
|
||||
_configService = configService;
|
||||
_uiSharedService = uiSharedService;
|
||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||
_locationShareService = locationShareService;
|
||||
_charaDataManager = charaDataManager;
|
||||
_selectTagForSyncshellUi = selectTagForSyncshellUi;
|
||||
_renameSyncshellTagUi = renameSyncshellTagUi;
|
||||
_selectSyncshellForTagUi = selectSyncshellForTagUi;
|
||||
_pairLedger = pairLedger;
|
||||
_pairFactory = pairFactory;
|
||||
_lifestreamIpc = lifestreamIpc;
|
||||
}
|
||||
|
||||
public DrawFolderGroup CreateGroupFolder(
|
||||
@@ -162,8 +169,10 @@ public class DrawEntityFactory
|
||||
_uiSharedService,
|
||||
_playerPerformanceConfigService,
|
||||
_configService,
|
||||
_locationShareService,
|
||||
_charaDataManager,
|
||||
_pairLedger);
|
||||
_pairLedger,
|
||||
_lifestreamIpc);
|
||||
}
|
||||
|
||||
public IReadOnlyList<PairUiEntry> GetAllEntries()
|
||||
@@ -213,6 +222,7 @@ public class DrawEntityFactory
|
||||
entry.PairStatus,
|
||||
handler?.LastAppliedDataBytes ?? -1,
|
||||
handler?.LastAppliedDataTris ?? -1,
|
||||
handler?.LastAppliedApproximateEffectiveTris ?? -1,
|
||||
handler?.LastAppliedApproximateVRAMBytes ?? -1,
|
||||
handler?.LastAppliedApproximateEffectiveVRAMBytes ?? -1,
|
||||
handler);
|
||||
|
||||
@@ -103,10 +103,19 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
|
||||
_cancellationTokenSource.Cancel();
|
||||
|
||||
if (_dalamudUtilService.IsOnFrameworkThread)
|
||||
{
|
||||
_logger.LogDebug("Skipping Lightfinder DTR wait on framework thread during shutdown.");
|
||||
_cancellationTokenSource.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _runTask!.ConfigureAwait(false);
|
||||
if (_runTask != null)
|
||||
await _runTask.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
@@ -415,7 +415,9 @@ public class IdDisplayHandler
|
||||
var vramBytes = pair.LastAppliedApproximateEffectiveVRAMBytes >= 0
|
||||
? pair.LastAppliedApproximateEffectiveVRAMBytes
|
||||
: pair.LastAppliedApproximateVRAMBytes;
|
||||
var triangleCount = pair.LastAppliedDataTris;
|
||||
var triangleCount = pair.LastAppliedApproximateEffectiveTris >= 0
|
||||
? pair.LastAppliedApproximateEffectiveTris
|
||||
: pair.LastAppliedDataTris;
|
||||
if (vramBytes < 0 && triangleCount < 0)
|
||||
{
|
||||
return null;
|
||||
|
||||
@@ -6,12 +6,12 @@ using Dalamud.Utility;
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.Localization;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
@@ -21,7 +21,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly CacheMonitor _cacheMonitor;
|
||||
private readonly Dictionary<string, string> _languages = new(StringComparer.Ordinal) { { "English", "en" }, { "Deutsch", "de" }, { "Français", "fr" } };
|
||||
private readonly Dictionary<string, string> _languages = new(StringComparer.Ordinal) { { "English", "en" }, { "Deutsch", "de" }, { "Français", "fr" }, { "中文", "zh"} };
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly UiSharedService _uiShared;
|
||||
@@ -31,7 +31,6 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
||||
private string _secretKey = string.Empty;
|
||||
private string _timeoutLabel = string.Empty;
|
||||
private Task? _timeoutTask;
|
||||
private string[]? _tosParagraphs;
|
||||
private bool _useLegacyLogin = false;
|
||||
|
||||
public IntroUi(ILogger<IntroUi> logger, UiSharedService uiShared, LightlessConfigService configService,
|
||||
@@ -50,8 +49,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
||||
WindowBuilder.For(this)
|
||||
.SetSizeConstraints(new Vector2(600, 400), new Vector2(600, 2000))
|
||||
.Apply();
|
||||
|
||||
GetToSLocalization();
|
||||
|
||||
|
||||
Mediator.Subscribe<SwitchToMainUiMessage>(this, (_) => IsOpen = false);
|
||||
Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) =>
|
||||
@@ -88,7 +86,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
for (int i = 60; i > 0; i--)
|
||||
{
|
||||
_timeoutLabel = $"{Strings.ToS.ButtonWillBeAvailableIn} {i}s";
|
||||
_timeoutLabel = $"{Resources.Resources.ToSStrings_ButtonWillBeAvailableIn} {i}s";
|
||||
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
|
||||
}
|
||||
});
|
||||
@@ -102,44 +100,46 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
||||
Vector2 textSize;
|
||||
using (_uiShared.UidFont.Push())
|
||||
{
|
||||
textSize = ImGui.CalcTextSize(Strings.ToS.LanguageLabel);
|
||||
ImGui.TextUnformatted(Strings.ToS.AgreementLabel);
|
||||
textSize = ImGui.CalcTextSize(Resources.Resources.ToSStrings_LanguageLabel);
|
||||
ImGui.TextUnformatted(Resources.Resources.ToSStrings_AgreementLabel);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
var languageSize = ImGui.CalcTextSize(Strings.ToS.LanguageLabel);
|
||||
var languageSize = ImGui.CalcTextSize(Resources.Resources.ToSStrings_LanguageLabel);
|
||||
ImGui.SetCursorPosX(ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - languageSize.X - 80);
|
||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + textSize.Y / 2 - languageSize.Y / 2);
|
||||
|
||||
ImGui.TextUnformatted(Strings.ToS.LanguageLabel);
|
||||
ImGui.TextUnformatted(Resources.Resources.ToSStrings_LanguageLabel);
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + textSize.Y / 2 - (languageSize.Y + ImGui.GetStyle().FramePadding.Y) / 2);
|
||||
ImGui.SetNextItemWidth(80);
|
||||
if (ImGui.Combo("", ref _currentLanguage, _languages.Keys.ToArray(), _languages.Count))
|
||||
{
|
||||
GetToSLocalization(_currentLanguage);
|
||||
var culture = new CultureInfo(_languages.Values.ToArray()[_currentLanguage]);
|
||||
CultureInfo.DefaultThreadCurrentCulture = culture;
|
||||
CultureInfo.DefaultThreadCurrentUICulture = culture;
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
ImGui.SetWindowFontScale(1.5f);
|
||||
string readThis = Strings.ToS.ReadLabel;
|
||||
string readThis = Resources.Resources.ToSStrings_ReadLabel;
|
||||
textSize = ImGui.CalcTextSize(readThis);
|
||||
ImGui.SetCursorPosX(ImGui.GetWindowSize().X / 2 - textSize.X / 2);
|
||||
UiSharedService.ColorText(readThis, ImGuiColors.DalamudRed);
|
||||
ImGui.SetWindowFontScale(1.0f);
|
||||
ImGui.Separator();
|
||||
|
||||
UiSharedService.TextWrapped(_tosParagraphs![0]);
|
||||
UiSharedService.TextWrapped(_tosParagraphs![1]);
|
||||
UiSharedService.TextWrapped(_tosParagraphs![2]);
|
||||
UiSharedService.TextWrapped(_tosParagraphs![3]);
|
||||
UiSharedService.TextWrapped(_tosParagraphs![4]);
|
||||
UiSharedService.TextWrapped(_tosParagraphs![5]);
|
||||
UiSharedService.TextWrapped(Resources.Resources.ToSStrings_Paragraph1);
|
||||
UiSharedService.TextWrapped(Resources.Resources.ToSStrings_Paragraph2);
|
||||
UiSharedService.TextWrapped(Resources.Resources.ToSStrings_Paragraph3);
|
||||
UiSharedService.TextWrapped(Resources.Resources.ToSStrings_Paragraph4);
|
||||
UiSharedService.TextWrapped(Resources.Resources.ToSStrings_Paragraph5);
|
||||
UiSharedService.TextWrapped(Resources.Resources.ToSStrings_Paragraph6);
|
||||
|
||||
ImGui.Separator();
|
||||
if (_timeoutTask?.IsCompleted ?? true)
|
||||
{
|
||||
if (ImGui.Button(Strings.ToS.AgreeLabel + "##toSetup"))
|
||||
if (ImGui.Button(Resources.Resources.ToSStrings_AgreeLabel + "##toSetup"))
|
||||
{
|
||||
_configService.Current.AcceptedAgreement = true;
|
||||
_configService.Save();
|
||||
@@ -349,16 +349,6 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
private void GetToSLocalization(int changeLanguageTo = -1)
|
||||
{
|
||||
if (changeLanguageTo != -1)
|
||||
{
|
||||
_uiShared.LoadLocalization(_languages.ElementAt(changeLanguageTo).Value);
|
||||
}
|
||||
|
||||
_tosParagraphs = [Strings.ToS.Paragraph1, Strings.ToS.Paragraph2, Strings.ToS.Paragraph3, Strings.ToS.Paragraph4, Strings.ToS.Paragraph5, Strings.ToS.Paragraph6];
|
||||
}
|
||||
|
||||
[GeneratedRegex("^[A-F0-9]{64}$", RegexOptions.Compiled | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex SecretRegex();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -47,7 +47,9 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
|
||||
ImGuiWindowFlags.NoCollapse |
|
||||
ImGuiWindowFlags.NoTitleBar |
|
||||
ImGuiWindowFlags.NoScrollbar |
|
||||
ImGuiWindowFlags.AlwaysAutoResize;
|
||||
ImGuiWindowFlags.NoFocusOnAppearing |
|
||||
ImGuiWindowFlags.AlwaysAutoResize;
|
||||
|
||||
|
||||
PositionCondition = ImGuiCond.Always;
|
||||
SizeCondition = ImGuiCond.FirstUseEver;
|
||||
|
||||
@@ -21,6 +21,7 @@ public sealed record PairUiEntry(
|
||||
IndividualPairStatus? PairStatus,
|
||||
long LastAppliedDataBytes,
|
||||
long LastAppliedDataTris,
|
||||
long LastAppliedApproximateEffectiveTris,
|
||||
long LastAppliedApproximateVramBytes,
|
||||
long LastAppliedApproximateEffectiveVramBytes,
|
||||
IPairHandlerAdapter? Handler)
|
||||
|
||||
8
LightlessSync/UI/Models/TextureFormatSortMode.cs
Normal file
8
LightlessSync/UI/Models/TextureFormatSortMode.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace LightlessSync.UI.Models;
|
||||
|
||||
public enum TextureFormatSortMode
|
||||
{
|
||||
None = 0,
|
||||
CompressedFirst = 1,
|
||||
UncompressedFirst = 2
|
||||
}
|
||||
@@ -7,4 +7,5 @@ public enum VisiblePairSortMode
|
||||
EffectiveVramUsage = 2,
|
||||
TriangleCount = 3,
|
||||
PreferredDirectPairs = 4,
|
||||
EffectiveTriangleCount = 5,
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Utility;
|
||||
using Lifestream.Enums;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Comparer;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
@@ -14,6 +15,7 @@ using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services;
|
||||
@@ -23,6 +25,7 @@ using LightlessSync.Services.LightFinder;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.PairProcessing;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.UI.Components;
|
||||
using LightlessSync.UI.Models;
|
||||
using LightlessSync.UI.Services;
|
||||
using LightlessSync.UI.Style;
|
||||
@@ -40,6 +43,7 @@ using System.Globalization;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
@@ -51,7 +55,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
private readonly CacheMonitor _cacheMonitor;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly UiThemeConfigService _themeConfigService;
|
||||
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
||||
private readonly ConcurrentDictionary<GameObjectHandler, IReadOnlyDictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
@@ -63,12 +67,15 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly PerformanceCollectorService _performanceCollector;
|
||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||
private readonly OptimizationSettingsPanel _optimizationSettingsPanel;
|
||||
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly UiSharedService _uiShared;
|
||||
private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress;
|
||||
private readonly NameplateService _nameplateService;
|
||||
private readonly AnimatedHeader _animatedHeader = new();
|
||||
|
||||
private (int, int, FileCacheEntity) _currentProgress;
|
||||
private bool _deleteAccountPopupModalShown = false;
|
||||
private bool _deleteFilesPopupModalShown = false;
|
||||
@@ -105,8 +112,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
};
|
||||
private readonly UiSharedService.TabOption<TransferSettingsTab>[] _transferTabOptions = new UiSharedService.TabOption<TransferSettingsTab>[2];
|
||||
private readonly List<UiSharedService.TabOption<ServerSettingsTab>> _serverTabOptions = new(4);
|
||||
private readonly string[] _generalTreeNavOrder = new[]
|
||||
{
|
||||
private readonly string[] _generalTreeNavOrder =
|
||||
[
|
||||
"Import & Export",
|
||||
"Popup & Auto Fill",
|
||||
"Behavior",
|
||||
@@ -116,7 +123,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
"Colors",
|
||||
"Server Info Bar",
|
||||
"Nameplate",
|
||||
};
|
||||
"Animation & Bones"
|
||||
];
|
||||
private static readonly HashSet<string> _generalNavSeparatorAfter = new(StringComparer.Ordinal)
|
||||
{
|
||||
"Popup & Auto Fill",
|
||||
@@ -127,6 +135,12 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
private readonly Dictionary<string, double> _generalTreeHighlights = new(StringComparer.Ordinal);
|
||||
private const float GeneralTreeHighlightDuration = 1.5f;
|
||||
private readonly SeluneBrush _generalSeluneBrush = new();
|
||||
private string? _performanceScrollTarget = null;
|
||||
private string? _performanceOpenTreeTarget = null;
|
||||
private const string PerformanceWarningsLabel = "Warnings";
|
||||
private const string PerformanceAutoPauseLabel = "Auto Pause";
|
||||
private const string PerformanceTextureOptimizationLabel = "Texture Optimization";
|
||||
private const string PerformanceModelOptimizationLabel = "Model Optimization";
|
||||
|
||||
private static readonly (string Label, SeIconChar Icon)[] LightfinderIconPresets = new[]
|
||||
{
|
||||
@@ -202,10 +216,14 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
_httpClient = httpClient;
|
||||
_fileCompactor = fileCompactor;
|
||||
_uiShared = uiShared;
|
||||
_optimizationSettingsPanel = new OptimizationSettingsPanel(_uiShared, _playerPerformanceConfigService, _pairUiService);
|
||||
_nameplateService = nameplateService;
|
||||
_actorObjectService = actorObjectService;
|
||||
_validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v);
|
||||
|
||||
_animatedHeader.Height = 120f;
|
||||
_animatedHeader.EnableBottomGradient = true;
|
||||
_animatedHeader.GradientHeight = 250f;
|
||||
_animatedHeader.EnableParticles = _configService.Current.EnableParticleEffects;
|
||||
WindowBuilder.For(this)
|
||||
.AllowPinning(true)
|
||||
.AllowClickthrough(false)
|
||||
@@ -220,6 +238,11 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
_selectGeneralTabOnNextDraw = true;
|
||||
FocusGeneralTree("Lightfinder");
|
||||
});
|
||||
Mediator.Subscribe<OpenPerformanceSettingsMessage>(this, msg =>
|
||||
{
|
||||
IsOpen = true;
|
||||
FocusPerformanceSection(msg.Section);
|
||||
});
|
||||
Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) => IsOpen = false);
|
||||
Mediator.Subscribe<CutsceneStartMessage>(this, (_) => UiSharedService_GposeStart());
|
||||
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd());
|
||||
@@ -241,6 +264,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
|
||||
public override void OnClose()
|
||||
{
|
||||
_animatedHeader.ClearParticles();
|
||||
_uiShared.EditTrackerPosition = false;
|
||||
_uidToAddForIgnore = string.Empty;
|
||||
_secretKeysConversionCts = _secretKeysConversionCts.CancelRecreate();
|
||||
@@ -255,8 +279,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
|
||||
protected override void DrawInternal()
|
||||
{
|
||||
_animatedHeader.Draw(ImGui.GetContentRegionAvail().X, (_, _) => { });
|
||||
_ = _uiShared.DrawOtherPluginState();
|
||||
|
||||
DrawSettingsContent();
|
||||
}
|
||||
private static Vector3 PackedColorToVector3(uint color)
|
||||
@@ -506,74 +530,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTextureDownscaleCounters()
|
||||
{
|
||||
HashSet<Pair> trackedPairs = new();
|
||||
|
||||
var snapshot = _pairUiService.GetSnapshot();
|
||||
|
||||
foreach (var pair in snapshot.DirectPairs)
|
||||
{
|
||||
trackedPairs.Add(pair);
|
||||
}
|
||||
|
||||
foreach (var group in snapshot.GroupPairs.Values)
|
||||
{
|
||||
foreach (var pair in group)
|
||||
{
|
||||
trackedPairs.Add(pair);
|
||||
}
|
||||
}
|
||||
|
||||
long totalOriginalBytes = 0;
|
||||
long totalEffectiveBytes = 0;
|
||||
var hasData = false;
|
||||
|
||||
foreach (var pair in trackedPairs)
|
||||
{
|
||||
if (!pair.IsVisible)
|
||||
continue;
|
||||
|
||||
var original = pair.LastAppliedApproximateVRAMBytes;
|
||||
var effective = pair.LastAppliedApproximateEffectiveVRAMBytes;
|
||||
|
||||
if (original >= 0)
|
||||
{
|
||||
hasData = true;
|
||||
totalOriginalBytes += original;
|
||||
}
|
||||
|
||||
if (effective >= 0)
|
||||
{
|
||||
hasData = true;
|
||||
totalEffectiveBytes += effective;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasData)
|
||||
{
|
||||
ImGui.TextDisabled("VRAM usage has not been calculated yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
var savedBytes = Math.Max(0L, totalOriginalBytes - totalEffectiveBytes);
|
||||
var originalText = UiSharedService.ByteToString(totalOriginalBytes, addSuffix: true);
|
||||
var effectiveText = UiSharedService.ByteToString(totalEffectiveBytes, addSuffix: true);
|
||||
var savedText = UiSharedService.ByteToString(savedBytes, addSuffix: true);
|
||||
|
||||
ImGui.TextUnformatted($"Total VRAM usage (original): {originalText}");
|
||||
ImGui.TextUnformatted($"Total VRAM usage (effective): {effectiveText}");
|
||||
|
||||
if (savedBytes > 0)
|
||||
{
|
||||
UiSharedService.ColorText($"VRAM saved by downscaling: {savedText}", UIColors.Get("LightlessGreen"));
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted($"VRAM saved by downscaling: {savedText}");
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawThemeVectorRow(MainStyle.StyleVector2Option option)
|
||||
{
|
||||
ImGui.TableNextRow();
|
||||
@@ -863,10 +819,11 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
|
||||
_uiShared.DrawHelpText(
|
||||
$"The download window will show the current progress of outstanding downloads.{Environment.NewLine}{Environment.NewLine}" +
|
||||
$"What do W/Q/P/D stand for?{Environment.NewLine}W = Waiting for Slot (see Maximum Parallel Downloads){Environment.NewLine}" +
|
||||
$"What do W/Q/P/D/C stand for?{Environment.NewLine}W = Waiting for Slot (see Maximum Parallel Downloads){Environment.NewLine}" +
|
||||
$"Q = Queued on Server, waiting for queue ready signal{Environment.NewLine}" +
|
||||
$"P = Processing download (aka downloading){Environment.NewLine}" +
|
||||
$"D = Decompressing download");
|
||||
$"D = Decompressing download{Environment.NewLine}" +
|
||||
$"C = Completed download");
|
||||
if (!_configService.Current.ShowTransferWindow) ImGui.BeginDisabled();
|
||||
ImGui.Indent();
|
||||
|
||||
@@ -1141,7 +1098,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
|
||||
private async Task<List<string>?> RunSpeedTest(List<string> servers, CancellationToken token)
|
||||
{
|
||||
List<string> speedTestResults = new();
|
||||
List<string> speedTestResults = [];
|
||||
foreach (var server in servers)
|
||||
{
|
||||
HttpResponseMessage? result = null;
|
||||
@@ -1243,7 +1200,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
|
||||
ImGui.TreePop();
|
||||
}
|
||||
#endif
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Copy, "[DEBUG] Copy Last created Character Data to clipboard"))
|
||||
{
|
||||
if (LastCreatedCharacterData != null)
|
||||
@@ -1259,6 +1215,39 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
|
||||
UiSharedService.AttachToolTip("Use this when reporting mods being rejected from the server.");
|
||||
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Home, "Teleport to Limsa [LIFESTREAM TEST]") && _ipcManager.Lifestream.APIAvailable)
|
||||
{
|
||||
_ipcManager.Lifestream.ExecuteLifestreamCommand("limsa");
|
||||
}
|
||||
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Home, "Teleport to JoyHouse [LIFESTREAM TEST]") && _ipcManager.Lifestream.APIAvailable)
|
||||
{
|
||||
var twintania = _dalamudUtilService.WorldData.Value
|
||||
.FirstOrDefault(kvp => kvp.Value.Equals("Twintania", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
int ward = 29;
|
||||
int plot = 7;
|
||||
|
||||
AddressBookEntryTuple addressEntry = (
|
||||
Name: "",
|
||||
World: (int)twintania.Key,
|
||||
City: (int)ResidentialAetheryteKind.Kugane,
|
||||
Ward: ward,
|
||||
PropertyType: 0,
|
||||
Plot: plot,
|
||||
Apartment: 1,
|
||||
ApartmentSubdivision: false,
|
||||
AliasEnabled: false,
|
||||
Alias: ""
|
||||
);
|
||||
|
||||
_logger.LogInformation("going to: {address}", addressEntry);
|
||||
|
||||
_ipcManager.Lifestream.GoToHousingAddress(addressEntry);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
_uiShared.DrawCombo("Log Level", Enum.GetValues<LogLevel>(), (l) => l.ToString(), (l) =>
|
||||
{
|
||||
_configService.Current.LogLevel = l;
|
||||
@@ -1462,6 +1451,24 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
ImGui.SameLine();
|
||||
ImGui.TextColored(statusColor, $"[{(pair.IsVisible ? "Visible" : pair.IsOnline ? "Online" : "Offline")}]");
|
||||
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Copy, "Copy Pair Diagnostics##pairDebugCopy"))
|
||||
{
|
||||
ImGui.SetClipboardText(BuildPairDiagnosticsClipboard(pair, snapshot));
|
||||
}
|
||||
|
||||
UiSharedService.AttachToolTip("Copies the current pair diagnostics to the clipboard.");
|
||||
|
||||
ImGui.SameLine();
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Copy, "Copy Last Data JSON##pairDebugCopyLastData"))
|
||||
{
|
||||
var lastDataForClipboard = pair.LastReceivedCharacterData;
|
||||
ImGui.SetClipboardText(lastDataForClipboard is null
|
||||
? "ERROR: No character data has been received for this pair."
|
||||
: JsonSerializer.Serialize(lastDataForClipboard, DebugJsonOptions));
|
||||
}
|
||||
|
||||
UiSharedService.AttachToolTip("Copies the last received character data JSON to the clipboard.");
|
||||
|
||||
if (ImGui.BeginTable("##pairDebugProperties", 2, ImGuiTableFlags.SizingStretchProp))
|
||||
{
|
||||
DrawPairPropertyRow("UID", pair.UserData.UID);
|
||||
@@ -1478,8 +1485,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
DrawPairPropertyRow("Has Handler", FormatBool(debugInfo.HasHandler));
|
||||
DrawPairPropertyRow("Handler Initialized", FormatBool(debugInfo.HandlerInitialized));
|
||||
DrawPairPropertyRow("Handler Visible", FormatBool(debugInfo.HandlerVisible));
|
||||
DrawPairPropertyRow("Last Time person rendered in", FormatTimestamp(debugInfo.InvisibleSinceUtc));
|
||||
DrawPairPropertyRow("Handler Timer Temp Collection removal", FormatCountdown(debugInfo.VisibilityEvictionRemainingSeconds));
|
||||
DrawPairPropertyRow("Handler Scheduled For Deletion", FormatBool(debugInfo.HandlerScheduledForDeletion));
|
||||
|
||||
DrawPairPropertyRow("Note", pair.GetNote() ?? "(none)");
|
||||
@@ -1494,6 +1499,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
DrawPairPropertyRow("Approx. VRAM", FormatBytes(pair.LastAppliedApproximateVRAMBytes));
|
||||
DrawPairPropertyRow("Effective VRAM", FormatBytes(pair.LastAppliedApproximateEffectiveVRAMBytes));
|
||||
DrawPairPropertyRow("Last Triangles", pair.LastAppliedDataTris < 0 ? "n/a" : pair.LastAppliedDataTris.ToString(CultureInfo.InvariantCulture));
|
||||
DrawPairPropertyRow("Effective Triangles", pair.LastAppliedApproximateEffectiveTris < 0 ? "n/a" : pair.LastAppliedApproximateEffectiveTris.ToString(CultureInfo.InvariantCulture));
|
||||
ImGui.EndTable();
|
||||
}
|
||||
|
||||
@@ -1590,6 +1596,139 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
DrawPairEventLog(pair);
|
||||
}
|
||||
|
||||
private string BuildPairDiagnosticsClipboard(Pair pair, PairUiSnapshot snapshot)
|
||||
{
|
||||
var debugInfo = pair.GetDebugInfo();
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine("Lightless Pair Diagnostics");
|
||||
sb.AppendLine($"Generated: {DateTime.Now.ToString("G", CultureInfo.CurrentCulture)}");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("Pair");
|
||||
sb.AppendLine($"Alias/UID: {pair.UserData.AliasOrUID}");
|
||||
sb.AppendLine($"UID: {pair.UserData.UID}");
|
||||
sb.AppendLine($"Alias: {(string.IsNullOrEmpty(pair.UserData.Alias) ? "(none)" : pair.UserData.Alias)}");
|
||||
sb.AppendLine($"Player Name: {pair.PlayerName ?? "(not cached)"}");
|
||||
sb.AppendLine($"Handler Ident: {(string.IsNullOrEmpty(pair.Ident) ? "(not bound)" : pair.Ident)}");
|
||||
sb.AppendLine($"Character Id: {FormatCharacterId(pair.PlayerCharacterId)}");
|
||||
sb.AppendLine($"Direct Pair: {FormatBool(pair.IsDirectlyPaired)}");
|
||||
sb.AppendLine($"Individual Status: {pair.IndividualPairStatus}");
|
||||
sb.AppendLine($"Any Connection: {FormatBool(pair.HasAnyConnection())}");
|
||||
sb.AppendLine($"Paused: {FormatBool(pair.IsPaused)}");
|
||||
sb.AppendLine($"Visible: {FormatBool(pair.IsVisible)}");
|
||||
sb.AppendLine($"Online: {FormatBool(pair.IsOnline)}");
|
||||
sb.AppendLine($"Has Handler: {FormatBool(debugInfo.HasHandler)}");
|
||||
sb.AppendLine($"Handler Initialized: {FormatBool(debugInfo.HandlerInitialized)}");
|
||||
sb.AppendLine($"Handler Visible: {FormatBool(debugInfo.HandlerVisible)}");
|
||||
sb.AppendLine($"Handler Scheduled For Deletion: {FormatBool(debugInfo.HandlerScheduledForDeletion)}");
|
||||
sb.AppendLine($"Note: {pair.GetNote() ?? "(none)"}");
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Applied Data");
|
||||
sb.AppendLine($"Last Data Size: {FormatBytes(pair.LastAppliedDataBytes)}");
|
||||
sb.AppendLine($"Approx. VRAM: {FormatBytes(pair.LastAppliedApproximateVRAMBytes)}");
|
||||
sb.AppendLine($"Effective VRAM: {FormatBytes(pair.LastAppliedApproximateEffectiveVRAMBytes)}");
|
||||
sb.AppendLine($"Last Triangles: {(pair.LastAppliedDataTris < 0 ? "n/a" : pair.LastAppliedDataTris.ToString(CultureInfo.InvariantCulture))}");
|
||||
sb.AppendLine($"Effective Triangles: {(pair.LastAppliedApproximateEffectiveTris < 0 ? "n/a" : pair.LastAppliedApproximateEffectiveTris.ToString(CultureInfo.InvariantCulture))}");
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Last Received Character Data");
|
||||
var lastData = pair.LastReceivedCharacterData;
|
||||
if (lastData is null)
|
||||
{
|
||||
sb.AppendLine("None");
|
||||
}
|
||||
else
|
||||
{
|
||||
var fileReplacementCount = lastData.FileReplacements.Values.Sum(list => list?.Count ?? 0);
|
||||
var totalGamePaths = lastData.FileReplacements.Values.Sum(list => list?.Sum(replacement => replacement.GamePaths.Length) ?? 0);
|
||||
sb.AppendLine($"File replacements: {fileReplacementCount} entries across {totalGamePaths} game paths.");
|
||||
sb.AppendLine($"Customize+: {lastData.CustomizePlusData.Count}, Glamourer entries: {lastData.GlamourerData.Count}");
|
||||
sb.AppendLine($"Manipulation length: {lastData.ManipulationData.Length}, Heels set: {FormatBool(!string.IsNullOrEmpty(lastData.HeelsData))}");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Application Timeline");
|
||||
sb.AppendLine($"Last Data Received: {FormatTimestamp(debugInfo.LastDataReceivedAt)}");
|
||||
sb.AppendLine($"Last Apply Attempt: {FormatTimestamp(debugInfo.LastApplyAttemptAt)}");
|
||||
sb.AppendLine($"Last Successful Apply: {FormatTimestamp(debugInfo.LastSuccessfulApplyAt)}");
|
||||
|
||||
if (!string.IsNullOrEmpty(debugInfo.LastFailureReason))
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"Last failure: {debugInfo.LastFailureReason}");
|
||||
if (debugInfo.BlockingConditions.Count > 0)
|
||||
{
|
||||
sb.AppendLine("Blocking conditions:");
|
||||
foreach (var condition in debugInfo.BlockingConditions)
|
||||
{
|
||||
sb.AppendLine($"- {condition}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Application & Download State");
|
||||
sb.AppendLine($"Applying Data: {FormatBool(debugInfo.IsApplying)}");
|
||||
sb.AppendLine($"Downloading: {FormatBool(debugInfo.IsDownloading)}");
|
||||
sb.AppendLine($"Pending Downloads: {debugInfo.PendingDownloadCount.ToString(CultureInfo.InvariantCulture)}");
|
||||
sb.AppendLine($"Forbidden Downloads: {debugInfo.ForbiddenDownloadCount.ToString(CultureInfo.InvariantCulture)}");
|
||||
sb.AppendLine($"Pending Mod Reapply: {FormatBool(debugInfo.PendingModReapply)}");
|
||||
sb.AppendLine($"Mod Apply Deferred: {FormatBool(debugInfo.ModApplyDeferred)}");
|
||||
sb.AppendLine($"Missing Critical Mods: {debugInfo.MissingCriticalMods.ToString(CultureInfo.InvariantCulture)}");
|
||||
sb.AppendLine($"Missing Non-Critical Mods: {debugInfo.MissingNonCriticalMods.ToString(CultureInfo.InvariantCulture)}");
|
||||
sb.AppendLine($"Missing Forbidden Mods: {debugInfo.MissingForbiddenMods.ToString(CultureInfo.InvariantCulture)}");
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Syncshell Memberships");
|
||||
if (snapshot.PairsWithGroups.TryGetValue(pair, out var groups) && groups.Count > 0)
|
||||
{
|
||||
foreach (var group in groups.OrderBy(g => g.Group.AliasOrGID, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var flags = group.GroupPairUserInfos.TryGetValue(pair.UserData.UID, out var info) ? info : GroupPairUserInfo.None;
|
||||
var flagLabel = flags switch
|
||||
{
|
||||
GroupPairUserInfo.None => string.Empty,
|
||||
_ => $" ({string.Join(", ", GetGroupInfoFlags(flags))})"
|
||||
};
|
||||
sb.AppendLine($"{group.Group.AliasOrGID} [{group.Group.GID}]{flagLabel}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine("Not a member of any syncshells.");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Pair DTO Snapshot");
|
||||
if (pair.UserPair is null)
|
||||
{
|
||||
sb.AppendLine("(unavailable)");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine(JsonSerializer.Serialize(pair.UserPair, DebugJsonOptions));
|
||||
}
|
||||
|
||||
var relevantEvents = GetRelevantPairEvents(pair, 40);
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Recent Events");
|
||||
if (relevantEvents.Count == 0)
|
||||
{
|
||||
sb.AppendLine("No recent events were logged for this pair.");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var ev in relevantEvents)
|
||||
{
|
||||
var timestamp = ev.EventTime.ToString("T", CultureInfo.CurrentCulture);
|
||||
sb.AppendLine($"{timestamp} [{ev.EventSource}] {ev.EventSeverity}: {ev.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetGroupInfoFlags(GroupPairUserInfo info)
|
||||
{
|
||||
if (info.HasFlag(GroupPairUserInfo.IsModerator))
|
||||
@@ -1603,23 +1742,28 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPairEventLog(Pair pair)
|
||||
private List<Event> GetRelevantPairEvents(Pair pair, int maxEvents)
|
||||
{
|
||||
ImGui.TextUnformatted("Recent Events");
|
||||
var events = _eventAggregator.EventList.Value;
|
||||
var alias = pair.UserData.Alias;
|
||||
var aliasOrUid = pair.UserData.AliasOrUID;
|
||||
var rawUid = pair.UserData.UID;
|
||||
var playerName = pair.PlayerName;
|
||||
|
||||
var relevantEvents = events.Where(e =>
|
||||
return events.Where(e =>
|
||||
EventMatchesIdentifier(e, rawUid)
|
||||
|| EventMatchesIdentifier(e, aliasOrUid)
|
||||
|| EventMatchesIdentifier(e, alias)
|
||||
|| (!string.IsNullOrEmpty(playerName) && string.Equals(e.Character, playerName, StringComparison.OrdinalIgnoreCase)))
|
||||
.OrderByDescending(e => e.EventTime)
|
||||
.Take(40)
|
||||
.Take(maxEvents)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private void DrawPairEventLog(Pair pair)
|
||||
{
|
||||
ImGui.TextUnformatted("Recent Events");
|
||||
var relevantEvents = GetRelevantPairEvents(pair, 40);
|
||||
|
||||
if (relevantEvents.Count == 0)
|
||||
{
|
||||
@@ -1925,14 +2069,25 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
using (ImRaii.PushIndent(20f))
|
||||
{
|
||||
if (_validationTask.IsCompleted)
|
||||
if (_validationTask.IsCompletedSuccessfully)
|
||||
{
|
||||
UiSharedService.TextWrapped(
|
||||
$"The storage validation has completed and removed {_validationTask.Result.Count} invalid files from storage.");
|
||||
}
|
||||
else if (_validationTask.IsCanceled)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped(
|
||||
"Storage validation was cancelled.",
|
||||
UIColors.Get("LightlessYellow"));
|
||||
}
|
||||
else if (_validationTask.IsFaulted)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped(
|
||||
"Storage validation failed with an error.",
|
||||
UIColors.Get("DimRed"));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
UiSharedService.TextWrapped(
|
||||
$"Storage validation is running: {_currentProgress.Item1}/{_currentProgress.Item2}");
|
||||
if (_currentProgress.Item3 != null)
|
||||
@@ -2089,7 +2244,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
|
||||
ImGui.Separator();
|
||||
var openPopupOnAddition = _configService.Current.OpenPopupOnAdd;
|
||||
|
||||
|
||||
using (var popupTree = BeginGeneralTree("Popup & Auto Fill", UIColors.Get("LightlessPurple")))
|
||||
{
|
||||
if (popupTree.Visible)
|
||||
@@ -2146,11 +2301,38 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
var groupInVisible = _configService.Current.ShowSyncshellUsersInVisible;
|
||||
var syncshellOfflineSeparate = _configService.Current.ShowSyncshellOfflineUsersSeparately;
|
||||
var greenVisiblePair = _configService.Current.ShowVisiblePairsGreenEye;
|
||||
var enableParticleEffects = _configService.Current.EnableParticleEffects;
|
||||
var showUiWhenUiHidden = _configService.Current.ShowUiWhenUiHidden;
|
||||
var showUiInGpose = _configService.Current.ShowUiInGpose;
|
||||
|
||||
using (var behaviorTree = BeginGeneralTree("Behavior", UIColors.Get("LightlessPurple")))
|
||||
{
|
||||
if (behaviorTree.Visible)
|
||||
{
|
||||
if (ImGui.Checkbox("Show Lightless windows when game UI is hidden", ref showUiWhenUiHidden))
|
||||
{
|
||||
_configService.Current.ShowUiWhenUiHidden = showUiWhenUiHidden;
|
||||
_configService.Save();
|
||||
}
|
||||
|
||||
_uiShared.DrawHelpText("When disabled, Lightless windows (except chat) are hidden when the game UI is hidden.");
|
||||
|
||||
if (ImGui.Checkbox("Show Lightless windows in group pose", ref showUiInGpose))
|
||||
{
|
||||
_configService.Current.ShowUiInGpose = showUiInGpose;
|
||||
_configService.Save();
|
||||
}
|
||||
|
||||
_uiShared.DrawHelpText("When disabled, Lightless windows (except chat) are hidden while in group pose.");
|
||||
|
||||
if (ImGui.Checkbox("Enable Particle Effects", ref enableParticleEffects))
|
||||
{
|
||||
_configService.Current.EnableParticleEffects = enableParticleEffects;
|
||||
_configService.Save();
|
||||
}
|
||||
|
||||
_uiShared.DrawHelpText("This will enable particle effects in the UI.");
|
||||
|
||||
if (ImGui.Checkbox("Enable Game Right Click Menu Entries", ref enableRightClickMenu))
|
||||
{
|
||||
_configService.Current.EnableRightClickMenus = enableRightClickMenu;
|
||||
@@ -2859,16 +3041,21 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
|
||||
var colorNames = new[]
|
||||
{
|
||||
("LightlessPurple", "Primary Purple", "Section titles and dividers"),
|
||||
("LightlessPurpleActive", "Primary Purple (Active)", "Active tabs and hover highlights"),
|
||||
("LightlessPurpleDefault", "Primary Purple (Inactive)", "Inactive tabs and default dividers"),
|
||||
("LightlessBlue", "Secondary Blue", "Secondary title colors, visable pairs"),
|
||||
("LightlessGreen", "Success Green", "Join buttons and success messages"),
|
||||
("LightlessYellow", "Warning Yellow", "Warning colors"),
|
||||
("LightlessOrange", "Performance Orange", "Performance notifications and warnings"),
|
||||
("PairBlue", "Syncshell Blue", "Syncshell headers, toggle highlights, and moderator actions"),
|
||||
("DimRed", "Error Red", "Error and offline colors")
|
||||
};
|
||||
("LightlessPurple", "Primary Purple", "Section titles and dividers"),
|
||||
("LightlessPurpleActive", "Primary Purple (Active)", "Active tabs and hover highlights"),
|
||||
("LightlessPurpleDefault", "Primary Purple (Inactive)", "Inactive tabs and default dividers"),
|
||||
("LightlessBlue", "Secondary Blue", "Secondary title colors, visable pairs"),
|
||||
("LightlessGreen", "Success Green", "Join buttons and success messages"),
|
||||
("LightlessYellow", "Warning Yellow", "Warning colors"),
|
||||
("LightlessOrange", "Performance Orange", "Performance notifications and warnings"),
|
||||
("PairBlue", "Syncshell Blue", "Syncshell headers, toggle highlights, and moderator actions"),
|
||||
("DimRed", "Error Red", "Error and offline colors"),
|
||||
("HeaderGradientTop", "Header Gradient (Top)", "Top color of the animated header background"),
|
||||
("HeaderGradientBottom", "Header Gradient (Bottom)", "Bottom color of the animated header background"),
|
||||
("HeaderStaticStar", "Header Stars", "Tint color for the static background stars in the header"),
|
||||
("HeaderShootingStar", "Header Shooting Star", "Tint color for the shooting star effect"),
|
||||
};
|
||||
|
||||
if (ImGui.BeginTable("##ColorTable", 3,
|
||||
ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit))
|
||||
{
|
||||
@@ -3074,10 +3261,102 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
ImGui.Dummy(new Vector2(10));
|
||||
_uiShared.BigText("Animation");
|
||||
|
||||
using (var animationTree = BeginGeneralTree("Animation & Bones", UIColors.Get("LightlessPurple")))
|
||||
{
|
||||
if (animationTree.Visible)
|
||||
{
|
||||
ImGui.TextUnformatted("Animation Options");
|
||||
|
||||
var modes = new[]
|
||||
{
|
||||
AnimationValidationMode.Unsafe,
|
||||
AnimationValidationMode.Safe,
|
||||
AnimationValidationMode.Safest,
|
||||
};
|
||||
|
||||
var labels = new[]
|
||||
{
|
||||
"Unsafe (Off)",
|
||||
"Safe (Race Check)",
|
||||
"Safest (Race + Bones Check)",
|
||||
};
|
||||
|
||||
var tooltips = new[]
|
||||
{
|
||||
"No validation. Fastest, but may allow incompatible animations.",
|
||||
"Validates skeleton race + modded skeleton check. Will be safer to use but will block some animations",
|
||||
"Requires matching skeleton race + bone compatibility. Will block alot, not recommended.",
|
||||
};
|
||||
|
||||
|
||||
var currentMode = _configService.Current.AnimationValidationMode;
|
||||
int selectedIndex = Array.IndexOf(modes, currentMode);
|
||||
if (selectedIndex < 0) selectedIndex = 1;
|
||||
|
||||
ImGui.SetNextItemWidth(140 * ImGuiHelpers.GlobalScale);
|
||||
|
||||
bool open = ImGui.BeginCombo("Animation validation", labels[selectedIndex]);
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip(tooltips[selectedIndex]);
|
||||
|
||||
if (open)
|
||||
{
|
||||
for (int i = 0; i < modes.Length; i++)
|
||||
{
|
||||
bool isSelected = (i == selectedIndex);
|
||||
|
||||
if (ImGui.Selectable(labels[i], isSelected))
|
||||
{
|
||||
selectedIndex = i;
|
||||
_configService.Current.AnimationValidationMode = modes[i];
|
||||
_configService.Save();
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip(tooltips[i]);
|
||||
|
||||
if (isSelected)
|
||||
ImGui.SetItemDefaultFocus();
|
||||
}
|
||||
|
||||
ImGui.EndCombo();
|
||||
}
|
||||
|
||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
|
||||
|
||||
var cfg = _configService.Current;
|
||||
|
||||
bool oneBased = cfg.AnimationAllowOneBasedShift;
|
||||
if (ImGui.Checkbox("Treat 1-based PAP indices as compatible", ref oneBased))
|
||||
{
|
||||
cfg.AnimationAllowOneBasedShift = oneBased;
|
||||
_configService.Save();
|
||||
}
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip("Fixes off-by-one PAPs (one bone differance in bones and PAP). Can also increase crashing, toggle off if alot of crashing is happening");
|
||||
|
||||
bool neighbor = cfg.AnimationAllowNeighborIndexTolerance;
|
||||
if (ImGui.Checkbox("Allow 1+- bone index tolerance", ref neighbor))
|
||||
{
|
||||
cfg.AnimationAllowNeighborIndexTolerance = neighbor;
|
||||
_configService.Save();
|
||||
}
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip("Looser matching on bone matching. Can reduce false blocks happening but also reduces safety and more prone to crashing.");
|
||||
|
||||
ImGui.TreePop();
|
||||
animationTree.MarkContentEnd();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndChild();
|
||||
ImGui.EndGroup();
|
||||
|
||||
ImGui.Separator();
|
||||
generalSelune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
|
||||
}
|
||||
}
|
||||
@@ -3152,6 +3431,43 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
_generalTreeHighlights[label] = ImGui.GetTime();
|
||||
}
|
||||
|
||||
private void FocusPerformanceSection(PerformanceSettingsSection section)
|
||||
{
|
||||
_selectGeneralTabOnNextDraw = false;
|
||||
_selectedMainTab = MainSettingsTab.Performance;
|
||||
var label = section switch
|
||||
{
|
||||
PerformanceSettingsSection.TextureOptimization => PerformanceTextureOptimizationLabel,
|
||||
PerformanceSettingsSection.ModelOptimization => PerformanceModelOptimizationLabel,
|
||||
_ => PerformanceTextureOptimizationLabel,
|
||||
};
|
||||
_performanceOpenTreeTarget = label;
|
||||
_performanceScrollTarget = label;
|
||||
}
|
||||
|
||||
private bool BeginPerformanceTree(string label, Vector4 color)
|
||||
{
|
||||
var shouldForceOpen = string.Equals(_performanceOpenTreeTarget, label, StringComparison.Ordinal);
|
||||
if (shouldForceOpen)
|
||||
{
|
||||
ImGui.SetNextItemOpen(true, ImGuiCond.Always);
|
||||
}
|
||||
|
||||
var open = _uiShared.MediumTreeNode(label, color);
|
||||
if (shouldForceOpen)
|
||||
{
|
||||
_performanceOpenTreeTarget = null;
|
||||
}
|
||||
|
||||
if (open && string.Equals(_performanceScrollTarget, label, StringComparison.Ordinal))
|
||||
{
|
||||
ImGui.SetScrollHereY(0f);
|
||||
_performanceScrollTarget = null;
|
||||
}
|
||||
|
||||
return open;
|
||||
}
|
||||
|
||||
private float GetGeneralTreeHighlightAlpha(string label)
|
||||
{
|
||||
if (!_generalTreeHighlights.TryGetValue(label, out var startTime))
|
||||
@@ -3167,6 +3483,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
return 1f - (elapsed / GeneralTreeHighlightDuration);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private struct GeneralTreeScope : IDisposable
|
||||
{
|
||||
private readonly bool _visible;
|
||||
@@ -3240,7 +3557,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
|
||||
bool showPerformanceIndicator = _playerPerformanceConfigService.Current.ShowPerformanceIndicator;
|
||||
|
||||
if (_uiShared.MediumTreeNode("Warnings", UIColors.Get("LightlessPurple")))
|
||||
if (BeginPerformanceTree(PerformanceWarningsLabel, UIColors.Get("LightlessPurple")))
|
||||
{
|
||||
if (ImGui.Checkbox("Show performance indicator", ref showPerformanceIndicator))
|
||||
{
|
||||
@@ -3336,7 +3653,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
bool autoPauseInCombat = _playerPerformanceConfigService.Current.PauseInCombat;
|
||||
bool autoPauseWhilePerforming = _playerPerformanceConfigService.Current.PauseWhilePerforming;
|
||||
|
||||
if (_uiShared.MediumTreeNode("Auto Pause", UIColors.Get("LightlessPurple")))
|
||||
if (BeginPerformanceTree(PerformanceAutoPauseLabel, UIColors.Get("LightlessPurple")))
|
||||
{
|
||||
if (ImGui.Checkbox("Auto pause sync while combat", ref autoPauseInCombat))
|
||||
{
|
||||
@@ -3433,99 +3750,12 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
if (_uiShared.MediumTreeNode("Texture Optimization", UIColors.Get("LightlessYellow")))
|
||||
{
|
||||
_uiShared.MediumText("Warning", UIColors.Get("DimRed"));
|
||||
_uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"),
|
||||
new SeStringUtils.RichTextEntry("Texture compression and downscaling is potentially a "),
|
||||
new SeStringUtils.RichTextEntry("destructive", UIColors.Get("DimRed"), true),
|
||||
new SeStringUtils.RichTextEntry(" process and may cause broken or incorrect character appearances."));
|
||||
|
||||
_uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"),
|
||||
new SeStringUtils.RichTextEntry("This feature is encouraged to help "),
|
||||
new SeStringUtils.RichTextEntry("lower-end systems with limited VRAM", UIColors.Get("LightlessYellow"), true),
|
||||
new SeStringUtils.RichTextEntry(" and for use in "),
|
||||
new SeStringUtils.RichTextEntry("performance-critical scenarios", UIColors.Get("LightlessYellow"), true),
|
||||
new SeStringUtils.RichTextEntry("."));
|
||||
|
||||
_uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"),
|
||||
new SeStringUtils.RichTextEntry("Runtime downscaling "),
|
||||
new SeStringUtils.RichTextEntry("MAY", UIColors.Get("DimRed"), true),
|
||||
new SeStringUtils.RichTextEntry(" cause higher load on the system when processing downloads."));
|
||||
|
||||
_uiShared.DrawNoteLine("!!! ", UIColors.Get("DimRed"),
|
||||
new SeStringUtils.RichTextEntry("When enabled, we cannot provide support for appearance issues caused by this setting!", UIColors.Get("DimRed"), true));
|
||||
|
||||
var textureConfig = _playerPerformanceConfigService.Current;
|
||||
var trimNonIndex = textureConfig.EnableNonIndexTextureMipTrim;
|
||||
if (ImGui.Checkbox("Trim mip levels for textures", ref trimNonIndex))
|
||||
{
|
||||
textureConfig.EnableNonIndexTextureMipTrim = trimNonIndex;
|
||||
_playerPerformanceConfigService.Save();
|
||||
}
|
||||
_uiShared.DrawHelpText("When enabled, Lightless will remove high-resolution mip levels from textures (not index) that exceed the size limit and are not compressed with any kind compression.");
|
||||
|
||||
var downscaleIndex = textureConfig.EnableIndexTextureDownscale;
|
||||
if (ImGui.Checkbox("Downscale index textures above limit", ref downscaleIndex))
|
||||
{
|
||||
textureConfig.EnableIndexTextureDownscale = downscaleIndex;
|
||||
_playerPerformanceConfigService.Save();
|
||||
}
|
||||
_uiShared.DrawHelpText("Controls whether Lightless reduces index textures that exceed the size limit.");
|
||||
|
||||
var dimensionOptions = new[] { 512, 1024, 2048, 4096 };
|
||||
var optionLabels = dimensionOptions.Select(static value => value.ToString()).ToArray();
|
||||
var currentDimension = textureConfig.TextureDownscaleMaxDimension;
|
||||
var selectedIndex = Array.IndexOf(dimensionOptions, currentDimension);
|
||||
if (selectedIndex < 0)
|
||||
{
|
||||
selectedIndex = Array.IndexOf(dimensionOptions, 2048);
|
||||
}
|
||||
|
||||
ImGui.SetNextItemWidth(140 * ImGuiHelpers.GlobalScale);
|
||||
if (ImGui.Combo("Maximum texture dimension", ref selectedIndex, optionLabels, optionLabels.Length))
|
||||
{
|
||||
textureConfig.TextureDownscaleMaxDimension = dimensionOptions[selectedIndex];
|
||||
_playerPerformanceConfigService.Save();
|
||||
}
|
||||
_uiShared.DrawHelpText($"Textures above this size will be reduced until their largest dimension is at or below the limit. Block-compressed textures are skipped when \"Only downscale uncompressed\" is enabled.{UiSharedService.TooltipSeparator}Default: 2048");
|
||||
|
||||
var keepOriginalTextures = textureConfig.KeepOriginalTextureFiles;
|
||||
if (ImGui.Checkbox("Keep original texture files", ref keepOriginalTextures))
|
||||
{
|
||||
textureConfig.KeepOriginalTextureFiles = keepOriginalTextures;
|
||||
_playerPerformanceConfigService.Save();
|
||||
}
|
||||
_uiShared.DrawHelpText("When disabled, Lightless removes the original texture after a downscaled copy is created.");
|
||||
ImGui.SameLine();
|
||||
_uiShared.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("If disabled, saved + effective VRAM usage information will not work.", UIColors.Get("LightlessYellow")));
|
||||
|
||||
if (!textureConfig.EnableNonIndexTextureMipTrim && !textureConfig.EnableIndexTextureDownscale)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("Both trimming and downscale are disabled. Lightless will keep original textures regardless of size.", UIColors.Get("DimRed"));
|
||||
}
|
||||
|
||||
ImGui.Dummy(new Vector2(5));
|
||||
|
||||
UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 3f);
|
||||
var onlyUncompressed = textureConfig.OnlyDownscaleUncompressedTextures;
|
||||
if (ImGui.Checkbox("Only downscale uncompressed textures", ref onlyUncompressed))
|
||||
{
|
||||
textureConfig.OnlyDownscaleUncompressedTextures = onlyUncompressed;
|
||||
_playerPerformanceConfigService.Save();
|
||||
}
|
||||
_uiShared.DrawHelpText("If disabled, compressed textures will be targeted for downscaling too.");
|
||||
UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 3f);
|
||||
|
||||
ImGui.Dummy(new Vector2(5));
|
||||
|
||||
DrawTextureDownscaleCounters();
|
||||
|
||||
ImGui.Dummy(new Vector2(5));
|
||||
|
||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f);
|
||||
ImGui.TreePop();
|
||||
}
|
||||
_optimizationSettingsPanel.DrawSettingsTrees(
|
||||
PerformanceTextureOptimizationLabel,
|
||||
UIColors.Get("LightlessYellow"),
|
||||
PerformanceModelOptimizationLabel,
|
||||
UIColors.Get("LightlessOrange"),
|
||||
BeginPerformanceTree);
|
||||
|
||||
ImGui.Separator();
|
||||
ImGui.Dummy(new Vector2(10));
|
||||
@@ -4400,7 +4630,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
ImGui.TextColored(UIColors.Get("LightlessBlue"),
|
||||
_apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture));
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted("Users Online");
|
||||
ImGui.TextUnformatted(Resources.Resources.Users_Online);
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(")");
|
||||
}
|
||||
|
||||
@@ -43,10 +43,23 @@ public class AnimatedHeader
|
||||
private const float _extendedParticleHeight = 40f;
|
||||
|
||||
public float Height { get; set; } = 150f;
|
||||
|
||||
// Color keys for theming
|
||||
public string? TopColorKey { get; set; } = "HeaderGradientTop";
|
||||
public string? BottomColorKey { get; set; } = "HeaderGradientBottom";
|
||||
public string? StaticStarColorKey { get; set; } = "HeaderStaticStar";
|
||||
public string? ShootingStarColorKey { get; set; } = "HeaderShootingStar";
|
||||
|
||||
// Fallbacks if the color keys are not found
|
||||
public Vector4 TopColor { get; set; } = new(0.08f, 0.05f, 0.15f, 1.0f);
|
||||
public Vector4 BottomColor { get; set; } = new(0.12f, 0.08f, 0.20f, 1.0f);
|
||||
public Vector4 StaticStarColor { get; set; } = new(1f, 1f, 1f, 1f);
|
||||
public Vector4 ShootingStarColor { get; set; } = new(0.4f, 0.8f, 1.0f, 1.0f);
|
||||
|
||||
public bool EnableParticles { get; set; } = true;
|
||||
public bool EnableBottomGradient { get; set; } = true;
|
||||
|
||||
public float GradientHeight { get; set; } = 60f;
|
||||
|
||||
/// <summary>
|
||||
/// Draws the animated header with some customizable content
|
||||
@@ -146,16 +159,21 @@ public class AnimatedHeader
|
||||
{
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
|
||||
var top = ResolveColor(TopColorKey, TopColor);
|
||||
var bottom = ResolveColor(BottomColorKey, BottomColor);
|
||||
|
||||
drawList.AddRectFilledMultiColor(
|
||||
headerStart,
|
||||
headerEnd,
|
||||
ImGui.GetColorU32(TopColor),
|
||||
ImGui.GetColorU32(TopColor),
|
||||
ImGui.GetColorU32(BottomColor),
|
||||
ImGui.GetColorU32(BottomColor)
|
||||
ImGui.GetColorU32(top),
|
||||
ImGui.GetColorU32(top),
|
||||
ImGui.GetColorU32(bottom),
|
||||
ImGui.GetColorU32(bottom)
|
||||
);
|
||||
|
||||
// Draw static background stars
|
||||
var starBase = ResolveColor(StaticStarColorKey, StaticStarColor);
|
||||
|
||||
var random = new Random(42);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
@@ -164,23 +182,28 @@ public class AnimatedHeader
|
||||
(float)random.NextDouble() * (headerEnd.Y - headerStart.Y)
|
||||
);
|
||||
var brightness = 0.3f + (float)random.NextDouble() * 0.4f;
|
||||
drawList.AddCircleFilled(starPos, 1f, ImGui.GetColorU32(new Vector4(1f, 1f, 1f, brightness)));
|
||||
var starColor = starBase with { W = starBase.W * brightness };
|
||||
|
||||
drawList.AddCircleFilled(starPos, 1f, ImGui.GetColorU32(starColor));
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawBottomGradient(Vector2 headerStart, Vector2 headerEnd, float width)
|
||||
{
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
var gradientHeight = 60f;
|
||||
var gradientHeight = GradientHeight;
|
||||
var bottom = ResolveColor(BottomColorKey, BottomColor);
|
||||
|
||||
for (int i = 0; i < gradientHeight; i++)
|
||||
{
|
||||
var progress = i / gradientHeight;
|
||||
var smoothProgress = progress * progress;
|
||||
var r = BottomColor.X + (0.0f - BottomColor.X) * smoothProgress;
|
||||
var g = BottomColor.Y + (0.0f - BottomColor.Y) * smoothProgress;
|
||||
var b = BottomColor.Z + (0.0f - BottomColor.Z) * smoothProgress;
|
||||
|
||||
var r = bottom.X + (0.0f - bottom.X) * smoothProgress;
|
||||
var g = bottom.Y + (0.0f - bottom.Y) * smoothProgress;
|
||||
var b = bottom.Z + (0.0f - bottom.Z) * smoothProgress;
|
||||
var alpha = 1f - smoothProgress;
|
||||
|
||||
var gradientColor = new Vector4(r, g, b, alpha);
|
||||
drawList.AddLine(
|
||||
new Vector2(headerStart.X, headerEnd.Y + i),
|
||||
@@ -308,9 +331,11 @@ public class AnimatedHeader
|
||||
? baseAlpha * (0.6f + 0.4f * MathF.Sin(particle.Twinkle))
|
||||
: baseAlpha;
|
||||
|
||||
var shootingBase = ResolveColor(ShootingStarColorKey, ShootingStarColor);
|
||||
|
||||
if (particle.Type == ParticleType.ShootingStar && particle.Trail != null && particle.Trail.Count > 1)
|
||||
{
|
||||
var cyanColor = new Vector4(0.4f, 0.8f, 1.0f, 1.0f);
|
||||
var baseColor = shootingBase;
|
||||
|
||||
for (int t = 1; t < particle.Trail.Count; t++)
|
||||
{
|
||||
@@ -319,17 +344,18 @@ public class AnimatedHeader
|
||||
var trailWidth = (1f - trailProgress) * 3f + 1f;
|
||||
|
||||
var glowAlpha = trailAlpha * 0.4f;
|
||||
|
||||
drawList.AddLine(
|
||||
bannerStart + particle.Trail[t - 1],
|
||||
bannerStart + particle.Trail[t],
|
||||
ImGui.GetColorU32(cyanColor with { W = glowAlpha }),
|
||||
ImGui.GetColorU32(baseColor with { W = glowAlpha }),
|
||||
trailWidth + 4f
|
||||
);
|
||||
|
||||
drawList.AddLine(
|
||||
bannerStart + particle.Trail[t - 1],
|
||||
bannerStart + particle.Trail[t],
|
||||
ImGui.GetColorU32(cyanColor with { W = trailAlpha }),
|
||||
ImGui.GetColorU32(baseColor with { W = trailAlpha }),
|
||||
trailWidth
|
||||
);
|
||||
}
|
||||
@@ -448,6 +474,13 @@ public class AnimatedHeader
|
||||
Hue = 270f
|
||||
});
|
||||
}
|
||||
private static Vector4 ResolveColor(string? key, Vector4 fallback)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
return fallback;
|
||||
|
||||
return UIColors.Get(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all active particles. Useful when closing or hiding a window with an animated header.
|
||||
|
||||
@@ -43,6 +43,7 @@ internal static class MainStyle
|
||||
new("color.titleBg", "Title Background", () => Rgba(24, 24, 24, 232), ImGuiCol.TitleBg),
|
||||
new("color.titleBgActive", "Title Background (Active)", () => Rgba(30, 30, 30, 255), ImGuiCol.TitleBgActive),
|
||||
new("color.titleBgCollapsed", "Title Background (Collapsed)", () => Rgba(27, 27, 27, 255), ImGuiCol.TitleBgCollapsed),
|
||||
|
||||
new("color.menuBarBg", "Menu Bar Background", () => Rgba(36, 36, 36, 255), ImGuiCol.MenuBarBg),
|
||||
new("color.scrollbarBg", "Scrollbar Background", () => Rgba(0, 0, 0, 0), ImGuiCol.ScrollbarBg),
|
||||
new("color.scrollbarGrab", "Scrollbar Grab", () => Rgba(62, 62, 62, 255), ImGuiCol.ScrollbarGrab),
|
||||
|
||||
@@ -29,6 +29,7 @@ public sealed class SeluneGradientSettings
|
||||
public Vector4 GradientColor { get; init; } = UIColors.Get("LightlessPurple");
|
||||
public Vector4? HighlightColor { get; init; }
|
||||
public float GradientPeakOpacity { get; init; } = 0.07f;
|
||||
public float GradientPeakPosition { get; init; } = 0.035f;
|
||||
public float HighlightPeakAlpha { get; init; } = 0.13f;
|
||||
public float HighlightEdgeAlpha { get; init; } = 0f;
|
||||
public float HighlightMidpoint { get; init; } = 0.45f;
|
||||
@@ -378,6 +379,7 @@ internal static class SeluneRenderer
|
||||
topColorVec,
|
||||
midColorVec,
|
||||
bottomColorVec,
|
||||
settings,
|
||||
settings.BackgroundMode);
|
||||
}
|
||||
|
||||
@@ -403,19 +405,21 @@ internal static class SeluneRenderer
|
||||
Vector4 topColorVec,
|
||||
Vector4 midColorVec,
|
||||
Vector4 bottomColorVec,
|
||||
SeluneGradientSettings settings,
|
||||
SeluneGradientMode mode)
|
||||
{
|
||||
var peakPosition = Math.Clamp(settings.GradientPeakPosition, 0.01f, 0.99f);
|
||||
switch (mode)
|
||||
{
|
||||
case SeluneGradientMode.Vertical:
|
||||
DrawVerticalBackground(drawList, gradientLeft, gradientRight, clampedTopY, clampedBottomY, topColorVec, midColorVec, bottomColorVec);
|
||||
DrawVerticalBackground(drawList, gradientLeft, gradientRight, clampedTopY, clampedBottomY, topColorVec, midColorVec, bottomColorVec, peakPosition);
|
||||
break;
|
||||
case SeluneGradientMode.Horizontal:
|
||||
DrawHorizontalBackground(drawList, gradientLeft, gradientRight, clampedTopY, clampedBottomY, topColorVec, midColorVec, bottomColorVec);
|
||||
DrawHorizontalBackground(drawList, gradientLeft, gradientRight, clampedTopY, clampedBottomY, topColorVec, midColorVec, bottomColorVec, peakPosition);
|
||||
break;
|
||||
case SeluneGradientMode.Both:
|
||||
DrawVerticalBackground(drawList, gradientLeft, gradientRight, clampedTopY, clampedBottomY, topColorVec, midColorVec, bottomColorVec);
|
||||
DrawHorizontalBackground(drawList, gradientLeft, gradientRight, clampedTopY, clampedBottomY, topColorVec, midColorVec, bottomColorVec);
|
||||
DrawVerticalBackground(drawList, gradientLeft, gradientRight, clampedTopY, clampedBottomY, topColorVec, midColorVec, bottomColorVec, peakPosition);
|
||||
DrawHorizontalBackground(drawList, gradientLeft, gradientRight, clampedTopY, clampedBottomY, topColorVec, midColorVec, bottomColorVec, peakPosition);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -428,13 +432,14 @@ internal static class SeluneRenderer
|
||||
float clampedBottomY,
|
||||
Vector4 topColorVec,
|
||||
Vector4 midColorVec,
|
||||
Vector4 bottomColorVec)
|
||||
Vector4 bottomColorVec,
|
||||
float peakPosition)
|
||||
{
|
||||
var topColor = ImGui.ColorConvertFloat4ToU32(topColorVec);
|
||||
var midColor = ImGui.ColorConvertFloat4ToU32(midColorVec);
|
||||
var bottomColor = ImGui.ColorConvertFloat4ToU32(bottomColorVec);
|
||||
|
||||
var midY = clampedTopY + (clampedBottomY - clampedTopY) * 0.035f;
|
||||
var midY = clampedTopY + (clampedBottomY - clampedTopY) * peakPosition;
|
||||
drawList.AddRectFilledMultiColor(
|
||||
new Vector2(gradientLeft, clampedTopY),
|
||||
new Vector2(gradientRight, midY),
|
||||
@@ -460,13 +465,14 @@ internal static class SeluneRenderer
|
||||
float clampedBottomY,
|
||||
Vector4 leftColorVec,
|
||||
Vector4 midColorVec,
|
||||
Vector4 rightColorVec)
|
||||
Vector4 rightColorVec,
|
||||
float peakPosition)
|
||||
{
|
||||
var leftColor = ImGui.ColorConvertFloat4ToU32(leftColorVec);
|
||||
var midColor = ImGui.ColorConvertFloat4ToU32(midColorVec);
|
||||
var rightColor = ImGui.ColorConvertFloat4ToU32(rightColorVec);
|
||||
|
||||
var midX = gradientLeft + (gradientRight - gradientLeft) * 0.035f;
|
||||
var midX = gradientLeft + (gradientRight - gradientLeft) * peakPosition;
|
||||
drawList.AddRectFilledMultiColor(
|
||||
new Vector2(gradientLeft, clampedTopY),
|
||||
new Vector2(midX, clampedBottomY),
|
||||
|
||||
@@ -4,9 +4,11 @@ using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
@@ -42,13 +44,32 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
private Task<int>? _pruneTask;
|
||||
private int _pruneDays = 14;
|
||||
|
||||
// Ban management fields
|
||||
private Task<List<BannedGroupUserDto>>? _bannedUsersTask;
|
||||
private bool _bannedUsersLoaded;
|
||||
private string? _bannedUsersLoadError;
|
||||
|
||||
private string _newBanUid = string.Empty;
|
||||
private string _newBanReason = string.Empty;
|
||||
private Task? _newBanTask;
|
||||
private string? _newBanError;
|
||||
private DateTime _newBanBusyUntilUtc;
|
||||
|
||||
// Ban editing fields
|
||||
private string? _editingBanUid;
|
||||
private readonly Dictionary<string, string> _banReasonEdits = new(StringComparer.Ordinal);
|
||||
|
||||
private Task? _banEditTask;
|
||||
private string? _banEditError;
|
||||
|
||||
private Task<GroupPruneSettingsDto>? _pruneSettingsTask;
|
||||
private bool _pruneSettingsLoaded;
|
||||
private bool _autoPruneEnabled;
|
||||
private int _autoPruneDays = 14;
|
||||
private readonly PairFactory _pairFactory;
|
||||
|
||||
public SyncshellAdminUI(ILogger<SyncshellAdminUI> logger, LightlessMediator mediator, ApiController apiController,
|
||||
UiSharedService uiSharedService, PairUiService pairUiService, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, LightlessProfileManager lightlessProfileManager)
|
||||
UiSharedService uiSharedService, PairUiService pairUiService, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, LightlessProfileManager lightlessProfileManager, PairFactory pairFactory)
|
||||
: base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService)
|
||||
{
|
||||
GroupFullInfo = groupFullInfo;
|
||||
@@ -76,6 +97,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
MaximumSize = new(700, 2000),
|
||||
};
|
||||
_pairUiService = pairUiService;
|
||||
_pairFactory = pairFactory;
|
||||
}
|
||||
|
||||
public GroupFullInfoDto GroupFullInfo { get; private set; }
|
||||
@@ -654,34 +676,345 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
_uiSharedService.MediumText("User Bans", UIColors.Get("LightlessYellow"));
|
||||
ImGuiHelpers.ScaledDummy(3f);
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist from Server"))
|
||||
EnsureBanListLoaded();
|
||||
|
||||
DrawNewBanEntryRow();
|
||||
|
||||
ImGuiHelpers.ScaledDummy(4f);
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist"))
|
||||
{
|
||||
_bannedUsers = _apiController.GroupGetBannedUsers(new GroupDto(GroupFullInfo.Group)).Result;
|
||||
QueueBanListRefresh(force: true);
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(2f);
|
||||
|
||||
if (!_bannedUsersLoaded)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("Loading banlist from server...", ImGuiColors.DalamudGrey);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_bannedUsersLoadError))
|
||||
{
|
||||
UiSharedService.ColorTextWrapped(_bannedUsersLoadError!, ImGuiColors.DalamudRed);
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui.BeginChild("bannedListScroll#" + GroupFullInfo.GID, new Vector2(0, 0), true);
|
||||
|
||||
var style = ImGui.GetStyle();
|
||||
float fullW = ImGui.GetContentRegionAvail().X;
|
||||
float scale = ImGuiHelpers.GlobalScale;
|
||||
|
||||
float frame = ImGui.GetFrameHeight();
|
||||
float actionIcons = 3;
|
||||
float colActions = actionIcons * frame + (actionIcons - 1) * style.ItemSpacing.X + 10f * scale;
|
||||
|
||||
float colIdentity = fullW * 0.45f;
|
||||
float colMeta = fullW * 0.35f;
|
||||
float colActions = fullW - colIdentity - colMeta - style.ItemSpacing.X * 2.0f;
|
||||
|
||||
// Header
|
||||
float colIdentity = fullW - colMeta - colActions - style.ItemSpacing.X * 2.0f;
|
||||
|
||||
float minIdentity = fullW * 0.40f;
|
||||
if (colIdentity < minIdentity)
|
||||
{
|
||||
colIdentity = minIdentity;
|
||||
colMeta = fullW - colIdentity - colActions - style.ItemSpacing.X * 2.0f;
|
||||
if (colMeta < 80f * scale) colMeta = 80f * scale;
|
||||
}
|
||||
|
||||
DrawBannedListHeader(colIdentity, colMeta);
|
||||
|
||||
int rowIndex = 0;
|
||||
foreach (var bannedUser in _bannedUsers.ToList())
|
||||
{
|
||||
// Each row
|
||||
DrawBannedRow(bannedUser, rowIndex++, colIdentity, colMeta, colActions);
|
||||
}
|
||||
|
||||
ImGui.EndChild();
|
||||
}
|
||||
|
||||
private void DrawNewBanEntryRow()
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"));
|
||||
ImGui.TextUnformatted("Add new ban");
|
||||
ImGui.PopStyleColor();
|
||||
|
||||
UiSharedService.TextWrapped("Enter a UID (Not Alias!) and optional reason. (Hold CTRL to enable the ban button.)");
|
||||
|
||||
var style = ImGui.GetStyle();
|
||||
float fullW = ImGui.GetContentRegionAvail().X;
|
||||
|
||||
float uidW = fullW * 0.35f;
|
||||
float reasonW = fullW * 0.50f;
|
||||
float btnW = fullW - uidW - reasonW - style.ItemSpacing.X * 2f;
|
||||
|
||||
// UID
|
||||
ImGui.SetNextItemWidth(uidW);
|
||||
ImGui.InputTextWithHint("##newBanUid", "UID...", ref _newBanUid, 128);
|
||||
|
||||
// Reason
|
||||
ImGui.SameLine(0f, style.ItemSpacing.X);
|
||||
ImGui.SetNextItemWidth(reasonW);
|
||||
ImGui.InputTextWithHint("##newBanReason", "Reason (optional)...", ref _newBanReason, 256);
|
||||
|
||||
// Ban button
|
||||
ImGui.SameLine(0f, style.ItemSpacing.X);
|
||||
|
||||
var trimmedUid = (_newBanUid ?? string.Empty).Trim();
|
||||
var now = DateTime.UtcNow;
|
||||
bool taskRunning = _newBanTask != null && !_newBanTask.IsCompleted;
|
||||
bool busyLatched = now < _newBanBusyUntilUtc;
|
||||
bool busy = taskRunning || busyLatched;
|
||||
|
||||
bool canBan = UiSharedService.CtrlPressed()
|
||||
&& !string.IsNullOrWhiteSpace(_newBanUid)
|
||||
&& !busy;
|
||||
|
||||
using (ImRaii.Disabled(!canBan))
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("DimRed")))
|
||||
{
|
||||
ImGui.SetNextItemWidth(btnW);
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, "Ban"))
|
||||
{
|
||||
_newBanError = null;
|
||||
|
||||
_newBanBusyUntilUtc = DateTime.UtcNow.AddMilliseconds(750);
|
||||
|
||||
_newBanTask = SubmitNewBanByUidAsync(trimmedUid, _newBanReason);
|
||||
}
|
||||
}
|
||||
UiSharedService.AttachToolTip("Hold CTRL to enable banning by UID.");
|
||||
|
||||
if (busy)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("Banning user...", ImGuiColors.DalamudGrey);
|
||||
}
|
||||
|
||||
if (_newBanTask != null && _newBanTask.IsCompleted && DateTime.UtcNow >= _newBanBusyUntilUtc)
|
||||
{
|
||||
if (_newBanTask.IsFaulted)
|
||||
{
|
||||
var _ = _newBanTask.Exception;
|
||||
_newBanError ??= "Ban failed (see log).";
|
||||
}
|
||||
|
||||
QueueBanListRefresh(force: true);
|
||||
_newBanTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SubmitNewBanByUidAsync(string uidOrAlias, string reason)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
uidOrAlias = (uidOrAlias ?? string.Empty).Trim();
|
||||
reason = (reason ?? string.Empty).Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(uidOrAlias))
|
||||
{
|
||||
_newBanError = "UID is empty.";
|
||||
return;
|
||||
}
|
||||
|
||||
string targetUid = uidOrAlias;
|
||||
string? typedAlias = null;
|
||||
|
||||
var snap = _pairUiService.GetSnapshot();
|
||||
if (snap.GroupPairs.TryGetValue(GroupFullInfo, out var pairs))
|
||||
{
|
||||
var match = pairs.FirstOrDefault(p =>
|
||||
string.Equals(p.UserData.UID, uidOrAlias, StringComparison.Ordinal) ||
|
||||
string.Equals(p.UserData.AliasOrUID, uidOrAlias, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (match != null)
|
||||
{
|
||||
targetUid = match.UserData.UID;
|
||||
typedAlias = match.UserData.Alias;
|
||||
}
|
||||
else
|
||||
{
|
||||
typedAlias = null;
|
||||
}
|
||||
}
|
||||
|
||||
var userData = new UserData(UID: targetUid, Alias: typedAlias);
|
||||
|
||||
await _apiController
|
||||
.GroupBanUser(new GroupPairDto(GroupFullInfo.Group, userData), reason)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
_newBanUid = string.Empty;
|
||||
_newBanReason = string.Empty;
|
||||
_newBanError = null;
|
||||
|
||||
QueueBanListRefresh(force: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to ban '{uidOrAlias}' in group {gid}", uidOrAlias, GroupFullInfo.Group.GID);
|
||||
_newBanError = "Failed to ban user (see log).";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveBanReasonViaBanUserAsync(string uid)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_banReasonEdits.TryGetValue(uid, out var newReason))
|
||||
newReason = string.Empty;
|
||||
|
||||
newReason = (newReason ?? string.Empty).Trim();
|
||||
|
||||
var userData = new UserData(uid.Trim());
|
||||
|
||||
await _apiController
|
||||
.GroupBanUser(new GroupPairDto(GroupFullInfo.Group, userData), newReason)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
_editingBanUid = null;
|
||||
_banEditError = null;
|
||||
|
||||
await Task.Delay(450).ConfigureAwait(false);
|
||||
|
||||
QueueBanListRefresh(force: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to edit ban reason for {uid} in group {gid}", uid, GroupFullInfo.Group.GID);
|
||||
_banEditError = "Failed to update reason (see log).";
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawBannedRow(BannedGroupUserDto bannedUser, int rowIndex, float colIdentity, float colMeta, float colActions)
|
||||
{
|
||||
using var id = ImRaii.PushId("banRow_" + bannedUser.UID);
|
||||
|
||||
var style = ImGui.GetStyle();
|
||||
float x0 = ImGui.GetCursorPosX();
|
||||
|
||||
if (rowIndex % 2 == 0)
|
||||
{
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
var pMin = ImGui.GetCursorScreenPos();
|
||||
var rowHeight = ImGui.GetTextLineHeightWithSpacing() * 2.6f;
|
||||
var pMax = new Vector2(
|
||||
pMin.X + colIdentity + colMeta + colActions + style.ItemSpacing.X * 2.0f,
|
||||
pMin.Y + rowHeight);
|
||||
|
||||
var bgColor = UIColors.Get("FullBlack").WithAlpha(0.10f);
|
||||
drawList.AddRectFilled(pMin, pMax, ImGui.ColorConvertFloat4ToU32(bgColor));
|
||||
}
|
||||
|
||||
ImGui.SetCursorPosX(x0);
|
||||
ImGui.AlignTextToFramePadding();
|
||||
|
||||
string alias = bannedUser.UserAlias ?? string.Empty;
|
||||
string line1 = string.IsNullOrEmpty(alias)
|
||||
? bannedUser.UID
|
||||
: $"{alias} ({bannedUser.UID})";
|
||||
|
||||
ImGui.TextUnformatted(line1);
|
||||
|
||||
var fullReason = bannedUser.Reason ?? string.Empty;
|
||||
|
||||
if (string.Equals(_editingBanUid, bannedUser.UID, StringComparison.Ordinal))
|
||||
{
|
||||
_banReasonEdits.TryGetValue(bannedUser.UID, out var editReason);
|
||||
editReason ??= StripAliasSuffix(fullReason);
|
||||
|
||||
ImGui.SetCursorPosX(x0);
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
|
||||
ImGui.SetNextItemWidth(colIdentity);
|
||||
ImGui.InputTextWithHint("##banReasonEdit", "Reason...", ref editReason, 255);
|
||||
ImGui.PopStyleColor();
|
||||
|
||||
_banReasonEdits[bannedUser.UID] = editReason;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_banEditError))
|
||||
UiSharedService.ColorTextWrapped(_banEditError!, ImGuiColors.DalamudRed);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(fullReason))
|
||||
{
|
||||
ImGui.SetCursorPosX(x0);
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
|
||||
|
||||
ImGui.PushTextWrapPos(x0 + colIdentity);
|
||||
UiSharedService.TextWrapped(fullReason);
|
||||
ImGui.PopTextWrapPos();
|
||||
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosX(x0 + colIdentity + style.ItemSpacing.X);
|
||||
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted($"By: {bannedUser.BannedBy}");
|
||||
|
||||
var dateText = bannedUser.BannedOn.ToLocalTime().ToString(CultureInfo.CurrentCulture);
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
|
||||
ImGui.TextUnformatted(dateText);
|
||||
ImGui.PopStyleColor();
|
||||
ImGui.SameLine();
|
||||
|
||||
float frame = ImGui.GetFrameHeight();
|
||||
float actionsX0 = x0 + colIdentity + colMeta + style.ItemSpacing.X * 2.0f;
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosX(actionsX0);
|
||||
|
||||
bool isEditing = string.Equals(_editingBanUid, bannedUser.UID, StringComparison.Ordinal);
|
||||
int actionCount = 1 + (isEditing ? 2 : 1);
|
||||
|
||||
float totalW = actionCount * frame + (actionCount - 1) * style.ItemSpacing.X;
|
||||
float startX = actionsX0 + MathF.Max(0, colActions - totalW) - 36f;
|
||||
ImGui.SetCursorPosX(startX);
|
||||
|
||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Check))
|
||||
{
|
||||
_apiController.GroupUnbanUser(bannedUser);
|
||||
_bannedUsers.RemoveAll(b => string.Equals(b.UID, bannedUser.UID, StringComparison.Ordinal));
|
||||
}
|
||||
UiSharedService.AttachToolTip("Unban");
|
||||
|
||||
ImGui.SameLine(0f, style.ItemSpacing.X);
|
||||
|
||||
if (!isEditing)
|
||||
{
|
||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Edit))
|
||||
{
|
||||
_banEditError = null;
|
||||
_editingBanUid = bannedUser.UID;
|
||||
_banReasonEdits[bannedUser.UID] = StripAliasSuffix(bannedUser.Reason ?? string.Empty);
|
||||
}
|
||||
UiSharedService.AttachToolTip("Edit reason");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Save))
|
||||
{
|
||||
_banEditError = null;
|
||||
_banEditTask = SaveBanReasonViaBanUserAsync(bannedUser.UID);
|
||||
}
|
||||
UiSharedService.AttachToolTip("Save");
|
||||
|
||||
ImGui.SameLine(0f, style.ItemSpacing.X);
|
||||
|
||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Times))
|
||||
{
|
||||
_banEditError = null;
|
||||
_editingBanUid = null;
|
||||
}
|
||||
UiSharedService.AttachToolTip("Cancel");
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawInvites(GroupPermissions perm)
|
||||
{
|
||||
var inviteTab = ImRaii.TabItem("Invites");
|
||||
@@ -902,7 +1235,9 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
if (buttonCount == 0)
|
||||
return;
|
||||
|
||||
float totalWidth = buttonCount * frameH + (buttonCount - 1) * style.ItemSpacing.X;
|
||||
float totalWidth = _isOwner
|
||||
? buttonCount * frameH + buttonCount * style.ItemSpacing.X + 20f
|
||||
: buttonCount * frameH + buttonCount * style.ItemSpacing.X;
|
||||
|
||||
float curX = ImGui.GetCursorPosX();
|
||||
float avail = ImGui.GetContentRegionAvail().X;
|
||||
@@ -1031,69 +1366,40 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.0f);
|
||||
}
|
||||
|
||||
private void DrawBannedRow(BannedGroupUserDto bannedUser, int rowIndex, float colIdentity, float colMeta, float colActions)
|
||||
private void QueueBanListRefresh(bool force = false)
|
||||
{
|
||||
using var id = ImRaii.PushId("banRow_" + bannedUser.UID);
|
||||
|
||||
var style = ImGui.GetStyle();
|
||||
float x0 = ImGui.GetCursorPosX();
|
||||
|
||||
if (rowIndex % 2 == 0)
|
||||
if (!force)
|
||||
{
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
var pMin = ImGui.GetCursorScreenPos();
|
||||
var rowHeight = ImGui.GetTextLineHeightWithSpacing() * 2.6f;
|
||||
var pMax = new Vector2(
|
||||
pMin.X + colIdentity + colMeta + colActions + style.ItemSpacing.X * 2.0f,
|
||||
pMin.Y + rowHeight);
|
||||
|
||||
var bgColor = UIColors.Get("FullBlack").WithAlpha(0.10f);
|
||||
drawList.AddRectFilled(pMin, pMax, ImGui.ColorConvertFloat4ToU32(bgColor));
|
||||
if (_bannedUsersTask != null && !_bannedUsersTask.IsCompleted)
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui.SetCursorPosX(x0);
|
||||
ImGui.AlignTextToFramePadding();
|
||||
_bannedUsersLoaded = false;
|
||||
_bannedUsersLoadError = null;
|
||||
|
||||
string alias = bannedUser.UserAlias ?? string.Empty;
|
||||
string line1 = string.IsNullOrEmpty(alias)
|
||||
? bannedUser.UID
|
||||
: $"{alias} ({bannedUser.UID})";
|
||||
_bannedUsersTask = _apiController.GroupGetBannedUsers(new GroupDto(GroupFullInfo.Group));
|
||||
}
|
||||
|
||||
ImGui.TextUnformatted(line1);
|
||||
private void EnsureBanListLoaded()
|
||||
{
|
||||
_bannedUsersTask ??= _apiController.GroupGetBannedUsers(new GroupDto(GroupFullInfo.Group));
|
||||
|
||||
var reason = bannedUser.Reason ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(reason))
|
||||
if (_bannedUsersLoaded || _bannedUsersTask == null)
|
||||
return;
|
||||
|
||||
if (!_bannedUsersTask.IsCompleted)
|
||||
return;
|
||||
|
||||
if (_bannedUsersTask.IsFaulted || _bannedUsersTask.IsCanceled)
|
||||
{
|
||||
var reasonPos = new Vector2(x0, ImGui.GetCursorPosY());
|
||||
ImGui.SetCursorPos(reasonPos);
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
|
||||
UiSharedService.TextWrapped(reason);
|
||||
ImGui.PopStyleColor();
|
||||
_bannedUsersLoadError = "Failed to load banlist from server.";
|
||||
_bannedUsers = [];
|
||||
_bannedUsersLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosX(x0 + colIdentity + style.ItemSpacing.X);
|
||||
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted($"By: {bannedUser.BannedBy}");
|
||||
|
||||
var dateText = bannedUser.BannedOn.ToLocalTime().ToString(CultureInfo.CurrentCulture);
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
|
||||
ImGui.TextUnformatted(dateText);
|
||||
ImGui.PopStyleColor();
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosX(x0 + colIdentity + colMeta + style.ItemSpacing.X * 2.0f);
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Check, "Unban"))
|
||||
{
|
||||
_apiController.GroupUnbanUser(bannedUser);
|
||||
_bannedUsers.RemoveAll(b => string.Equals(b.UID, bannedUser.UID, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
UiSharedService.AttachToolTip($"Unban {alias} ({bannedUser.UID}) from this Syncshell");
|
||||
|
||||
ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale));
|
||||
_bannedUsers = _bannedUsersTask.GetAwaiter().GetResult() ?? [];
|
||||
_bannedUsersLoaded = true;
|
||||
}
|
||||
|
||||
private void SavePruneSettings()
|
||||
@@ -1116,6 +1422,13 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
private static string StripAliasSuffix(string reason)
|
||||
{
|
||||
const string marker = " (Alias at time of ban:";
|
||||
var idx = reason.IndexOf(marker, StringComparison.Ordinal);
|
||||
return idx >= 0 ? reason[..idx] : reason;
|
||||
}
|
||||
|
||||
private static bool MatchesUserFilter(Pair pair, string filterLower)
|
||||
{
|
||||
var note = pair.GetNote() ?? string.Empty;
|
||||
@@ -1127,6 +1440,11 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
|| alias.Contains(filterLower, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public override void OnOpen()
|
||||
{
|
||||
base.OnOpen();
|
||||
QueueBanListRefresh(force: true);
|
||||
}
|
||||
public override void OnClose()
|
||||
{
|
||||
Mediator.Publish(new RemoveWindowMessage(this));
|
||||
|
||||
@@ -1,855 +0,0 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Plugin.Services;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.LightFinder;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI.Services;
|
||||
using LightlessSync.UI.Tags;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Numerics;
|
||||
|
||||
namespace LightlessSync.UI;
|
||||
|
||||
public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
{
|
||||
private readonly ApiController _apiController;
|
||||
private readonly LightFinderService _broadcastService;
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private readonly LightFinderScannerService _broadcastScannerService;
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
|
||||
private Vector4 _tagBackgroundColor = new(0.18f, 0.18f, 0.18f, 0.95f);
|
||||
private Vector4 _tagBorderColor = new(0.35f, 0.35f, 0.35f, 0.4f);
|
||||
|
||||
private readonly List<SeStringUtils.SeStringSegment> _seResolvedSegments = new();
|
||||
private readonly List<GroupJoinDto> _nearbySyncshells = [];
|
||||
private List<GroupFullInfoDto> _currentSyncshells = [];
|
||||
private int _selectedNearbyIndex = -1;
|
||||
private int _syncshellPageIndex = 0;
|
||||
private readonly HashSet<string> _recentlyJoined = new(StringComparer.Ordinal);
|
||||
|
||||
private GroupJoinDto? _joinDto;
|
||||
private GroupJoinInfoDto? _joinInfo;
|
||||
private DefaultPermissionsDto _ownPermissions = null!;
|
||||
private bool _useTestSyncshells = false;
|
||||
|
||||
private bool _compactView = false;
|
||||
private readonly LightlessProfileManager _lightlessProfileManager;
|
||||
|
||||
public SyncshellFinderUI(
|
||||
ILogger<SyncshellFinderUI> logger,
|
||||
LightlessMediator mediator,
|
||||
PerformanceCollectorService performanceCollectorService,
|
||||
LightFinderService broadcastService,
|
||||
UiSharedService uiShared,
|
||||
ApiController apiController,
|
||||
LightFinderScannerService broadcastScannerService,
|
||||
PairUiService pairUiService,
|
||||
DalamudUtilService dalamudUtilService,
|
||||
LightlessProfileManager lightlessProfileManager) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService)
|
||||
{
|
||||
_broadcastService = broadcastService;
|
||||
_uiSharedService = uiShared;
|
||||
_apiController = apiController;
|
||||
_broadcastScannerService = broadcastScannerService;
|
||||
_pairUiService = pairUiService;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_lightlessProfileManager = lightlessProfileManager;
|
||||
|
||||
IsOpen = false;
|
||||
WindowBuilder.For(this)
|
||||
.SetSizeConstraints(new Vector2(600, 400), new Vector2(600, 550))
|
||||
.Apply();
|
||||
|
||||
Mediator.Subscribe<SyncshellBroadcastsUpdatedMessage>(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false));
|
||||
Mediator.Subscribe<BroadcastStatusChangedMessage>(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false));
|
||||
Mediator.Subscribe<UserLeftSyncshell>(this, async _ => await RefreshSyncshellsAsync(_.gid).ConfigureAwait(false));
|
||||
Mediator.Subscribe<UserJoinedSyncshell>(this, async _ => await RefreshSyncshellsAsync(_.gid).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
public override async void OnOpen()
|
||||
{
|
||||
_ownPermissions = _apiController.DefaultPermissions.DeepClone()!;
|
||||
await RefreshSyncshellsAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override void DrawInternal()
|
||||
{
|
||||
ImGui.BeginGroup();
|
||||
_uiSharedService.MediumText("Nearby Syncshells", UIColors.Get("LightlessPurple"));
|
||||
|
||||
#if DEBUG
|
||||
if (ImGui.SmallButton("Show test syncshells"))
|
||||
{
|
||||
_useTestSyncshells = !_useTestSyncshells;
|
||||
_ = Task.Run(async () => await RefreshSyncshellsAsync().ConfigureAwait(false));
|
||||
}
|
||||
ImGui.SameLine();
|
||||
#endif
|
||||
|
||||
string checkboxLabel = "Compact view";
|
||||
float availWidth = ImGui.GetContentRegionAvail().X;
|
||||
float checkboxWidth = ImGui.CalcTextSize(checkboxLabel).X + ImGui.GetFrameHeight();
|
||||
|
||||
float rightX = ImGui.GetCursorPosX() + availWidth - checkboxWidth - 4.0f;
|
||||
ImGui.SetCursorPosX(rightX);
|
||||
ImGui.Checkbox(checkboxLabel, ref _compactView);
|
||||
ImGui.EndGroup();
|
||||
|
||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"));
|
||||
ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale));
|
||||
if (_nearbySyncshells.Count == 0)
|
||||
{
|
||||
ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted.");
|
||||
|
||||
if (!_broadcastService.IsBroadcasting)
|
||||
{
|
||||
|
||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"));
|
||||
|
||||
ImGui.TextColored(UIColors.Get("LightlessYellow"), "Lightfinder is currently disabled, to locate nearby syncshells, Lightfinder must be active.");
|
||||
ImGuiHelpers.ScaledDummy(0.5f);
|
||||
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10.0f);
|
||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessPurple"));
|
||||
|
||||
if (ImGui.Button("Open Lightfinder", new Vector2(200 * ImGuiHelpers.GlobalScale, 0)))
|
||||
{
|
||||
Mediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
||||
}
|
||||
|
||||
ImGui.PopStyleColor();
|
||||
ImGui.PopStyleVar();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().ToList() ?? [];
|
||||
_broadcastScannerService.TryGetLocalHashedCid(out var localHashedCid);
|
||||
|
||||
var cardData = new List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)>();
|
||||
|
||||
foreach (var shell in _nearbySyncshells)
|
||||
{
|
||||
string broadcasterName;
|
||||
|
||||
if (shell?.Group == null || string.IsNullOrEmpty(shell.Group.GID))
|
||||
continue;
|
||||
|
||||
if (_useTestSyncshells)
|
||||
{
|
||||
var displayName = !string.IsNullOrEmpty(shell.Group.Alias)
|
||||
? shell.Group.Alias
|
||||
: shell.Group.GID;
|
||||
|
||||
broadcasterName = $"{displayName} (Tester of TestWorld)";
|
||||
}
|
||||
else
|
||||
{
|
||||
var broadcast = broadcasts
|
||||
.FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal));
|
||||
|
||||
if (broadcast == null)
|
||||
continue;
|
||||
|
||||
var (name, address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID);
|
||||
if (string.IsNullOrEmpty(name))
|
||||
continue;
|
||||
|
||||
var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(address);
|
||||
broadcasterName = !string.IsNullOrEmpty(worldName)
|
||||
? $"{name} ({worldName})"
|
||||
: name;
|
||||
|
||||
var isSelfBroadcast = !string.IsNullOrEmpty(localHashedCid)
|
||||
&& string.Equals(broadcast.HashedCID, localHashedCid, StringComparison.Ordinal);
|
||||
|
||||
cardData.Add((shell, broadcasterName, isSelfBroadcast));
|
||||
continue;
|
||||
}
|
||||
|
||||
cardData.Add((shell, broadcasterName, false));
|
||||
}
|
||||
|
||||
if (cardData.Count == 0)
|
||||
{
|
||||
ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_compactView)
|
||||
{
|
||||
DrawSyncshellGrid(cardData);
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawSyncshellList(cardData);
|
||||
}
|
||||
|
||||
|
||||
if (_joinDto != null && _joinInfo != null && _joinInfo.Success)
|
||||
DrawConfirmation();
|
||||
}
|
||||
|
||||
private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)> listData)
|
||||
{
|
||||
const int shellsPerPage = 3;
|
||||
var totalPages = (int)Math.Ceiling(listData.Count / (float)shellsPerPage);
|
||||
if (totalPages <= 0)
|
||||
totalPages = 1;
|
||||
|
||||
_syncshellPageIndex = Math.Clamp(_syncshellPageIndex, 0, totalPages - 1);
|
||||
|
||||
var firstIndex = _syncshellPageIndex * shellsPerPage;
|
||||
var lastExclusive = Math.Min(firstIndex + shellsPerPage, listData.Count);
|
||||
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8.0f);
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1.0f);
|
||||
|
||||
for (int index = firstIndex; index < lastExclusive; index++)
|
||||
{
|
||||
var (shell, broadcasterName, isSelfBroadcast) = listData[index];
|
||||
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
|
||||
? (isSelfBroadcast ? "You" : string.Empty)
|
||||
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
|
||||
|
||||
ImGui.PushID(shell.Group.GID);
|
||||
float rowHeight = 74f * ImGuiHelpers.GlobalScale;
|
||||
|
||||
ImGui.BeginChild($"ShellRow##{shell.Group.GID}", new Vector2(-1, rowHeight), border: true);
|
||||
|
||||
var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID;
|
||||
|
||||
var style = ImGui.GetStyle();
|
||||
float startX = ImGui.GetCursorPosX();
|
||||
float regionW = ImGui.GetContentRegionAvail().X;
|
||||
float rightTxtW = ImGui.CalcTextSize(broadcasterLabel).X;
|
||||
|
||||
_uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple"));
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip("Click to open profile.");
|
||||
if (ImGui.IsItemClicked())
|
||||
{
|
||||
Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group));
|
||||
}
|
||||
|
||||
float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X;
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosX(rightX);
|
||||
ImGui.TextUnformatted(broadcasterLabel);
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip("Broadcaster of the syncshell.");
|
||||
|
||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"));
|
||||
|
||||
var groupProfile = _lightlessProfileManager.GetLightlessGroupProfile(shell.Group);
|
||||
|
||||
IReadOnlyList<ProfileTagDefinition> groupTags =
|
||||
groupProfile != null && groupProfile.Tags.Count > 0
|
||||
? ProfileTagService.ResolveTags(groupProfile.Tags)
|
||||
: [];
|
||||
|
||||
var limitedTags = groupTags.Count > 3
|
||||
? [.. groupTags.Take(3)]
|
||||
: groupTags;
|
||||
|
||||
float tagScale = ImGuiHelpers.GlobalScale * 0.9f;
|
||||
|
||||
Vector2 rowStartLocal = ImGui.GetCursorPos();
|
||||
|
||||
float tagsWidth = 0f;
|
||||
float tagsHeight = 0f;
|
||||
|
||||
if (limitedTags.Count > 0)
|
||||
{
|
||||
(tagsWidth, tagsHeight) = RenderProfileTagsSingleRow(limitedTags, tagScale);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.SetCursorPosX(startX);
|
||||
ImGui.TextDisabled("-- No tags set --");
|
||||
ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale));
|
||||
}
|
||||
|
||||
float btnBaselineY = rowStartLocal.Y;
|
||||
float joinX = rowStartLocal.X + (tagsWidth > 0 ? tagsWidth + style.ItemSpacing.X : 0f);
|
||||
|
||||
ImGui.SetCursorPos(new Vector2(joinX, btnBaselineY));
|
||||
DrawJoinButton(shell, isSelfBroadcast);
|
||||
|
||||
float btnHeight = ImGui.GetFrameHeightWithSpacing();
|
||||
float rowHeightUsed = MathF.Max(tagsHeight, btnHeight);
|
||||
|
||||
ImGui.SetCursorPos(new Vector2(
|
||||
rowStartLocal.X,
|
||||
rowStartLocal.Y + rowHeightUsed));
|
||||
|
||||
ImGui.EndChild();
|
||||
ImGui.PopID();
|
||||
|
||||
ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale));
|
||||
}
|
||||
|
||||
ImGui.PopStyleVar(2);
|
||||
|
||||
DrawPagination(totalPages);
|
||||
}
|
||||
|
||||
private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)> cardData)
|
||||
{
|
||||
const int shellsPerPage = 4;
|
||||
var totalPages = (int)Math.Ceiling(cardData.Count / (float)shellsPerPage);
|
||||
if (totalPages <= 0)
|
||||
totalPages = 1;
|
||||
|
||||
_syncshellPageIndex = Math.Clamp(_syncshellPageIndex, 0, totalPages - 1);
|
||||
|
||||
var firstIndex = _syncshellPageIndex * shellsPerPage;
|
||||
var lastExclusive = Math.Min(firstIndex + shellsPerPage, cardData.Count);
|
||||
|
||||
var avail = ImGui.GetContentRegionAvail();
|
||||
var spacing = ImGui.GetStyle().ItemSpacing;
|
||||
|
||||
var cardWidth = (avail.X - spacing.X) / 2.0f;
|
||||
var cardHeight = (avail.Y - spacing.Y - (ImGui.GetFrameHeightWithSpacing() * 2.0f)) / 2.0f;
|
||||
cardHeight = MathF.Max(110f * ImGuiHelpers.GlobalScale, cardHeight);
|
||||
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8.0f);
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1.0f);
|
||||
|
||||
for (int index = firstIndex; index < lastExclusive; index++)
|
||||
{
|
||||
var localIndex = index - firstIndex;
|
||||
var (shell, broadcasterName, isSelfBroadcast) = cardData[index];
|
||||
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
|
||||
? (isSelfBroadcast ? "You" : string.Empty)
|
||||
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
|
||||
|
||||
if (localIndex % 2 != 0)
|
||||
ImGui.SameLine();
|
||||
|
||||
ImGui.PushID(shell.Group.GID);
|
||||
|
||||
ImGui.BeginGroup();
|
||||
_ = ImGui.BeginChild("ShellCard##" + shell.Group.GID, new Vector2(cardWidth, cardHeight), border: true);
|
||||
|
||||
var displayName = !string.IsNullOrEmpty(shell.Group.Alias)
|
||||
? shell.Group.Alias
|
||||
: shell.Group.GID;
|
||||
|
||||
var style = ImGui.GetStyle();
|
||||
float startX = ImGui.GetCursorPosX();
|
||||
float availW = ImGui.GetContentRegionAvail().X;
|
||||
|
||||
ImGui.BeginGroup();
|
||||
|
||||
_uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple"));
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip("Click to open profile.");
|
||||
if (ImGui.IsItemClicked())
|
||||
{
|
||||
Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group));
|
||||
}
|
||||
|
||||
float nameRightX = ImGui.GetItemRectMax().X;
|
||||
|
||||
var regionMinScreen = ImGui.GetCursorScreenPos();
|
||||
float regionRightX = regionMinScreen.X + availW;
|
||||
|
||||
float minBroadcasterX = nameRightX + style.ItemSpacing.X;
|
||||
|
||||
float maxBroadcasterWidth = regionRightX - minBroadcasterX;
|
||||
|
||||
string broadcasterToShow = broadcasterLabel;
|
||||
|
||||
if (!string.IsNullOrEmpty(broadcasterLabel) && maxBroadcasterWidth > 0f)
|
||||
{
|
||||
float bcFullWidth = ImGui.CalcTextSize(broadcasterLabel).X;
|
||||
string toolTip;
|
||||
|
||||
if (bcFullWidth > maxBroadcasterWidth)
|
||||
{
|
||||
broadcasterToShow = TruncateTextToWidth(broadcasterLabel, maxBroadcasterWidth);
|
||||
toolTip = broadcasterLabel + Environment.NewLine + Environment.NewLine + "Broadcaster of the syncshell.";
|
||||
}
|
||||
else
|
||||
{
|
||||
toolTip = "Broadcaster of the syncshell.";
|
||||
}
|
||||
|
||||
float bcWidth = ImGui.CalcTextSize(broadcasterToShow).X;
|
||||
|
||||
float broadX = regionRightX - bcWidth;
|
||||
|
||||
broadX = MathF.Max(broadX, minBroadcasterX);
|
||||
|
||||
ImGui.SameLine();
|
||||
var curPos = ImGui.GetCursorPos();
|
||||
ImGui.SetCursorPos(new Vector2(broadX - regionMinScreen.X + startX, curPos.Y + 3f * ImGuiHelpers.GlobalScale));
|
||||
ImGui.TextUnformatted(broadcasterToShow);
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip(toolTip);
|
||||
}
|
||||
|
||||
ImGui.EndGroup();
|
||||
|
||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"));
|
||||
|
||||
ImGui.Dummy(new Vector2(0, 6 * ImGuiHelpers.GlobalScale));
|
||||
|
||||
var groupProfile = _lightlessProfileManager.GetLightlessGroupProfile(shell.Group);
|
||||
|
||||
IReadOnlyList<ProfileTagDefinition> groupTags =
|
||||
groupProfile != null && groupProfile.Tags.Count > 0
|
||||
? ProfileTagService.ResolveTags(groupProfile.Tags)
|
||||
: [];
|
||||
|
||||
float tagScale = ImGuiHelpers.GlobalScale * 0.9f;
|
||||
|
||||
if (groupTags.Count > 0)
|
||||
{
|
||||
var limitedTags = groupTags.Count > 2
|
||||
? [.. groupTags.Take(2)]
|
||||
: groupTags;
|
||||
|
||||
ImGui.SetCursorPosX(startX);
|
||||
|
||||
var (_, tagsHeight) = RenderProfileTagsSingleRow(limitedTags, tagScale);
|
||||
|
||||
ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale));
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.SetCursorPosX(startX);
|
||||
ImGui.TextDisabled("-- No tags set --");
|
||||
ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale));
|
||||
}
|
||||
|
||||
var buttonHeight = ImGui.GetFrameHeightWithSpacing();
|
||||
var remainingY = ImGui.GetContentRegionAvail().Y - buttonHeight;
|
||||
if (remainingY > 0)
|
||||
ImGui.Dummy(new Vector2(0, remainingY));
|
||||
|
||||
DrawJoinButton(shell, isSelfBroadcast);
|
||||
|
||||
ImGui.EndChild();
|
||||
ImGui.EndGroup();
|
||||
|
||||
ImGui.PopID();
|
||||
}
|
||||
|
||||
ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale));
|
||||
ImGui.PopStyleVar(2);
|
||||
|
||||
DrawPagination(totalPages);
|
||||
}
|
||||
|
||||
private void DrawPagination(int totalPages)
|
||||
{
|
||||
if (totalPages > 1)
|
||||
{
|
||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"));
|
||||
|
||||
var style = ImGui.GetStyle();
|
||||
string pageLabel = $"Page {_syncshellPageIndex + 1}/{totalPages}";
|
||||
|
||||
float prevWidth = ImGui.CalcTextSize("<").X + style.FramePadding.X * 2;
|
||||
float nextWidth = ImGui.CalcTextSize(">").X + style.FramePadding.X * 2;
|
||||
float textWidth = ImGui.CalcTextSize(pageLabel).X;
|
||||
|
||||
float totalWidth = prevWidth + textWidth + nextWidth + style.ItemSpacing.X * 2;
|
||||
|
||||
float availWidth = ImGui.GetContentRegionAvail().X;
|
||||
float offsetX = (availWidth - totalWidth) * 0.5f;
|
||||
|
||||
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + offsetX);
|
||||
|
||||
if (ImGui.Button("<##PrevSyncshellPage") && _syncshellPageIndex > 0)
|
||||
_syncshellPageIndex--;
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.Text(pageLabel);
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button(">##NextSyncshellPage") && _syncshellPageIndex < totalPages - 1)
|
||||
_syncshellPageIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawJoinButton(GroupJoinDto shell, bool isSelfBroadcast)
|
||||
{
|
||||
const string visibleLabel = "Join";
|
||||
var label = $"{visibleLabel}##{shell.Group.GID}";
|
||||
|
||||
var isAlreadyMember = _currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal));
|
||||
var isRecentlyJoined = _recentlyJoined.Contains(shell.GID);
|
||||
|
||||
Vector2 buttonSize;
|
||||
|
||||
if (!_compactView)
|
||||
{
|
||||
var style = ImGui.GetStyle();
|
||||
var textSize = ImGui.CalcTextSize(visibleLabel);
|
||||
|
||||
var width = textSize.X + style.FramePadding.X * 20f;
|
||||
buttonSize = new Vector2(width, 30f);
|
||||
|
||||
float availX = ImGui.GetContentRegionAvail().X;
|
||||
float curX = ImGui.GetCursorPosX();
|
||||
float newX = curX + (availX - buttonSize.X);
|
||||
ImGui.SetCursorPosX(newX);
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonSize = new Vector2(-1, 0);
|
||||
}
|
||||
|
||||
if (!isAlreadyMember && !isRecentlyJoined && !isSelfBroadcast)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"));
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f));
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f));
|
||||
if (ImGui.Button(label, buttonSize))
|
||||
{
|
||||
_logger.LogInformation($"Join requested for Syncshell {shell.Group.GID} ({shell.Group.Alias})");
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = await _apiController.GroupJoinHashed(new GroupJoinHashedDto(
|
||||
shell.Group,
|
||||
shell.Password,
|
||||
shell.GroupUserPreferredPermissions
|
||||
)).ConfigureAwait(false);
|
||||
|
||||
if (info != null && info.Success)
|
||||
{
|
||||
_joinDto = new GroupJoinDto(shell.Group, shell.Password, shell.GroupUserPreferredPermissions);
|
||||
_joinInfo = info;
|
||||
_ownPermissions = _apiController.DefaultPermissions.DeepClone()!;
|
||||
|
||||
_logger.LogInformation($"Fetched join info for {shell.Group.GID}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning($"Failed to join {shell.Group.GID}: info was null or unsuccessful");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Join failed for {shell.Group.GID}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("DimRed"));
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("DimRed").WithAlpha(0.85f));
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("DimRed").WithAlpha(0.75f));
|
||||
|
||||
using (ImRaii.Disabled())
|
||||
{
|
||||
ImGui.Button(label, buttonSize);
|
||||
}
|
||||
|
||||
UiSharedService.AttachToolTip(isSelfBroadcast
|
||||
? "This is your own Syncshell."
|
||||
: "Already a member or owner of this Syncshell.");
|
||||
}
|
||||
|
||||
ImGui.PopStyleColor(3);
|
||||
}
|
||||
|
||||
private (float widthUsed, float rowHeight) RenderProfileTagsSingleRow(IReadOnlyList<ProfileTagDefinition> tags, float scale)
|
||||
{
|
||||
if (tags == null || tags.Count == 0)
|
||||
return (0f, 0f);
|
||||
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
var style = ImGui.GetStyle();
|
||||
var defaultTextColorU32 = ImGui.GetColorU32(ImGuiCol.Text);
|
||||
|
||||
var baseLocal = ImGui.GetCursorPos();
|
||||
var baseScreen = ImGui.GetCursorScreenPos();
|
||||
float availableWidth = ImGui.GetContentRegionAvail().X;
|
||||
if (availableWidth <= 0f)
|
||||
availableWidth = 1f;
|
||||
|
||||
float cursorLocalX = baseLocal.X;
|
||||
float cursorScreenX = baseScreen.X;
|
||||
float rowHeight = 0f;
|
||||
|
||||
for (int i = 0; i < tags.Count; i++)
|
||||
{
|
||||
var tag = tags[i];
|
||||
if (!tag.HasContent)
|
||||
continue;
|
||||
|
||||
var tagSize = ProfileTagRenderer.MeasureTag(tag, scale, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _seResolvedSegments, GetIconWrap, _logger);
|
||||
|
||||
float tagWidth = tagSize.X;
|
||||
float tagHeight = tagSize.Y;
|
||||
|
||||
if (cursorLocalX > baseLocal.X && cursorLocalX + tagWidth > baseLocal.X + availableWidth)
|
||||
break;
|
||||
|
||||
var tagScreenPos = new Vector2(cursorScreenX, baseScreen.Y);
|
||||
ImGui.SetCursorScreenPos(tagScreenPos);
|
||||
ImGui.InvisibleButton($"##profileTagInline_{i}", tagSize);
|
||||
|
||||
ProfileTagRenderer.RenderTag(tag, tagScreenPos, scale, drawList, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _seResolvedSegments, GetIconWrap, _logger);
|
||||
|
||||
cursorLocalX += tagWidth + style.ItemSpacing.X;
|
||||
cursorScreenX += tagWidth + style.ItemSpacing.X;
|
||||
rowHeight = MathF.Max(rowHeight, tagHeight);
|
||||
}
|
||||
|
||||
ImGui.SetCursorPos(new Vector2(baseLocal.X, baseLocal.Y + rowHeight));
|
||||
|
||||
float widthUsed = cursorLocalX - baseLocal.X;
|
||||
return (widthUsed, rowHeight);
|
||||
}
|
||||
private static string TruncateTextToWidth(string text, float maxWidth)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return text;
|
||||
|
||||
const string ellipsis = "...";
|
||||
float ellipsisWidth = ImGui.CalcTextSize(ellipsis).X;
|
||||
|
||||
if (maxWidth <= ellipsisWidth)
|
||||
return ellipsis;
|
||||
|
||||
int low = 0;
|
||||
int high = text.Length;
|
||||
string best = ellipsis;
|
||||
|
||||
while (low <= high)
|
||||
{
|
||||
int mid = (low + high) / 2;
|
||||
string candidate = string.Concat(text.AsSpan(0, mid), ellipsis);
|
||||
float width = ImGui.CalcTextSize(candidate).X;
|
||||
|
||||
if (width <= maxWidth)
|
||||
{
|
||||
best = candidate;
|
||||
low = mid + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
private IDalamudTextureWrap? GetIconWrap(uint iconId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_uiSharedService.TryGetIcon(iconId, out var wrap) && wrap != null)
|
||||
return wrap;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to resolve icon {IconId} for profile tags", iconId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void DrawConfirmation()
|
||||
{
|
||||
if (_joinDto != null && _joinInfo != null)
|
||||
{
|
||||
ImGui.Separator();
|
||||
ImGui.TextUnformatted($"Join Syncshell: {_joinDto.Group.AliasOrGID} by {_joinInfo.OwnerAliasOrUID}");
|
||||
ImGuiHelpers.ScaledDummy(2f);
|
||||
ImGui.TextUnformatted("Suggested Syncshell Permissions:");
|
||||
|
||||
DrawPermissionRow("Sounds", _joinInfo.GroupPermissions.IsPreferDisableSounds(), _ownPermissions.DisableGroupSounds, v => _ownPermissions.DisableGroupSounds = v);
|
||||
DrawPermissionRow("Animations", _joinInfo.GroupPermissions.IsPreferDisableAnimations(), _ownPermissions.DisableGroupAnimations, v => _ownPermissions.DisableGroupAnimations = v);
|
||||
DrawPermissionRow("VFX", _joinInfo.GroupPermissions.IsPreferDisableVFX(), _ownPermissions.DisableGroupVFX, v => _ownPermissions.DisableGroupVFX = v);
|
||||
|
||||
ImGui.NewLine();
|
||||
ImGui.NewLine();
|
||||
|
||||
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Plus, $"Finalize and join {_joinDto.Group.AliasOrGID}"))
|
||||
{
|
||||
var finalPermissions = GroupUserPreferredPermissions.NoneSet;
|
||||
finalPermissions.SetDisableSounds(_ownPermissions.DisableGroupSounds);
|
||||
finalPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations);
|
||||
finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX);
|
||||
|
||||
_ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions));
|
||||
|
||||
_recentlyJoined.Add(_joinDto.Group.GID);
|
||||
|
||||
_joinDto = null;
|
||||
_joinInfo = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPermissionRow(string label, bool suggested, bool current, Action<bool> apply)
|
||||
{
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted($"- {label}");
|
||||
|
||||
ImGui.SameLine(150 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted("Current:");
|
||||
ImGui.SameLine();
|
||||
_uiSharedService.BooleanToColoredIcon(!current);
|
||||
|
||||
ImGui.SameLine(300 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted("Suggested:");
|
||||
ImGui.SameLine();
|
||||
_uiSharedService.BooleanToColoredIcon(!suggested);
|
||||
|
||||
ImGui.SameLine(450 * ImGuiHelpers.GlobalScale);
|
||||
using var id = ImRaii.PushId(label);
|
||||
if (current != suggested)
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Apply"))
|
||||
apply(suggested);
|
||||
}
|
||||
|
||||
ImGui.NewLine();
|
||||
}
|
||||
|
||||
private async Task RefreshSyncshellsAsync(string? gid = null)
|
||||
{
|
||||
var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts();
|
||||
var snapshot = _pairUiService.GetSnapshot();
|
||||
_currentSyncshells = [.. snapshot.GroupPairs.Keys];
|
||||
|
||||
_recentlyJoined.RemoveWhere(gid =>
|
||||
_currentSyncshells.Exists(s => string.Equals(s.GID, gid, StringComparison.Ordinal)));
|
||||
|
||||
List<GroupJoinDto>? updatedList = [];
|
||||
|
||||
if (_useTestSyncshells)
|
||||
{
|
||||
updatedList = BuildTestSyncshells();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (syncshellBroadcasts.Count == 0)
|
||||
{
|
||||
ClearSyncshells();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts)
|
||||
.ConfigureAwait(false);
|
||||
updatedList = groups?.DistinctBy(g => g.Group.GID).ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to refresh broadcasted syncshells.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedList == null || updatedList.Count == 0)
|
||||
{
|
||||
ClearSyncshells();
|
||||
return;
|
||||
}
|
||||
|
||||
if (gid != null && _recentlyJoined.Contains(gid))
|
||||
{
|
||||
_recentlyJoined.Clear();
|
||||
}
|
||||
|
||||
var previousGid = GetSelectedGid();
|
||||
|
||||
_nearbySyncshells.Clear();
|
||||
_nearbySyncshells.AddRange(updatedList);
|
||||
|
||||
if (previousGid != null)
|
||||
{
|
||||
var newIndex = _nearbySyncshells.FindIndex(s =>
|
||||
string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal));
|
||||
|
||||
if (newIndex >= 0)
|
||||
{
|
||||
_selectedNearbyIndex = newIndex;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ClearSelection();
|
||||
}
|
||||
|
||||
private static List<GroupJoinDto> BuildTestSyncshells()
|
||||
{
|
||||
var testGroup1 = new GroupData("TEST-ALPHA", "Alpha Shell");
|
||||
var testGroup2 = new GroupData("TEST-BETA", "Beta Shell");
|
||||
var testGroup3 = new GroupData("TEST-GAMMA", "Gamma Shell");
|
||||
var testGroup4 = new GroupData("TEST-DELTA", "Delta Shell");
|
||||
var testGroup5 = new GroupData("TEST-CHARLIE", "Charlie Shell");
|
||||
var testGroup6 = new GroupData("TEST-OMEGA", "Omega Shell");
|
||||
var testGroup7 = new GroupData("TEST-POINT", "Point Shell");
|
||||
var testGroup8 = new GroupData("TEST-HOTEL", "Hotel Shell");
|
||||
|
||||
return
|
||||
[
|
||||
new(testGroup1, "", GroupUserPreferredPermissions.NoneSet),
|
||||
new(testGroup2, "", GroupUserPreferredPermissions.NoneSet),
|
||||
new(testGroup3, "", GroupUserPreferredPermissions.NoneSet),
|
||||
new(testGroup4, "", GroupUserPreferredPermissions.NoneSet),
|
||||
new(testGroup5, "", GroupUserPreferredPermissions.NoneSet),
|
||||
new(testGroup6, "", GroupUserPreferredPermissions.NoneSet),
|
||||
new(testGroup7, "", GroupUserPreferredPermissions.NoneSet),
|
||||
new(testGroup8, "", GroupUserPreferredPermissions.NoneSet),
|
||||
];
|
||||
}
|
||||
|
||||
private void ClearSyncshells()
|
||||
{
|
||||
if (_nearbySyncshells.Count == 0)
|
||||
return;
|
||||
|
||||
_nearbySyncshells.Clear();
|
||||
ClearSelection();
|
||||
}
|
||||
|
||||
private void ClearSelection()
|
||||
{
|
||||
_selectedNearbyIndex = -1;
|
||||
_syncshellPageIndex = 0;
|
||||
_joinDto = null;
|
||||
_joinInfo = null;
|
||||
}
|
||||
|
||||
private string? GetSelectedGid()
|
||||
{
|
||||
if (_selectedNearbyIndex < 0 || _selectedNearbyIndex >= _nearbySyncshells.Count)
|
||||
return null;
|
||||
|
||||
return _nearbySyncshells[_selectedNearbyIndex].Group.GID;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -162,24 +162,32 @@ public class TopTabMenu
|
||||
ImGui.SameLine();
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
{
|
||||
var x = ImGui.GetCursorScreenPos();
|
||||
if (ImGui.Button(FontAwesomeIcon.Compass.ToIconString(), buttonSize))
|
||||
{
|
||||
TabSelection = TabSelection == SelectedTab.Lightfinder ? SelectedTab.None : SelectedTab.Lightfinder;
|
||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
||||
}
|
||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
|
||||
{
|
||||
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
var xAfter = ImGui.GetCursorScreenPos();
|
||||
if (TabSelection == SelectedTab.Lightfinder)
|
||||
drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y },
|
||||
xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X },
|
||||
underlineColor, 2);
|
||||
}
|
||||
UiSharedService.AttachToolTip("Lightfinder");
|
||||
|
||||
var nearbyCount = GetNearbySyncshellCount();
|
||||
if (nearbyCount > 0)
|
||||
{
|
||||
var buttonMax = ImGui.GetItemRectMax();
|
||||
var badgeRadius = 8f * ImGuiHelpers.GlobalScale;
|
||||
var badgeCenter = new Vector2(buttonMax.X - badgeRadius * 1.3f, buttonMax.Y - buttonSize.Y + badgeRadius * 0.5f);
|
||||
var badgeText = nearbyCount > 99 ? "99+" : nearbyCount.ToString();
|
||||
var textSize = ImGui.CalcTextSize(badgeText);
|
||||
|
||||
drawList.AddCircleFilled(badgeCenter, badgeRadius + 1f, ImGui.GetColorU32(new Vector4(0, 0, 0, 0.6f)));
|
||||
drawList.AddCircleFilled(badgeCenter, badgeRadius, ImGui.GetColorU32(UIColors.Get("LightlessPurple")));
|
||||
|
||||
var textPos = new Vector2(badgeCenter.X - textSize.X * 0.45f, badgeCenter.Y - textSize.Y * 0.55f);
|
||||
drawList.AddText(textPos, ImGui.GetColorU32(new Vector4(1, 1, 1, 1)), badgeText);
|
||||
}
|
||||
UiSharedService.AttachToolTip(nearbyCount > 0 ? $"Lightfinder ({nearbyCount} nearby)" : "Open Lightfinder");
|
||||
|
||||
ImGui.SameLine();
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
@@ -234,10 +242,7 @@ public class TopTabMenu
|
||||
DrawSyncshellMenu(availableWidth, spacing.X);
|
||||
DrawGlobalSyncshellButtons(availableWidth, spacing.X);
|
||||
}
|
||||
else if (TabSelection == SelectedTab.Lightfinder)
|
||||
{
|
||||
DrawLightfinderMenu(availableWidth, spacing.X);
|
||||
}
|
||||
|
||||
else if (TabSelection == SelectedTab.UserConfig)
|
||||
{
|
||||
DrawUserConfig(availableWidth, spacing.X);
|
||||
@@ -776,53 +781,22 @@ public class TopTabMenu
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawLightfinderMenu(float availableWidth, float spacingX)
|
||||
{
|
||||
var buttonX = (availableWidth - (spacingX)) / 2f;
|
||||
|
||||
var lightFinderLabel = GetLightfinderFinderLabel();
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCirclePlus, lightFinderLabel, buttonX, center: true))
|
||||
{
|
||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
var syncshellFinderLabel = GetSyncshellFinderLabel();
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Globe, syncshellFinderLabel, buttonX, center: true))
|
||||
{
|
||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(SyncshellFinderUI)));
|
||||
}
|
||||
}
|
||||
|
||||
private string GetLightfinderFinderLabel()
|
||||
{
|
||||
string label = "Lightfinder";
|
||||
|
||||
if (_lightFinderService.IsBroadcasting)
|
||||
{
|
||||
var hashExclude = _dalamudUtilService.GetCID().ToString().GetHash256();
|
||||
var nearbyCount = _lightFinderScannerService.GetActiveBroadcasts(hashExclude).Count;
|
||||
return $"{label} ({nearbyCount})";
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
private string GetSyncshellFinderLabel()
|
||||
|
||||
private int GetNearbySyncshellCount()
|
||||
{
|
||||
if (!_lightFinderService.IsBroadcasting)
|
||||
return "Syncshell Finder";
|
||||
return 0;
|
||||
|
||||
var nearbyCount = _lightFinderScannerService
|
||||
.GetActiveSyncshellBroadcasts(excludeLocal: true)
|
||||
.Where(b => !string.IsNullOrEmpty(b.GID))
|
||||
var myHashedCid = _dalamudUtilService.GetCID().ToString().GetHash256();
|
||||
|
||||
return _lightFinderScannerService
|
||||
.GetActiveSyncshellBroadcasts()
|
||||
.Where(b =>
|
||||
!string.IsNullOrEmpty(b.GID) &&
|
||||
!string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal))
|
||||
.Select(b => b.GID!)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Count();
|
||||
|
||||
return nearbyCount > 0 ? $"Syncshell Finder ({nearbyCount})" : "Syncshell Finder";
|
||||
}
|
||||
|
||||
private void DrawUserConfig(float availableWidth, float spacingX)
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace LightlessSync.UI
|
||||
{
|
||||
internal static class UIColors
|
||||
{
|
||||
private static readonly Dictionary<string, string> DefaultHexColors = new(StringComparer.OrdinalIgnoreCase)
|
||||
private static readonly Dictionary<string, string> _defaultHexColors = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "LightlessPurple", "#ad8af5" },
|
||||
{ "LightlessPurpleActive", "#be9eff" },
|
||||
@@ -31,6 +31,12 @@ namespace LightlessSync.UI
|
||||
|
||||
{ "ProfileBodyGradientTop", "#2f283fff" },
|
||||
{ "ProfileBodyGradientBottom", "#372d4d00" },
|
||||
|
||||
{ "HeaderGradientTop", "#140D26FF" },
|
||||
{ "HeaderGradientBottom", "#1F1433FF" },
|
||||
|
||||
{ "HeaderStaticStar", "#FFFFFFFF" },
|
||||
{ "HeaderShootingStar", "#66CCFFFF" },
|
||||
};
|
||||
|
||||
private static LightlessConfigService? _configService;
|
||||
@@ -45,7 +51,7 @@ namespace LightlessSync.UI
|
||||
if (_configService?.Current.CustomUIColors.TryGetValue(name, out var customColorHex) == true)
|
||||
return HexToRgba(customColorHex);
|
||||
|
||||
if (!DefaultHexColors.TryGetValue(name, out var hex))
|
||||
if (!_defaultHexColors.TryGetValue(name, out var hex))
|
||||
throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name));
|
||||
|
||||
return HexToRgba(hex);
|
||||
@@ -53,7 +59,7 @@ namespace LightlessSync.UI
|
||||
|
||||
public static void Set(string name, Vector4 color)
|
||||
{
|
||||
if (!DefaultHexColors.ContainsKey(name))
|
||||
if (!_defaultHexColors.ContainsKey(name))
|
||||
throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name));
|
||||
|
||||
if (_configService != null)
|
||||
@@ -83,7 +89,7 @@ namespace LightlessSync.UI
|
||||
|
||||
public static Vector4 GetDefault(string name)
|
||||
{
|
||||
if (!DefaultHexColors.TryGetValue(name, out var hex))
|
||||
if (!_defaultHexColors.TryGetValue(name, out var hex))
|
||||
throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name));
|
||||
|
||||
return HexToRgba(hex);
|
||||
@@ -96,7 +102,7 @@ namespace LightlessSync.UI
|
||||
|
||||
public static IEnumerable<string> GetColorNames()
|
||||
{
|
||||
return DefaultHexColors.Keys;
|
||||
return _defaultHexColors.Keys;
|
||||
}
|
||||
|
||||
public static Vector4 HexToRgba(string hexColor)
|
||||
|
||||
@@ -18,7 +18,6 @@ using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.Localization;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
@@ -79,6 +78,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
private readonly Dictionary<string, DateTime> _oauthTokenExpiry = [];
|
||||
private bool _penumbraExists = false;
|
||||
private bool _petNamesExists = false;
|
||||
private bool _lifestreamExists = false;
|
||||
private int _serverSelectionIndex = -1;
|
||||
public UiSharedService(ILogger<UiSharedService> logger, IpcManager ipcManager, ApiController apiController,
|
||||
CacheMonitor cacheMonitor, FileDialogManager fileDialogManager,
|
||||
@@ -112,6 +112,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
_moodlesExists = _ipcManager.Moodles.APIAvailable;
|
||||
_petNamesExists = _ipcManager.PetNames.APIAvailable;
|
||||
_brioExists = _ipcManager.Brio.APIAvailable;
|
||||
_lifestreamExists = _ipcManager.Lifestream.APIAvailable;
|
||||
});
|
||||
|
||||
UidFont = _pluginInterface.UiBuilder.FontAtlas.NewDelegateFontHandle(e =>
|
||||
@@ -1105,6 +1106,10 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
ColorText("Brio", GetBoolColor(_brioExists));
|
||||
AttachToolTip(BuildPluginTooltip("Brio", _brioExists, _ipcManager.Brio.State));
|
||||
|
||||
ImGui.SameLine();
|
||||
ColorText("Lifestream", GetBoolColor(_lifestreamExists));
|
||||
AttachToolTip(BuildPluginTooltip("Lifestream", _lifestreamExists, _ipcManager.Lifestream.State));
|
||||
|
||||
if (!_penumbraExists || !_glamourerExists)
|
||||
{
|
||||
ImGui.TextColored(ImGuiColors.DalamudRed, "You need to install both Penumbra and Glamourer and keep them up to date to use Lightless Sync.");
|
||||
@@ -1462,12 +1467,6 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
return false;
|
||||
}
|
||||
|
||||
public void LoadLocalization(string languageCode)
|
||||
{
|
||||
_localization.SetupWithLangCode(languageCode);
|
||||
Strings.ToS = new Strings.ToSStrings();
|
||||
}
|
||||
|
||||
internal static void DistanceSeparator()
|
||||
{
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
|
||||
@@ -40,6 +40,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
|
||||
logger.LogInformation("UpdateNotesUi constructor called");
|
||||
_uiShared = uiShared;
|
||||
_configService = configService;
|
||||
_animatedHeader.EnableParticles = _configService.Current.EnableParticleEffects;
|
||||
|
||||
RespectCloseHotkey = true;
|
||||
ShowCloseButton = true;
|
||||
@@ -48,7 +49,8 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
|
||||
ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove;
|
||||
|
||||
PositionCondition = ImGuiCond.Always;
|
||||
|
||||
|
||||
|
||||
WindowBuilder.For(this)
|
||||
.AllowPinning(false)
|
||||
.AllowClickthrough(false)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user