2.0.0 (#92)
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m27s

2.0.0 Changes:

- Reworked shell finder UI with compact or list view with profile tags showing with the listing, allowing moderators to broadcast the syncshell as well to have it be used more.
- Reworked user list in syncshell admin screen to have filter visible and moved away from table to its own thing, allowing to copy uid/note/alias when clicking on the name.
- Reworked download bars and download box to make it look more modern, removed the jitter around, so it shouldn't vibrate around much.
- Chat has been added to the top menu, working in Zone or in Syncshells to be used there.
- Paired system has been revamped to make pausing and unpausing faster, and loading people should be faster as well.
- Moved to the internal object table to have faster load times for users; people should load in faster
- Compactor is running on a multi-threaded level instead of single-threaded; this should increase the speed of compacting files
- Nameplate Service has been reworked so it wouldn't use the nameplate handler anymore.
- Files can be resized when downloading to reduce load on users if they aren't compressed. (can be toggled to resize all).
- Penumbra Collections are now only made when people are visible, reducing the load on boot-up when having many syncshells in your list.
- Lightfinder plates have been moved away from using Nameplates, but will use an overlay.
- Main UI has been changed a bit with a gradient, and on hover will glow up now.
- Reworked Profile UI for Syncshell and Users to be more user-facing with more customizable items.
- Reworked Settings UI to look more modern.
- Performance should be better due to new systems that would dispose of the collections and better caching of items.

Co-authored-by: defnotken <itsdefnotken@gmail.com>
Co-authored-by: azyges <aaaaaa@aaa.aaa>
Co-authored-by: choco <choco@patat.nl>
Co-authored-by: cake <admin@cakeandbanana.nl>
Co-authored-by: Minmoose <KennethBohr@outlook.com>
Reviewed-on: #92
This commit was merged in pull request #92.
This commit is contained in:
2025-12-21 17:19:34 +00:00
parent 906f401940
commit 835a0a637d
191 changed files with 32636 additions and 8841 deletions

View File

@@ -13,13 +13,15 @@ internal sealed partial class CharaDataHubUi
AccessTypeDto.AllPairs => "All Pairs",
AccessTypeDto.ClosePairs => "Direct Pairs",
AccessTypeDto.Individuals => "Specified",
AccessTypeDto.Public => "Everyone"
AccessTypeDto.Public => "Everyone",
_ => throw new NotSupportedException()
};
private static string GetShareTypeString(ShareTypeDto dto) => dto switch
{
ShareTypeDto.Private => "Code Only",
ShareTypeDto.Shared => "Shared"
ShareTypeDto.Shared => "Shared",
_ => throw new NotSupportedException()
};
private static string GetWorldDataTooltipText(PoseEntryExtended poseEntry)
@@ -31,7 +33,7 @@ internal sealed partial class CharaDataHubUi
private void GposeMetaInfoAction(Action<CharaDataMetaInfoExtendedDto?> gposeActionDraw, string actionDescription, CharaDataMetaInfoExtendedDto? dto, bool hasValidGposeTarget, bool isSpawning)
{
StringBuilder sb = new StringBuilder();
StringBuilder sb = new();
sb.AppendLine(actionDescription);
bool isDisabled = false;

View File

@@ -406,7 +406,7 @@ internal sealed partial class CharaDataHubUi
{
_uiSharedService.BigText("Poses");
var poseCount = updateDto.PoseList.Count();
using (ImRaii.Disabled(poseCount >= maxPoses))
using (ImRaii.Disabled(poseCount >= _maxPoses))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add new Pose"))
{
@@ -414,8 +414,8 @@ internal sealed partial class CharaDataHubUi
}
}
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"), poseCount == maxPoses))
ImGui.TextUnformatted($"{poseCount}/{maxPoses} poses attached");
using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"), poseCount == _maxPoses))
ImGui.TextUnformatted($"{poseCount}/{_maxPoses} poses attached");
ImGuiHelpers.ScaledDummy(5);
using var indent = ImRaii.PushIndent(10f);
@@ -463,12 +463,16 @@ internal sealed partial class CharaDataHubUi
else
{
var desc = pose.Description;
if (ImGui.InputTextWithHint("##description", "Description", ref desc, 100))
if (desc != null)
{
pose.Description = desc;
updateDto.UpdatePoseList();
if (ImGui.InputTextWithHint("##description", "Description", ref desc, 100))
{
pose.Description = desc;
updateDto.UpdatePoseList();
}
ImGui.SameLine();
}
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete"))
{
updateDto.RemovePose(pose);
@@ -795,11 +799,12 @@ internal sealed partial class CharaDataHubUi
{
UiSharedService.DrawTree("Access for Specific Individuals / Syncshells", () =>
{
var snapshot = _pairUiService.GetSnapshot();
using (ImRaii.PushId("user"))
{
using (ImRaii.Group())
{
InputComboHybrid("##AliasToAdd", "##AliasToAddPicker", ref _specificIndividualAdd, _pairManager.PairsWithGroups.Keys,
InputComboHybrid("##AliasToAdd", "##AliasToAddPicker", ref _specificIndividualAdd, snapshot.PairsWithGroups.Keys,
static pair => (pair.UserData.UID, pair.UserData.Alias, pair.UserData.AliasOrUID, pair.GetNote()));
ImGui.SameLine();
using (ImRaii.Disabled(string.IsNullOrEmpty(_specificIndividualAdd)
@@ -868,8 +873,8 @@ internal sealed partial class CharaDataHubUi
{
using (ImRaii.Group())
{
InputComboHybrid("##GroupAliasToAdd", "##GroupAliasToAddPicker", ref _specificGroupAdd, _pairManager.Groups.Keys,
group => (group.GID, group.Alias, group.AliasOrGID, _serverConfigurationManager.GetNoteForGid(group.GID)));
InputComboHybrid("##GroupAliasToAdd", "##GroupAliasToAddPicker", ref _specificGroupAdd, snapshot.Groups,
group => (group.GID, group.GroupAliasOrGID, group.GroupAliasOrGID, _serverConfigurationManager.GetNoteForGid(group.GID)));
ImGui.SameLine();
using (ImRaii.Disabled(string.IsNullOrEmpty(_specificGroupAdd)
|| updateDto.GroupList.Any(f => string.Equals(f.GID, _specificGroupAdd, StringComparison.Ordinal) || string.Equals(f.Alias, _specificGroupAdd, StringComparison.Ordinal))))

View File

@@ -7,7 +7,6 @@ using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Dto.CharaData;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.CharaData;
using LightlessSync.Services.CharaData.Models;
@@ -15,24 +14,26 @@ using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.Utils;
using Microsoft.Extensions.Logging;
using LightlessSync.UI.Services;
using LightlessSync.PlayerData.Pairs;
namespace LightlessSync.UI;
internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
{
private const int maxPoses = 10;
private const int _maxPoses = 10;
private readonly CharaDataManager _charaDataManager;
private readonly CharaDataNearbyManager _charaDataNearbyManager;
private readonly CharaDataConfigService _configService;
private readonly DalamudUtilService _dalamudUtilService;
private readonly FileDialogManager _fileDialogManager;
private readonly PairManager _pairManager;
private readonly PairUiService _pairUiService;
private readonly CharaDataGposeTogetherManager _charaDataGposeTogetherManager;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly UiSharedService _uiSharedService;
private CancellationTokenSource _closalCts = new();
private bool _disableUI = false;
private CancellationTokenSource _disposalCts = new();
private readonly CancellationTokenSource _disposalCts = new();
private string _exportDescription = string.Empty;
private string _filterCodeNote = string.Empty;
private string _filterDescription = string.Empty;
@@ -77,7 +78,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
public CharaDataHubUi(ILogger<CharaDataHubUi> logger, LightlessMediator mediator, PerformanceCollectorService performanceCollectorService,
CharaDataManager charaDataManager, CharaDataNearbyManager charaDataNearbyManager, CharaDataConfigService configService,
UiSharedService uiSharedService, ServerConfigurationManager serverConfigurationManager,
DalamudUtilService dalamudUtilService, FileDialogManager fileDialogManager, PairManager pairManager,
DalamudUtilService dalamudUtilService, FileDialogManager fileDialogManager, PairUiService pairUiService,
CharaDataGposeTogetherManager charaDataGposeTogetherManager)
: base(logger, mediator, "Lightless Sync Character Data Hub###LightlessSyncCharaDataUI", performanceCollectorService)
{
@@ -90,7 +91,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
_serverConfigurationManager = serverConfigurationManager;
_dalamudUtilService = dalamudUtilService;
_fileDialogManager = fileDialogManager;
_pairManager = pairManager;
_pairUiService = pairUiService;
_charaDataGposeTogetherManager = charaDataGposeTogetherManager;
Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen |= _configService.Current.OpenLightlessHubOnGposeStart);
Mediator.Subscribe<OpenCharaDataHubWithFilterMessage>(this, (msg) =>
@@ -144,6 +145,8 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
{
_closalCts.CancelDispose();
_disposalCts.CancelDispose();
_disposalCts.Dispose();
_closalCts.Dispose();
}
base.Dispose(disposing);

View File

@@ -2,8 +2,6 @@ using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Utility;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.Group;
using LightlessSync.Interop.Ipc;
@@ -11,24 +9,26 @@ using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Handlers;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.LightFinder;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Components;
using LightlessSync.UI.Handlers;
using LightlessSync.UI.Models;
using LightlessSync.UI.Services;
using LightlessSync.UI.Style;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using LightlessSync.WebAPI.Files;
using LightlessSync.WebAPI.Files.Models;
using LightlessSync.WebAPI.SignalR.Utils;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Reflection;
using System.Runtime.InteropServices;
namespace LightlessSync.UI;
@@ -38,11 +38,12 @@ public class CompactUi : WindowMediatorSubscriberBase
private readonly ApiController _apiController;
private readonly LightlessConfigService _configService;
private readonly LightlessMediator _lightlessMediator;
private readonly PairLedger _pairLedger;
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
private readonly DrawEntityFactory _drawEntityFactory;
private readonly FileUploadManager _fileTransferManager;
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
private readonly PairManager _pairManager;
private readonly PairUiService _pairUiService;
private readonly SelectTagForPairUi _selectTagForPairUi;
private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi;
private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi;
@@ -54,7 +55,8 @@ public class CompactUi : WindowMediatorSubscriberBase
private readonly TopTabMenu _tabMenu;
private readonly TagHandler _tagHandler;
private readonly UiSharedService _uiSharedService;
private readonly BroadcastService _broadcastService;
private readonly LightFinderService _broadcastService;
private readonly DalamudUtilService _dalamudUtilService;
private List<IDrawFolder> _drawFolders;
private Pair? _lastAddedUser;
@@ -65,13 +67,18 @@ public class CompactUi : WindowMediatorSubscriberBase
private float _transferPartHeight;
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;
public CompactUi(
ILogger<CompactUi> logger,
UiSharedService uiShared,
LightlessConfigService configService,
ApiController apiController,
PairManager pairManager,
PairUiService pairUiService,
ServerConfigurationManager serverManager,
LightlessMediator mediator,
FileUploadManager fileTransferManager,
@@ -85,14 +92,14 @@ public class CompactUi : WindowMediatorSubscriberBase
RenameSyncshellTagUi renameSyncshellTagUi,
PerformanceCollectorService performanceCollectorService,
IpcManager ipcManager,
BroadcastService broadcastService,
LightFinderService broadcastService,
CharacterAnalyzer characterAnalyzer,
PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService, PairLedger pairLedger, LightFinderScannerService lightFinderScannerService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
{
_uiSharedService = uiShared;
_configService = configService;
_apiController = apiController;
_pairManager = pairManager;
_pairUiService = pairUiService;
_serverManager = serverManager;
_fileTransferManager = fileTransferManager;
_tagHandler = tagHandler;
@@ -105,43 +112,19 @@ public class CompactUi : WindowMediatorSubscriberBase
_renamePairTagUi = renameTagUi;
_ipcManager = ipcManager;
_broadcastService = broadcastService;
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService, lightlessNotificationService);
_pairLedger = pairLedger;
_dalamudUtilService = dalamudUtilService;
_tabMenu = new TopTabMenu(Mediator, _apiController, _uiSharedService, pairRequestService, dalamudUtilService, lightlessNotificationService, broadcastService, lightFinderScannerService);
Mediator.Subscribe<PairFocusCharacterMessage>(this, msg => RegisterFocusCharacter(msg.Pair));
AllowPinning = true;
AllowClickthrough = false;
TitleBarButtons = new()
{
new TitleBarButton()
{
Icon = FontAwesomeIcon.Cog,
Click = (msg) =>
{
Mediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
},
IconOffset = new(2,1),
ShowTooltip = () =>
{
ImGui.BeginTooltip();
ImGui.Text("Open Lightless Settings");
ImGui.EndTooltip();
}
},
new TitleBarButton()
{
Icon = FontAwesomeIcon.Book,
Click = (msg) =>
{
Mediator.Publish(new UiToggleMessage(typeof(EventViewerUI)));
},
IconOffset = new(2,1),
ShowTooltip = () =>
{
ImGui.BeginTooltip();
ImGui.Text("Open Lightless Event Viewer");
ImGui.EndTooltip();
}
},
};
WindowBuilder.For(this)
.AllowPinning(true)
.AllowClickthrough(false)
.SetSizeConstraints(new Vector2(375, 400), new Vector2(375, 2000))
.AddFlags(ImGuiWindowFlags.NoDocking)
.AddTitleBarButton(FontAwesomeIcon.Cog, "Open Lightless Settings", () => Mediator.Publish(new UiToggleMessage(typeof(SettingsUi))))
.AddTitleBarButton(FontAwesomeIcon.Book, "Open Lightless Event Viewer", () => Mediator.Publish(new UiToggleMessage(typeof(EventViewerUI))))
.Apply();
_drawFolders = [.. DrawFolders];
@@ -162,20 +145,24 @@ public class CompactUi : WindowMediatorSubscriberBase
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
Mediator.Subscribe<RefreshUiMessage>(this, (msg) => _drawFolders = DrawFolders.ToList());
Flags |= ImGuiWindowFlags.NoDocking;
SizeConstraints = new WindowSizeConstraints()
{
MinimumSize = new Vector2(375, 400),
MaximumSize = new Vector2(375, 2000),
};
_characterAnalyzer = characterAnalyzer;
_playerPerformanceConfig = playerPerformanceConfig;
_lightlessMediator = mediator;
}
public override void OnClose()
{
ForceReleaseFocus();
base.OnClose();
}
protected override void DrawInternal()
{
var drawList = ImGui.GetWindowDrawList();
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
using var selune = Selune.Begin(_seluneBrush, drawList, windowPos, windowSize);
_windowContentWidth = UiSharedService.GetWindowContentRegionWidth();
if (!_apiController.IsCurrentVersion)
{
@@ -223,29 +210,49 @@ public class CompactUi : WindowMediatorSubscriberBase
using (ImRaii.PushId("header")) DrawUIDHeader();
_uiSharedService.RoundedSeparator(UIColors.Get("LightlessPurple"), 2.5f, 1f, 12f);
using (ImRaii.PushId("serverstatus")) DrawServerStatus();
using (ImRaii.PushId("serverstatus"))
{
DrawServerStatus();
}
selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
var style = ImGui.GetStyle();
var contentMinY = windowPos.Y + ImGui.GetWindowContentRegionMin().Y;
var gradientInset = 4f * ImGuiHelpers.GlobalScale;
var gradientTop = MathF.Max(contentMinY, ImGui.GetCursorScreenPos().Y - style.ItemSpacing.Y + gradientInset);
ImGui.Separator();
if (_apiController.ServerState is ServerState.Connected)
{
using (ImRaii.PushId("global-topmenu")) _tabMenu.Draw();
var pairSnapshot = _pairUiService.GetSnapshot();
using (ImRaii.PushId("global-topmenu")) _tabMenu.Draw(pairSnapshot);
using (ImRaii.PushId("pairlist")) DrawPairs();
ImGui.Separator();
var transfersTop = ImGui.GetCursorScreenPos().Y;
var gradientBottom = MathF.Max(gradientTop, transfersTop - style.ItemSpacing.Y - gradientInset);
selune.DrawGradient(gradientTop, gradientBottom, ImGui.GetIO().DeltaTime);
float pairlistEnd = ImGui.GetCursorPosY();
using (ImRaii.PushId("transfers")) DrawTransfers();
_transferPartHeight = ImGui.GetCursorPosY() - pairlistEnd - ImGui.GetTextLineHeight();
using (ImRaii.PushId("group-pair-popup")) _selectPairsForGroupUi.Draw(_pairManager.DirectPairs);
using (ImRaii.PushId("group-syncshell-popup")) _selectSyncshellForTagUi.Draw([.. _pairManager.Groups.Values]);
using (ImRaii.PushId("group-pair-popup")) _selectPairsForGroupUi.Draw(pairSnapshot.DirectPairs);
using (ImRaii.PushId("group-syncshell-popup")) _selectSyncshellForTagUi.Draw(pairSnapshot.Groups);
using (ImRaii.PushId("group-pair-edit")) _renamePairTagUi.Draw();
using (ImRaii.PushId("group-syncshell-edit")) _renameSyncshellTagUi.Draw();
using (ImRaii.PushId("grouping-pair-popup")) _selectTagForPairUi.Draw();
using (ImRaii.PushId("grouping-syncshell-popup")) _selectTagForSyncshellUi.Draw();
}
if (_configService.Current.OpenPopupOnAdd && _pairManager.LastAddedUser != null)
else
{
_lastAddedUser = _pairManager.LastAddedUser;
_pairManager.LastAddedUser = null;
selune.Animate(ImGui.GetIO().DeltaTime);
}
ProcessFocusTracker();
var lastAddedPair = _pairUiService.GetLastAddedPair();
if (_configService.Current.OpenPopupOnAdd && lastAddedPair is not null)
{
_lastAddedUser = lastAddedPair;
_pairUiService.ClearLastAddedPair();
ImGui.OpenPopup("Set Notes for New User");
_showModalForUserAddition = true;
_lastAddedUserComment = string.Empty;
@@ -285,20 +292,22 @@ public class CompactUi : WindowMediatorSubscriberBase
private void DrawPairs()
{
var ySize = _transferPartHeight == 0
float ySize = Math.Abs(_transferPartHeight) < 0.0001f
? 1
: (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y
+ ImGui.GetTextLineHeight() - ImGui.GetStyle().WindowPadding.Y - ImGui.GetStyle().WindowBorderSize) - _transferPartHeight - ImGui.GetCursorPosY();
ImGui.BeginChild("list", new Vector2(_windowContentWidth, ySize), border: false);
foreach (var item in _drawFolders)
if (ImGui.BeginChild("list", new Vector2(_windowContentWidth, ySize), border: false))
{
item.Draw();
foreach (var item in _drawFolders)
{
item.Draw();
}
}
ImGui.EndChild();
}
private void DrawServerStatus()
{
var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Link);
@@ -371,6 +380,19 @@ public class CompactUi : WindowMediatorSubscriberBase
}
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(
ImGui.GetItemRectMin(),
ImGui.GetItemRectMax(),
SeluneHighlightMode.Both,
borderOnly: true,
borderThicknessOverride: _connectButtonHighlightThickness,
exactSize: true,
clipToElement: true,
roundingOverride: ImGui.GetStyle().FrameRounding);
}
UiSharedService.AttachToolTip(isConnectingOrConnected ? "Disconnect from " + _serverManager.CurrentServer.ServerName : "Connect to " + _serverManager.CurrentServer.ServerName);
}
}
@@ -470,6 +492,7 @@ public class CompactUi : WindowMediatorSubscriberBase
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;
@@ -477,7 +500,8 @@ public class CompactUi : WindowMediatorSubscriberBase
private void DrawUIDHeader()
{
var uidText = GetUidText();
var uidText = _apiController.ServerState.GetUidText(_apiController.DisplayName);
var uidColor = _apiController.ServerState.GetUidColor();
Vector4? vanityTextColor = null;
Vector4? vanityGlowColor = null;
@@ -527,6 +551,17 @@ public class CompactUi : WindowMediatorSubscriberBase
if (ImGui.IsItemHovered())
{
var padding = new Vector2(10f * ImGuiHelpers.GlobalScale);
Selune.RegisterHighlight(
ImGui.GetItemRectMin() - padding,
ImGui.GetItemRectMax() + padding,
SeluneHighlightMode.Point,
exactSize: true,
clipToElement: true,
clipPadding: padding,
highlightColorOverride: UIColors.Get("LightlessGreen"),
highlightAlphaOverride: 0.2f);
ImGui.BeginTooltip();
ImGui.PushTextWrapPos(ImGui.GetFontSize() * 32f);
@@ -539,7 +574,7 @@ public class CompactUi : WindowMediatorSubscriberBase
ImGui.PopStyleColor();
ImGuiHelpers.ScaledDummy(0.2f);
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
if (_configService.Current.BroadcastEnabled)
{
@@ -581,7 +616,7 @@ public class CompactUi : WindowMediatorSubscriberBase
}
if (ImGui.IsItemClicked())
_lightlessMediator.Publish(new UiToggleMessage(typeof(BroadcastUI)));
_lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
}
ImGui.SetCursorPosY(cursorY);
@@ -594,15 +629,30 @@ public class CompactUi : WindowMediatorSubscriberBase
{
var seString = SeStringUtils.BuildFormattedPlayerName(uidText, vanityTextColor, vanityGlowColor);
var cursorPos = ImGui.GetCursorScreenPos();
var fontPtr = ImGui.GetFont();
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr, "uid-header");
var targetFontSize = ImGui.GetFontSize();
var font = ImGui.GetFont();
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, targetFontSize ,font , "uid-header");
}
else
{
ImGui.TextColored(GetUidColor(), uidText);
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)
@@ -667,12 +717,27 @@ public class CompactUi : WindowMediatorSubscriberBase
{
var seString = SeStringUtils.BuildFormattedPlayerName(_apiController.UID, vanityTextColor, vanityGlowColor);
var cursorPos = ImGui.GetCursorScreenPos();
var fontPtr = ImGui.GetFont();
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr, "uid-footer");
var targetFontSize = ImGui.GetFontSize();
var font = ImGui.GetFont();
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, targetFontSize, font, "uid-footer");
}
else
{
ImGui.TextColored(GetUidColor(), _apiController.UID);
ImGui.TextColored(uidColor, _apiController.UID);
}
if (ImGui.IsItemHovered())
{
var padding = new Vector2(30f * ImGuiHelpers.GlobalScale);
Selune.RegisterHighlight(
ImGui.GetItemRectMin() - padding,
ImGui.GetItemRectMax() + padding,
SeluneHighlightMode.Point,
exactSize: true,
clipToElement: true,
clipPadding: padding,
highlightColorOverride: vanityGlowColor,
highlightAlphaOverride: 0.05f);
}
bool uidFooterClicked = ImGui.IsItemClicked();
@@ -685,7 +750,7 @@ public class CompactUi : WindowMediatorSubscriberBase
}
else
{
UiSharedService.ColorTextWrapped(GetServerError(), GetUidColor());
UiSharedService.ColorTextWrapped(_apiController.ServerState.GetServerError(_apiController.AuthFailureMessage), uidColor);
}
}
@@ -696,28 +761,45 @@ public class CompactUi : WindowMediatorSubscriberBase
var drawFolders = new List<IDrawFolder>();
var filter = _tabMenu.Filter;
var allPairs = _pairManager.PairsWithGroups.ToDictionary(k => k.Key, k => k.Value);
var filteredPairs = allPairs.Where(p => PassesFilter(p.Key, filter)).ToDictionary(k => k.Key, k => k.Value);
var allEntries = _drawEntityFactory.GetAllEntries().ToList();
var filteredEntries = string.IsNullOrEmpty(filter)
? allEntries
: allEntries.Where(e => PassesFilter(e, filter)).ToList();
var syncshells = _pairLedger.GetAllSyncshells();
var groupInfos = syncshells.Values
.Select(s => s.GroupFullInfo)
.OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase)
.ToList();
var entryLookup = allEntries.ToDictionary(e => e.DisplayEntry.Ident.UserId, StringComparer.Ordinal);
var filteredEntryLookup = filteredEntries.ToDictionary(e => e.DisplayEntry.Ident.UserId, StringComparer.Ordinal);
//Filter of online/visible pairs
if (_configService.Current.ShowVisibleUsersSeparately)
{
var allVisiblePairs = ImmutablePairList(allPairs.Where(p => FilterVisibleUsers(p.Key)));
var filteredVisiblePairs = BasicSortedDictionary(filteredPairs.Where(p => FilterVisibleUsers(p.Key)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomVisibleTag, filteredVisiblePairs, allVisiblePairs));
var allVisiblePairs = SortVisibleEntries(allEntries.Where(FilterVisibleUsers));
if (allVisiblePairs.Count > 0)
{
var filteredVisiblePairs = SortVisibleEntries(filteredEntries.Where(FilterVisibleUsers));
drawFolders.Add(_drawEntityFactory.CreateTagFolder(TagHandler.CustomVisibleTag, filteredVisiblePairs, allVisiblePairs));
}
}
//Filter of not foldered syncshells
var groupFolders = new List<GroupFolder>();
foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase))
foreach (var group in groupInfos)
{
GetGroups(allPairs, filteredPairs, group, out ImmutableList<Pair> allGroupPairs, out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs);
if (FilterNotTaggedSyncshells(group))
if (!FilterNotTaggedSyncshells(group))
{
groupFolders.Add(new GroupFolder(group, _drawEntityFactory.CreateDrawGroupFolder(group, filteredGroupPairs, allGroupPairs)));
continue;
}
var allGroupEntries = ResolveGroupEntries(entryLookup, syncshells, group, applyFilters: false);
var filteredGroupEntries = ResolveGroupEntries(filteredEntryLookup, syncshells, group, applyFilters: true);
// Always create the folder so empty syncshells remain visible in the UI.
var drawGroupFolder = _drawEntityFactory.CreateGroupFolder(group.Group.GID, group, filteredGroupEntries, allGroupEntries);
groupFolders.Add(new GroupFolder(group, drawGroupFolder));
}
//Filter of grouped up syncshells (All Syncshells Folder)
@@ -730,190 +812,224 @@ public class CompactUi : WindowMediatorSubscriberBase
//Filter of grouped/foldered pairs
foreach (var tag in _tagHandler.GetAllPairTagsSorted())
{
var allTagPairs = ImmutablePairList(allPairs.Where(p => FilterTagUsers(p.Key, tag)));
var filteredTagPairs = BasicSortedDictionary(filteredPairs.Where(p => FilterTagUsers(p.Key, tag) && FilterOnlineOrPausedSelf(p.Key)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(tag, filteredTagPairs, allTagPairs));
var allTagPairs = SortEntries(allEntries.Where(e => FilterTagUsers(e, tag)));
if (allTagPairs.Count > 0)
{
var filteredTagPairs = SortEntries(filteredEntries.Where(e => FilterTagUsers(e, tag) && FilterOnlineOrPausedSelf(e)));
drawFolders.Add(_drawEntityFactory.CreateTagFolder(tag, filteredTagPairs, allTagPairs));
}
}
//Filter of grouped/foldered syncshells
foreach (var syncshellTag in _tagHandler.GetAllSyncshellTagsSorted())
{
var syncshellFolderTags = new List<GroupFolder>();
foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase))
foreach (var group in groupInfos)
{
if (_tagHandler.HasSyncshellTag(group.GID, syncshellTag))
if (!_tagHandler.HasSyncshellTag(group.Group.GID, syncshellTag))
{
GetGroups(allPairs, filteredPairs, group,
out ImmutableList<Pair> allGroupPairs,
out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs);
syncshellFolderTags.Add(new GroupFolder(group, _drawEntityFactory.CreateDrawGroupFolder($"tag_{group.GID}", group, filteredGroupPairs, allGroupPairs)));
continue;
}
var allGroupEntries = ResolveGroupEntries(entryLookup, syncshells, group, applyFilters: false);
var filteredGroupEntries = ResolveGroupEntries(filteredEntryLookup, syncshells, group, applyFilters: true);
// Keep tagged syncshells rendered regardless of whether membership info has loaded.
var taggedGroupFolder = _drawEntityFactory.CreateGroupFolder($"tag_{group.Group.GID}", group, filteredGroupEntries, allGroupEntries);
syncshellFolderTags.Add(new GroupFolder(group, taggedGroupFolder));
}
drawFolders.Add(new DrawGroupedGroupFolder(syncshellFolderTags, _tagHandler, _apiController, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, syncshellTag));
}
//Filter of not grouped/foldered and offline pairs
var allOnlineNotTaggedPairs = ImmutablePairList(allPairs.Where(p => FilterNotTaggedUsers(p.Key)));
var onlineNotTaggedPairs = BasicSortedDictionary(filteredPairs.Where(p => FilterNotTaggedUsers(p.Key) && FilterOnlineOrPausedSelf(p.Key)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder((_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag), onlineNotTaggedPairs, allOnlineNotTaggedPairs));
var allOnlineNotTaggedPairs = SortEntries(allEntries.Where(FilterNotTaggedUsers));
if (allOnlineNotTaggedPairs.Count > 0 && _configService.Current.ShowOfflineUsersSeparately) {
var filteredOnlineEntries = SortOnlineEntries(filteredEntries.Where(e => FilterNotTaggedUsers(e) && FilterOnlineOrPausedSelf(e)));
drawFolders.Add(_drawEntityFactory.CreateTagFolder(
TagHandler.CustomOnlineTag,
filteredOnlineEntries,
allOnlineNotTaggedPairs));
} else if (allOnlineNotTaggedPairs.Count > 0 && !_configService.Current.ShowOfflineUsersSeparately) {
var onlineNotTaggedPairs = SortEntries(filteredEntries.Where(FilterNotTaggedUsers));
drawFolders.Add(_drawEntityFactory.CreateTagFolder(
TagHandler.CustomAllTag,
onlineNotTaggedPairs,
allOnlineNotTaggedPairs));
}
if (_configService.Current.ShowOfflineUsersSeparately)
{
var allOfflinePairs = ImmutablePairList(allPairs.Where(p => FilterOfflineUsers(p.Key, p.Value)));
var filteredOfflinePairs = BasicSortedDictionary(filteredPairs.Where(p => FilterOfflineUsers(p.Key, p.Value)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineTag, filteredOfflinePairs, allOfflinePairs));
var allOfflinePairs = SortEntries(allEntries.Where(FilterOfflineUsers));
if (allOfflinePairs.Count > 0)
{
var filteredOfflinePairs = SortEntries(filteredEntries.Where(FilterOfflineUsers));
drawFolders.Add(_drawEntityFactory.CreateTagFolder(TagHandler.CustomOfflineTag, filteredOfflinePairs, allOfflinePairs));
}
if (_configService.Current.ShowSyncshellOfflineUsersSeparately)
{
var allOfflineSyncshellUsers = ImmutablePairList(allPairs.Where(p => FilterOfflineSyncshellUsers(p.Key)));
var filteredOfflineSyncshellUsers = BasicSortedDictionary(filteredPairs.Where(p => FilterOfflineSyncshellUsers(p.Key)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineSyncshellTag, filteredOfflineSyncshellUsers, allOfflineSyncshellUsers));
var allOfflineSyncshellUsers = SortEntries(allEntries.Where(FilterOfflineSyncshellUsers));
if (allOfflineSyncshellUsers.Count > 0)
{
var filteredOfflineSyncshellUsers = SortEntries(filteredEntries.Where(FilterOfflineSyncshellUsers));
drawFolders.Add(_drawEntityFactory.CreateTagFolder(TagHandler.CustomOfflineSyncshellTag, filteredOfflineSyncshellUsers, allOfflineSyncshellUsers));
}
}
}
//Unpaired
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomUnpairedTag,
BasicSortedDictionary(filteredPairs.Where(p => p.Key.IsOneSidedPair)),
ImmutablePairList(allPairs.Where(p => p.Key.IsOneSidedPair))));
//Unpaired
var unpairedAllEntries = SortEntries(allEntries.Where(e => e.IsOneSided));
if (unpairedAllEntries.Count > 0)
{
var unpairedFiltered = SortEntries(filteredEntries.Where(e => e.IsOneSided));
drawFolders.Add(_drawEntityFactory.CreateTagFolder(TagHandler.CustomUnpairedTag, unpairedFiltered, unpairedAllEntries));
}
return drawFolders;
}
}
private static bool PassesFilter(Pair pair, string filter)
private static bool PassesFilter(PairUiEntry entry, string filter)
{
if (string.IsNullOrEmpty(filter)) return true;
return pair.UserData.AliasOrUID.Contains(filter, StringComparison.OrdinalIgnoreCase) || (pair.GetNote()?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false) || (pair.PlayerName?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false);
return entry.AliasOrUid.Contains(filter, StringComparison.OrdinalIgnoreCase)
|| (!string.IsNullOrEmpty(entry.Note) && entry.Note.Contains(filter, StringComparison.OrdinalIgnoreCase))
|| (!string.IsNullOrEmpty(entry.DisplayName) && entry.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase));
}
private string AlphabeticalSortKey(Pair pair)
private string AlphabeticalSortKey(PairUiEntry entry)
{
if (_configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(pair.PlayerName))
if (_configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(entry.DisplayName))
{
return _configService.Current.PreferNotesOverNamesForVisible ? (pair.GetNote() ?? string.Empty) : pair.PlayerName;
return _configService.Current.PreferNotesOverNamesForVisible ? (entry.Note ?? string.Empty) : entry.DisplayName;
}
return pair.GetNote() ?? pair.UserData.AliasOrUID;
return !string.IsNullOrEmpty(entry.Note) ? entry.Note : entry.AliasOrUid;
}
private bool FilterOnlineOrPausedSelf(Pair pair) => pair.IsOnline || (!pair.IsOnline && !_configService.Current.ShowOfflineUsersSeparately) || pair.UserPair.OwnPermissions.IsPaused();
private bool FilterOnlineOrPausedSelf(PairUiEntry entry) => entry.IsOnline || (!entry.IsOnline && !_configService.Current.ShowOfflineUsersSeparately) || entry.SelfPermissions.IsPaused();
private bool FilterVisibleUsers(Pair pair) => pair.IsVisible && (_configService.Current.ShowSyncshellUsersInVisible || pair.IsDirectlyPaired);
private bool FilterVisibleUsers(PairUiEntry entry) => entry.IsVisible && entry.IsOnline && (_configService.Current.ShowSyncshellUsersInVisible || entry.IsDirectlyPaired);
private bool FilterTagUsers(Pair pair, string tag) => pair.IsDirectlyPaired && !pair.IsOneSidedPair && _tagHandler.HasPairTag(pair.UserData.UID, tag);
private bool FilterTagUsers(PairUiEntry entry, string tag) => entry.IsDirectlyPaired && !entry.IsOneSided && _tagHandler.HasPairTag(entry.DisplayEntry.Ident.UserId, tag);
private static bool FilterGroupUsers(List<GroupFullInfoDto> groups, GroupFullInfoDto group) => groups.Exists(g => string.Equals(g.GID, group.GID, StringComparison.Ordinal));
private bool FilterNotTaggedUsers(Pair pair) => pair.IsDirectlyPaired && !pair.IsOneSidedPair && !_tagHandler.HasAnyPairTag(pair.UserData.UID);
private bool FilterNotTaggedUsers(PairUiEntry entry) => entry.IsDirectlyPaired && !entry.IsOneSided && !_tagHandler.HasAnyPairTag(entry.DisplayEntry.Ident.UserId);
private bool FilterNotTaggedSyncshells(GroupFullInfoDto group) => !_tagHandler.HasAnySyncshellTag(group.GID) || _configService.Current.ShowGroupedSyncshellsInAll;
private bool FilterOfflineUsers(Pair pair, List<GroupFullInfoDto> groups) => ((pair.IsDirectlyPaired && _configService.Current.ShowSyncshellOfflineUsersSeparately) || !_configService.Current.ShowSyncshellOfflineUsersSeparately) && (!pair.IsOneSidedPair || groups.Count != 0) && !pair.IsOnline && !pair.UserPair.OwnPermissions.IsPaused();
private static bool FilterOfflineSyncshellUsers(Pair pair) => !pair.IsDirectlyPaired && !pair.IsOnline && !pair.UserPair.OwnPermissions.IsPaused();
private Dictionary<Pair, List<GroupFullInfoDto>> BasicSortedDictionary(IEnumerable<KeyValuePair<Pair, List<GroupFullInfoDto>>> pairs) => pairs.OrderByDescending(u => u.Key.IsVisible).ThenByDescending(u => u.Key.IsOnline).ThenBy(u => AlphabeticalSortKey(u.Key), StringComparer.OrdinalIgnoreCase).ToDictionary(u => u.Key, u => u.Value);
private static ImmutableList<Pair> ImmutablePairList(IEnumerable<KeyValuePair<Pair, List<GroupFullInfoDto>>> pairs) => [.. pairs.Select(k => k.Key)];
private void GetGroups(Dictionary<Pair, List<GroupFullInfoDto>> allPairs,
Dictionary<Pair, List<GroupFullInfoDto>> filteredPairs,
GroupFullInfoDto group,
out ImmutableList<Pair> allGroupPairs,
out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs)
private bool FilterOfflineUsers(PairUiEntry entry)
{
allGroupPairs = ImmutablePairList(allPairs
.Where(u => FilterGroupUsers(u.Value, group)));
filteredGroupPairs = filteredPairs
.Where(u => FilterGroupUsers(u.Value, group) && FilterOnlineOrPausedSelf(u.Key))
.OrderByDescending(u => u.Key.IsOnline)
.ThenBy(u =>
{
if (string.Equals(u.Key.UserData.UID, group.OwnerUID, StringComparison.Ordinal)) return 0;
if (group.GroupPairUserInfos.TryGetValue(u.Key.UserData.UID, out var info))
{
if (info.IsModerator()) return 1;
if (info.IsPinned()) return 2;
}
return u.Key.IsVisible ? 3 : 4;
})
.ThenBy(u => AlphabeticalSortKey(u.Key), StringComparer.OrdinalIgnoreCase)
.ToDictionary(k => k.Key, k => k.Value);
var groups = entry.DisplayEntry.Groups;
var includeDirect = _configService.Current.ShowSyncshellOfflineUsersSeparately ? entry.IsDirectlyPaired : true;
var includeGroup = !entry.IsOneSided || groups.Count != 0;
return includeDirect && includeGroup && !entry.IsOnline && !entry.SelfPermissions.IsPaused();
}
private string GetServerError()
private static bool FilterOfflineSyncshellUsers(PairUiEntry entry) => !entry.IsDirectlyPaired && !entry.IsOnline && !entry.SelfPermissions.IsPaused();
private ImmutableList<PairUiEntry> SortEntries(IEnumerable<PairUiEntry> entries)
{
return _apiController.ServerState switch
return [.. entries
.OrderByDescending(e => e.IsVisible)
.ThenByDescending(e => e.IsOnline)
.ThenBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)];
}
private ImmutableList<PairUiEntry> SortVisibleEntries(IEnumerable<PairUiEntry> entries)
{
var entryList = entries.ToList();
return _configService.Current.VisiblePairSortMode switch
{
ServerState.Connecting => "Attempting to connect to the server.",
ServerState.Reconnecting => "Connection to server interrupted, attempting to reconnect to the server.",
ServerState.Disconnected => "You are currently disconnected from the Lightless Sync server.",
ServerState.Disconnecting => "Disconnecting from the server",
ServerState.Unauthorized => "Server Response: " + _apiController.AuthFailureMessage,
ServerState.Offline => "Your selected Lightless Sync server is currently offline.",
ServerState.VersionMisMatch =>
"Your plugin or the server you are connecting to is out of date. Please update your plugin now. If you already did so, contact the server provider to update their server to the latest version.",
ServerState.RateLimited => "You are rate limited for (re)connecting too often. Disconnect, wait 10 minutes and try again.",
ServerState.Connected => string.Empty,
ServerState.NoSecretKey => "You have no secret key set for this current character. Open Settings -> Service Settings and set a secret key for the current character. You can reuse the same secret key for multiple characters.",
ServerState.MultiChara => "Your Character Configuration has multiple characters configured with same name and world. You will not be able to connect until you fix this issue. Remove the duplicates from the configuration in Settings -> Service Settings -> Character Management and reconnect manually after.",
ServerState.OAuthMisconfigured => "OAuth2 is enabled but not fully configured, verify in the Settings -> Service Settings that you have OAuth2 connected and, importantly, a UID assigned to your current character.",
ServerState.OAuthLoginTokenStale => "Your OAuth2 login token is stale and cannot be used to renew. Go to the Settings -> Service Settings and unlink then relink your OAuth2 configuration.",
ServerState.NoAutoLogon => "This character has automatic login into Lightless disabled. Press the connect button to connect to Lightless.",
_ => string.Empty
VisiblePairSortMode.VramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateVramBytes),
VisiblePairSortMode.EffectiveVramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateEffectiveVramBytes),
VisiblePairSortMode.TriangleCount => SortVisibleByMetric(entryList, e => e.LastAppliedDataTris),
VisiblePairSortMode.Alphabetical => [.. entryList.OrderBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)],
VisiblePairSortMode.PreferredDirectPairs => SortVisibleByPreferred(entryList),
_ => SortEntries(entryList),
};
}
private Vector4 GetUidColor()
private ImmutableList<PairUiEntry> SortOnlineEntries(IEnumerable<PairUiEntry> entries)
{
return _apiController.ServerState switch
var entryList = entries.ToList();
return _configService.Current.OnlinePairSortMode switch
{
ServerState.Connecting => UIColors.Get("LightlessYellow"),
ServerState.Reconnecting => UIColors.Get("DimRed"),
ServerState.Connected => UIColors.Get("LightlessPurple"),
ServerState.Disconnected => UIColors.Get("LightlessYellow"),
ServerState.Disconnecting => UIColors.Get("LightlessYellow"),
ServerState.Unauthorized => UIColors.Get("DimRed"),
ServerState.VersionMisMatch => UIColors.Get("DimRed"),
ServerState.Offline => UIColors.Get("DimRed"),
ServerState.RateLimited => UIColors.Get("LightlessYellow"),
ServerState.NoSecretKey => UIColors.Get("LightlessYellow"),
ServerState.MultiChara => UIColors.Get("LightlessYellow"),
ServerState.OAuthMisconfigured => UIColors.Get("DimRed"),
ServerState.OAuthLoginTokenStale => UIColors.Get("DimRed"),
ServerState.NoAutoLogon => UIColors.Get("LightlessYellow"),
_ => UIColors.Get("DimRed")
OnlinePairSortMode.Alphabetical => [.. entryList.OrderBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)],
OnlinePairSortMode.PreferredDirectPairs => SortVisibleByPreferred(entryList),
_ => SortEntries(entryList),
};
}
private string GetUidText()
private ImmutableList<PairUiEntry> SortVisibleByMetric(IEnumerable<PairUiEntry> entries, Func<PairUiEntry, long> selector)
{
return _apiController.ServerState switch
return [.. entries
.OrderByDescending(entry => selector(entry) >= 0)
.ThenByDescending(selector)
.ThenByDescending(entry => entry.IsOnline)
.ThenBy(entry => AlphabeticalSortKey(entry), StringComparer.OrdinalIgnoreCase)];
}
private ImmutableList<PairUiEntry> SortVisibleByPreferred(IEnumerable<PairUiEntry> entries)
{
return [.. entries
.OrderByDescending(entry => entry.IsDirectlyPaired && entry.SelfPermissions.IsSticky())
.ThenByDescending(entry => entry.IsDirectlyPaired)
.ThenByDescending(entry => entry.IsOnline)
.ThenBy(entry => AlphabeticalSortKey(entry), StringComparer.OrdinalIgnoreCase)];
}
private ImmutableList<PairUiEntry> SortGroupEntries(IEnumerable<PairUiEntry> entries, GroupFullInfoDto group)
{
return [.. entries
.OrderByDescending(e => e.IsOnline)
.ThenBy(e => GroupSortWeight(e, group))
.ThenBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)];
}
private int GroupSortWeight(PairUiEntry entry, GroupFullInfoDto group)
{
if (string.Equals(entry.DisplayEntry.Ident.UserId, group.OwnerUID, StringComparison.Ordinal))
{
ServerState.Reconnecting => "Reconnecting",
ServerState.Connecting => "Connecting",
ServerState.Disconnected => "Disconnected",
ServerState.Disconnecting => "Disconnecting",
ServerState.Unauthorized => "Unauthorized",
ServerState.VersionMisMatch => "Version mismatch",
ServerState.Offline => "Unavailable",
ServerState.RateLimited => "Rate Limited",
ServerState.NoSecretKey => "No Secret Key",
ServerState.MultiChara => "Duplicate Characters",
ServerState.OAuthMisconfigured => "Misconfigured OAuth2",
ServerState.OAuthLoginTokenStale => "Stale OAuth2",
ServerState.NoAutoLogon => "Auto Login disabled",
ServerState.Connected => _apiController.DisplayName,
_ => string.Empty
};
return 0;
}
if (group.GroupPairUserInfos.TryGetValue(entry.DisplayEntry.Ident.UserId, out var info))
{
if (info.IsModerator()) return 1;
if (info.IsPinned()) return 2;
}
return entry.IsVisible ? 3 : 4;
}
private ImmutableList<PairUiEntry> ResolveGroupEntries(
IReadOnlyDictionary<string, PairUiEntry> entryLookup,
IReadOnlyDictionary<string, Syncshell> syncshells,
GroupFullInfoDto group,
bool applyFilters)
{
if (!syncshells.TryGetValue(group.Group.GID, out var shell))
{
return ImmutableList<PairUiEntry>.Empty;
}
var entries = shell.Users.Keys
.Select(id => entryLookup.TryGetValue(id, out var entry) ? entry : null)
.Where(entry => entry is not null)
.Cast<PairUiEntry>();
if (applyFilters && _configService.Current.ShowOfflineUsersSeparately)
{
entries = entries.Where(entry => !FilterOfflineUsers(entry));
}
if (applyFilters && _configService.Current.ShowSyncshellOfflineUsersSeparately)
{
entries = entries.Where(entry => !FilterOfflineSyncshellUsers(entry));
}
return SortGroupEntries(entries, group);
}
private void UiSharedService_GposeEnd()
@@ -926,4 +1042,50 @@ public class CompactUi : WindowMediatorSubscriberBase
_wasOpen = IsOpen;
IsOpen = false;
}
private void RegisterFocusCharacter(Pair pair)
{
_pendingFocusPair = pair;
_pendingFocusFrame = ImGui.GetFrameCount();
}
private void ProcessFocusTracker()
{
var frame = ImGui.GetFrameCount();
Pair? character = _pendingFocusFrame == frame ? _pendingFocusPair : null;
if (!ReferenceEquals(character, _focusedPair))
{
if (character is null)
{
_dalamudUtilService.ReleaseVisiblePairFocus();
}
else
{
_dalamudUtilService.FocusVisiblePair(character);
}
_focusedPair = character;
}
if (_pendingFocusFrame == frame)
{
_pendingFocusPair = null;
_pendingFocusFrame = -1;
}
}
private void ForceReleaseFocus()
{
if (_focusedPair is null)
{
_pendingFocusPair = null;
_pendingFocusFrame = -1;
return;
}
_dalamudUtilService.ReleaseVisiblePairFocus();
_focusedPair = null;
_pendingFocusPair = null;
_pendingFocusFrame = -1;
}
}

View File

@@ -1,9 +1,11 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.UI.Handlers;
using LightlessSync.UI.Models;
using System.Collections.Immutable;
using LightlessSync.UI.Style;
using OtterGui.Text;
namespace LightlessSync.UI.Components;
@@ -11,16 +13,18 @@ public abstract class DrawFolderBase : IDrawFolder
{
public IImmutableList<DrawUserPair> DrawPairs { get; init; }
protected readonly string _id;
protected readonly IImmutableList<Pair> _allPairs;
protected readonly IImmutableList<PairUiEntry> _allPairs;
protected readonly TagHandler _tagHandler;
protected readonly UiSharedService _uiSharedService;
private float _menuWidth = -1;
public int OnlinePairs => DrawPairs.Count(u => u.Pair.IsOnline);
public int OnlinePairs => DrawPairs.Count(u => u.DisplayEntry.Connection.IsOnline);
public int TotalPairs => _allPairs.Count;
private bool _wasHovered = false;
private bool _suppressNextRowToggle;
private bool _rowClickArmed;
protected DrawFolderBase(string id, IImmutableList<DrawUserPair> drawPairs,
IImmutableList<Pair> allPairs, TagHandler tagHandler, UiSharedService uiSharedService)
IImmutableList<PairUiEntry> allPairs, TagHandler tagHandler, UiSharedService uiSharedService)
{
_id = id;
DrawPairs = drawPairs;
@@ -31,11 +35,14 @@ public abstract class DrawFolderBase : IDrawFolder
protected abstract bool RenderIfEmpty { get; }
protected abstract bool RenderMenu { get; }
protected virtual bool EnableRowClick => true;
public void Draw()
{
if (!RenderIfEmpty && !DrawPairs.Any()) return;
_suppressNextRowToggle = false;
using var id = ImRaii.PushId("folder_" + _id);
var color = ImRaii.PushColor(ImGuiCol.ChildBg, ImGui.GetColorU32(ImGuiCol.FrameBgHovered), _wasHovered);
using (ImRaii.Child("folder__" + _id, new System.Numerics.Vector2(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetCursorPosX(), ImGui.GetFrameHeight())))
@@ -48,7 +55,8 @@ public abstract class DrawFolderBase : IDrawFolder
_uiSharedService.IconText(icon);
if (ImGui.IsItemClicked())
{
_tagHandler.SetTagOpen(_id, !_tagHandler.IsTagOpen(_id));
ToggleFolderOpen();
SuppressNextRowToggle();
}
ImGui.SameLine();
@@ -62,10 +70,41 @@ public abstract class DrawFolderBase : IDrawFolder
DrawName(rightSideStart - leftSideEnd);
}
_wasHovered = ImGui.IsItemHovered();
var rowHovered = ImGui.IsItemHovered();
_wasHovered = rowHovered;
if (EnableRowClick)
{
if (rowHovered && ImGui.IsMouseClicked(ImGuiMouseButton.Left) && !_suppressNextRowToggle)
{
_rowClickArmed = true;
}
if (_rowClickArmed && rowHovered && ImGui.IsMouseReleased(ImGuiMouseButton.Left))
{
ToggleFolderOpen();
_rowClickArmed = false;
}
if (!ImGui.IsMouseDown(ImGuiMouseButton.Left))
{
_rowClickArmed = false;
}
}
else
{
_rowClickArmed = false;
}
if (_wasHovered)
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), spanFullWidth: true);
}
color.Dispose();
_suppressNextRowToggle = false;
ImGui.Separator();
// if opened draw content
@@ -74,9 +113,13 @@ public abstract class DrawFolderBase : IDrawFolder
using var indent = ImRaii.PushIndent(_uiSharedService.GetIconSize(FontAwesomeIcon.EllipsisV).X + ImGui.GetStyle().ItemSpacing.X, false);
if (DrawPairs.Any())
{
foreach (var item in DrawPairs)
using var clipper = ImUtf8.ListClipper(DrawPairs.Count, ImGui.GetFrameHeightWithSpacing());
while (clipper.Step())
{
item.DrawPairedClient();
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
{
DrawPairs[i].DrawPairedClient();
}
}
}
else
@@ -110,6 +153,7 @@ public abstract class DrawFolderBase : IDrawFolder
ImGui.SameLine(windowEndX - barButtonSize.X);
if (_uiSharedService.IconButton(FontAwesomeIcon.EllipsisV))
{
SuppressNextRowToggle();
ImGui.OpenPopup("User Flyout Menu");
}
if (ImGui.BeginPopup("User Flyout Menu"))
@@ -123,7 +167,16 @@ public abstract class DrawFolderBase : IDrawFolder
_menuWidth = 0;
}
}
return DrawRightSide(rightSideStart);
}
protected void SuppressNextRowToggle()
{
_suppressNextRowToggle = true;
}
private void ToggleFolderOpen()
{
_tagHandler.SetTagOpen(_id, !_tagHandler.IsTagOpen(_id));
}
}

View File

@@ -5,9 +5,9 @@ using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.Group;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator;
using LightlessSync.UI.Handlers;
using LightlessSync.UI.Models;
using LightlessSync.WebAPI;
using System.Collections.Immutable;
@@ -22,7 +22,7 @@ public class DrawFolderGroup : DrawFolderBase
private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi;
public DrawFolderGroup(string id, GroupFullInfoDto groupFullInfoDto, ApiController apiController,
IImmutableList<DrawUserPair> drawPairs, IImmutableList<Pair> allPairs, TagHandler tagHandler, IdDisplayHandler idDisplayHandler,
IImmutableList<DrawUserPair> drawPairs, IImmutableList<PairUiEntry> allPairs, TagHandler tagHandler, IdDisplayHandler idDisplayHandler,
LightlessMediator lightlessMediator, UiSharedService uiSharedService, SelectTagForSyncshellUi selectTagForSyncshellUi) :
base(id, drawPairs, allPairs, tagHandler, uiSharedService)
{
@@ -35,6 +35,7 @@ public class DrawFolderGroup : DrawFolderBase
protected override bool RenderIfEmpty => true;
protected override bool RenderMenu => true;
protected override bool EnableRowClick => false;
private bool IsModerator => IsOwner || _groupFullInfoDto.GroupUserInfo.IsModerator();
private bool IsOwner => string.Equals(_groupFullInfoDto.OwnerUID, _apiController.UID, StringComparison.Ordinal);
private bool IsPinned => _groupFullInfoDto.GroupUserInfo.IsPinned();
@@ -87,6 +88,13 @@ public class DrawFolderGroup : DrawFolderBase
ImGui.Separator();
ImGui.TextUnformatted("General Syncshell Actions");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.AddressCard, "Open Syncshell Profile", menuWidth, true))
{
ImGui.CloseCurrentPopup();
_lightlessMediator.Publish(new GroupProfileOpenStandaloneMessage(_groupFullInfoDto.Group));
}
UiSharedService.AttachToolTip("Opens the profile for this syncshell in a new window.");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Copy, "Copy ID", menuWidth, true))
{
ImGui.CloseCurrentPopup();
@@ -111,6 +119,7 @@ public class DrawFolderGroup : DrawFolderBase
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleLeft, "Leave Syncshell", menuWidth, true) && UiSharedService.CtrlPressed())
{
_ = _apiController.GroupLeave(_groupFullInfoDto);
_lightlessMediator.Publish(new UserLeftSyncshell(_groupFullInfoDto.GID));
ImGui.CloseCurrentPopup();
}
UiSharedService.AttachToolTip("Hold CTRL and click to leave this Syncshell" + (!string.Equals(_groupFullInfoDto.OwnerUID, _apiController.UID, StringComparison.Ordinal)
@@ -160,6 +169,14 @@ public class DrawFolderGroup : DrawFolderBase
{
ImGui.Separator();
ImGui.TextUnformatted("Syncshell Admin Functions");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserEdit, "Open Profile Editor", menuWidth, true))
{
ImGui.CloseCurrentPopup();
_lightlessMediator.Publish(new OpenGroupProfileEditorMessage(_groupFullInfoDto));
}
UiSharedService.AttachToolTip("Open the syncshell profile editor.");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Cog, "Open Admin Panel", menuWidth, true))
{
ImGui.CloseCurrentPopup();
@@ -244,6 +261,7 @@ public class DrawFolderGroup : DrawFolderBase
ImGui.SameLine();
if (_uiSharedService.IconButton(pauseIcon))
{
SuppressNextRowToggle();
var perm = _groupFullInfoDto.GroupUserPermissions;
perm.SetPaused(!perm.IsPaused());
_ = _apiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(_groupFullInfoDto.Group, new(_apiController.UID), perm));

View File

@@ -1,11 +1,18 @@
using Dalamud.Bindings.ImGui;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.LightlessConfiguration;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Handlers;
using LightlessSync.UI.Models;
using LightlessSync.WebAPI;
using System.Collections.Immutable;
namespace LightlessSync.UI.Components;
@@ -14,14 +21,30 @@ public class DrawFolderTag : DrawFolderBase
private readonly ApiController _apiController;
private readonly SelectPairForTagUi _selectPairForTagUi;
private readonly RenamePairTagUi _renameTagUi;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly LightlessConfigService _configService;
private readonly LightlessMediator _mediator;
public DrawFolderTag(string id, IImmutableList<DrawUserPair> drawPairs, IImmutableList<Pair> allPairs,
TagHandler tagHandler, ApiController apiController, SelectPairForTagUi selectPairForTagUi, RenamePairTagUi renameTagUi, UiSharedService uiSharedService)
public DrawFolderTag(
string id,
IImmutableList<DrawUserPair> drawPairs,
IImmutableList<PairUiEntry> allPairs,
TagHandler tagHandler,
ApiController apiController,
SelectPairForTagUi selectPairForTagUi,
RenamePairTagUi renameTagUi,
UiSharedService uiSharedService,
ServerConfigurationManager serverConfigurationManager,
LightlessConfigService configService,
LightlessMediator mediator)
: base(id, drawPairs, allPairs, tagHandler, uiSharedService)
{
_apiController = apiController;
_selectPairForTagUi = selectPairForTagUi;
_renameTagUi = renameTagUi;
_serverConfigurationManager = serverConfigurationManager;
_configService = configService;
_mediator = mediator;
}
protected override bool RenderIfEmpty => _id switch
@@ -86,15 +109,18 @@ public class DrawFolderTag : DrawFolderBase
if (RenderCount)
{
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemSpacing.X / 2f }))
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemSpacing.X / 2f }))
{
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("[" + OnlinePairs.ToString() + "]");
ImGui.TextUnformatted($"[{OnlinePairs}]");
}
UiSharedService.AttachToolTip(OnlinePairs + " online" + Environment.NewLine + TotalPairs + " total");
UiSharedService.AttachToolTip($"{OnlinePairs} online{Environment.NewLine}{TotalPairs} total");
}
ImGui.SameLine();
return ImGui.GetCursorPosX();
}
@@ -102,19 +128,24 @@ public class DrawFolderTag : DrawFolderBase
protected override void DrawMenu(float menuWidth)
{
ImGui.TextUnformatted("Group Menu");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Users, "Select Pairs", menuWidth, isInPopup: true))
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Users, "Select Pairs", menuWidth, true))
{
_selectPairForTagUi.Open(_id);
}
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Edit, "Rename Pair Group", menuWidth, isInPopup: true))
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Edit, "Rename Pair Group", menuWidth, true))
{
_renameTagUi.Open(_id);
}
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Pair Group", menuWidth, isInPopup: true) && UiSharedService.CtrlPressed())
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Pair Group", menuWidth, true) &&
UiSharedService.CtrlPressed())
{
_tagHandler.RemovePairTag(_id);
}
UiSharedService.AttachToolTip("Hold CTRL to remove this Group permanently." + Environment.NewLine +
UiSharedService.AttachToolTip(
"Hold CTRL to remove this Group permanently." + Environment.NewLine +
"Note: this will not unpair with users in this Group.");
}
@@ -122,7 +153,7 @@ public class DrawFolderTag : DrawFolderBase
{
ImGui.AlignTextToFramePadding();
string name = _id switch
var name = _id switch
{
TagHandler.CustomUnpairedTag => "One-sided Individual Pairs",
TagHandler.CustomOnlineTag => "Online / Paused by you",
@@ -138,16 +169,30 @@ public class DrawFolderTag : DrawFolderBase
protected override float DrawRightSide(float currentRightSideX)
{
if (!RenderPause) return currentRightSideX;
var allArePaused = _allPairs.All(pair => pair.UserPair!.OwnPermissions.IsPaused());
var pauseButton = allArePaused ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
var pauseButtonX = _uiSharedService.GetIconButtonSize(pauseButton).X;
var buttonPauseOffset = currentRightSideX - pauseButtonX;
ImGui.SameLine(buttonPauseOffset);
if (_uiSharedService.IconButton(pauseButton))
if (string.Equals(_id, TagHandler.CustomVisibleTag, StringComparison.Ordinal))
{
return DrawVisibleFilter(currentRightSideX);
}
if (string.Equals(_id, TagHandler.CustomOnlineTag, StringComparison.Ordinal))
{
return DrawOnlineFilter(currentRightSideX);
}
if (!RenderPause)
{
return currentRightSideX;
}
var allArePaused = _allPairs.All(entry => entry.SelfPermissions.IsPaused());
var pauseIcon = allArePaused ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
var pauseButtonSize = _uiSharedService.GetIconButtonSize(pauseIcon);
var buttonPauseOffset = currentRightSideX - pauseButtonSize.X;
ImGui.SameLine(buttonPauseOffset);
if (_uiSharedService.IconButton(pauseIcon))
{
SuppressNextRowToggle();
if (allArePaused)
{
ResumeAllPairs(_allPairs);
@@ -157,39 +202,138 @@ public class DrawFolderTag : DrawFolderBase
PauseRemainingPairs(_allPairs);
}
}
if (allArePaused)
{
UiSharedService.AttachToolTip($"Resume pairing with all pairs in {_id}");
}
else
{
UiSharedService.AttachToolTip($"Pause pairing with all pairs in {_id}");
}
UiSharedService.AttachToolTip(allArePaused
? $"Resume pairing with all pairs in {_id}"
: $"Pause pairing with all pairs in {_id}");
return currentRightSideX;
}
private void PauseRemainingPairs(IEnumerable<Pair> availablePairs)
private void PauseRemainingPairs(IEnumerable<PairUiEntry> entries)
{
_ = _apiController.SetBulkPermissions(new(availablePairs
.ToDictionary(g => g.UserData.UID, g =>
{
var perm = g.UserPair.OwnPermissions;
perm.SetPaused(paused: true);
return perm;
}, StringComparer.Ordinal), new(StringComparer.Ordinal)))
_ = _apiController.SetBulkPermissions(new(
entries.ToDictionary(entry => entry.DisplayEntry.User.UID, entry =>
{
var permissions = entry.SelfPermissions;
permissions.SetPaused(true);
return permissions;
}, StringComparer.Ordinal),
new(StringComparer.Ordinal)))
.ConfigureAwait(false);
}
private void ResumeAllPairs(IEnumerable<Pair> availablePairs)
private void ResumeAllPairs(IEnumerable<PairUiEntry> entries)
{
_ = _apiController.SetBulkPermissions(new(availablePairs
.ToDictionary(g => g.UserData.UID, g =>
{
var perm = g.UserPair.OwnPermissions;
perm.SetPaused(paused: false);
return perm;
}, StringComparer.Ordinal), new(StringComparer.Ordinal)))
_ = _apiController.SetBulkPermissions(new(
entries.ToDictionary(entry => entry.DisplayEntry.User.UID, entry =>
{
var permissions = entry.SelfPermissions;
permissions.SetPaused(false);
return permissions;
}, StringComparer.Ordinal),
new(StringComparer.Ordinal)))
.ConfigureAwait(false);
}
private float DrawVisibleFilter(float currentRightSideX)
{
var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Filter);
var spacingX = ImGui.GetStyle().ItemSpacing.X;
var buttonStart = currentRightSideX - buttonSize.X;
ImGui.SameLine(buttonStart);
if (_uiSharedService.IconButton(FontAwesomeIcon.Filter))
{
SuppressNextRowToggle();
ImGui.OpenPopup($"visible-filter-{_id}");
}
UiSharedService.AttachToolTip("Adjust how visible pairs are ordered.");
if (ImGui.BeginPopup($"visible-filter-{_id}"))
{
ImGui.TextUnformatted("Visible Pair Ordering");
ImGui.Separator();
foreach (VisiblePairSortMode mode in Enum.GetValues<VisiblePairSortMode>())
{
var selected = _configService.Current.VisiblePairSortMode == mode;
if (ImGui.MenuItem(GetSortVisibleLabel(mode), string.Empty, selected))
{
if (!selected)
{
_configService.Current.VisiblePairSortMode = mode;
_configService.Save();
_mediator.Publish(new RefreshUiMessage());
}
ImGui.CloseCurrentPopup();
}
}
ImGui.EndPopup();
}
return buttonStart - spacingX;
}
private float DrawOnlineFilter(float currentRightSideX)
{
var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Filter);
var spacingX = ImGui.GetStyle().ItemSpacing.X;
var buttonStart = currentRightSideX - buttonSize.X;
ImGui.SameLine(buttonStart);
if (_uiSharedService.IconButton(FontAwesomeIcon.Filter))
{
SuppressNextRowToggle();
ImGui.OpenPopup($"online-filter-{_id}");
}
UiSharedService.AttachToolTip("Adjust how online pairs are ordered.");
if (ImGui.BeginPopup($"online-filter-{_id}"))
{
ImGui.TextUnformatted("Online Pair Ordering");
ImGui.Separator();
foreach (OnlinePairSortMode mode in Enum.GetValues<OnlinePairSortMode>())
{
var selected = _configService.Current.OnlinePairSortMode == mode;
if (ImGui.MenuItem(GetSortOnlineLabel(mode), string.Empty, selected))
{
if (!selected)
{
_configService.Current.OnlinePairSortMode = mode;
_configService.Save();
_mediator.Publish(new RefreshUiMessage());
}
ImGui.CloseCurrentPopup();
}
}
ImGui.EndPopup();
}
return buttonStart - spacingX;
}
private static string GetSortVisibleLabel(VisiblePairSortMode mode) => mode switch
{
VisiblePairSortMode.Alphabetical => "Alphabetical",
VisiblePairSortMode.VramUsage => "VRAM usage (descending)",
VisiblePairSortMode.EffectiveVramUsage => "Effective VRAM usage (descending)",
VisiblePairSortMode.TriangleCount => "Triangle count (descending)",
VisiblePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
_ => "Default",
};
private static string GetSortOnlineLabel(OnlinePairSortMode mode) => mode switch
{
OnlinePairSortMode.Alphabetical => "Alphabetical",
OnlinePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
_ => "Default",
};
}

View File

@@ -1,9 +1,11 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.UI;
using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.Group;
using LightlessSync.UI.Handlers;
using LightlessSync.UI.Style;
using LightlessSync.UI.Models;
using LightlessSync.WebAPI;
using System.Collections.Immutable;
@@ -22,6 +24,7 @@ public class DrawGroupedGroupFolder : IDrawFolder
private readonly RenameSyncshellTagUi _renameSyncshellTagUi;
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();
@@ -48,7 +51,9 @@ public class DrawGroupedGroupFolder : IDrawFolder
using var id = ImRaii.PushId(_id);
var color = ImRaii.PushColor(ImGuiCol.ChildBg, ImGui.GetColorU32(ImGuiCol.FrameBgHovered), _wasHovered);
using (ImRaii.Child("folder__" + _id, new Vector2(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetCursorPosX(), ImGui.GetFrameHeight())))
var allowRowClick = string.IsNullOrEmpty(_tag);
var suppressRowToggle = false;
using (ImRaii.Child("folder__" + _id, new System.Numerics.Vector2(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetCursorPosX(), ImGui.GetFrameHeight())))
{
ImGui.Dummy(new Vector2(0f, ImGui.GetFrameHeight()));
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(0f, 0f)))
@@ -61,6 +66,7 @@ public class DrawGroupedGroupFolder : IDrawFolder
if (ImGui.IsItemClicked())
{
_tagHandler.SetTagOpen(_id, !_tagHandler.IsTagOpen(_id));
suppressRowToggle = true;
}
ImGui.SameLine();
@@ -92,7 +98,7 @@ public class DrawGroupedGroupFolder : IDrawFolder
ImGui.SameLine();
DrawPauseButton();
ImGui.SameLine();
DrawMenu();
DrawMenu(ref suppressRowToggle);
} else
{
ImGui.TextUnformatted("All Syncshells");
@@ -102,7 +108,36 @@ public class DrawGroupedGroupFolder : IDrawFolder
}
}
color.Dispose();
_wasHovered = ImGui.IsItemHovered();
var rowHovered = ImGui.IsItemHovered();
_wasHovered = rowHovered;
if (allowRowClick)
{
if (rowHovered && ImGui.IsMouseClicked(ImGuiMouseButton.Left) && !suppressRowToggle)
{
_rowClickArmed = true;
}
if (_rowClickArmed && rowHovered && ImGui.IsMouseReleased(ImGuiMouseButton.Left))
{
_tagHandler.SetTagOpen(_id, !_tagHandler.IsTagOpen(_id));
_rowClickArmed = false;
}
if (!ImGui.IsMouseDown(ImGuiMouseButton.Left))
{
_rowClickArmed = false;
}
}
else
{
_rowClickArmed = false;
}
if (_wasHovered)
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), spanFullWidth: true);
}
ImGui.Separator();
@@ -154,7 +189,7 @@ public class DrawGroupedGroupFolder : IDrawFolder
}
}
protected void DrawMenu()
protected void DrawMenu(ref bool suppressRowToggle)
{
var barButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.EllipsisV);
var windowEndX = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth();
@@ -162,6 +197,7 @@ public class DrawGroupedGroupFolder : IDrawFolder
ImGui.SameLine(windowEndX - barButtonSize.X);
if (_uiSharedService.IconButton(FontAwesomeIcon.EllipsisV))
{
suppressRowToggle = true;
ImGui.OpenPopup("User Flyout Menu");
}
if (ImGui.BeginPopup("User Flyout Menu"))

View File

@@ -12,9 +12,10 @@ using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Handlers;
using LightlessSync.UI.Models;
using LightlessSync.UI.Style;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Text;
@@ -27,29 +28,43 @@ public class DrawUserPair
protected readonly LightlessMediator _mediator;
protected readonly List<GroupFullInfoDto> _syncedGroups;
private readonly GroupFullInfoDto? _currentGroup;
protected Pair _pair;
protected Pair? _pair;
private PairUiEntry _uiEntry;
protected PairDisplayEntry _displayEntry;
private readonly string _id;
private readonly SelectTagForPairUi _selectTagForPairUi;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly UiSharedService _uiSharedService;
private readonly PlayerPerformanceConfigService _performanceConfigService;
private readonly LightlessConfigService _configService;
private readonly CharaDataManager _charaDataManager;
private readonly PairLedger _pairLedger;
private float _menuWidth = -1;
private bool _wasHovered = false;
private TooltipSnapshot _tooltipSnapshot = TooltipSnapshot.Empty;
private string _cachedTooltip = string.Empty;
public DrawUserPair(string id, Pair entry, List<GroupFullInfoDto> syncedGroups,
public DrawUserPair(
string id,
PairUiEntry uiEntry,
Pair? legacyPair,
GroupFullInfoDto? currentGroup,
ApiController apiController, IdDisplayHandler uIDDisplayHandler,
LightlessMediator lightlessMediator, SelectTagForPairUi selectTagForPairUi,
ApiController apiController,
IdDisplayHandler uIDDisplayHandler,
LightlessMediator lightlessMediator,
SelectTagForPairUi selectTagForPairUi,
ServerConfigurationManager serverConfigurationManager,
UiSharedService uiSharedService, PlayerPerformanceConfigService performanceConfigService,
CharaDataManager charaDataManager)
UiSharedService uiSharedService,
PlayerPerformanceConfigService performanceConfigService,
LightlessConfigService configService,
CharaDataManager charaDataManager,
PairLedger pairLedger)
{
_id = id;
_pair = entry;
_syncedGroups = syncedGroups;
_uiEntry = uiEntry;
_displayEntry = uiEntry.DisplayEntry;
_pair = legacyPair ?? throw new ArgumentNullException(nameof(legacyPair));
_syncedGroups = uiEntry.DisplayEntry.Groups.ToList();
_currentGroup = currentGroup;
_apiController = apiController;
_displayHandler = uIDDisplayHandler;
@@ -58,7 +73,20 @@ public class DrawUserPair
_serverConfigurationManager = serverConfigurationManager;
_uiSharedService = uiSharedService;
_performanceConfigService = performanceConfigService;
_configService = configService;
_charaDataManager = charaDataManager;
_pairLedger = pairLedger;
}
public PairDisplayEntry DisplayEntry => _displayEntry;
public PairUiEntry UiEntry => _uiEntry;
public void UpdateDisplayEntry(PairUiEntry entry)
{
_uiEntry = entry;
_displayEntry = entry.DisplayEntry;
_syncedGroups.Clear();
_syncedGroups.AddRange(entry.DisplayEntry.Groups);
}
public Pair Pair => _pair;
@@ -77,6 +105,10 @@ public class DrawUserPair
DrawName(posX, rightSide);
}
_wasHovered = ImGui.IsItemHovered();
if (_wasHovered)
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), spanFullWidth: true);
}
color.Dispose();
}
@@ -103,7 +135,7 @@ public class DrawUserPair
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Cycle pause state", _menuWidth, true))
{
_ = _apiController.CyclePauseAsync(_pair.UserData);
_ = _apiController.CyclePauseAsync(_pair);
ImGui.CloseCurrentPopup();
}
ImGui.Separator();
@@ -197,6 +229,11 @@ public class DrawUserPair
private void DrawLeftSide()
{
ImGui.AlignTextToFramePadding();
if (_pair == null)
{
return;
}
if (_pair.IsPaused)
{
@@ -213,7 +250,19 @@ public class DrawUserPair
}
else if (_pair.IsVisible)
{
_uiSharedService.IconText(FontAwesomeIcon.Eye, UIColors.Get("LightlessBlue"));
if (_configService.Current.ShowVisiblePairsGreenEye)
{
_uiSharedService.IconText(FontAwesomeIcon.Eye, UIColors.Get("LightlessGreen"));
}
else
{
_uiSharedService.IconText(FontAwesomeIcon.Eye, UIColors.Get("LightlessBlue"));
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem | ImGuiHoveredFlags.AllowWhenOverlapped | ImGuiHoveredFlags.AllowWhenDisabled))
{
_mediator.Publish(new PairFocusCharacterMessage(_pair));
}
if (ImGui.IsItemClicked())
{
_mediator.Publish(new TargetPairMessage(_pair));
@@ -313,6 +362,7 @@ public class DrawUserPair
_pair.PlayerName ?? string.Empty,
_pair.LastAppliedDataBytes,
_pair.LastAppliedApproximateVRAMBytes,
_pair.LastAppliedApproximateEffectiveVRAMBytes,
_pair.LastAppliedDataTris,
_pair.IsPaired,
groupDisplays is null ? ImmutableArray<string>.Empty : ImmutableArray.CreateRange(groupDisplays));
@@ -381,7 +431,14 @@ public class DrawUserPair
{
builder.Append(Environment.NewLine);
builder.Append("Approx. VRAM Usage: ");
builder.Append(UiSharedService.ByteToString(snapshot.LastAppliedApproximateVRAMBytes, true));
var originalText = UiSharedService.ByteToString(snapshot.LastAppliedApproximateVRAMBytes, true);
builder.Append(originalText);
if (snapshot.LastAppliedApproximateEffectiveVRAMBytes >= 0)
{
builder.Append(" (Effective: ");
builder.Append(UiSharedService.ByteToString(snapshot.LastAppliedApproximateEffectiveVRAMBytes, true));
builder.Append(')');
}
}
if (snapshot.LastAppliedDataTris >= 0)
@@ -420,12 +477,13 @@ public class DrawUserPair
string PlayerName,
long LastAppliedDataBytes,
long LastAppliedApproximateVRAMBytes,
long LastAppliedApproximateEffectiveVRAMBytes,
long LastAppliedDataTris,
bool IsPaired,
ImmutableArray<string> GroupDisplays)
{
public static TooltipSnapshot Empty { get; } =
new(false, false, false, IndividualPairStatus.None, string.Empty, string.Empty, -1, -1, -1, false, ImmutableArray<string>.Empty);
new(false, false, false, IndividualPairStatus.None, string.Empty, string.Empty, -1, -1, -1, -1, false, ImmutableArray<string>.Empty);
}
private void DrawPairedClientMenu()
@@ -647,7 +705,13 @@ public class DrawUserPair
private void DrawSyncshellMenu(GroupFullInfoDto group, bool selfIsOwner, bool selfIsModerator, bool userIsPinned, bool userIsModerator)
{
if (selfIsOwner || ((selfIsModerator) && (!userIsModerator)))
var showModeratorActions = selfIsOwner || (selfIsModerator && !userIsModerator);
var showOwnerActions = selfIsOwner;
if (showModeratorActions || showOwnerActions)
ImGui.Separator();
if (showModeratorActions)
{
ImGui.TextUnformatted("Syncshell Moderator Functions");
var pinText = userIsPinned ? "Unpin user" : "Pin user";
@@ -683,7 +747,7 @@ public class DrawUserPair
ImGui.Separator();
}
if (selfIsOwner)
if (showOwnerActions)
{
ImGui.TextUnformatted("Syncshell Owner Functions");
string modText = userIsModerator ? "Demod user" : "Mod user";

View File

@@ -1,6 +1,7 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using LightlessSync.API.Dto.Group;
using LightlessSync.PlayerData.Factories;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator;
using LightlessSync.WebAPI;
@@ -12,14 +13,16 @@ public class BanUserPopupHandler : IPopupHandler
{
private readonly ApiController _apiController;
private readonly UiSharedService _uiSharedService;
private readonly PairFactory _pairFactory;
private string _banReason = string.Empty;
private GroupFullInfoDto _group = null!;
private Pair _reportedPair = null!;
public BanUserPopupHandler(ApiController apiController, UiSharedService uiSharedService)
public BanUserPopupHandler(ApiController apiController, UiSharedService uiSharedService, PairFactory pairFactory)
{
_apiController = apiController;
_uiSharedService = uiSharedService;
_pairFactory = pairFactory;
}
public Vector2 PopupSize => new(500, 250);
@@ -43,7 +46,7 @@ public class BanUserPopupHandler : IPopupHandler
public void Open(OpenBanUserPopupMessage message)
{
_reportedPair = message.PairToBan;
_reportedPair = _pairFactory.Create(message.PairToBan.UniqueIdent) ?? message.PairToBan;
_group = message.GroupFullInfoDto;
_banReason = string.Empty;
}

View File

@@ -23,7 +23,7 @@ public class SelectPairForTagUi
_uidDisplayHandler = uidDisplayHandler;
}
public void Draw(List<Pair> pairs)
public void Draw(IReadOnlyList<Pair> pairs)
{
var workHeight = ImGui.GetMainViewport().WorkSize.Y / ImGuiHelpers.GlobalScale;
var minSize = new Vector2(300, workHeight < 400 ? workHeight : 400) * ImGuiHelpers.GlobalScale;

View File

@@ -21,7 +21,7 @@ public class SelectSyncshellForTagUi
_tagHandler = tagHandler;
}
public void Draw(List<GroupFullInfoDto> groups)
public void Draw(IReadOnlyCollection<GroupFullInfoDto> groups)
{
var workHeight = ImGui.GetMainViewport().WorkSize.Y / ImGuiHelpers.GlobalScale;
var minSize = new Vector2(300, workHeight < 400 ? workHeight : 400) * ImGuiHelpers.GlobalScale;

View File

@@ -5,6 +5,7 @@ using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.Group;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging;
using System.Numerics;
@@ -24,13 +25,10 @@ public class CreateSyncshellUI : WindowMediatorSubscriberBase
{
_apiController = apiController;
_uiSharedService = uiSharedService;
SizeConstraints = new()
{
MinimumSize = new(550, 330),
MaximumSize = new(550, 330)
};
Flags = ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse;
WindowBuilder.For(this)
.SetFixedSize(new Vector2(550, 330))
.AddFlags(ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse)
.Apply();
Mediator.Subscribe<DisconnectedMessage>(this, (_) => IsOpen = false);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,11 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Handlers;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.PairProcessing;
using LightlessSync.WebAPI.Files;
using LightlessSync.WebAPI.Files.Models;
using Microsoft.Extensions.Logging;
@@ -22,6 +23,10 @@ public class DownloadUi : WindowMediatorSubscriberBase
private readonly UiSharedService _uiShared;
private readonly PairProcessingLimiter _pairProcessingLimiter;
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
private readonly Dictionary<GameObjectHandler, Vector2> _smoothed = [];
private readonly Dictionary<GameObjectHandler, DownloadSpeedTracker> _downloadSpeeds = [];
private byte _transferBoxTransparency = 100;
private bool _notificationDismissed = true;
private int _lastDownloadStateHash = 0;
@@ -95,155 +100,32 @@ public class DownloadUi : WindowMediatorSubscriberBase
if (_configService.Current.ShowTransferWindow)
{
var limiterSnapshot = _pairProcessingLimiter.GetSnapshot();
try
// Check if download notifications are enabled (not set to TextOverlay)
var useNotifications =
_configService.Current.UseLightlessNotifications &&
_configService.Current.LightlessDownloadNotification == NotificationLocation.LightlessUi;
if (useNotifications)
{
if (_fileTransferManager.IsUploading)
if (!_currentDownloads.IsEmpty)
{
var currentUploads = _fileTransferManager.GetCurrentUploadsSnapshot();
var totalUploads = currentUploads.Count;
var doneUploads = currentUploads.Count(c => c.IsTransferred);
var totalUploaded = currentUploads.Sum(c => c.Transferred);
var totalToUpload = currentUploads.Sum(c => c.Total);
UiSharedService.DrawOutlinedFont($"▲", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
ImGui.SameLine();
var xDistance = ImGui.GetCursorPosX();
UiSharedService.DrawOutlinedFont($"Compressing+Uploading {doneUploads}/{totalUploads}",
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
ImGui.NewLine();
ImGui.SameLine(xDistance);
UiSharedService.DrawOutlinedFont(
$"{UiSharedService.ByteToString(totalUploaded, addSuffix: false)}/{UiSharedService.ByteToString(totalToUpload)}",
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
if (_currentDownloads.Any()) ImGui.Separator();
}
}
catch
{
_logger.LogDebug("Error drawing upload progress");
}
try
{
// Check if download notifications are enabled (not set to TextOverlay)
var useNotifications = _configService.Current.UseLightlessNotifications
? _configService.Current.LightlessDownloadNotification != NotificationLocation.TextOverlay
: _configService.Current.UseNotificationsForDownloads;
if (useNotifications)
{
// Use notification system
if (_currentDownloads.Any())
{
UpdateDownloadNotificationIfChanged(limiterSnapshot);
_notificationDismissed = false;
}
else if (!_notificationDismissed)
{
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
_notificationDismissed = true;
_lastDownloadStateHash = 0;
}
}
else
{
// Use text overlay
if (limiterSnapshot.IsEnabled)
{
var queueColor = limiterSnapshot.Waiting > 0 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudGrey;
var queueText = $"Pair queue {limiterSnapshot.InFlight}/{limiterSnapshot.Limit}";
queueText += limiterSnapshot.Waiting > 0 ? $" ({limiterSnapshot.Waiting} waiting, {limiterSnapshot.Remaining} free)" : $" ({limiterSnapshot.Remaining} free)";
UiSharedService.DrawOutlinedFont(queueText, queueColor, new Vector4(0, 0, 0, 255), 1);
ImGui.NewLine();
}
else
{
UiSharedService.DrawOutlinedFont("Pair apply limiter disabled", ImGuiColors.DalamudGrey, new Vector4(0, 0, 0, 255), 1);
ImGui.NewLine();
}
foreach (var item in _currentDownloads.ToList())
{
var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot);
var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue);
var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading);
var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing);
var totalFiles = item.Value.Sum(c => c.Value.TotalFiles);
var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles);
var totalBytes = item.Value.Sum(c => c.Value.TotalBytes);
var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes);
UiSharedService.DrawOutlinedFont($"▼", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
ImGui.SameLine();
var xDistance = ImGui.GetCursorPosX();
UiSharedService.DrawOutlinedFont(
$"{item.Key.Name} [W:{dlSlot}/Q:{dlQueue}/P:{dlProg}/D:{dlDecomp}]",
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
ImGui.NewLine();
ImGui.SameLine(xDistance);
UiSharedService.DrawOutlinedFont(
$"{transferredFiles}/{totalFiles} ({UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)})",
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
}
}
}
catch
{
_logger.LogDebug("Error drawing download progress");
}
}
if (_configService.Current.ShowTransferBars)
{
const int transparency = 100;
const int dlBarBorder = 3;
foreach (var transfer in _currentDownloads.ToList())
{
var screenPos = _dalamudUtilService.WorldToScreen(transfer.Key.GetGameObject());
if (screenPos == Vector2.Zero) continue;
var totalBytes = transfer.Value.Sum(c => c.Value.TotalBytes);
var transferredBytes = transfer.Value.Sum(c => c.Value.TransferredBytes);
var maxDlText = $"{UiSharedService.ByteToString(totalBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
var textSize = _configService.Current.TransferBarsShowText ? ImGui.CalcTextSize(maxDlText) : new Vector2(10, 10);
int dlBarHeight = _configService.Current.TransferBarsHeight > ((int)textSize.Y + 5) ? _configService.Current.TransferBarsHeight : (int)textSize.Y + 5;
int dlBarWidth = _configService.Current.TransferBarsWidth > ((int)textSize.X + 10) ? _configService.Current.TransferBarsWidth : (int)textSize.X + 10;
var dlBarStart = new Vector2(screenPos.X - dlBarWidth / 2f, screenPos.Y - dlBarHeight / 2f);
var dlBarEnd = new Vector2(screenPos.X + dlBarWidth / 2f, screenPos.Y + dlBarHeight / 2f);
var drawList = ImGui.GetBackgroundDrawList();
drawList.AddRectFilled(
dlBarStart with { X = dlBarStart.X - dlBarBorder - 1, Y = dlBarStart.Y - dlBarBorder - 1 },
dlBarEnd with { X = dlBarEnd.X + dlBarBorder + 1, Y = dlBarEnd.Y + dlBarBorder + 1 },
UiSharedService.Color(0, 0, 0, transparency), 1);
drawList.AddRectFilled(dlBarStart with { X = dlBarStart.X - dlBarBorder, Y = dlBarStart.Y - dlBarBorder },
dlBarEnd with { X = dlBarEnd.X + dlBarBorder, Y = dlBarEnd.Y + dlBarBorder },
UiSharedService.Color(220, 220, 220, transparency), 1);
drawList.AddRectFilled(dlBarStart, dlBarEnd,
UiSharedService.Color(0, 0, 0, transparency), 1);
var dlProgressPercent = transferredBytes / (double)totalBytes;
drawList.AddRectFilled(dlBarStart,
dlBarEnd with { X = dlBarStart.X + (float)(dlProgressPercent * dlBarWidth) },
UiSharedService.Color(UIColors.Get("LightlessPurple")));
if (_configService.Current.TransferBarsShowText)
{
var downloadText = $"{UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
UiSharedService.DrawOutlinedFont(drawList, downloadText,
screenPos with { X = screenPos.X - textSize.X / 2f - 1, Y = screenPos.Y - textSize.Y / 2f - 1 },
UiSharedService.Color(255, 255, 255, transparency),
UiSharedService.Color(0, 0, 0, transparency), 1);
UpdateDownloadNotificationIfChanged(limiterSnapshot);
_notificationDismissed = false;
}
else if (!_notificationDismissed)
{
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
_notificationDismissed = true;
_lastDownloadStateHash = 0;
}
}
DrawDownloadSummaryBox();
if (_configService.Current.ShowUploading)
{
const int transparency = 100;
foreach (var player in _uploadingPlayers.Select(p => p.Key).ToList())
{
var screenPos = _dalamudUtilService.WorldToScreen(player.GetGameObject());
@@ -253,29 +135,621 @@ public class DownloadUi : WindowMediatorSubscriberBase
{
using var _ = _uiShared.UidFont.Push();
var uploadText = "Uploading";
var textSize = ImGui.CalcTextSize(uploadText);
var drawList = ImGui.GetBackgroundDrawList();
UiSharedService.DrawOutlinedFont(drawList, uploadText,
screenPos with { X = screenPos.X - textSize.X / 2f - 1, Y = screenPos.Y - textSize.Y / 2f - 1 },
UiSharedService.DrawOutlinedFont(drawList, uploadText, screenPos with { X = screenPos.X - textSize.X / 2f - 1, Y = screenPos.Y - textSize.Y / 2f - 1 },
UiSharedService.Color(255, 255, 0, transparency),
UiSharedService.Color(0, 0, 0, transparency), 2);
UiSharedService.Color(0, 0, 0, transparency),
2
);
}
catch
{
_logger.LogDebug("Error drawing upload progress");
_logger.LogDebug("Error drawing upload progress");
}
}
}
}
if (_configService.Current.ShowTransferBars)
{
DrawTransferBar();
}
}
private void DrawTransferBar()
{
const int dlBarBorder = 3;
const float rounding = 6f;
var shadowOffset = new Vector2(2, 2);
foreach (var transfer in _currentDownloads.ToList())
{
var transferKey = transfer.Key;
var rawPos = _dalamudUtilService.WorldToScreen(transferKey.GetGameObject());
// If RawPos is zero, remove it from smoothed dictionary
if (rawPos == Vector2.Zero)
{
_smoothed.Remove(transferKey);
continue;
}
// Smoothing out the movement and fix jitter around the position.
Vector2 screenPos = _smoothed.TryGetValue(transferKey, out var lastPos)
? (rawPos - lastPos).Length() < 4f ? lastPos : rawPos
: rawPos;
_smoothed[transferKey] = screenPos;
var totalBytes = transfer.Value.Sum(c => c.Value.TotalBytes);
var transferredBytes = transfer.Value.Sum(c => c.Value.TransferredBytes);
// Per-player state counts
var dlSlot = 0;
var dlQueue = 0;
var dlProg = 0;
var dlDecomp = 0;
foreach (var entry in transfer.Value)
{
var fileStatus = entry.Value;
switch (fileStatus.DownloadStatus)
{
case DownloadStatus.WaitingForSlot:
dlSlot++;
break;
case DownloadStatus.WaitingForQueue:
dlQueue++;
break;
case DownloadStatus.Downloading:
dlProg++;
break;
case DownloadStatus.Decompressing:
dlDecomp++;
break;
}
}
string statusText;
if (dlProg > 0)
{
statusText = "Downloading";
}
else if (dlDecomp > 0 || (totalBytes > 0 && transferredBytes >= totalBytes))
{
statusText = "Decompressing";
}
else if (dlQueue > 0)
{
statusText = "Waiting for queue";
}
else if (dlSlot > 0)
{
statusText = "Waiting for slot";
}
else
{
statusText = "Waiting";
}
var hasValidSize = totalBytes > 0;
var maxDlText = $"{UiSharedService.ByteToString(totalBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
var textSize = _configService.Current.TransferBarsShowText
? ImGui.CalcTextSize(maxDlText)
: new Vector2(10, 10);
int dlBarHeight = _configService.Current.TransferBarsHeight > ((int)textSize.Y + 5)
? _configService.Current.TransferBarsHeight
: (int)textSize.Y + 5;
int dlBarWidth = _configService.Current.TransferBarsWidth > ((int)textSize.X + 10)
? _configService.Current.TransferBarsWidth
: (int)textSize.X + 10;
var dlBarStart = new Vector2(screenPos.X - dlBarWidth / 2f, screenPos.Y - dlBarHeight / 2f);
var dlBarEnd = new Vector2(screenPos.X + dlBarWidth / 2f, screenPos.Y + dlBarHeight / 2f);
// Precompute rects
var outerStart = new Vector2(dlBarStart.X - dlBarBorder - 1, dlBarStart.Y - dlBarBorder - 1);
var outerEnd = new Vector2(dlBarEnd.X + dlBarBorder + 1, dlBarEnd.Y + dlBarBorder + 1);
var borderStart = new Vector2(dlBarStart.X - dlBarBorder, dlBarStart.Y - dlBarBorder);
var borderEnd = new Vector2(dlBarEnd.X + dlBarBorder, dlBarEnd.Y + dlBarBorder);
var drawList = ImGui.GetBackgroundDrawList();
drawList.AddRectFilled(
outerStart + shadowOffset,
outerEnd + shadowOffset,
UiSharedService.Color(0, 0, 0, 100 / 2),
rounding + 2
);
drawList.AddRectFilled(
outerStart,
outerEnd,
UiSharedService.Color(0, 0, 0, 100),
rounding + 2
);
drawList.AddRectFilled(
borderStart,
borderEnd,
UiSharedService.Color(ImGuiColors.DalamudGrey),
rounding
);
drawList.AddRectFilled(
dlBarStart,
dlBarEnd,
UiSharedService.Color(0, 0, 0, 100),
rounding
);
bool showFill = false;
double fillPercent = 0.0;
if (hasValidSize)
{
if (dlProg > 0)
{
fillPercent = transferredBytes / (double)totalBytes;
showFill = true;
}
else if (dlDecomp > 0 || transferredBytes >= totalBytes)
{
fillPercent = 1.0;
showFill = true;
}
}
if (showFill)
{
if (fillPercent < 0) fillPercent = 0;
if (fillPercent > 1) fillPercent = 1;
var progressEndX = dlBarStart.X + (float)(fillPercent * dlBarWidth);
var progressEnd = new Vector2(progressEndX, dlBarEnd.Y);
drawList.AddRectFilled(
dlBarStart,
progressEnd,
UiSharedService.Color(UIColors.Get("LightlessPurple")),
rounding
);
}
if (_configService.Current.TransferBarsShowText)
{
string downloadText;
if (dlProg > 0 && hasValidSize)
{
downloadText =
$"{statusText} {UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
}
else if ((dlDecomp > 0 || transferredBytes >= totalBytes) && hasValidSize)
{
downloadText = "Decompressing";
}
else
{
// Waiting states
downloadText = statusText;
}
var actualTextSize = ImGui.CalcTextSize(downloadText);
UiSharedService.DrawOutlinedFont(
drawList,
downloadText,
screenPos with
{
X = screenPos.X - actualTextSize.X / 2f - 1,
Y = screenPos.Y - actualTextSize.Y / 2f - 1
},
UiSharedService.Color(ImGuiColors.DalamudGrey),
UiSharedService.Color(0, 0, 0, 100),
1
);
}
}
if (_configService.Current.ShowUploading)
{
foreach (var player in _uploadingPlayers.Select(p => p.Key).ToList())
{
var screenPos = _dalamudUtilService.WorldToScreen(player.GetGameObject());
if (screenPos == Vector2.Zero) continue;
try
{
using var _ = _uiShared.UidFont.Push();
var uploadText = "Uploading";
var textSize = ImGui.CalcTextSize(uploadText);
var drawList = ImGui.GetBackgroundDrawList();
UiSharedService.DrawOutlinedFont(drawList, uploadText, screenPos with { X = screenPos.X - textSize.X / 2f - 1, Y = screenPos.Y - textSize.Y / 2f - 1 },
UiSharedService.Color(ImGuiColors.DalamudYellow),
UiSharedService.Color(0, 0, 0, 100),
2
);
}
catch
{
_logger.LogDebug("Error drawing upload progress");
}
}
}
}
private void DrawDownloadSummaryBox()
{
if (_currentDownloads.IsEmpty)
return;
const float padding = 6f;
const float spacingY = 2f;
const float minBoxWidth = 320f;
var now = ImGui.GetTime();
int totalFiles = 0;
int transferredFiles = 0;
long totalBytes = 0;
long transferredBytes = 0;
var totalDlSlot = 0;
var totalDlQueue = 0;
var totalDlProg = 0;
var totalDlDecomp = 0;
var perPlayer = new List<(
string Name,
int TransferredFiles,
int TotalFiles,
long TransferredBytes,
long TotalBytes,
double SpeedBytesPerSecond,
int DlSlot,
int DlQueue,
int DlProg,
int DlDecomp)>();
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 playerTransferredBytes = statuses.Sum(s => s.TransferredBytes);
totalFiles += playerTotalFiles;
transferredFiles += playerTransferredFiles;
totalBytes += playerTotalBytes;
transferredBytes += playerTransferredBytes;
// per-player W/Q/P/D
var playerDlSlot = 0;
var playerDlQueue = 0;
var playerDlProg = 0;
var playerDlDecomp = 0;
foreach (var entry in transfer.Value)
{
var fileStatus = entry.Value;
switch (fileStatus.DownloadStatus)
{
case DownloadStatus.WaitingForSlot:
playerDlSlot++;
totalDlSlot++;
break;
case DownloadStatus.WaitingForQueue:
playerDlQueue++;
totalDlQueue++;
break;
case DownloadStatus.Downloading:
playerDlProg++;
totalDlProg++;
break;
case DownloadStatus.Decompressing:
playerDlDecomp++;
totalDlDecomp++;
break;
}
}
double speed = 0;
if (playerTotalBytes > 0)
{
if (!_downloadSpeeds.TryGetValue(handler, out var tracker))
{
tracker = new DownloadSpeedTracker(windowSeconds: 3.0);
_downloadSpeeds[handler] = tracker;
}
speed = tracker.Update(now, playerTransferredBytes);
}
perPlayer.Add((
handler.Name,
playerTransferredFiles,
playerTotalFiles,
playerTransferredBytes,
playerTotalBytes,
speed,
playerDlSlot,
playerDlQueue,
playerDlProg,
playerDlDecomp
));
}
// Clean speed trackers for players with no active downloads
foreach (var handler in _downloadSpeeds.Keys.ToList())
{
if (!_currentDownloads.ContainsKey(handler))
_downloadSpeeds.Remove(handler);
}
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}]";
var bytesText =
$"{UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
var totalSpeed = perPlayer.Sum(p => p.SpeedBytesPerSecond);
var speedText = totalSpeed > 0
? $"{UiSharedService.ByteToString((long)totalSpeed)}/s"
: "Calculating in lightspeed...";
var headerSize = ImGui.CalcTextSize(headerText);
var bytesSize = ImGui.CalcTextSize(bytesText);
var totalSpeedSize = ImGui.CalcTextSize(speedText);
float contentWidth = headerSize.X;
if (bytesSize.X > contentWidth) contentWidth = bytesSize.X;
if (totalSpeedSize.X > contentWidth) contentWidth = totalSpeedSize.X;
if (_configService.Current.ShowPlayerLinesTransferWindow)
{
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}";
var lineSize = ImGui.CalcTextSize(line);
if (lineSize.X > contentWidth)
contentWidth = lineSize.X;
}
}
var lineHeight = ImGui.GetTextLineHeight();
var globalBarHeight = lineHeight * 0.8f;
var perPlayerBarHeight = lineHeight * 0.4f;
// Box width
float boxWidth = contentWidth + padding * 2;
if (boxWidth < minBoxWidth)
boxWidth = minBoxWidth;
// Box height
float boxHeight = 0;
boxHeight += padding;
boxHeight += globalBarHeight;
boxHeight += padding;
boxHeight += lineHeight + spacingY;
boxHeight += lineHeight + spacingY;
boxHeight += lineHeight * 1.4f + spacingY;
if (_configService.Current.ShowPlayerLinesTransferWindow)
{
foreach (var p in perPlayer)
{
boxHeight += lineHeight + spacingY;
var showBar = _configService.Current.ShowPlayerSpeedBarsTransferWindow
&& p.TransferredBytes > 0;
if (showBar)
{
boxHeight += perPlayerBarHeight + spacingY;
}
}
}
boxHeight += padding;
var boxMin = windowPos;
var boxMax = new Vector2(windowPos.X + boxWidth, windowPos.Y + boxHeight);
// Background + border
drawList.AddRectFilled(boxMin, boxMax, UiSharedService.Color(0, 0, 0, _transferBoxTransparency), 5f);
drawList.AddRect(boxMin, boxMax, UiSharedService.Color(ImGuiColors.DalamudGrey), 5f);
var cursor = boxMin + new Vector2(padding, padding);
var barMin = cursor;
var barMax = new Vector2(boxMin.X + boxWidth - padding, cursor.Y + globalBarHeight);
var progress = (float)transferredBytes / totalBytes;
if (progress < 0f) progress = 0f;
if (progress > 1f) progress = 1f;
drawList.AddRectFilled(barMin, barMax, UiSharedService.Color(40, 40, 40, _transferBoxTransparency), 3f);
drawList.AddRectFilled(
barMin,
new Vector2(barMin.X + (barMax.X - barMin.X) * progress, barMax.Y),
UiSharedService.Color(UIColors.Get("LightlessPurple")),
3f
);
cursor.Y = barMax.Y + padding;
// Header
UiSharedService.DrawOutlinedFont(
drawList,
headerText,
cursor,
UiSharedService.Color(ImGuiColors.DalamudWhite),
UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
1
);
cursor.Y += lineHeight + spacingY;
// Bytes
UiSharedService.DrawOutlinedFont(
drawList,
bytesText,
cursor,
UiSharedService.Color(ImGuiColors.DalamudWhite),
UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
1
);
cursor.Y += lineHeight + spacingY;
// Total speed
UiSharedService.DrawOutlinedFont(
drawList,
speedText,
cursor,
UiSharedService.Color(UIColors.Get("LightlessPurple")),
UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
1
);
cursor.Y += lineHeight * 1.4f + spacingY;
var orderedPlayers = perPlayer.OrderByDescending(p => p.TotalBytes).ToList();
foreach (var p in orderedPlayers)
{
var hasSpeed = p.SpeedBytesPerSecond > 0;
var playerSpeedText = hasSpeed
? $"{UiSharedService.ByteToString((long)p.SpeedBytesPerSecond)}/s"
: "-";
var showBar = _configService.Current.ShowPlayerSpeedBarsTransferWindow
&& p.TransferredBytes > 0;
var labelLine =
$"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}] {p.TransferredFiles}/{p.TotalFiles}";
if (!showBar)
{
UiSharedService.DrawOutlinedFont(
drawList,
labelLine,
cursor,
UiSharedService.Color(255, 255, 255, _transferBoxTransparency),
UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
1
);
cursor.Y += lineHeight + spacingY;
continue;
}
UiSharedService.DrawOutlinedFont(
drawList,
labelLine,
cursor,
UiSharedService.Color(255, 255, 255, _transferBoxTransparency),
UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
1
);
cursor.Y += lineHeight + spacingY;
// Bar background
var barBgMin = new Vector2(boxMin.X + padding, cursor.Y);
var barBgMax = new Vector2(boxMax.X - padding, cursor.Y + perPlayerBarHeight);
drawList.AddRectFilled(
barBgMin,
barBgMax,
UiSharedService.Color(40, 40, 40, _transferBoxTransparency),
3f
);
// Fill based on Progress of download
float ratio = 0f;
if (p.TotalBytes > 0)
ratio = (float)p.TransferredBytes / p.TotalBytes;
if (ratio < 0f) ratio = 0f;
if (ratio > 1f) ratio = 1f;
var fillX = barBgMin.X + (barBgMax.X - barBgMin.X) * ratio;
var barFillMax = new Vector2(fillX, barBgMax.Y);
drawList.AddRectFilled(
barBgMin,
barFillMax,
UiSharedService.Color(UIColors.Get("LightlessPurple")),
3f
);
// Text inside bar: downloading vs decompressing
string barText;
var isDecompressing = p.DlDecomp > 0 && p.TransferredBytes >= p.TotalBytes && p.TotalBytes > 0;
if (isDecompressing)
{
// Keep bar full, static text showing decompressing
barText = "Decompressing...";
}
else
{
var bytesInside =
$"{UiSharedService.ByteToString(p.TransferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(p.TotalBytes)}";
barText = hasSpeed
? $"{bytesInside} @ {playerSpeedText}"
: bytesInside;
}
if (!string.IsNullOrEmpty(barText))
{
var barTextSize = ImGui.CalcTextSize(barText);
var barTextPos = new Vector2(
barBgMin.X + ((barBgMax.X - barBgMin.X) - barTextSize.X) / 2f - 1,
barBgMin.Y + ((perPlayerBarHeight - barTextSize.Y) / 2f) - 1
);
UiSharedService.DrawOutlinedFont(
drawList,
barText,
barTextPos,
UiSharedService.Color(255, 255, 255, _transferBoxTransparency),
UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
1
);
}
cursor.Y += perPlayerBarHeight + spacingY;
}
}
public override bool DrawConditions()
{
if (_uiShared.EditTrackerPosition) return true;
if (!_configService.Current.ShowTransferWindow && !_configService.Current.ShowTransferBars) return false;
if (!_currentDownloads.Any() && !_fileTransferManager.IsUploading && !_uploadingPlayers.Any()) return false;
if (_currentDownloads.IsEmpty && !_fileTransferManager.IsUploading && _uploadingPlayers.IsEmpty) return false;
if (!IsOpen) return false;
return true;
}
@@ -369,4 +843,70 @@ public class DownloadUi : WindowMediatorSubscriberBase
}
}
}
private sealed class DownloadSpeedTracker
{
private readonly Queue<(double Time, long Bytes)> _samples = new();
private readonly double _windowSeconds;
public double SpeedBytesPerSecond { get; private set; }
public DownloadSpeedTracker(double windowSeconds = 3.0)
{
_windowSeconds = windowSeconds;
}
public double Update(double now, long totalBytes)
{
if (_samples.Count > 0 && totalBytes < _samples.Last().Bytes)
{
_samples.Clear();
}
_samples.Enqueue((now, totalBytes));
while (_samples.Count > 0 && now - _samples.Peek().Time > _windowSeconds)
_samples.Dequeue();
if (_samples.Count < 2)
{
SpeedBytesPerSecond = 0;
return SpeedBytesPerSecond;
}
var oldest = _samples.Peek();
var newest = _samples.Last();
var dt = newest.Time - oldest.Time;
if (dt <= 0.0001)
{
SpeedBytesPerSecond = 0;
return SpeedBytesPerSecond;
}
var dBytes = newest.Bytes - oldest.Bytes;
if (dBytes <= 0)
{
SpeedBytesPerSecond = 0;
return SpeedBytesPerSecond;
}
const long minBytesForSpeed = 32 * 1024;
if (dBytes < minBytesForSpeed)
{
return SpeedBytesPerSecond;
}
var avg = dBytes / dt;
const double alpha = 0.3;
SpeedBytesPerSecond = SpeedBytesPerSecond <= 0
? avg
: SpeedBytesPerSecond * (1 - alpha) + avg * alpha;
return SpeedBytesPerSecond;
}
}
}

View File

@@ -1,14 +1,21 @@
using LightlessSync.API.Dto.Group;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.Group;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Factories;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Components;
using LightlessSync.UI.Handlers;
using LightlessSync.UI.Models;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging;
using System.Collections.Immutable;
namespace LightlessSync.UI;
@@ -19,6 +26,7 @@ public class DrawEntityFactory
private readonly LightlessMediator _mediator;
private readonly SelectPairForTagUi _selectPairForTagUi;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly LightlessConfigService _configService;
private readonly UiSharedService _uiSharedService;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly CharaDataManager _charaDataManager;
@@ -29,13 +37,28 @@ public class DrawEntityFactory
private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi;
private readonly TagHandler _tagHandler;
private readonly IdDisplayHandler _uidDisplayHandler;
private readonly PairLedger _pairLedger;
private readonly PairFactory _pairFactory;
public DrawEntityFactory(ILogger<DrawEntityFactory> logger, ApiController apiController, IdDisplayHandler uidDisplayHandler,
SelectTagForPairUi selectTagForPairUi, RenamePairTagUi renamePairTagUi, LightlessMediator mediator,
TagHandler tagHandler, SelectPairForTagUi selectPairForTagUi,
ServerConfigurationManager serverConfigurationManager, UiSharedService uiSharedService,
PlayerPerformanceConfigService playerPerformanceConfigService, CharaDataManager charaDataManager,
SelectTagForSyncshellUi selectTagForSyncshellUi, RenameSyncshellTagUi renameSyncshellTagUi, SelectSyncshellForTagUi selectSyncshellForTagUi)
public DrawEntityFactory(
ILogger<DrawEntityFactory> logger,
ApiController apiController,
IdDisplayHandler uidDisplayHandler,
SelectTagForPairUi selectTagForPairUi,
RenamePairTagUi renamePairTagUi,
LightlessMediator mediator,
TagHandler tagHandler,
SelectPairForTagUi selectPairForTagUi,
ServerConfigurationManager serverConfigurationManager,
LightlessConfigService configService,
UiSharedService uiSharedService,
PlayerPerformanceConfigService playerPerformanceConfigService,
CharaDataManager charaDataManager,
SelectTagForSyncshellUi selectTagForSyncshellUi,
RenameSyncshellTagUi renameSyncshellTagUi,
SelectSyncshellForTagUi selectSyncshellForTagUi,
PairLedger pairLedger,
PairFactory pairFactory)
{
_logger = logger;
_apiController = apiController;
@@ -46,44 +69,152 @@ public class DrawEntityFactory
_tagHandler = tagHandler;
_selectPairForTagUi = selectPairForTagUi;
_serverConfigurationManager = serverConfigurationManager;
_configService = configService;
_uiSharedService = uiSharedService;
_playerPerformanceConfigService = playerPerformanceConfigService;
_charaDataManager = charaDataManager;
_selectTagForSyncshellUi = selectTagForSyncshellUi;
_renameSyncshellTagUi = renameSyncshellTagUi;
_selectSyncshellForTagUi = selectSyncshellForTagUi;
_pairLedger = pairLedger;
_pairFactory = pairFactory;
}
public DrawFolderGroup CreateDrawGroupFolder(GroupFullInfoDto groupFullInfoDto,
Dictionary<Pair, List<GroupFullInfoDto>> filteredPairs,
IImmutableList<Pair> allPairs)
public DrawFolderGroup CreateGroupFolder(
string id,
GroupFullInfoDto groupFullInfo,
IEnumerable<PairUiEntry> drawEntries,
IEnumerable<PairUiEntry> allEntries)
{
return new DrawFolderGroup(groupFullInfoDto.Group.GID, groupFullInfoDto, _apiController,
filteredPairs.Select(p => CreateDrawPair(groupFullInfoDto.Group.GID + p.Key.UserData.UID, p.Key, p.Value, groupFullInfoDto)).ToImmutableList(),
allPairs, _tagHandler, _uidDisplayHandler, _mediator, _uiSharedService, _selectTagForSyncshellUi);
var drawPairs = drawEntries
.Select(entry => CreateDrawPair($"{id}:{entry.DisplayEntry.Ident.UserId}", entry, groupFullInfo))
.Where(draw => draw is not null)
.Cast<DrawUserPair>()
.ToImmutableList();
var allPairs = allEntries.ToImmutableList();
return new DrawFolderGroup(
id,
groupFullInfo,
_apiController,
drawPairs,
allPairs,
_tagHandler,
_uidDisplayHandler,
_mediator,
_uiSharedService,
_selectTagForSyncshellUi);
}
public DrawFolderGroup CreateDrawGroupFolder(string id, GroupFullInfoDto groupFullInfoDto,
Dictionary<Pair, List<GroupFullInfoDto>> filteredPairs,
IImmutableList<Pair> allPairs)
public DrawFolderTag CreateTagFolder(
string tag,
IEnumerable<PairUiEntry> drawEntries,
IEnumerable<PairUiEntry> allEntries)
{
return new DrawFolderGroup(id, groupFullInfoDto, _apiController,
filteredPairs.Select(p => CreateDrawPair(groupFullInfoDto.Group.GID + p.Key.UserData.UID, p.Key, p.Value, groupFullInfoDto)).ToImmutableList(),
allPairs, _tagHandler, _uidDisplayHandler, _mediator, _uiSharedService, _selectTagForSyncshellUi);
var drawPairs = drawEntries
.Select(entry => CreateDrawPair($"{tag}:{entry.DisplayEntry.Ident.UserId}", entry))
.Where(draw => draw is not null)
.Cast<DrawUserPair>()
.ToImmutableList();
var allPairs = allEntries.ToImmutableList();
return new DrawFolderTag(
tag,
drawPairs,
allPairs,
_tagHandler,
_apiController,
_selectPairForTagUi,
_renamePairTagUi,
_uiSharedService,
_serverConfigurationManager,
_configService,
_mediator);
}
public DrawFolderTag CreateDrawTagFolder(string tag,
Dictionary<Pair, List<GroupFullInfoDto>> filteredPairs,
IImmutableList<Pair> allPairs)
public DrawUserPair? CreateDrawPair(
string id,
PairUiEntry entry,
GroupFullInfoDto? currentGroup = null)
{
return new(tag, filteredPairs.Select(u => CreateDrawPair(tag, u.Key, u.Value, currentGroup: null)).ToImmutableList(),
allPairs, _tagHandler, _apiController, _selectPairForTagUi, _renamePairTagUi, _uiSharedService);
var pair = _pairFactory.Create(entry.DisplayEntry);
if (pair is null)
{
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Skipping draw pair for {UserId}: legacy pair not found.", entry.DisplayEntry.Ident.UserId);
}
return null;
}
return new DrawUserPair(
id,
entry,
pair,
currentGroup,
_apiController,
_uidDisplayHandler,
_mediator,
_selectTagForPairUi,
_serverConfigurationManager,
_uiSharedService,
_playerPerformanceConfigService,
_configService,
_charaDataManager,
_pairLedger);
}
public DrawUserPair CreateDrawPair(string id, Pair user, List<GroupFullInfoDto> groups, GroupFullInfoDto? currentGroup)
public IReadOnlyList<PairUiEntry> GetAllEntries()
{
return new DrawUserPair(id + user.UserData.UID, user, groups, currentGroup, _apiController, _uidDisplayHandler,
_mediator, _selectTagForPairUi, _serverConfigurationManager, _uiSharedService, _playerPerformanceConfigService,
_charaDataManager);
try
{
return _pairLedger.GetAllEntries()
.Select(BuildUiEntry)
.ToList();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to build pair display entries.");
return Array.Empty<PairUiEntry>();
}
}
private PairUiEntry BuildUiEntry(PairDisplayEntry entry)
{
var handler = entry.Handler;
var alias = entry.User.AliasOrUID;
if (string.IsNullOrWhiteSpace(alias))
{
alias = entry.Ident.UserId;
}
var displayName = !string.IsNullOrWhiteSpace(handler?.PlayerName)
? handler!.PlayerName!
: alias;
var note = _serverConfigurationManager.GetNoteForUid(entry.Ident.UserId) ?? string.Empty;
var isPaused = entry.SelfPermissions.IsPaused();
return new PairUiEntry(
entry,
alias,
displayName,
note,
entry.IsVisible,
entry.IsOnline,
entry.IsDirectlyPaired,
entry.IsOneSided,
entry.HasAnyConnection,
isPaused,
entry.SelfPermissions,
entry.OtherPermissions,
entry.PairStatus,
handler?.LastAppliedDataBytes ?? -1,
handler?.LastAppliedDataTris ?? -1,
handler?.LastAppliedApproximateVRAMBytes ?? -1,
handler?.LastAppliedApproximateEffectiveVRAMBytes ?? -1,
handler);
}
}

View File

@@ -2,10 +2,8 @@ using Dalamud.Game.Gui.Dtr;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Configurations;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
@@ -14,10 +12,11 @@ using LightlessSync.WebAPI;
using LightlessSync.WebAPI.SignalR.Utils;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using System.Runtime.InteropServices;
using System.Text;
using LightlessSync.UI.Services;
using static LightlessSync.Services.PairRequestService;
using LightlessSync.Services.LightFinder;
namespace LightlessSync.UI;
@@ -34,10 +33,10 @@ public sealed class DtrEntry : IDisposable, IHostedService
private readonly Lazy<IDtrBarEntry> _statusEntry;
private readonly Lazy<IDtrBarEntry> _lightfinderEntry;
private readonly ILogger<DtrEntry> _logger;
private readonly BroadcastService _broadcastService;
private readonly BroadcastScannerService _broadcastScannerService;
private readonly LightFinderService _broadcastService;
private readonly LightFinderScannerService _broadcastScannerService;
private readonly LightlessMediator _lightlessMediator;
private readonly PairManager _pairManager;
private readonly PairUiService _pairUiService;
private readonly PairRequestService _pairRequestService;
private readonly DalamudUtilService _dalamudUtilService;
private Task? _runTask;
@@ -57,12 +56,12 @@ public sealed class DtrEntry : IDisposable, IHostedService
IDtrBar dtrBar,
ConfigurationServiceBase<LightlessConfig> configService,
LightlessMediator lightlessMediator,
PairManager pairManager,
PairUiService pairUiService,
PairRequestService pairRequestService,
ApiController apiController,
ServerConfigurationManager serverManager,
BroadcastService broadcastService,
BroadcastScannerService broadcastScannerService,
LightFinderService broadcastService,
LightFinderScannerService broadcastScannerService,
DalamudUtilService dalamudUtilService)
{
_logger = logger;
@@ -71,7 +70,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
_lightfinderEntry = new(CreateLightfinderEntry);
_configService = configService;
_lightlessMediator = lightlessMediator;
_pairManager = pairManager;
_pairUiService = pairUiService;
_pairRequestService = pairRequestService;
_apiController = apiController;
_serverManager = serverManager;
@@ -165,7 +164,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
entry.OnClick = interactionEvent => OnLightfinderEntryClick(interactionEvent);
return entry;
}
private void OnStatusEntryClick(DtrInteractionEvent interactionEvent)
{
if (interactionEvent.ClickType.Equals(MouseClickType.Left))
@@ -254,16 +253,15 @@ public sealed class DtrEntry : IDisposable, IHostedService
if (_apiController.IsConnected)
{
var pairCount = _pairManager.GetVisibleUserCount();
var snapshot = _pairUiService.GetSnapshot();
var visiblePairsQuery = snapshot.PairsByUid.Values.Where(x => x.IsVisible && !x.IsPaused);
var pairCount = visiblePairsQuery.Count();
text = $"\uE044 {pairCount}";
if (pairCount > 0)
{
var preferNote = config.PreferNoteInDtrTooltip;
var showUid = config.ShowUidInDtrTooltip;
var visiblePairsQuery = _pairManager.GetOnlineUserPairs()
.Where(x => x.IsVisible);
IEnumerable<string> visiblePairs = showUid
? visiblePairsQuery.Select(x => string.Format("{0} ({1})", preferNote ? x.GetNote() ?? x.PlayerName : x.PlayerName, x.UserData.AliasOrUID))
: visiblePairsQuery.Select(x => string.Format("{0}", preferNote ? x.GetNote() ?? x.PlayerName : x.PlayerName));
@@ -445,7 +443,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
return ($"{icon} OFF", colors, tooltip.ToString());
}
private (string, Colors, string) FormatTooltip(string title, IEnumerable<string> names, string icon, Colors color)
private static (string, Colors, string) FormatTooltip(string title, IEnumerable<string> names, string icon, Colors color)
{
var list = names.Where(x => !string.IsNullOrEmpty(x)).ToList();
var tooltip = new StringBuilder()
@@ -490,7 +488,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
private const byte _colorTypeForeground = 0x13;
private const byte _colorTypeGlow = 0x14;
private static Colors SwapColorChannels(Colors colors)
internal static Colors SwapColorChannels(Colors colors)
=> new(SwapColorComponent(colors.Foreground), SwapColorComponent(colors.Glow));
private static uint SwapColorComponent(uint color)

View File

@@ -0,0 +1,691 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Dto.Group;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.Profiles;
using LightlessSync.UI.Tags;
using LightlessSync.Utils;
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using System.Numerics;
namespace LightlessSync.UI;
public partial class EditProfileUi
{
private void OpenGroupEditor(GroupFullInfoDto groupInfo)
{
_mode = ProfileEditorMode.Group;
_groupInfo = groupInfo;
var profile = _lightlessProfileManager.GetLightlessGroupProfile(groupInfo.Group);
_groupProfileData = profile;
SyncGroupProfileState(profile, resetSelection: true);
var scale = ImGuiHelpers.GlobalScale;
var viewport = ImGui.GetMainViewport();
ProfileEditorLayoutCoordinator.Enable(groupInfo.Group.GID);
ProfileEditorLayoutCoordinator.EnsureAnchor(viewport.WorkPos, scale);
Mediator.Publish(new GroupProfileOpenStandaloneMessage(groupInfo.Group));
IsOpen = true;
_wasOpen = true;
}
private void ResetGroupEditorState()
{
_groupInfo = null;
_groupProfileData = null;
_groupIsNsfw = false;
_groupIsDisabled = false;
_groupServerIsNsfw = false;
_groupServerIsDisabled = false;
_queuedProfileImage = null;
_queuedBannerImage = null;
_profileImage = Array.Empty<byte>();
_bannerImage = Array.Empty<byte>();
_profileDescription = string.Empty;
_descriptionText = string.Empty;
_profileTagIds = Array.Empty<int>();
_tagEditorSelection.Clear();
_pfpTextureWrap?.Dispose();
_pfpTextureWrap = null;
_bannerTextureWrap?.Dispose();
_bannerTextureWrap = null;
_showProfileImageError = false;
_showBannerImageError = false;
_groupVisibilityInitialized = false;
}
private void DrawGroupEditor(float scale)
{
if (_groupInfo is null)
{
UiSharedService.TextWrapped("Open the Syncshell admin panel and choose a group to edit its profile.");
return;
}
var viewport = ImGui.GetMainViewport();
var linked = ProfileEditorLayoutCoordinator.IsActive(_groupInfo.Group.GID);
if (linked)
{
ProfileEditorLayoutCoordinator.EnsureAnchor(viewport.WorkPos, scale);
var desiredSize = ProfileEditorLayoutCoordinator.GetEditorSize(scale);
if (!ProfileEditorLayoutCoordinator.NearlyEquals(ImGui.GetWindowSize(), desiredSize))
ImGui.SetWindowSize(desiredSize, ImGuiCond.Always);
var currentPos = ImGui.GetWindowPos();
if (IsWindowBeingDragged())
ProfileEditorLayoutCoordinator.UpdateAnchorFromEditor(currentPos, scale);
var desiredPos = ProfileEditorLayoutCoordinator.GetEditorPosition(scale);
if (!ProfileEditorLayoutCoordinator.NearlyEquals(currentPos, desiredPos))
ImGui.SetWindowPos(desiredPos, ImGuiCond.Always);
}
else
{
var defaultProfilePos = viewport.WorkPos + new Vector2(50f, 70f) * scale;
var defaultEditorPos = defaultProfilePos + ProfileEditorLayoutCoordinator.GetEditorOffset(scale);
ImGui.SetWindowPos(defaultEditorPos, ImGuiCond.FirstUseEver);
}
if (_queuedProfileImage is not null)
ApplyQueuedGroupProfileImage();
if (_queuedBannerImage is not null)
ApplyQueuedGroupBannerImage();
var profile = _lightlessProfileManager.GetLightlessGroupProfile(_groupInfo.Group);
_groupProfileData = profile;
SyncGroupProfileState(profile, resetSelection: false);
var accent = UIColors.Get("LightlessPurple");
var accentBg = new Vector4(accent.X, accent.Y, accent.Z, 0.015f);
var accentBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.07f);
using var panelBg = ImRaii.PushColor(ImGuiCol.ChildBg, accentBg);
using var panelBorder = ImRaii.PushColor(ImGuiCol.ChildBg, accentBorder);
ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 4f * scale);
if (ImGui.BeginChild("##GroupProfileEditorCanvas", -Vector2.One, true))
{
DrawGroupGuidelinesSection(scale);
ImGui.Dummy(new Vector2(0f, 4f * scale));
DrawGroupProfileContent(profile, scale);
}
ImGui.EndChild();
ImGui.PopStyleVar();
}
private void DrawGroupGuidelinesSection(float scale)
{
DrawSection("Guidelines", scale, () =>
{
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1f, 1f));
_uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "All users that are paired and unpaused with you will be able to see your profile pictures, tags and description.");
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Other users have the possibility to report this profile for breaking the rules.");
_uiSharedService.DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Anything as profile image that can be considered highly illegal or obscene (bestiality, anything that could be considered a sexual act with a minor (that includes Lalafells), etc.)");
_uiSharedService.DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Slurs of any kind in the description that can be considered highly offensive");
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "In case of valid reports from other users this can lead to disabling the profile forever or terminating syncshell owner's Lightless account indefinitely.");
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Judgement of the profile validity from reports through staff is not up to debate and the decisions to disable the profile or your account permanent.");
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessBlue"), "If the profile picture or profile description could be considered NSFW, enable the toggle in visibility settings.");
ImGui.PopStyleVar();
});
}
private void DrawGroupProfileContent(LightlessGroupProfileData profile, float scale)
{
DrawSection("Profile Preview", scale, () => DrawGroupProfileSnapshot(profile, scale));
DrawSection("Profile Image", scale, DrawGroupProfileImageControls);
DrawSection("Profile Banner", scale, DrawGroupProfileBannerControls);
DrawSection("Profile Description", scale, DrawGroupProfileDescriptionEditor);
DrawSection("Profile Tags", scale, () => DrawGroupProfileTagsEditor(scale));
DrawSection("Visibility", scale, DrawGroupProfileVisibilityControls);
}
private void DrawGroupProfileSnapshot(LightlessGroupProfileData profile, float scale)
{
var bannerHeight = 140f * scale;
ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 4f * scale);
if (ImGui.BeginChild("##GroupProfileBannerPreview", new Vector2(-1f, bannerHeight), true))
{
if (_bannerTextureWrap != null)
{
var childSize = ImGui.GetWindowSize();
var padding = ImGui.GetStyle().WindowPadding;
var contentSize = new Vector2(
MathF.Max(childSize.X - padding.X * 2f, 1f),
MathF.Max(childSize.Y - padding.Y * 2f, 1f));
var imageSize = ImGuiHelpers.ScaledVector2(_bannerTextureWrap.Width, _bannerTextureWrap.Height);
if (imageSize.X > contentSize.X || imageSize.Y > contentSize.Y)
{
var ratio = MathF.Min(contentSize.X / MathF.Max(imageSize.X, 1f), contentSize.Y / MathF.Max(imageSize.Y, 1f));
imageSize *= ratio;
}
var offset = new Vector2(
MathF.Max((contentSize.X - imageSize.X) * 0.5f, 0f),
MathF.Max((contentSize.Y - imageSize.Y) * 0.5f, 0f));
ImGui.SetCursorPos(padding + offset);
ImGui.Image(_bannerTextureWrap.Handle, imageSize);
}
else
{
ImGui.TextColored(UIColors.Get("LightlessPurple"), "No profile banner uploaded.");
}
}
ImGui.EndChild();
ImGui.PopStyleVar();
ImGui.Dummy(new Vector2(0f, 6f * scale));
if (_pfpTextureWrap != null)
{
var size = ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height);
var maxEdge = 160f * scale;
if (size.X > maxEdge || size.Y > maxEdge)
{
var ratio = MathF.Min(maxEdge / MathF.Max(size.X, 1f), maxEdge / MathF.Max(size.Y, 1f));
size *= ratio;
}
ImGui.Image(_pfpTextureWrap.Handle, size);
}
else
{
ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 4f * scale);
if (ImGui.BeginChild("##GroupProfileImagePlaceholder", new Vector2(160f * scale, 160f * scale), true))
ImGui.TextColored(UIColors.Get("LightlessPurple"), "No profile picture uploaded.");
ImGui.EndChild();
ImGui.PopStyleVar();
}
ImGui.SameLine();
ImGui.BeginGroup();
ImGui.TextColored(UIColors.Get("LightlessBlue"), _groupInfo!.GroupAliasOrGID);
ImGui.TextDisabled($"ID: {_groupInfo.Group.GID}");
ImGui.TextDisabled($"Owner: {_groupInfo.Owner.AliasOrUID}");
ImGui.EndGroup();
ImGui.Dummy(new Vector2(0f, 4f * scale));
ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 4f * scale);
if (ImGui.BeginChild("##GroupProfileDescriptionPreview", new Vector2(-1f, 120f * scale), true))
{
var hasDescription = !string.IsNullOrWhiteSpace(profile.Description);
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X);
if (!hasDescription)
{
ImGui.TextDisabled("Syncshell has no description set.");
}
else if (!SeStringUtils.TryRenderSeStringMarkupAtCursor(profile.Description!))
{
UiSharedService.TextWrapped(profile.Description);
}
ImGui.PopTextWrapPos();
}
ImGui.EndChild();
ImGui.PopStyleVar();
}
private void DrawGroupProfileImageControls()
{
_uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "Profile pictures must be 512x512 and under 2 MiB.");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture"))
{
_fileDialogManager.OpenFileDialog("Select syncshell profile picture", _imageFileDialogFilter, (success, file) =>
{
if (!success || string.IsNullOrEmpty(file))
return;
_showProfileImageError = false;
_ = SubmitGroupProfilePicture(file);
});
}
UiSharedService.AttachToolTip("Select an image up to 512x512 pixels and <= 2 MiB (PNG/JPG/JPEG/WEBP/BMP).");
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear profile picture"))
{
_ = ClearGroupProfilePicture();
}
UiSharedService.AttachToolTip("Remove the current profile picture from this syncshell.");
if (_showProfileImageError)
{
UiSharedService.ColorTextWrapped("Image must be no larger than 512x512 pixels and under 2 MiB.", ImGuiColors.DalamudRed);
}
}
private void DrawGroupProfileBannerControls()
{
_uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "Profile banners must be 840x260 and under 2 MiB.");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile banner"))
{
_fileDialogManager.OpenFileDialog("Select syncshell profile banner", _imageFileDialogFilter, (success, file) =>
{
if (!success || string.IsNullOrEmpty(file))
return;
_showBannerImageError = false;
_ = SubmitGroupProfileBanner(file);
});
}
UiSharedService.AttachToolTip("Select an image up to 840x260 pixels and <= 2 MiB (PNG/JPG/JPEG/WEBP/BMP).");
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear profile banner"))
{
_ = ClearGroupProfileBanner();
}
UiSharedService.AttachToolTip("Remove the current profile banner.");
if (_showBannerImageError)
{
UiSharedService.ColorTextWrapped("Banner must be no larger than 840x260 pixels and under 2 MiB.", ImGuiColors.DalamudRed);
}
}
private void DrawGroupProfileDescriptionEditor()
{
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(6f, 4f) * ImGuiHelpers.GlobalScale);
var descriptionBoxSize = new Vector2(-1f, 160f * ImGuiHelpers.GlobalScale);
ImGui.InputTextMultiline("##GroupDescription", ref _descriptionText, 1500, descriptionBoxSize);
ImGui.PopStyleVar();
ImGui.TextDisabled($"{_descriptionText.Length}/1500 characters");
ImGui.SameLine();
ImGuiComponents.HelpMarker(DescriptionMacroTooltip);
bool changed = !string.Equals(_descriptionText, _profileDescription, StringComparison.Ordinal);
if (!changed)
ImGui.BeginDisabled();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description"))
{
_ = SubmitGroupDescription(_descriptionText);
}
UiSharedService.AttachToolTip("Apply the text above to the syncshell profile description.");
if (!changed)
ImGui.EndDisabled();
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description"))
{
_ = SubmitGroupDescription(string.Empty);
}
UiSharedService.AttachToolTip("Remove the profile description.");
}
private void DrawGroupProfileTagsEditor(float scale)
{
DrawTagEditor(
scale,
contextPrefix: "group",
saveTooltip: "Apply the selected tags to this syncshell profile.",
submitAction: payload => SubmitGroupTagChanges(payload),
allowReorder: true,
sortPayloadBeforeSubmit: false,
onPayloadPrepared: payload =>
{
_tagEditorSelection.Clear();
if (payload.Length > 0)
_tagEditorSelection.AddRange(payload);
});
}
private void DrawGroupProfileVisibilityControls()
{
EnsureGroupVisibilityStateInitialised();
bool changedNsfw = DrawCheckboxRow("Profile is NSFW", _groupIsNsfw, out var newNsfw, "Flag this profile as not safe for work.");
if (changedNsfw)
_groupIsNsfw = newNsfw;
bool changedDisabled = DrawCheckboxRow("Disable profile for viewers", _groupIsDisabled, out var newDisabled, "Temporarily hide this profile from members.");
if (changedDisabled)
_groupIsDisabled = newDisabled;
bool visibilityChanged = (_groupIsNsfw != _groupServerIsNsfw) || (_groupIsDisabled != _groupServerIsDisabled);
if (!visibilityChanged)
ImGui.BeginDisabled();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Apply Visibility Changes"))
{
_ = SubmitGroupVisibilityChanges(_groupIsNsfw, _groupIsDisabled);
}
UiSharedService.AttachToolTip("Apply the visibility toggles above.");
if (!visibilityChanged)
ImGui.EndDisabled();
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.SyncAlt, "Reset"))
{
_groupIsNsfw = _groupServerIsNsfw;
_groupIsDisabled = _groupServerIsDisabled;
}
}
private string? GetCurrentGroupProfileImageBase64()
{
if (_queuedProfileImage is not null && _queuedProfileImage.Length > 0)
return Convert.ToBase64String(_queuedProfileImage);
if (!string.IsNullOrWhiteSpace(_groupProfileData?.Base64ProfilePicture))
return _groupProfileData!.Base64ProfilePicture;
return _profileImage.Length > 0 ? Convert.ToBase64String(_profileImage) : null;
}
private string? GetCurrentGroupBannerBase64()
{
if (_queuedBannerImage is not null && _queuedBannerImage.Length > 0)
return Convert.ToBase64String(_queuedBannerImage);
if (!string.IsNullOrWhiteSpace(_groupProfileData?.Base64BannerPicture))
return _groupProfileData!.Base64BannerPicture;
return _bannerImage.Length > 0 ? Convert.ToBase64String(_bannerImage) : null;
}
private async Task SubmitGroupProfilePicture(string filePath)
{
if (_groupInfo is null)
return;
try
{
var fileContent = await File.ReadAllBytesAsync(filePath).ConfigureAwait(false);
var stream = new MemoryStream(fileContent);
await using (stream.ConfigureAwait(false))
{
var format = await Image.DetectFormatAsync(stream).ConfigureAwait(false);
if (!IsSupportedImageFormat(format))
{
_showProfileImageError = true;
return;
}
using var image = Image.Load<Rgba32>(fileContent);
if (image.Width > 512 || image.Height > 512 || fileContent.Length > 2000 * 1024)
{
_showProfileImageError = true;
return;
}
await _apiController.GroupSetProfile(new GroupProfileDto(
_groupInfo.Group,
Description: null,
Tags: null,
PictureBase64: Convert.ToBase64String(fileContent),
BannerBase64: null,
IsNsfw: null,
IsDisabled: null)).ConfigureAwait(false);
_showProfileImageError = false;
_queuedProfileImage = fileContent;
Mediator.Publish(new ClearProfileGroupDataMessage(_groupInfo.Group));
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to upload syncshell profile picture.");
}
}
private async Task ClearGroupProfilePicture()
{
if (_groupInfo is null)
return;
try
{
await _apiController.GroupSetProfile(new GroupProfileDto(
_groupInfo.Group,
Description: null,
Tags: null,
PictureBase64: null,
BannerBase64: null,
IsNsfw: null,
IsDisabled: null)).ConfigureAwait(false);
_queuedProfileImage = Array.Empty<byte>();
Mediator.Publish(new ClearProfileGroupDataMessage(_groupInfo.Group));
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to clear syncshell profile picture.");
}
}
private async Task SubmitGroupProfileBanner(string filePath)
{
if (_groupInfo is null)
return;
try
{
var fileContent = await File.ReadAllBytesAsync(filePath).ConfigureAwait(false);
var stream = new MemoryStream(fileContent);
await using (stream.ConfigureAwait(false))
{
var format = await Image.DetectFormatAsync(stream).ConfigureAwait(false);
if (!IsSupportedImageFormat(format))
{
_showBannerImageError = true;
return;
}
using var image = Image.Load<Rgba32>(fileContent);
if (image.Width > 840 || image.Height > 260 || fileContent.Length > 2000 * 1024)
{
_showBannerImageError = true;
return;
}
await _apiController.GroupSetProfile(new GroupProfileDto(
_groupInfo.Group,
Description: null,
Tags: null,
PictureBase64: null,
BannerBase64: Convert.ToBase64String(fileContent),
IsNsfw: null,
IsDisabled: null)).ConfigureAwait(false);
_showBannerImageError = false;
_queuedBannerImage = fileContent;
Mediator.Publish(new ClearProfileGroupDataMessage(_groupInfo.Group));
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to upload syncshell profile banner.");
}
}
private async Task ClearGroupProfileBanner()
{
if (_groupInfo is null)
return;
try
{
await _apiController.GroupSetProfile(new GroupProfileDto(
_groupInfo.Group,
Description: null,
Tags: null,
PictureBase64: null,
BannerBase64: null,
IsNsfw: null,
IsDisabled: null)).ConfigureAwait(false);
_queuedBannerImage = Array.Empty<byte>();
Mediator.Publish(new ClearProfileGroupDataMessage(_groupInfo.Group));
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to clear syncshell profile banner.");
}
}
private async Task SubmitGroupDescription(string description)
{
if (_groupInfo is null)
return;
try
{
await _apiController.GroupSetProfile(new GroupProfileDto(
_groupInfo.Group,
Description: description,
Tags: null,
PictureBase64: GetCurrentGroupProfileImageBase64(),
BannerBase64: GetCurrentGroupBannerBase64(),
IsNsfw: null,
IsDisabled: null)).ConfigureAwait(false);
_profileDescription = description;
Mediator.Publish(new ClearProfileGroupDataMessage(_groupInfo.Group));
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to update syncshell profile description.");
}
}
private void EnsureGroupVisibilityStateInitialised()
{
if (_groupInfo == null || _groupVisibilityInitialized)
return;
_groupIsNsfw = _groupServerIsNsfw;
_groupIsDisabled = _groupServerIsDisabled;
_groupVisibilityInitialized = true;
}
private async Task SubmitGroupTagChanges(int[] payload)
{
if (_groupInfo is null)
return;
try
{
await _apiController.GroupSetProfile(new GroupProfileDto(
_groupInfo.Group,
Description: null,
Tags: payload,
PictureBase64: GetCurrentGroupProfileImageBase64(),
BannerBase64: GetCurrentGroupBannerBase64(),
IsNsfw: null,
IsDisabled: null)).ConfigureAwait(false);
_profileTagIds = payload.Length == 0 ? [] : [.. payload];
Mediator.Publish(new ClearProfileGroupDataMessage(_groupInfo.Group));
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to update syncshell profile tags.");
}
}
private async Task SubmitGroupVisibilityChanges(bool isNsfw, bool isDisabled)
{
if (_groupInfo is null)
return;
try
{
await _apiController.GroupSetProfile(new GroupProfileDto(
_groupInfo.Group,
Description: null,
Tags: null,
PictureBase64: GetCurrentGroupProfileImageBase64(),
BannerBase64: GetCurrentGroupBannerBase64(),
IsNsfw: isNsfw,
IsDisabled: isDisabled)).ConfigureAwait(false);
_groupServerIsNsfw = isNsfw;
_groupServerIsDisabled = isDisabled;
Mediator.Publish(new ClearProfileGroupDataMessage(_groupInfo.Group));
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to update syncshell profile visibility.");
}
}
private void ApplyQueuedGroupProfileImage()
{
if (_queuedProfileImage is null)
return;
_profileImage = _queuedProfileImage;
_pfpTextureWrap?.Dispose();
_pfpTextureWrap = _profileImage.Length > 0 ? _uiSharedService.LoadImage(_profileImage) : null;
_queuedProfileImage = null;
}
private void ApplyQueuedGroupBannerImage()
{
if (_queuedBannerImage is null)
return;
_bannerImage = _queuedBannerImage;
_bannerTextureWrap?.Dispose();
_bannerTextureWrap = _bannerImage.Length > 0 ? _uiSharedService.LoadImage(_bannerImage) : null;
_queuedBannerImage = null;
}
private void SyncGroupProfileState(LightlessGroupProfileData profile, bool resetSelection)
{
if (!_profileImage.SequenceEqual(profile.ProfileImageData.Value))
{
_profileImage = profile.ProfileImageData.Value;
_pfpTextureWrap?.Dispose();
_pfpTextureWrap = _profileImage.Length > 0 ? _uiSharedService.LoadImage(_profileImage) : null;
}
if (!_bannerImage.SequenceEqual(profile.BannerImageData.Value))
{
_bannerImage = profile.BannerImageData.Value;
_bannerTextureWrap?.Dispose();
_bannerTextureWrap = _bannerImage.Length > 0 ? _uiSharedService.LoadImage(_bannerImage) : null;
}
if (!string.Equals(_profileDescription, profile.Description, StringComparison.Ordinal))
{
_profileDescription = profile.Description;
_descriptionText = _profileDescription;
}
var tags = profile.Tags ?? Array.Empty<int>();
if (!TagsEqual(tags, _profileTagIds))
{
_profileTagIds = tags.Count == 0 ? Array.Empty<int>() : tags.ToArray();
if (resetSelection)
{
_tagEditorSelection.Clear();
if (_profileTagIds.Length > 0)
_tagEditorSelection.AddRange(_profileTagIds);
}
}
_groupServerIsNsfw = profile.IsNsfw;
_groupServerIsDisabled = profile.IsDisabled;
if (!_groupVisibilityInitialized)
{
_groupIsNsfw = _groupServerIsNsfw;
_groupIsDisabled = _groupServerIsDisabled;
_groupVisibilityInitialized = true;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ using Dalamud.Interface.Utility.Raii;
using LightlessSync.Services;
using LightlessSync.Services.Events;
using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.Globalization;
@@ -43,11 +44,9 @@ internal class EventViewerUI : WindowMediatorSubscriberBase
{
_eventAggregator = eventAggregator;
_uiSharedService = uiSharedService;
SizeConstraints = new()
{
MinimumSize = new(600, 500),
MaximumSize = new(1000, 2000)
};
WindowBuilder.For(this)
.SetSizeConstraints(new Vector2(600, 500), new Vector2(1000, 2000))
.Apply();
_filteredEvents = RecreateFilter();
}
@@ -205,17 +204,37 @@ internal class EventViewerUI : WindowMediatorSubscriberBase
var posX = ImGui.GetCursorPosX();
var maxTextLength = ImGui.GetWindowContentRegionMax().X - posX;
var textSize = ImGui.CalcTextSize(ev.Message).X;
var msg = ev.Message;
while (textSize > maxTextLength)
var msg = ev.Message ?? string.Empty;
var maxEventTextLength = ImGui.GetContentRegionAvail().X;
if (maxEventTextLength <= 0f)
{
msg = msg[..^5] + "...";
textSize = ImGui.CalcTextSize(msg).X;
ImGui.TextUnformatted(string.Empty);
return;
}
var eventTextSize = ImGui.CalcTextSize(msg).X;
if (eventTextSize > maxEventTextLength)
{
const string ellipsis = "...";
while (eventTextSize > maxTextLength && msg.Length > ellipsis.Length)
{
var cut = Math.Min(5, msg.Length - ellipsis.Length);
msg = msg[..^cut] + ellipsis;
eventTextSize = ImGui.CalcTextSize(msg).X;
}
if (textSize > maxEventTextLength)
msg = ellipsis;
}
ImGui.TextUnformatted(msg);
if (!string.Equals(msg, ev.Message, StringComparison.Ordinal))
{
UiSharedService.AttachToolTip(ev.Message);
}
}
}
}

View File

@@ -10,13 +10,16 @@ using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Style;
using LightlessSync.Utils;
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Text;
namespace LightlessSync.UI.Handlers;
public class IdDisplayHandler
{
private readonly LightlessConfigService _lightlessConfigService;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly LightlessMediator _mediator;
private readonly ServerConfigurationManager _serverManager;
private readonly Dictionary<string, bool> _showIdForEntry = new(StringComparer.Ordinal);
@@ -30,11 +33,16 @@ public class IdDisplayHandler
private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f);
private float _highlightBoost;
public IdDisplayHandler(LightlessMediator mediator, ServerConfigurationManager serverManager, LightlessConfigService lightlessConfigService)
public IdDisplayHandler(
LightlessMediator mediator,
ServerConfigurationManager serverManager,
LightlessConfigService lightlessConfigService,
PlayerPerformanceConfigService playerPerformanceConfigService)
{
_mediator = mediator;
_serverManager = serverManager;
_lightlessConfigService = lightlessConfigService;
_playerPerformanceConfigService = playerPerformanceConfigService;
}
public void DrawGroupText(string id, GroupFullInfoDto group, float textPosX, Func<float> editBoxWidth)
@@ -43,10 +51,18 @@ public class IdDisplayHandler
(bool textIsUid, string playerText) = GetGroupText(group);
if (!string.Equals(_editEntry, group.GID, StringComparison.Ordinal))
{
ImGui.AlignTextToFramePadding();
using (ImRaii.PushFont(UiBuilder.MonoFont, textIsUid))
{
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(playerText);
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Left click to switch between ID display and alias"
+ Environment.NewLine + "Right click to edit notes for this syncshell"
+ Environment.NewLine + "Middle Mouse Button to open syncshell profile in a separate window");
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
@@ -73,6 +89,11 @@ public class IdDisplayHandler
_editEntry = group.GID;
_editIsUid = false;
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Middle))
{
_mediator.Publish(new GroupProfileOpenStandaloneMessage(group.Group));
}
}
else
{
@@ -97,112 +118,121 @@ public class IdDisplayHandler
{
ImGui.SameLine(textPosX);
(bool textIsUid, string playerText) = GetPlayerText(pair);
var compactPerformanceText = BuildCompactPerformanceUsageText(pair);
if (!string.Equals(_editEntry, pair.UserData.UID, StringComparison.Ordinal))
{
ImGui.AlignTextToFramePadding();
var targetFontSize = ImGui.GetFontSize();
var font = textIsUid ? UiBuilder.MonoFont : ImGui.GetFont();
var rowWidth = MathF.Max(editBoxWidth.Invoke(), 0f);
float rowRightLimit = 0f;
Vector2 nameRectMin = Vector2.Zero;
Vector2 nameRectMax = Vector2.Zero;
float rowTopForStats = 0f;
float frameHeightForStats = 0f;
Vector4? textColor = null;
Vector4? glowColor = null;
if (pair.UserData.HasVanity)
{
if (!string.IsNullOrWhiteSpace(pair.UserData.TextColorHex))
{
textColor = UIColors.HexToRgba(pair.UserData.TextColorHex);
}
if (!string.IsNullOrWhiteSpace(pair.UserData.TextGlowColorHex))
{
glowColor = UIColors.HexToRgba(pair.UserData.TextGlowColorHex);
}
}
var useVanityColors = _lightlessConfigService.Current.useColoredUIDs && (textColor != null || glowColor != null);
var seString = useVanityColors
? SeStringUtils.BuildFormattedPlayerName(playerText, textColor, glowColor)
: SeStringUtils.BuildPlain(playerText);
var rowStart = ImGui.GetCursorScreenPos();
var drawList = ImGui.GetWindowDrawList();
bool useHighlight = false;
float highlightPadX = 0f;
float highlightPadY = 0f;
if (useVanityColors)
{
float boost = Luminance.ComputeHighlight(textColor, glowColor);
if (boost > 0f)
{
var style = ImGui.GetStyle();
useHighlight = true;
highlightPadX = MathF.Max(style.FramePadding.X * 0.6f, 2f * ImGuiHelpers.GlobalScale);
highlightPadY = MathF.Max(style.FramePadding.Y * 0.55f, 1.25f * ImGuiHelpers.GlobalScale);
drawList.ChannelsSplit(2);
drawList.ChannelsSetCurrent(1);
_highlightBoost = boost;
}
else
{
_highlightBoost = 0f;
}
}
Vector2 itemMin;
Vector2 itemMax;
Vector2 textSize;
using (ImRaii.PushFont(font, textIsUid))
{
SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, font, pair.UserData.UID);
itemMin = ImGui.GetItemRectMin();
itemMax = ImGui.GetItemRectMax();
//textSize = itemMax - itemMin;
}
ImGui.AlignTextToFramePadding();
var rowStart = ImGui.GetCursorScreenPos();
rowRightLimit = rowStart.X + rowWidth;
if (useHighlight)
Vector4? textColor = null;
Vector4? glowColor = null;
if (pair.UserData.HasVanity)
{
if (!string.IsNullOrWhiteSpace(pair.UserData.TextColorHex))
{
textColor = UIColors.HexToRgba(pair.UserData.TextColorHex);
}
if (!string.IsNullOrWhiteSpace(pair.UserData.TextGlowColorHex))
{
glowColor = UIColors.HexToRgba(pair.UserData.TextGlowColorHex);
}
}
var useVanityColors = _lightlessConfigService.Current.useColoredUIDs && (textColor != null || glowColor != null);
var seString = useVanityColors
? SeStringUtils.BuildFormattedPlayerName(playerText, textColor, glowColor)
: SeStringUtils.BuildPlain(playerText);
var drawList = ImGui.GetWindowDrawList();
bool useHighlight = false;
float highlightPadX = 0f;
float highlightPadY = 0f;
if (useVanityColors)
{
float boost = Luminance.ComputeHighlight(textColor, glowColor);
if (boost > 0f)
{
var style = ImGui.GetStyle();
useHighlight = true;
highlightPadX = MathF.Max(style.FramePadding.X * 0.6f, 2f * ImGuiHelpers.GlobalScale);
highlightPadY = MathF.Max(style.FramePadding.Y * 0.55f, 1.25f * ImGuiHelpers.GlobalScale);
drawList.ChannelsSplit(2);
drawList.ChannelsSetCurrent(1);
_highlightBoost = boost;
}
else
{
_highlightBoost = 0f;
}
}
SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, targetFontSize, font, pair.UserData.UID);
nameRectMin = ImGui.GetItemRectMin();
nameRectMax = ImGui.GetItemRectMax();
if (useHighlight)
{
var style = ImGui.GetStyle();
var frameHeight = ImGui.GetFrameHeight();
var rowTop = rowStart.Y - style.FramePadding.Y;
var rowBottom = rowTop + frameHeight;
var highlightMin = new Vector2(nameRectMin.X - highlightPadX, rowTop - highlightPadY);
var highlightMax = new Vector2(nameRectMax.X + highlightPadX, rowBottom + highlightPadY);
var windowPos = ImGui.GetWindowPos();
var contentMin = windowPos + ImGui.GetWindowContentRegionMin();
var contentMax = windowPos + ImGui.GetWindowContentRegionMax();
highlightMin.X = MathF.Max(highlightMin.X, contentMin.X);
highlightMax.X = MathF.Min(highlightMax.X, contentMax.X);
highlightMin.Y = MathF.Max(highlightMin.Y, contentMin.Y);
highlightMax.Y = MathF.Min(highlightMax.Y, contentMax.Y);
var highlightColor = new Vector4(
0.25f + _highlightBoost,
0.25f + _highlightBoost,
0.25f + _highlightBoost,
1f
);
highlightColor = Luminance.BackgroundContrast(textColor, glowColor, highlightColor, ref _currentBg);
float rounding = style.FrameRounding > 0f ? style.FrameRounding : 5f * ImGuiHelpers.GlobalScale;
drawList.ChannelsSetCurrent(0);
drawList.AddRectFilled(highlightMin, highlightMax, ImGui.GetColorU32(highlightColor), rounding);
var borderColor = style.Colors[(int)ImGuiCol.Border];
borderColor.W *= 0.25f;
drawList.AddRect(highlightMin, highlightMax, ImGui.GetColorU32(borderColor), rounding);
drawList.ChannelsMerge();
}
}
{
var style = ImGui.GetStyle();
var frameHeight = ImGui.GetFrameHeight();
var rowTop = rowStart.Y - style.FramePadding.Y;
var rowBottom = rowTop + frameHeight;
var highlightMin = new Vector2(itemMin.X - highlightPadX, rowTop - highlightPadY);
var highlightMax = new Vector2(itemMax.X + highlightPadX, rowBottom + highlightPadY);
var windowPos = ImGui.GetWindowPos();
var contentMin = windowPos + ImGui.GetWindowContentRegionMin();
var contentMax = windowPos + ImGui.GetWindowContentRegionMax();
highlightMin.X = MathF.Max(highlightMin.X, contentMin.X);
highlightMax.X = MathF.Min(highlightMax.X, contentMax.X);
highlightMin.Y = MathF.Max(highlightMin.Y, contentMin.Y);
highlightMax.Y = MathF.Min(highlightMax.Y, contentMax.Y);
var highlightColor = new Vector4(
0.25f + _highlightBoost,
0.25f + _highlightBoost,
0.25f + _highlightBoost,
1f
);
highlightColor = Luminance.BackgroundContrast(textColor, glowColor, highlightColor, ref _currentBg);
float rounding = style.FrameRounding > 0f ? style.FrameRounding : 5f * ImGuiHelpers.GlobalScale;
drawList.ChannelsSetCurrent(0);
drawList.AddRectFilled(highlightMin, highlightMax, ImGui.GetColorU32(highlightColor), rounding);
var borderColor = style.Colors[(int)ImGuiCol.Border];
borderColor.W *= 0.25f;
drawList.AddRect(highlightMin, highlightMax, ImGui.GetColorU32(borderColor), rounding);
drawList.ChannelsMerge();
frameHeightForStats = ImGui.GetFrameHeight();
rowTopForStats = nameRectMin.Y - style.FramePadding.Y;
}
if (ImGui.IsItemHovered())
{
if (!string.Equals(_lastMouseOverUid, id))
if (!string.Equals(_lastMouseOverUid, id, StringComparison.Ordinal))
{
_popupTime = DateTime.UtcNow.AddSeconds(_lightlessConfigService.Current.ProfileDelay);
}
@@ -223,7 +253,7 @@ public class IdDisplayHandler
}
else
{
if (string.Equals(_lastMouseOverUid, id))
if (string.Equals(_lastMouseOverUid, id, StringComparison.Ordinal))
{
_mediator.Publish(new ProfilePopoutToggle(Pair: null));
_lastMouseOverUid = string.Empty;
@@ -261,12 +291,40 @@ public class IdDisplayHandler
{
_mediator.Publish(new ProfileOpenStandaloneMessage(pair));
}
if (!string.IsNullOrEmpty(compactPerformanceText))
{
ImGui.SameLine();
const float compactFontScale = 0.85f;
ImGui.SetWindowFontScale(compactFontScale);
var compactHeight = ImGui.GetTextLineHeight();
var targetPos = ImGui.GetCursorScreenPos();
var availableWidth = MathF.Max(rowRightLimit - targetPos.X, 0f);
var centeredY = rowTopForStats + MathF.Max((frameHeightForStats - compactHeight) * 0.5f, 0f);
ImGui.SetCursorScreenPos(new Vector2(targetPos.X, centeredY));
var performanceText = string.Empty;
var wasTruncated = false;
if (availableWidth > 0f)
{
performanceText = TruncateTextToWidth(compactPerformanceText, availableWidth, out wasTruncated);
}
ImGui.TextDisabled(performanceText);
ImGui.SetWindowFontScale(1f);
if (wasTruncated && ImGui.IsItemHovered())
{
ImGui.SetTooltip(compactPerformanceText);
}
}
}
else
{
ImGui.AlignTextToFramePadding();
ImGui.SetNextItemWidth(editBoxWidth.Invoke());
ImGui.SetNextItemWidth(MathF.Max(editBoxWidth.Invoke(), 0f));
if (ImGui.InputTextWithHint("##" + pair.UserData.UID, "Nick/Notes", ref _editComment, 255, ImGuiInputTextFlags.EnterReturnsTrue))
{
_serverManager.SetNoteForUid(pair.UserData.UID, _editComment);
@@ -346,6 +404,57 @@ public class IdDisplayHandler
return (textIsUid, playerText!);
}
private string? BuildCompactPerformanceUsageText(Pair pair)
{
var config = _playerPerformanceConfigService.Current;
if (!config.ShowPerformanceIndicator || !config.ShowPerformanceUsageNextToName)
{
return null;
}
var vramBytes = pair.LastAppliedApproximateEffectiveVRAMBytes >= 0
? pair.LastAppliedApproximateEffectiveVRAMBytes
: pair.LastAppliedApproximateVRAMBytes;
var triangleCount = pair.LastAppliedDataTris;
if (vramBytes < 0 && triangleCount < 0)
{
return null;
}
var segments = new List<string>(2);
if (vramBytes >= 0)
{
segments.Add(UiSharedService.ByteToString(vramBytes));
}
if (triangleCount >= 0)
{
segments.Add(FormatTriangleCount(triangleCount));
}
return segments.Count == 0 ? null : string.Join(" / ", segments);
}
private static string FormatTriangleCount(long triangleCount)
{
if (triangleCount < 0)
{
return string.Empty;
}
if (triangleCount >= 1_000_000)
{
return FormattableString.Invariant($"{triangleCount / 1_000_000d:0.#}m tris");
}
if (triangleCount >= 1_000)
{
return FormattableString.Invariant($"{triangleCount / 1_000d:0.#}k tris");
}
return $"{triangleCount} tris";
}
internal void Clear()
{
_editEntry = string.Empty;
@@ -370,4 +479,52 @@ public class IdDisplayHandler
return showidInsteadOfName;
}
}
private static string TruncateTextToWidth(string text, float maxWidth, out bool wasTruncated)
{
wasTruncated = false;
if (string.IsNullOrEmpty(text) || maxWidth <= 0f)
{
return string.Empty;
}
var fullWidth = ImGui.CalcTextSize(text).X;
if (fullWidth <= maxWidth)
{
return text;
}
wasTruncated = true;
const string Ellipsis = "...";
var ellipsisWidth = ImGui.CalcTextSize(Ellipsis).X;
if (ellipsisWidth >= maxWidth)
{
return Ellipsis;
}
var builder = new StringBuilder(text.Length);
var remainingWidth = maxWidth - ellipsisWidth;
foreach (var rune in text.EnumerateRunes())
{
var runeText = rune.ToString();
var runeWidth = ImGui.CalcTextSize(runeText).X;
if (runeWidth > remainingWidth)
{
break;
}
builder.Append(runeText);
remainingWidth -= runeWidth;
}
if (builder.Length == 0)
{
return Ellipsis;
}
builder.Append(Ellipsis);
return builder.ToString();
}
}

View File

@@ -10,6 +10,7 @@ using LightlessSync.Localization;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.Utils;
using Microsoft.Extensions.Logging;
using System.Numerics;
using System.Text.RegularExpressions;
@@ -46,11 +47,9 @@ public partial class IntroUi : WindowMediatorSubscriberBase
ShowCloseButton = false;
RespectCloseHotkey = false;
SizeConstraints = new WindowSizeConstraints()
{
MinimumSize = new Vector2(600, 400),
MaximumSize = new Vector2(600, 2000),
};
WindowBuilder.For(this)
.SetSizeConstraints(new Vector2(600, 400), new Vector2(600, 2000))
.Apply();
GetToSLocalization();
@@ -267,7 +266,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
{
UiSharedService.ColorTextWrapped("Your secret key must be exactly 64 characters long. Don't enter your Lodestone auth here.", ImGuiColors.DalamudRed);
}
else if (_secretKey.Length == 64 && !HexRegex().IsMatch(_secretKey))
else if (_secretKey.Length == 64 && !SecretRegex().IsMatch(_secretKey))
{
UiSharedService.ColorTextWrapped("Your secret key can only contain ABCDEF and the numbers 0-9.", ImGuiColors.DalamudRed);
}
@@ -360,6 +359,6 @@ public partial class IntroUi : WindowMediatorSubscriberBase
_tosParagraphs = [Strings.ToS.Paragraph1, Strings.ToS.Paragraph2, Strings.ToS.Paragraph3, Strings.ToS.Paragraph4, Strings.ToS.Paragraph5, Strings.ToS.Paragraph6];
}
[GeneratedRegex("^([A-F0-9]{2})+")]
private static partial Regex HexRegex();
[GeneratedRegex("^[A-F0-9]{64}$", RegexOptions.Compiled | RegexOptions.CultureInvariant)]
private static partial Regex SecretRegex();
}

View File

@@ -1,5 +1,4 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data.Enum;
@@ -174,6 +173,7 @@ internal class JoinSyncshellUI : WindowMediatorSubscriberBase
joinPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations);
joinPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX);
_ = _apiController.GroupJoinFinalize(new GroupJoinDto(_groupJoinInfo.Group, _previousPassword, joinPermissions));
Mediator.Publish(new UserJoinedSyncshell(_groupJoinInfo.Group.GID));
IsOpen = false;
}
}

View File

@@ -3,9 +3,11 @@ using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.Group;
using LightlessSync.LightlessConfiguration;
using LightlessSync.Services;
using LightlessSync.Services.LightFinder;
using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
@@ -14,28 +16,28 @@ using System.Numerics;
namespace LightlessSync.UI
{
public class BroadcastUI : WindowMediatorSubscriberBase
public class LightFinderUI : WindowMediatorSubscriberBase
{
private readonly ApiController _apiController;
private readonly LightlessConfigService _configService;
private readonly BroadcastService _broadcastService;
private readonly LightFinderService _broadcastService;
private readonly UiSharedService _uiSharedService;
private readonly BroadcastScannerService _broadcastScannerService;
private readonly LightFinderScannerService _broadcastScannerService;
private IReadOnlyList<GroupFullInfoDto> _allSyncshells;
private IReadOnlyList<GroupFullInfoDto> _allSyncshells = Array.Empty<GroupFullInfoDto>();
private string _userUid = string.Empty;
private readonly List<(string Label, string? GID, bool IsAvailable)> _syncshellOptions = new();
public BroadcastUI(
ILogger<BroadcastUI> logger,
public LightFinderUI(
ILogger<LightFinderUI> logger,
LightlessMediator mediator,
PerformanceCollectorService performanceCollectorService,
BroadcastService broadcastService,
LightFinderService broadcastService,
LightlessConfigService configService,
UiSharedService uiShared,
ApiController apiController,
BroadcastScannerService broadcastScannerService
LightFinderScannerService broadcastScannerService
) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService)
{
_broadcastService = broadcastService;
@@ -45,19 +47,17 @@ namespace LightlessSync.UI
_broadcastScannerService = broadcastScannerService;
IsOpen = false;
this.SizeConstraints = new()
{
MinimumSize = new(600, 465),
MaximumSize = new(750, 525)
};
WindowBuilder.For(this)
.SetSizeConstraints(new Vector2(600, 465), new Vector2(750, 525))
.Apply();
}
private void RebuildSyncshellDropdownOptions()
{
var selectedGid = _configService.Current.SelectedFinderSyncshell;
var allSyncshells = _allSyncshells ?? Array.Empty<GroupFullInfoDto>();
var ownedSyncshells = allSyncshells
.Where(g => string.Equals(g.OwnerUID, _userUid, StringComparison.Ordinal))
var allSyncshells = _allSyncshells ?? [];
var filteredSyncshells = allSyncshells
.Where(g => string.Equals(g.OwnerUID, _userUid, StringComparison.Ordinal) || g.GroupUserInfo.IsModerator())
.ToList();
_syncshellOptions.Clear();
@@ -65,7 +65,7 @@ namespace LightlessSync.UI
var addedGids = new HashSet<string>(StringComparer.Ordinal);
foreach (var shell in ownedSyncshells)
foreach (var shell in filteredSyncshells)
{
var label = shell.GroupAliasOrGID ?? shell.GID;
_syncshellOptions.Add((label, shell.GID, true));
@@ -143,9 +143,9 @@ namespace LightlessSync.UI
_uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"),"This lets other Lightless users know you use Lightless. While enabled, you and others using Lightfinder can see each other identified as Lightless users.");
ImGui.Indent(15f);
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
ImGui.Text("- This is done using a 'Lightless' label above player nameplates.");
ImGui.PopStyleColor();
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
ImGui.Text("- This is done using a 'Lightless' label above player nameplates.");
ImGui.PopStyleColor();
ImGui.Unindent(15f);
ImGuiHelpers.ScaledDummy(3f);
@@ -192,7 +192,7 @@ namespace LightlessSync.UI
ImGui.PopStyleVar();
ImGuiHelpers.ScaledDummy(3f);
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
if (_configService.Current.BroadcastEnabled)
{
@@ -288,7 +288,7 @@ namespace LightlessSync.UI
_uiSharedService.MediumText("Syncshell Finder", UIColors.Get("PairBlue"));
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
ImGui.PushTextWrapPos();
ImGui.Text("Allow your owned Syncshell to be indexed by the Nearby Syncshell Finder.");
@@ -296,11 +296,19 @@ namespace LightlessSync.UI
ImGui.PopTextWrapPos();
ImGuiHelpers.ScaledDummy(0.2f);
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
bool ShellFinderEnabled = _configService.Current.SyncshellFinderEnabled;
bool isBroadcasting = _broadcastService.IsBroadcasting;
if (isBroadcasting)
{
var warningColor = UIColors.Get("LightlessYellow");
_uiSharedService.DrawNoteLine("! ", warningColor,
new SeStringUtils.RichTextEntry("Syncshell Finder can only be changed while Lightfinder is disabled.", warningColor));
ImGuiHelpers.ScaledDummy(0.2f);
}
if (isBroadcasting)
ImGui.BeginDisabled();
@@ -369,7 +377,7 @@ namespace LightlessSync.UI
ImGui.EndTabItem();
}
#if DEBUG
#if DEBUG
if (ImGui.BeginTabItem("Debug"))
{
ImGui.Text("Broadcast Cache");
@@ -428,7 +436,7 @@ namespace LightlessSync.UI
ImGui.EndTabItem();
}
#endif
#endif
ImGui.EndTabBar();
}

View File

@@ -1,13 +1,12 @@
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.UI.Models;
using LightlessSync.UI.Style;
using Microsoft.Extensions.Logging;
using System.Numerics;
@@ -27,11 +26,12 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
private const float _titleMessageSpacing = 4f;
private const float _actionButtonSpacing = 8f;
private readonly List<LightlessNotification> _notifications = new();
private readonly List<LightlessNotification> _notifications = [];
private readonly object _notificationLock = new();
private readonly LightlessConfigService _configService;
private readonly Dictionary<string, float> _notificationYOffsets = new();
private readonly Dictionary<string, float> _notificationTargetYOffsets = new();
private readonly Dictionary<string, float> _notificationYOffsets = [];
private readonly Dictionary<string, float> _notificationTargetYOffsets = [];
private readonly Dictionary<string, Vector4> _notificationBackgrounds = [];
public LightlessNotificationUi(ILogger<LightlessNotificationUi> logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService)
: base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector)
@@ -45,7 +45,6 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
ImGuiWindowFlags.NoNav |
ImGuiWindowFlags.NoBackground |
ImGuiWindowFlags.NoCollapse |
ImGuiWindowFlags.NoInputs |
ImGuiWindowFlags.NoTitleBar |
ImGuiWindowFlags.NoScrollbar |
ImGuiWindowFlags.AlwaysAutoResize;
@@ -68,7 +67,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
{
lock (_notificationLock)
{
var existingNotification = _notifications.FirstOrDefault(n => n.Id == notification.Id);
var existingNotification = _notifications.FirstOrDefault(n => string.Equals(n.Id, notification.Id, StringComparison.Ordinal));
if (existingNotification != null)
{
UpdateExistingNotification(existingNotification, notification);
@@ -76,7 +75,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
else
{
_notifications.Add(notification);
_logger.LogDebug("Added new notification: {Title}", notification.Title);
_logger.LogTrace("Added new notification: {Title}", notification.Title);
}
if (!IsOpen) IsOpen = true;
@@ -96,14 +95,14 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
existing.CreatedAt = DateTime.UtcNow;
}
_logger.LogDebug("Updated existing notification: {Title}", updated.Title);
_logger.LogTrace("Updated existing notification: {Title}", updated.Title);
}
public void RemoveNotification(string id)
{
lock (_notificationLock)
{
var notification = _notifications.FirstOrDefault(n => n.Id == id);
var notification = _notifications.FirstOrDefault(n => string.Equals(n.Id, id, StringComparison.Ordinal));
if (notification != null)
{
StartOutAnimation(notification);
@@ -122,13 +121,13 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
}
}
private void StartOutAnimation(LightlessNotification notification)
private static void StartOutAnimation(LightlessNotification notification)
{
notification.IsAnimatingOut = true;
notification.IsAnimatingIn = false;
}
private bool ShouldRemoveNotification(LightlessNotification notification) =>
private static bool ShouldRemoveNotification(LightlessNotification notification) =>
notification.IsAnimatingOut && notification.AnimationProgress <= 0.01f;
protected override void DrawInternal()
@@ -162,30 +161,30 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
{
var corner = _configService.Current.NotificationCorner;
var offsetX = _configService.Current.NotificationOffsetX;
var offsetY = _configService.Current.NotificationOffsetY;
var width = _configService.Current.NotificationWidth;
float posX = corner == NotificationCorner.Left
? viewport.WorkPos.X + offsetX - _windowPaddingOffset
: viewport.WorkPos.X + viewport.WorkSize.X - width - offsetX - _windowPaddingOffset;
return new Vector2(posX, viewport.WorkPos.Y);
float posY = viewport.WorkPos.Y + offsetY;
return new Vector2(posX, posY);
}
private void DrawAllNotifications()
{
var offsetY = _configService.Current.NotificationOffsetY;
var startY = ImGui.GetCursorPosY() + offsetY;
var startY = ImGui.GetCursorPosY();
for (int i = 0; i < _notifications.Count; i++)
{
var notification = _notifications[i];
if (_notificationYOffsets.TryGetValue(notification.Id, out var yOffset))
{
ImGui.SetCursorPosY(startY + yOffset);
}
DrawNotification(notification, i);
DrawNotification(notification);
}
}
@@ -228,6 +227,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
_notifications.RemoveAt(i);
_notificationYOffsets.Remove(notification.Id);
_notificationTargetYOffsets.Remove(notification.Id);
_notificationBackgrounds.Remove(notification.Id);
}
}
}
@@ -304,7 +304,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
return corner == NotificationCorner.Left ? new Vector2(-distance, 0) : new Vector2(distance, 0);
}
private void DrawNotification(LightlessNotification notification, int index)
private void DrawNotification(LightlessNotification notification)
{
var alpha = notification.AnimationProgress;
if (alpha <= 0f) return;
@@ -336,14 +336,15 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
var bgColor = CalculateBackgroundColor(alpha, ImGui.IsWindowHovered());
var accentColor = GetNotificationAccentColor(notification.Type);
accentColor.W *= alpha;
var bgColor = CalculateBackgroundColor(notification, alpha, ImGui.IsWindowHovered(), accentColor);
var accentColorWithAlpha = accentColor;
accentColorWithAlpha.W *= alpha;
DrawShadow(drawList, windowPos, windowSize, alpha);
HandleClickToDismiss(notification);
DrawBackground(drawList, windowPos, windowSize, bgColor);
DrawAccentBar(drawList, windowPos, windowSize, accentColor);
DrawAccentBar(drawList, windowPos, windowSize, accentColorWithAlpha);
DrawDurationProgressBar(notification, alpha, windowPos, windowSize, drawList);
// Draw download progress bar above duration bar for download notifications
@@ -355,22 +356,44 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
DrawNotificationText(notification, alpha);
}
private Vector4 CalculateBackgroundColor(float alpha, bool isHovered)
private Vector4 CalculateBackgroundColor(LightlessNotification notification, float alpha, bool isHovered, Vector4 accentColor)
{
var baseOpacity = _configService.Current.NotificationOpacity;
var finalOpacity = baseOpacity * alpha;
var bgColor = new Vector4(30f/255f, 30f/255f, 30f/255f, finalOpacity);
float boost = Luminance.ComputeHighlight(null, accentColor);
var baseBg = new Vector4(
30f/255f + boost,
30f/255f + boost,
30f/255f + boost,
finalOpacity
);
if (!_notificationBackgrounds.ContainsKey(notification.Id))
{
_notificationBackgrounds[notification.Id] = baseBg;
}
var currentBg = _notificationBackgrounds[notification.Id];
var bgColor = Luminance.BackgroundContrast(null, accentColor, baseBg, ref currentBg);
_notificationBackgrounds[notification.Id] = currentBg;
bgColor.W = finalOpacity;
if (isHovered)
{
bgColor *= 1.1f;
bgColor.W = Math.Min(bgColor.W, 0.98f);
bgColor = new Vector4(
bgColor.X * 1.1f,
bgColor.Y * 1.1f,
bgColor.Z * 1.1f,
Math.Min(bgColor.W, 0.98f)
);
}
return bgColor;
}
private void DrawShadow(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float alpha)
private static void DrawShadow(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float alpha)
{
var shadowOffset = new Vector2(1f, 1f);
var shadowColor = new Vector4(0f, 0f, 0f, 0.4f * alpha);
@@ -384,9 +407,13 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
private void HandleClickToDismiss(LightlessNotification notification)
{
if (ImGui.IsWindowHovered() &&
var pos = ImGui.GetWindowPos();
var size = ImGui.GetWindowSize();
bool hovered = ImGui.IsMouseHoveringRect(pos, new Vector2(pos.X + size.X, pos.Y + size.Y));
if ((hovered || ImGui.IsWindowHovered()) &&
_configService.Current.DismissNotificationOnClick &&
!notification.Actions.Any() &&
notification.Actions.Count == 0 &&
ImGui.IsMouseClicked(ImGuiMouseButton.Left))
{
notification.IsDismissed = true;
@@ -394,7 +421,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
}
}
private void DrawBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 bgColor)
private static void DrawBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 bgColor)
{
drawList.AddRectFilled(
windowPos,
@@ -431,14 +458,14 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
);
}
private void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList)
private static void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList)
{
var progress = CalculateDurationProgress(notification);
var progressBarColor = UIColors.Get("LightlessBlue");
var progressHeight = 2f;
var progressY = windowPos.Y + windowSize.Y - progressHeight;
var progressWidth = windowSize.X * progress;
DrawProgressBackground(drawList, windowPos, windowSize, progressY, progressHeight, progressBarColor, alpha);
if (progress > 0)
@@ -447,7 +474,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
}
}
private void DrawDownloadProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList)
private static void DrawDownloadProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList)
{
var progress = Math.Clamp(notification.Progress, 0f, 1f);
var progressBarColor = UIColors.Get("LightlessGreen");
@@ -455,7 +482,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
// Position above the duration bar (2px duration bar + 1px spacing)
var progressY = windowPos.Y + windowSize.Y - progressHeight - 3f;
var progressWidth = windowSize.X * progress;
DrawProgressBackground(drawList, windowPos, windowSize, progressY, progressHeight, progressBarColor, alpha);
if (progress > 0)
@@ -464,14 +491,14 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
}
}
private float CalculateDurationProgress(LightlessNotification notification)
private static float CalculateDurationProgress(LightlessNotification notification)
{
// Calculate duration timer progress
var elapsed = DateTime.UtcNow - notification.CreatedAt;
return Math.Min(1.0f, (float)(elapsed.TotalSeconds / notification.Duration.TotalSeconds));
}
private void DrawProgressBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float progressY, float progressHeight, Vector4 progressBarColor, float alpha)
private static void DrawProgressBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float progressY, float progressHeight, Vector4 progressBarColor, float alpha)
{
var bgProgressColor = new Vector4(progressBarColor.X * 0.3f, progressBarColor.Y * 0.3f, progressBarColor.Z * 0.3f, 0.5f * alpha);
drawList.AddRectFilled(
@@ -482,7 +509,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
);
}
private void DrawProgressForeground(ImDrawListPtr drawList, Vector2 windowPos, float progressY, float progressHeight, float progressWidth, Vector4 progressBarColor, float alpha)
private static void DrawProgressForeground(ImDrawListPtr drawList, Vector2 windowPos, float progressY, float progressHeight, float progressWidth, Vector4 progressBarColor, float alpha)
{
var progressColor = progressBarColor;
progressColor.W *= alpha;
@@ -512,13 +539,13 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
}
}
private float CalculateContentWidth(float windowWidth) =>
private static float CalculateContentWidth(float windowWidth) =>
windowWidth - (_contentPaddingX * 2);
private bool HasActions(LightlessNotification notification) =>
private static bool HasActions(LightlessNotification notification) =>
notification.Actions.Count > 0;
private void PositionActionsAtBottom(float windowHeight)
private static void PositionActionsAtBottom(float windowHeight)
{
var actionHeight = ImGui.GetFrameHeight();
var bottomY = windowHeight - _contentPaddingY - actionHeight;
@@ -546,7 +573,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
return $"[{timestamp}] {notification.Title}";
}
private float DrawWrappedText(string text, float wrapWidth)
private static float DrawWrappedText(string text, float wrapWidth)
{
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + wrapWidth);
var startY = ImGui.GetCursorPosY();
@@ -556,7 +583,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
return height;
}
private void DrawMessage(LightlessNotification notification, Vector2 contentPos, float contentWidth, float titleHeight, float alpha)
private static void DrawMessage(LightlessNotification notification, Vector2 contentPos, float contentWidth, float titleHeight, float alpha)
{
if (string.IsNullOrEmpty(notification.Message)) return;
@@ -575,7 +602,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
{
var buttonWidth = CalculateActionButtonWidth(notification.Actions.Count, availableWidth);
_logger.LogDebug("Drawing {ActionCount} notification actions, buttonWidth: {ButtonWidth}, availableWidth: {AvailableWidth}",
_logger.LogTrace("Drawing {ActionCount} notification actions, buttonWidth: {ButtonWidth}, availableWidth: {AvailableWidth}",
notification.Actions.Count, buttonWidth, availableWidth);
var startX = ImGui.GetCursorPosX();
@@ -591,13 +618,13 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
}
}
private float CalculateActionButtonWidth(int actionCount, float availableWidth)
private static float CalculateActionButtonWidth(int actionCount, float availableWidth)
{
var totalSpacing = (actionCount - 1) * _actionButtonSpacing;
return (availableWidth - totalSpacing) / actionCount;
}
private void PositionActionButton(int index, float startX, float buttonWidth)
private static void PositionActionButton(int index, float startX, float buttonWidth)
{
var xPosition = startX + index * (buttonWidth + _actionButtonSpacing);
ImGui.SetCursorPosX(xPosition);
@@ -605,7 +632,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
private void DrawActionButton(LightlessNotificationAction action, LightlessNotification notification, float alpha, float buttonWidth)
{
_logger.LogDebug("Drawing action button: {ActionId} - {ActionLabel}, width: {ButtonWidth}", action.Id, action.Label, buttonWidth);
_logger.LogTrace("Drawing action button: {ActionId} - {ActionLabel}, width: {ButtonWidth}", action.Id, action.Label, buttonWidth);
var buttonColor = action.Color;
buttonColor.W *= alpha;
@@ -625,22 +652,22 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
if (action.Icon != FontAwesomeIcon.None)
{
buttonPressed = DrawIconTextButton(action.Icon, action.Label, buttonWidth, alpha);
buttonPressed = DrawIconTextButton(action.Icon, action.Label, buttonWidth);
}
else
{
buttonPressed = ImGui.Button(action.Label, new Vector2(buttonWidth, 0));
}
_logger.LogDebug("Button {ActionId} pressed: {ButtonPressed}", action.Id, buttonPressed);
_logger.LogTrace("Button {ActionId} pressed: {ButtonPressed}", action.Id, buttonPressed);
if (buttonPressed)
{
try
{
_logger.LogDebug("Executing action: {ActionId}", action.Id);
_logger.LogTrace("Executing action: {ActionId}", action.Id);
action.OnClick(notification);
_logger.LogDebug("Action executed successfully: {ActionId}", action.Id);
_logger.LogTrace("Action executed successfully: {ActionId}", action.Id);
}
catch (Exception ex)
{
@@ -650,10 +677,10 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
}
}
private bool DrawIconTextButton(FontAwesomeIcon icon, string text, float width, float alpha)
private static bool DrawIconTextButton(FontAwesomeIcon icon, string text, float width)
{
var drawList = ImGui.GetWindowDrawList();
var cursorPos = ImGui.GetCursorScreenPos();
ImGui.GetCursorScreenPos();
var frameHeight = ImGui.GetFrameHeight();
Vector2 iconSize;
@@ -729,7 +756,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
return ImGui.CalcTextSize(titleText, true, contentWidth).Y;
}
private float CalculateMessageHeight(LightlessNotification notification, float contentWidth)
private static float CalculateMessageHeight(LightlessNotification notification, float contentWidth)
{
if (string.IsNullOrEmpty(notification.Message)) return 0f;
@@ -737,7 +764,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
return 4f + messageHeight;
}
private Vector4 GetNotificationAccentColor(NotificationType type)
private static Vector4 GetNotificationAccentColor(NotificationType type)
{
return type switch
{

View File

@@ -1,43 +0,0 @@
namespace LightlessSync.UI.Models
{
public class ChangelogFile
{
public string Tagline { get; init; } = string.Empty;
public string Subline { get; init; } = string.Empty;
public List<ChangelogEntry> Changelog { get; init; } = new();
public List<CreditCategory>? Credits { get; init; }
}
public class ChangelogEntry
{
public string Name { get; init; } = string.Empty;
public string Date { get; init; } = string.Empty;
public string Tagline { get; init; } = string.Empty;
public bool? IsCurrent { get; init; }
public string? Message { get; init; }
public List<ChangelogVersion>? Versions { get; init; }
}
public class ChangelogVersion
{
public string Number { get; init; } = string.Empty;
public List<string> Items { get; init; } = new();
}
public class CreditCategory
{
public string Category { get; init; } = string.Empty;
public List<CreditItem> Items { get; init; } = new();
}
public class CreditItem
{
public string Name { get; init; } = string.Empty;
public string Role { get; init; } = string.Empty;
}
public class CreditsFile
{
public List<CreditCategory> Credits { get; init; } = new();
}
}

View File

@@ -0,0 +1,12 @@
namespace LightlessSync.UI.Models
{
public class ChangelogEntry
{
public string Name { get; init; } = string.Empty;
public string Date { get; init; } = string.Empty;
public string Tagline { get; init; } = string.Empty;
public bool? IsCurrent { get; init; }
public string? Message { get; init; }
public List<ChangelogVersion>? Versions { get; init; }
}
}

View File

@@ -0,0 +1,10 @@
namespace LightlessSync.UI.Models
{
public class ChangelogFile
{
public string Tagline { get; init; } = string.Empty;
public string Subline { get; init; } = string.Empty;
public List<ChangelogEntry> Changelog { get; init; } = new();
public List<CreditCategory>? Credits { get; init; }
}
}

View File

@@ -0,0 +1,8 @@
namespace LightlessSync.UI.Models
{
public class ChangelogVersion
{
public string Number { get; init; } = string.Empty;
public List<string> Items { get; init; } = [];
}
}

View File

@@ -0,0 +1,8 @@
namespace LightlessSync.UI.Models
{
public class CreditCategory
{
public string Category { get; init; } = string.Empty;
public List<CreditItem> Items { get; init; } = [];
}
}

View File

@@ -0,0 +1,8 @@
namespace LightlessSync.UI.Models
{
public class CreditItem
{
public string Name { get; init; } = string.Empty;
public string Role { get; init; } = string.Empty;
}
}

View File

@@ -0,0 +1,7 @@
namespace LightlessSync.UI.Models
{
public class CreditsFile
{
public List<CreditCategory> Credits { get; init; } = [];
}
}

View File

@@ -1,7 +1,7 @@
using Dalamud.Interface;
using LightlessSync.LightlessConfiguration.Models;
using System.Numerics;
namespace LightlessSync.UI.Models;
public class LightlessNotification
{
public string Id { get; set; } = Guid.NewGuid().ToString();
@@ -20,13 +20,3 @@ public class LightlessNotification
public bool IsAnimatingOut { get; set; } = false;
public uint? SoundEffectId { get; set; } = null;
}
public class LightlessNotificationAction
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Label { get; set; } = string.Empty;
public FontAwesomeIcon Icon { get; set; } = FontAwesomeIcon.None;
public Vector4 Color { get; set; } = Vector4.One;
public Action<LightlessNotification> OnClick { get; set; } = _ => { };
public bool IsPrimary { get; set; } = false;
public bool IsDestructive { get; set; } = false;
}

View File

@@ -0,0 +1,15 @@
using Dalamud.Interface;
using System.Numerics;
namespace LightlessSync.UI.Models;
public class LightlessNotificationAction
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Label { get; set; } = string.Empty;
public FontAwesomeIcon Icon { get; set; } = FontAwesomeIcon.None;
public Vector4 Color { get; set; } = Vector4.One;
public Action<LightlessNotification> OnClick { get; set; } = _ => { };
public bool IsPrimary { get; set; } = false;
public bool IsDestructive { get; set; } = false;
}

View File

@@ -0,0 +1,7 @@
namespace LightlessSync.UI.Models;
public enum OnlinePairSortMode
{
Alphabetical = 0,
PreferredDirectPairs = 1,
}

View File

@@ -0,0 +1,25 @@
using System.Collections.Generic;
using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Dto.Group;
using LightlessSync.PlayerData.Pairs;
namespace LightlessSync.UI.Models;
public sealed record PairDisplayEntry(
PairUniqueIdentifier Ident,
PairConnection Connection,
IReadOnlyList<GroupFullInfoDto> Groups,
IPairHandlerAdapter? Handler)
{
public UserData User => Connection.User;
public bool IsOnline => Connection.IsOnline;
public bool IsVisible => Handler?.IsVisible ?? false;
public bool IsDirectlyPaired => Connection.IsDirectlyPaired;
public bool IsOneSided => Connection.IsOneSided;
public bool HasAnyConnection => Connection.HasAnyConnection;
public string? IdentString => Connection.Ident;
public UserPermissions SelfPermissions => Connection.SelfToOtherPermissions;
public UserPermissions OtherPermissions => Connection.OtherToSelfPermissions;
public IndividualPairStatus? PairStatus => Connection.IndividualPairStatus;
}

View File

@@ -0,0 +1,30 @@
using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Dto.Group;
using LightlessSync.PlayerData.Pairs;
namespace LightlessSync.UI.Models;
public sealed record PairUiEntry(
PairDisplayEntry DisplayEntry,
string AliasOrUid,
string DisplayName,
string Note,
bool IsVisible,
bool IsOnline,
bool IsDirectlyPaired,
bool IsOneSided,
bool HasAnyConnection,
bool IsPaused,
UserPermissions SelfPermissions,
UserPermissions OtherPermissions,
IndividualPairStatus? PairStatus,
long LastAppliedDataBytes,
long LastAppliedDataTris,
long LastAppliedApproximateVramBytes,
long LastAppliedApproximateEffectiveVramBytes,
IPairHandlerAdapter? Handler)
{
public PairUniqueIdentifier Ident => DisplayEntry.Ident;
public IReadOnlyList<GroupFullInfoDto> Groups => DisplayEntry.Groups;
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using LightlessSync.API.Dto.Group;
using LightlessSync.PlayerData.Pairs;
namespace LightlessSync.UI.Models;
public sealed record PairUiSnapshot(
IReadOnlyDictionary<string, Pair> PairsByUid,
IReadOnlyList<Pair> DirectPairs,
IReadOnlyDictionary<GroupFullInfoDto, IReadOnlyList<Pair>> GroupPairs,
IReadOnlyDictionary<Pair, IReadOnlyList<GroupFullInfoDto>> PairsWithGroups,
IReadOnlyDictionary<string, GroupFullInfoDto> GroupsByGid,
IReadOnlyCollection<GroupFullInfoDto> Groups)
{
public static PairUiSnapshot Empty { get; } = new(
new ReadOnlyDictionary<string, Pair>(new Dictionary<string, Pair>()),
Array.Empty<Pair>(),
new ReadOnlyDictionary<GroupFullInfoDto, IReadOnlyList<Pair>>(new Dictionary<GroupFullInfoDto, IReadOnlyList<Pair>>()),
new ReadOnlyDictionary<Pair, IReadOnlyList<GroupFullInfoDto>>(new Dictionary<Pair, IReadOnlyList<GroupFullInfoDto>>()),
new ReadOnlyDictionary<string, GroupFullInfoDto>(new Dictionary<string, GroupFullInfoDto>()),
Array.Empty<GroupFullInfoDto>());
}

View File

@@ -0,0 +1,10 @@
namespace LightlessSync.UI.Models;
public enum VisiblePairSortMode
{
Alphabetical = 0,
VramUsage = 1,
EffectiveVramUsage = 2,
TriangleCount = 3,
PreferredDirectPairs = 4,
}

View File

@@ -9,6 +9,7 @@ using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging;
using System.Numerics;
namespace LightlessSync.UI;
@@ -28,12 +29,10 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase
_uiSharedService = uiSharedService;
_apiController = apiController;
_ownPermissions = pair.UserPair.OwnPermissions.DeepClone();
Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoResize;
SizeConstraints = new()
{
MinimumSize = new(450, 100),
MaximumSize = new(450, 500)
};
WindowBuilder.For(this)
.SetSizeConstraints(new Vector2(450, 100), new Vector2(450, 500))
.AddFlags(ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoResize)
.Apply();
IsOpen = true;
}

View File

@@ -4,10 +4,11 @@ using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using LightlessSync.API.Data.Extensions;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Services;
using LightlessSync.PlayerData.Pairs;
using Microsoft.Extensions.Logging;
using System.Numerics;
@@ -16,7 +17,7 @@ namespace LightlessSync.UI;
public class PopoutProfileUi : WindowMediatorSubscriberBase
{
private readonly LightlessProfileManager _lightlessProfileManager;
private readonly PairManager _pairManager;
private readonly PairUiService _pairUiService;
private readonly ServerConfigurationManager _serverManager;
private readonly UiSharedService _uiSharedService;
private Vector2 _lastMainPos = Vector2.Zero;
@@ -29,12 +30,12 @@ public class PopoutProfileUi : WindowMediatorSubscriberBase
public PopoutProfileUi(ILogger<PopoutProfileUi> logger, LightlessMediator mediator, UiSharedService uiBuilder,
ServerConfigurationManager serverManager, LightlessConfigService lightlessConfigService,
LightlessProfileManager lightlessProfileManager, PairManager pairManager, PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "###LightlessSyncPopoutProfileUI", performanceCollectorService)
LightlessProfileManager lightlessProfileManager, PairUiService pairUiService, PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "###LightlessSyncPopoutProfileUI", performanceCollectorService)
{
_uiSharedService = uiBuilder;
_serverManager = serverManager;
_lightlessProfileManager = lightlessProfileManager;
_pairManager = pairManager;
_pairUiService = pairUiService;
Flags = ImGuiWindowFlags.NoDecoration;
Mediator.Subscribe<ProfilePopoutToggle>(this, (msg) =>
@@ -143,13 +144,17 @@ public class PopoutProfileUi : WindowMediatorSubscriberBase
UiSharedService.ColorText("They: paused", UIColors.Get("LightlessYellow"));
}
}
var snapshot = _pairUiService.GetSnapshot();
if (_pair.UserPair.Groups.Any())
{
ImGui.TextUnformatted("Paired through Syncshells:");
foreach (var group in _pair.UserPair.Groups)
{
var groupNote = _serverManager.GetNoteForGid(group);
var groupName = _pairManager.GroupPairs.First(f => string.Equals(f.Key.GID, group, StringComparison.Ordinal)).Key.GroupAliasOrGID;
var groupName = snapshot.GroupsByGid.TryGetValue(group, out var groupInfo)
? groupInfo.GroupAliasOrGID
: group;
var groupString = string.IsNullOrEmpty(groupNote) ? groupName : $"{groupNote} ({groupName})";
ImGui.TextUnformatted("- " + groupString);
}

View File

@@ -0,0 +1,84 @@
using System;
using System.Numerics;
using System.Threading;
namespace LightlessSync.UI;
internal static class ProfileEditorLayoutCoordinator
{
private static readonly Lock Gate = new();
private static string? _activeUid;
private static Vector2? _anchor;
private const float ProfileWidth = 840f;
private const float ProfileHeight = 525f;
private const float EditorWidth = 380f;
private const float Spacing = 0f;
private static readonly Vector2 DefaultOffset = new(50f, 70f);
public static void Enable(string uid)
{
using var _ = Gate.EnterScope();
if (!string.Equals(_activeUid, uid, StringComparison.Ordinal))
_anchor = null;
_activeUid = uid;
}
public static void Disable(string uid)
{
using var _ = Gate.EnterScope();
if (string.Equals(_activeUid, uid, StringComparison.Ordinal))
{
_activeUid = null;
_anchor = null;
}
}
public static bool IsActive(string uid)
{
using var _ = Gate.EnterScope();
return string.Equals(_activeUid, uid, StringComparison.Ordinal);
}
public static Vector2 GetProfileSize(float scale) => new(ProfileWidth * scale, ProfileHeight * scale);
public static Vector2 GetEditorSize(float scale) => new(EditorWidth * scale, ProfileHeight * scale);
public static Vector2 GetEditorOffset(float scale) => new((ProfileWidth + Spacing) * scale, 0f);
public static Vector2 EnsureAnchor(Vector2 viewportOrigin, float scale)
{
using var _ = Gate.EnterScope();
if (_anchor is null)
_anchor = viewportOrigin + DefaultOffset * scale;
return _anchor.Value;
}
public static void UpdateAnchorFromProfile(Vector2 profilePosition)
{
using var _ = Gate.EnterScope();
_anchor = profilePosition;
}
public static void UpdateAnchorFromEditor(Vector2 editorPosition, float scale)
{
using var _ = Gate.EnterScope();
_anchor = editorPosition - GetEditorOffset(scale);
}
public static Vector2 GetProfilePosition(float scale)
{
using var _ = Gate.EnterScope();
return _anchor ?? Vector2.Zero;
}
public static Vector2 GetEditorPosition(float scale)
{
using var _ = Gate.EnterScope();
return (_anchor ?? Vector2.Zero) + GetEditorOffset(scale);
}
public static bool NearlyEquals(Vector2 current, Vector2 target, float epsilon = 0.5f)
{
return MathF.Abs(current.X - target.X) <= epsilon && MathF.Abs(current.Y - target.Y) <= epsilon;
}
}

View File

@@ -1,12 +0,0 @@
namespace LightlessSync.UI
{
public enum ProfileTags
{
SFW = 0,
NSFW = 1,
RP = 2,
ERP = 3,
Venues = 4,
Gpose = 5
}
}

View File

@@ -0,0 +1,225 @@
using System.Collections.ObjectModel;
using LightlessSync.API.Dto.Group;
using LightlessSync.PlayerData.Factories;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator;
using LightlessSync.UI.Models;
using Microsoft.Extensions.Logging;
namespace LightlessSync.UI.Services;
public sealed class PairUiService : DisposableMediatorSubscriberBase
{
private readonly PairLedger _pairLedger;
private readonly PairFactory _pairFactory;
private readonly PairManager _pairManager;
private readonly object _snapshotGate = new();
private PairUiSnapshot _snapshot = PairUiSnapshot.Empty;
private Pair? _lastAddedPair;
private bool _needsRefresh = true;
public PairUiService(
ILogger<PairUiService> logger,
LightlessMediator mediator,
PairLedger pairLedger,
PairFactory pairFactory,
PairManager pairManager) : base(logger, mediator)
{
_pairLedger = pairLedger;
_pairFactory = pairFactory;
_pairManager = pairManager;
Mediator.Subscribe<PairDataChangedMessage>(this, _ => MarkDirty());
Mediator.Subscribe<GroupCollectionChangedMessage>(this, _ => MarkDirty());
Mediator.Subscribe<VisibilityChange>(this, _ => MarkDirty());
EnsureSnapshot();
}
public PairUiSnapshot GetSnapshot()
{
EnsureSnapshot();
lock (_snapshotGate)
{
return _snapshot;
}
}
public Pair? GetLastAddedPair()
{
EnsureSnapshot();
lock (_snapshotGate)
{
return _lastAddedPair;
}
}
public void ClearLastAddedPair()
{
_pairManager.ClearLastAddedUser();
lock (_snapshotGate)
{
_lastAddedPair = null;
}
}
private void MarkDirty()
{
lock (_snapshotGate)
{
_needsRefresh = true;
}
}
private void EnsureSnapshot()
{
bool shouldBuild;
lock (_snapshotGate)
{
shouldBuild = _needsRefresh;
if (shouldBuild)
{
_needsRefresh = false;
}
}
if (!shouldBuild)
{
return;
}
PairUiSnapshot snapshot;
Pair? lastAddedPair;
try
{
(snapshot, lastAddedPair) = BuildSnapshot();
}
catch
{
lock (_snapshotGate)
{
_needsRefresh = true;
}
throw;
}
lock (_snapshotGate)
{
_snapshot = snapshot;
_lastAddedPair = lastAddedPair;
}
Mediator.Publish(new PairUiUpdatedMessage(snapshot));
}
private (PairUiSnapshot Snapshot, Pair? LastAddedPair) BuildSnapshot()
{
var entries = _pairLedger.GetAllEntries();
var pairByUid = new Dictionary<string, Pair>(StringComparer.Ordinal);
var directPairsList = new List<Pair>();
var groupPairsTemp = new Dictionary<GroupFullInfoDto, List<Pair>>();
var pairsWithGroupsTemp = new Dictionary<Pair, List<GroupFullInfoDto>>();
foreach (var entry in entries)
{
var pair = _pairFactory.Create(entry);
if (pair is null)
{
continue;
}
pairByUid[entry.Ident.UserId] = pair;
if (entry.IsDirectlyPaired)
{
directPairsList.Add(pair);
}
var uniqueGroups = new HashSet<string>(StringComparer.Ordinal);
var groupList = new List<GroupFullInfoDto>();
foreach (var group in entry.Groups)
{
if (!uniqueGroups.Add(group.Group.GID))
{
continue;
}
if (!groupPairsTemp.TryGetValue(group, out var members))
{
members = new List<Pair>();
groupPairsTemp[group] = members;
}
members.Add(pair);
groupList.Add(group);
}
pairsWithGroupsTemp[pair] = groupList;
}
var allGroupsList = _pairLedger.GetAllSyncshells()
.Values
.Select(s => s.GroupFullInfo)
.ToList();
foreach (var group in allGroupsList)
{
if (!groupPairsTemp.ContainsKey(group))
{
groupPairsTemp[group] = new List<Pair>();
}
}
var directPairs = new ReadOnlyCollection<Pair>(directPairsList);
var groupPairsFinal = new Dictionary<GroupFullInfoDto, IReadOnlyList<Pair>>();
foreach (var (group, members) in groupPairsTemp)
{
groupPairsFinal[group] = new ReadOnlyCollection<Pair>(members);
}
var pairsWithGroupsFinal = new Dictionary<Pair, IReadOnlyList<GroupFullInfoDto>>();
foreach (var (pair, groups) in pairsWithGroupsTemp)
{
pairsWithGroupsFinal[pair] = new ReadOnlyCollection<GroupFullInfoDto>(groups);
}
var groupsReadOnly = new ReadOnlyCollection<GroupFullInfoDto>(allGroupsList);
var pairsByUidReadOnly = new ReadOnlyDictionary<string, Pair>(pairByUid);
var groupsByGidReadOnly = new ReadOnlyDictionary<string, GroupFullInfoDto>(allGroupsList.ToDictionary(g => g.Group.GID, g => g, StringComparer.Ordinal));
Pair? lastAddedPair = null;
var lastAdded = _pairManager.GetLastAddedUser();
if (lastAdded is not null)
{
if (!pairByUid.TryGetValue(lastAdded.User.UID, out lastAddedPair))
{
var groups = lastAdded.Groups.Keys
.Select(gid =>
{
var result = _pairManager.GetGroup(gid);
return result.Success ? result.Value.GroupFullInfo : null;
})
.Where(g => g is not null)
.Cast<GroupFullInfoDto>()
.ToList();
var entry = new PairDisplayEntry(new PairUniqueIdentifier(lastAdded.User.UID), lastAdded, groups, null);
lastAddedPair = _pairFactory.Create(entry);
}
}
var snapshot = new PairUiSnapshot(
pairsByUidReadOnly,
directPairs,
new ReadOnlyDictionary<GroupFullInfoDto, IReadOnlyList<Pair>>(groupPairsFinal),
new ReadOnlyDictionary<Pair, IReadOnlyList<GroupFullInfoDto>>(pairsWithGroupsFinal),
groupsByGidReadOnly,
groupsReadOnly);
return (snapshot, lastAddedPair);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,463 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using System.Numerics;
namespace LightlessSync.UI.Style;
/// <summary>
/// A reusable animated header component with a gradient background, some funny stars, and shooting star effects to match the lightless void theme a bit.
/// </summary>
public class AnimatedHeader
{
private struct Particle
{
public Vector2 Position;
public Vector2 Velocity;
public float Life;
public float MaxLife;
public float Size;
public ParticleType Type;
public List<Vector2>? Trail;
public float Twinkle;
public float Depth;
public float Hue;
}
private enum ParticleType
{
TwinklingStar,
ShootingStar
}
private readonly List<Particle> _particles = [];
private float _particleSpawnTimer;
private readonly Random _random = new();
private const float _particleSpawnInterval = 0.2f;
private const int _maxParticles = 50;
private const int _maxTrailLength = 50;
private const float _edgeFadeDistance = 30f;
private const float _extendedParticleHeight = 40f;
public float Height { get; set; } = 150f;
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 bool EnableParticles { get; set; } = true;
public bool EnableBottomGradient { get; set; } = true;
/// <summary>
/// Draws the animated header with some customizable content
/// </summary>
/// <param name="width">Width of the header</param>
/// <param name="drawContent">Action to draw custom content inside the header</param>
public void Draw(float width, Action<Vector2, Vector2> drawContent)
{
var windowPos = ImGui.GetWindowPos();
var windowPadding = ImGui.GetStyle().WindowPadding;
var headerStart = windowPos + new Vector2(windowPadding.X, windowPadding.Y);
var headerEnd = headerStart + new Vector2(width, Height);
var extendedParticleSize = new Vector2(width, Height + _extendedParticleHeight);
DrawGradientBackground(headerStart, headerEnd);
if (EnableParticles)
{
DrawParticleEffects(headerStart, extendedParticleSize);
}
drawContent(headerStart, headerEnd);
if (EnableBottomGradient)
{
DrawBottomGradient(headerStart, headerEnd, width);
}
}
/// <summary>
/// Draws a simple animated header with title and subtitle.
/// </summary>
public void DrawSimple(float width, string title, string subtitle, IFontHandle? titleFont = null, Vector4? titleColor = null, Vector4? subtitleColor = null)
{
Draw(width, (headerStart, headerEnd) =>
{
var textX = 20f;
var textY = 30f;
ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY));
if (titleFont != null)
{
using (titleFont.Push())
{
ImGui.TextColored(titleColor ?? new Vector4(0.95f, 0.95f, 0.95f, 1.0f), title);
}
}
else
{
ImGui.TextColored(titleColor ?? new Vector4(0.95f, 0.95f, 0.95f, 1.0f), title);
}
ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY + 45f));
ImGui.TextColored(subtitleColor ?? UIColors.Get("LightlessBlue"), subtitle);
});
}
/// <summary>
/// Draws a header with title, subtitle, and action buttons in the top-right corner.
/// </summary>
public void DrawWithButtons(float width, string title, string subtitle, List<HeaderButton> buttons, IFontHandle? titleFont = null)
{
Draw(width, (headerStart, headerEnd) =>
{
// Draw title and subtitle
var textX = 20f;
var textY = 30f;
ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY));
if (titleFont != null)
{
using (titleFont.Push())
{
ImGui.TextColored(new Vector4(0.95f, 0.95f, 0.95f, 1.0f), title);
}
}
else
{
ImGui.TextColored(new Vector4(0.95f, 0.95f, 0.95f, 1.0f), title);
}
ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY + 45f));
ImGui.TextColored(UIColors.Get("LightlessBlue"), subtitle);
// Draw buttons
if (buttons.Count > 0)
{
DrawHeaderButtons(headerStart, width, buttons);
}
});
}
private void DrawGradientBackground(Vector2 headerStart, Vector2 headerEnd)
{
var drawList = ImGui.GetWindowDrawList();
drawList.AddRectFilledMultiColor(
headerStart,
headerEnd,
ImGui.GetColorU32(TopColor),
ImGui.GetColorU32(TopColor),
ImGui.GetColorU32(BottomColor),
ImGui.GetColorU32(BottomColor)
);
// Draw static background stars
var random = new Random(42);
for (int i = 0; i < 50; i++)
{
var starPos = headerStart + new Vector2(
(float)random.NextDouble() * (headerEnd.X - headerStart.X),
(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)));
}
}
private void DrawBottomGradient(Vector2 headerStart, Vector2 headerEnd, float width)
{
var drawList = ImGui.GetWindowDrawList();
var gradientHeight = 60f;
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 alpha = 1f - smoothProgress;
var gradientColor = new Vector4(r, g, b, alpha);
drawList.AddLine(
new Vector2(headerStart.X, headerEnd.Y + i),
new Vector2(headerStart.X + width, headerEnd.Y + i),
ImGui.GetColorU32(gradientColor),
1f
);
}
}
private void DrawHeaderButtons(Vector2 headerStart, float headerWidth, List<HeaderButton> buttons)
{
var spacing = 8f * ImGuiHelpers.GlobalScale;
var rightPadding = 15f * ImGuiHelpers.GlobalScale;
var topPadding = 15f * ImGuiHelpers.GlobalScale;
var buttonY = headerStart.Y + topPadding;
using (ImRaii.PushFont(UiBuilder.IconFont))
{
// Calculate button size (assuming all buttons are the same size)
var buttonSize = ImGui.CalcTextSize(FontAwesomeIcon.Globe.ToIconString());
buttonSize += ImGui.GetStyle().FramePadding * 2;
float currentX = headerStart.X + headerWidth - rightPadding - buttonSize.X;
using (ImRaii.PushColor(ImGuiCol.Button, new Vector4(0, 0, 0, 0)))
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple") with { W = 0.3f }))
using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive") with { W = 0.5f }))
{
for (int i = buttons.Count - 1; i >= 0; i--)
{
var button = buttons[i];
ImGui.SetCursorScreenPos(new Vector2(currentX, buttonY));
if (ImGui.Button(button.Icon.ToIconString()))
{
button.OnClick?.Invoke();
}
if (ImGui.IsItemHovered() && !string.IsNullOrEmpty(button.Tooltip))
{
ImGui.SetTooltip(button.Tooltip);
}
currentX -= buttonSize.X + spacing;
}
}
}
}
private void DrawParticleEffects(Vector2 bannerStart, Vector2 bannerSize)
{
var deltaTime = ImGui.GetIO().DeltaTime;
_particleSpawnTimer += deltaTime;
if (_particleSpawnTimer > _particleSpawnInterval && _particles.Count < _maxParticles)
{
SpawnParticle(bannerSize);
_particleSpawnTimer = 0f;
}
if (_random.NextDouble() < 0.003)
{
SpawnShootingStar(bannerSize);
}
var drawList = ImGui.GetWindowDrawList();
for (int i = _particles.Count - 1; i >= 0; i--)
{
var particle = _particles[i];
var screenPos = bannerStart + particle.Position;
if (particle.Type == ParticleType.ShootingStar && particle.Trail != null)
{
particle.Trail.Insert(0, particle.Position);
if (particle.Trail.Count > _maxTrailLength)
particle.Trail.RemoveAt(particle.Trail.Count - 1);
}
if (particle.Type == ParticleType.TwinklingStar)
{
particle.Twinkle += 0.005f * particle.Depth;
}
particle.Position += particle.Velocity * deltaTime;
particle.Life -= deltaTime;
var isOutOfBounds = particle.Position.X < -50 || particle.Position.X > bannerSize.X + 50 ||
particle.Position.Y < -50 || particle.Position.Y > bannerSize.Y + 50;
if (particle.Life <= 0 || (particle.Type != ParticleType.TwinklingStar && isOutOfBounds))
{
_particles.RemoveAt(i);
continue;
}
if (particle.Type == ParticleType.TwinklingStar)
{
if (particle.Position.X < 0 || particle.Position.X > bannerSize.X)
particle.Velocity = particle.Velocity with { X = -particle.Velocity.X };
if (particle.Position.Y < 0 || particle.Position.Y > bannerSize.Y)
particle.Velocity = particle.Velocity with { Y = -particle.Velocity.Y };
}
var fadeIn = Math.Min(1f, (particle.MaxLife - particle.Life) / 20f);
var fadeOut = Math.Min(1f, particle.Life / 20f);
var lifeFade = Math.Min(fadeIn, fadeOut);
var edgeFadeX = Math.Min(
Math.Min(1f, (particle.Position.X + _edgeFadeDistance) / _edgeFadeDistance),
Math.Min(1f, (bannerSize.X - particle.Position.X + _edgeFadeDistance) / _edgeFadeDistance)
);
var edgeFadeY = Math.Min(
Math.Min(1f, (particle.Position.Y + _edgeFadeDistance) / _edgeFadeDistance),
Math.Min(1f, (bannerSize.Y - particle.Position.Y + _edgeFadeDistance) / _edgeFadeDistance)
);
var edgeFade = Math.Min(edgeFadeX, edgeFadeY);
var baseAlpha = lifeFade * edgeFade;
var finalAlpha = particle.Type == ParticleType.TwinklingStar
? baseAlpha * (0.6f + 0.4f * MathF.Sin(particle.Twinkle))
: baseAlpha;
if (particle.Type == ParticleType.ShootingStar && particle.Trail != null && particle.Trail.Count > 1)
{
var cyanColor = new Vector4(0.4f, 0.8f, 1.0f, 1.0f);
for (int t = 1; t < particle.Trail.Count; t++)
{
var trailProgress = (float)t / particle.Trail.Count;
var trailAlpha = Math.Min(1f, (1f - trailProgress) * finalAlpha * 1.8f);
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 }),
trailWidth + 4f
);
drawList.AddLine(
bannerStart + particle.Trail[t - 1],
bannerStart + particle.Trail[t],
ImGui.GetColorU32(cyanColor with { W = trailAlpha }),
trailWidth
);
}
}
else if (particle.Type == ParticleType.TwinklingStar)
{
DrawTwinklingStar(drawList, screenPos, particle.Size, particle.Hue, finalAlpha, particle.Depth);
}
_particles[i] = particle;
}
}
private static void DrawTwinklingStar(ImDrawListPtr drawList, Vector2 position, float size, float hue, float alpha, float depth)
{
var color = HslToRgb(hue, 1.0f, 0.85f);
color.W = alpha;
drawList.AddCircleFilled(position, size, ImGui.GetColorU32(color));
var glowColor = color with { W = alpha * 0.3f };
drawList.AddCircleFilled(position, size * (1.2f + depth * 0.3f), ImGui.GetColorU32(glowColor));
}
private static Vector4 HslToRgb(float h, float s, float l)
{
h = h / 360f;
float c = (1 - MathF.Abs(2 * l - 1)) * s;
float x = c * (1 - MathF.Abs((h * 6) % 2 - 1));
float m = l - c / 2;
float r, g, b;
if (h < 1f / 6f)
{
r = c; g = x; b = 0;
}
else if (h < 2f / 6f)
{
r = x; g = c; b = 0;
}
else if (h < 3f / 6f)
{
r = 0; g = c; b = x;
}
else if (h < 4f / 6f)
{
r = 0; g = x; b = c;
}
else if (h < 5f / 6f)
{
r = x; g = 0; b = c;
}
else
{
r = c; g = 0; b = x;
}
return new Vector4(r + m, g + m, b + m, 1.0f);
}
private void SpawnParticle(Vector2 bannerSize)
{
var position = new Vector2(
(float)_random.NextDouble() * bannerSize.X,
(float)_random.NextDouble() * bannerSize.Y
);
var depthLayers = new[] { 0.5f, 1.0f, 1.5f };
var depth = depthLayers[_random.Next(depthLayers.Length)];
var velocity = new Vector2(
((float)_random.NextDouble() - 0.5f) * 0.05f * depth,
((float)_random.NextDouble() - 0.5f) * 0.05f * depth
);
var isBlue = _random.NextDouble() < 0.5;
var hue = isBlue ? 220f + (float)_random.NextDouble() * 30f : 270f + (float)_random.NextDouble() * 40f;
var size = (0.5f + (float)_random.NextDouble() * 2f) * depth;
var maxLife = 120f + (float)_random.NextDouble() * 60f;
_particles.Add(new Particle
{
Position = position,
Velocity = velocity,
Life = maxLife,
MaxLife = maxLife,
Size = size,
Type = ParticleType.TwinklingStar,
Trail = null,
Twinkle = (float)_random.NextDouble() * MathF.PI * 2,
Depth = depth,
Hue = hue
});
}
private void SpawnShootingStar(Vector2 bannerSize)
{
var maxLife = 80f + (float)_random.NextDouble() * 40f;
var startX = bannerSize.X * (0.3f + (float)_random.NextDouble() * 0.6f);
var startY = -10f;
_particles.Add(new Particle
{
Position = new Vector2(startX, startY),
Velocity = new Vector2(
-50f - (float)_random.NextDouble() * 40f,
30f + (float)_random.NextDouble() * 40f
),
Life = maxLife,
MaxLife = maxLife,
Size = 2.5f,
Type = ParticleType.ShootingStar,
Trail = new List<Vector2>(),
Twinkle = 0,
Depth = 1.0f,
Hue = 270f
});
}
/// <summary>
/// Clears all active particles. Useful when closing or hiding a window with an animated header.
/// </summary>
public void ClearParticles()
{
_particles.Clear();
_particleSpawnTimer = 0f;
}
}
/// <summary>
/// Represents a button in the animated header.
/// </summary>
public record HeaderButton(FontAwesomeIcon Icon, string Tooltip, Action? OnClick = null);

View File

@@ -38,7 +38,7 @@ internal static class MainStyle
new("color.border", "Border", () => Rgba(65, 65, 65, 255), ImGuiCol.Border),
new("color.borderShadow", "Border Shadow", () => Rgba(0, 0, 0, 150), ImGuiCol.BorderShadow),
new("color.frameBg", "Frame Background", () => Rgba(40, 40, 40, 255), ImGuiCol.FrameBg),
new("color.frameBgHovered", "Frame Background (Hover)", () => Rgba(50, 50, 50, 255), ImGuiCol.FrameBgHovered),
new("color.frameBgHovered", "Frame Background (Hover)", () => Rgba(50, 50, 50, 100), ImGuiCol.FrameBgHovered),
new("color.frameBgActive", "Frame Background (Active)", () => Rgba(30, 30, 30, 255), ImGuiCol.FrameBgActive),
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),

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,20 @@
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.PlayerData.Pairs;
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;
@@ -20,48 +25,59 @@ namespace LightlessSync.UI;
public class SyncshellFinderUI : WindowMediatorSubscriberBase
{
private readonly ApiController _apiController;
private readonly BroadcastService _broadcastService;
private readonly LightFinderService _broadcastService;
private readonly UiSharedService _uiSharedService;
private readonly BroadcastScannerService _broadcastScannerService;
private readonly PairManager _pairManager;
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,
BroadcastService broadcastService,
LightFinderService broadcastService,
UiSharedService uiShared,
ApiController apiController,
BroadcastScannerService broadcastScannerService,
PairManager pairManager,
DalamudUtilService dalamudUtilService) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService)
LightFinderScannerService broadcastScannerService,
PairUiService pairUiService,
DalamudUtilService dalamudUtilService,
LightlessProfileManager lightlessProfileManager) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService)
{
_broadcastService = broadcastService;
_uiSharedService = uiShared;
_apiController = apiController;
_broadcastScannerService = broadcastScannerService;
_pairManager = pairManager;
_pairUiService = pairUiService;
_dalamudUtilService = dalamudUtilService;
_lightlessProfileManager = lightlessProfileManager;
IsOpen = false;
SizeConstraints = new()
{
MinimumSize = new(600, 400),
MaximumSize = new(600, 550)
};
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()
@@ -72,9 +88,29 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
protected override void DrawInternal()
{
_uiSharedService.MediumText("Nearby Syncshells", UIColors.Get("PairBlue"));
_uiSharedService.ColoredSeparator(UIColors.Get("PairBlue"));
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.");
@@ -82,17 +118,17 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
if (!_broadcastService.IsBroadcasting)
{
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"));
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("PairBlue"));
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessPurple"));
if (ImGui.Button("Open Lightfinder", new Vector2(200 * ImGuiHelpers.GlobalScale, 0)))
{
Mediator.Publish(new UiToggleMessage(typeof(BroadcastUI)));
Mediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
}
ImGui.PopStyleColor();
@@ -104,104 +140,536 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
return;
}
DrawSyncshellTable();
string? myHashedCid = null;
try
{
var cid = _dalamudUtilService.GetCID();
myHashedCid = cid.ToString().GetHash256();
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed to get CID, not excluding own broadcast.");
}
var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().Where(b => !string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal)).ToList() ?? [];
var cardData = new List<(GroupJoinDto Shell, string BroadcasterName)>();
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;
}
cardData.Add((shell, broadcasterName));
}
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 DrawSyncshellTable()
private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName)> listData)
{
if (ImGui.BeginTable("##NearbySyncshellsTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg))
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++)
{
ImGui.TableSetupColumn("Syncshell", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Broadcaster", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Join", ImGuiTableColumnFlags.WidthFixed, 80f * ImGuiHelpers.GlobalScale);
ImGui.TableHeadersRow();
var (shell, broadcasterName) = listData[index];
foreach (var shell in _nearbySyncshells)
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(broadcasterName).X;
_uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple"));
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Click to open profile.");
if (ImGui.IsItemClicked())
{
// Check if there is an active broadcast for this syncshell, if not, skipping this syncshell
var broadcast = _broadcastScannerService.GetActiveSyncshellBroadcasts()
.FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal));
if (broadcast == null)
continue; // no active broadcasts
var (Name, Address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID);
if (string.IsNullOrEmpty(Name))
continue; // broadcaster not found in area, skipping
ImGui.TableNextRow();
ImGui.TableNextColumn();
var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID;
ImGui.TextUnformatted(displayName);
ImGui.TableNextColumn();
var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(Address);
var broadcasterName = !string.IsNullOrEmpty(worldName) ? $"{Name} ({worldName})" : Name;
ImGui.TextUnformatted(broadcasterName);
ImGui.TableNextColumn();
var label = $"Join##{shell.Group.GID}";
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));
var isAlreadyMember = _currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal));
var isRecentlyJoined = _recentlyJoined.Contains(shell.GID);
if (!isAlreadyMember && !isRecentlyJoined)
{
if (ImGui.Button(label))
{
_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
{
using (ImRaii.Disabled())
{
ImGui.Button(label);
}
UiSharedService.AttachToolTip("Already a member or owner of this Syncshell.");
}
ImGui.PopStyleColor(3);
Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group));
}
ImGui.EndTable();
float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X;
ImGui.SameLine();
ImGui.SetCursorPosX(rightX);
ImGui.TextUnformatted(broadcasterName);
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);
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)> 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) = cardData[index];
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 = broadcasterName;
if (!string.IsNullOrEmpty(broadcasterName) && maxBroadcasterWidth > 0f)
{
float bcFullWidth = ImGui.CalcTextSize(broadcasterName).X;
string toolTip;
if (bcFullWidth > maxBroadcasterWidth)
{
broadcasterToShow = TruncateTextToWidth(broadcasterName, maxBroadcasterWidth);
toolTip = broadcasterName + 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);
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(dynamic shell)
{
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)
{
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("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()
@@ -228,9 +696,9 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX);
_ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions));
_recentlyJoined.Add(_joinDto.Group.GID);
_joinDto = null;
_joinInfo = null;
}
@@ -263,52 +731,97 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
ImGui.NewLine();
}
private async Task RefreshSyncshellsAsync()
private async Task RefreshSyncshellsAsync(string? gid = null)
{
var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts();
_currentSyncshells = [.. _pairManager.GroupPairs.Select(g => g.Key)];
_recentlyJoined.RemoveWhere(gid => _currentSyncshells.Any(s => string.Equals(s.GID, gid, StringComparison.Ordinal)));
var snapshot = _pairUiService.GetSnapshot();
_currentSyncshells = [.. snapshot.GroupPairs.Keys];
if (syncshellBroadcasts.Count == 0)
_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;
}
List<GroupJoinDto>? updatedList = [];
try
if (gid != null && _recentlyJoined.Contains(gid))
{
var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts).ConfigureAwait(false);
updatedList = groups?.ToList();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to refresh broadcasted syncshells.");
return;
_recentlyJoined.Clear();
}
if (updatedList != null)
var previousGid = GetSelectedGid();
_nearbySyncshells.Clear();
_nearbySyncshells.AddRange(updatedList);
if (previousGid != null)
{
var previousGid = GetSelectedGid();
var newIndex = _nearbySyncshells.FindIndex(s =>
string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal));
_nearbySyncshells.Clear();
_nearbySyncshells.AddRange(updatedList);
if (previousGid != null)
if (newIndex >= 0)
{
var newIndex = _nearbySyncshells.FindIndex(s => string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal));
if (newIndex >= 0)
{
_selectedNearbyIndex = newIndex;
return;
}
_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)
@@ -321,6 +834,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
private void ClearSelection()
{
_selectedNearbyIndex = -1;
_syncshellPageIndex = 0;
_joinDto = null;
_joinInfo = null;
}

View File

@@ -0,0 +1,30 @@
using System.Numerics;
namespace LightlessSync.UI.Tags;
public readonly record struct ProfileTagDefinition(
string? Text,
string? SeStringPayload = null,
bool UseTextureSegments = false,
Vector4? BackgroundColor = null,
Vector4? BorderColor = null,
Vector4? TextColor = null)
{
public bool HasContent => !string.IsNullOrWhiteSpace(Text) || !string.IsNullOrWhiteSpace(SeStringPayload);
public bool HasSeString => !string.IsNullOrWhiteSpace(SeStringPayload);
public ProfileTagDefinition WithColors(Vector4? background, Vector4? border, Vector4? textColor = null)
=> this with { BackgroundColor = background, BorderColor = border, TextColor = textColor };
public static ProfileTagDefinition FromText(string text, Vector4? background = null, Vector4? border = null, Vector4? textColor = null)
=> new(text, null, false, background, border, textColor);
public static ProfileTagDefinition FromIcon(uint iconId, Vector4? background = null, Vector4? border = null)
=> new(null, $"<icon({iconId})>", true, background, border, null);
public static ProfileTagDefinition FromIconAndText(uint iconId, string text, Vector4? background = null, Vector4? border = null, Vector4? textColor = null)
=> new(text, $"<icon({iconId})> {text}", true, background, border, textColor);
public static ProfileTagDefinition FromSeString(string payload, Vector4? background = null, Vector4? border = null, Vector4? textColor = null)
=> new(null, payload, true, background, border, textColor);
}

View File

@@ -0,0 +1,224 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using LightlessSync.Utils;
using Microsoft.Extensions.Logging;
using System.Numerics;
namespace LightlessSync.UI.Tags;
internal static class ProfileTagRenderer
{
public static Vector2 MeasureTag(
ProfileTagDefinition tag,
float scale,
ImGuiStylePtr style,
Vector4 fallbackBackground,
Vector4 fallbackBorder,
uint defaultTextColorU32,
List<SeStringUtils.SeStringSegment> segmentBuffer,
Func<uint, IDalamudTextureWrap?> iconResolver,
ILogger? logger)
=> RenderTagInternal(tag, Vector2.Zero, scale, default, style, fallbackBackground, fallbackBorder, defaultTextColorU32, segmentBuffer, iconResolver, logger, draw: false);
public static Vector2 RenderTag(
ProfileTagDefinition tag,
Vector2 screenMin,
float scale,
ImDrawListPtr drawList,
ImGuiStylePtr style,
Vector4 fallbackBackground,
Vector4 fallbackBorder,
uint defaultTextColorU32,
List<SeStringUtils.SeStringSegment> segmentBuffer,
Func<uint, IDalamudTextureWrap?> iconResolver,
ILogger? logger)
=> RenderTagInternal(tag, screenMin, scale, drawList, style, fallbackBackground, fallbackBorder, defaultTextColorU32, segmentBuffer, iconResolver, logger, draw: true);
private static Vector2 RenderTagInternal(
ProfileTagDefinition tag,
Vector2 screenMin,
float scale,
ImDrawListPtr drawList,
ImGuiStylePtr style,
Vector4 fallbackBackground,
Vector4 fallbackBorder,
uint defaultTextColorU32,
List<SeStringUtils.SeStringSegment> segmentBuffer,
Func<uint, IDalamudTextureWrap?> iconResolver,
ILogger? logger,
bool draw)
{
segmentBuffer.Clear();
var padding = new Vector2(10f * scale, 6f * scale);
var rounding = style.FrameRounding > 0f ? style.FrameRounding : 6f * scale;
var backgroundColor = tag.BackgroundColor ?? fallbackBackground;
var borderColor = tag.BorderColor ?? fallbackBorder;
var textColor = tag.TextColor ?? style.Colors[(int)ImGuiCol.Text];
var textColorU32 = tag.TextColor.HasValue ? ImGui.ColorConvertFloat4ToU32(tag.TextColor.Value) : defaultTextColorU32;
string? textContent = tag.Text;
Vector2 textSize = string.IsNullOrWhiteSpace(textContent) ? Vector2.Zero : ImGui.CalcTextSize(textContent);
var sePayload = tag.SeStringPayload;
bool hasSeString = !string.IsNullOrWhiteSpace(sePayload);
bool useTextureSegments = hasSeString && tag.UseTextureSegments;
bool useSeRenderer = hasSeString && !useTextureSegments;
Vector2 seSize = Vector2.Zero;
List<SeStringUtils.SeStringSegment>? seSegments = null;
if (hasSeString)
{
if (useSeRenderer)
{
try
{
var drawParams = new SeStringDrawParams
{
TargetDrawList = draw ? drawList : default,
ScreenOffset = draw ? screenMin + padding : Vector2.Zero,
WrapWidth = float.MaxValue
};
var measure = ImGuiHelpers.CompileSeStringWrapped(sePayload!, drawParams);
seSize = measure.Size;
if (seSize.Y <= 0f)
seSize.Y = ImGui.GetTextLineHeight();
textContent = null;
textSize = Vector2.Zero;
}
catch (Exception ex)
{
logger?.LogDebug(ex, "Failed to compile SeString payload '{Payload}' for profile tag", sePayload);
useSeRenderer = false;
}
}
if (!useSeRenderer && useTextureSegments)
{
segmentBuffer.Clear();
if (SeStringUtils.TryResolveSegments(sePayload!, scale, iconResolver, segmentBuffer, out seSize) && segmentBuffer.Count > 0)
{
seSegments = segmentBuffer;
textContent = null;
textSize = Vector2.Zero;
}
else
{
segmentBuffer.Clear();
var fallback = SeStringUtils.StripMarkup(sePayload!);
if (!string.IsNullOrWhiteSpace(fallback))
{
textContent = fallback;
textSize = ImGui.CalcTextSize(fallback);
}
}
}
else if (!useSeRenderer && string.IsNullOrWhiteSpace(textContent))
{
var fallback = SeStringUtils.StripMarkup(sePayload!);
if (!string.IsNullOrWhiteSpace(fallback))
{
textContent = fallback;
textSize = ImGui.CalcTextSize(fallback);
}
}
}
bool drewSeString = useSeRenderer || seSegments is { Count: > 0 };
var contentHeight = drewSeString ? seSize.Y : textSize.Y;
if (contentHeight <= 0f)
contentHeight = ImGui.GetTextLineHeight();
var contentWidth = drewSeString ? seSize.X : textSize.X;
if (contentWidth <= 0f)
contentWidth = textSize.X;
if (contentWidth <= 0f)
contentWidth = 40f * scale;
var tagSize = new Vector2(contentWidth + padding.X * 2f, contentHeight + padding.Y * 2f);
if (!draw)
{
if (seSegments is not null)
seSegments.Clear();
return tagSize;
}
var rectMin = screenMin;
var rectMax = rectMin + tagSize;
drawList.AddRectFilled(rectMin, rectMax, ImGui.ColorConvertFloat4ToU32(backgroundColor), rounding);
drawList.AddRect(rectMin, rectMax, ImGui.ColorConvertFloat4ToU32(borderColor), rounding);
var contentStart = rectMin + padding;
var verticalOffset = (tagSize.Y - padding.Y * 2f - contentHeight) * 0.5f;
var basePos = new Vector2(contentStart.X, contentStart.Y + MathF.Max(verticalOffset, 0f));
if (useSeRenderer && sePayload is { Length: > 0 })
{
var drawParams = new SeStringDrawParams
{
TargetDrawList = drawList,
ScreenOffset = basePos,
WrapWidth = float.MaxValue
};
try
{
ImGuiHelpers.CompileSeStringWrapped(sePayload!, drawParams);
}
catch (Exception ex)
{
logger?.LogDebug(ex, "Failed to draw SeString payload '{Payload}' for profile tag", sePayload);
var fallback = !string.IsNullOrWhiteSpace(textContent) ? textContent : SeStringUtils.StripMarkup(sePayload!);
if (!string.IsNullOrWhiteSpace(fallback))
drawList.AddText(basePos, textColorU32, fallback);
}
}
else if (seSegments is { Count: > 0 })
{
var segmentX = basePos.X;
foreach (var segment in seSegments)
{
var segmentPos = new Vector2(segmentX, basePos.Y + (contentHeight - segment.Size.Y) * 0.5f);
switch (segment.Type)
{
case SeStringUtils.SeStringSegmentType.Icon:
if (segment.Texture != null)
{
drawList.AddImage(segment.Texture.Handle, segmentPos, segmentPos + segment.Size);
}
else if (!string.IsNullOrEmpty(segment.Text))
{
drawList.AddText(segmentPos, textColorU32, segment.Text);
}
break;
case SeStringUtils.SeStringSegmentType.Text:
var colorU32 = segment.Color.HasValue
? ImGui.ColorConvertFloat4ToU32(segment.Color.Value)
: textColorU32;
drawList.AddText(segmentPos, colorU32, segment.Text ?? string.Empty);
break;
}
segmentX += segment.Size.X;
}
seSegments.Clear();
}
else if (!string.IsNullOrWhiteSpace(textContent))
{
drawList.AddText(basePos, textColorU32, textContent);
}
else
{
drawList.AddText(basePos, textColorU32, string.Empty);
}
return tagSize;
}
}

View File

@@ -0,0 +1,170 @@
using System.Numerics;
namespace LightlessSync.UI.Tags;
/// <summary>
/// Library of tags. That's it.
/// </summary>
public sealed class ProfileTagService
{
private static readonly IReadOnlyDictionary<int, ProfileTagDefinition> TagLibrary = CreateTagLibrary();
public static IReadOnlyDictionary<int, ProfileTagDefinition> GetTagLibrary()
=> TagLibrary;
public static IReadOnlyList<ProfileTagDefinition> ResolveTags(IReadOnlyList<int>? tagIds)
{
if (tagIds is null || tagIds.Count == 0)
return Array.Empty<ProfileTagDefinition>();
var result = new List<ProfileTagDefinition>(tagIds.Count);
foreach (var id in tagIds)
{
if (TagLibrary.TryGetValue(id, out var tag))
result.Add(tag);
}
return result;
}
public bool TryGetDefinition(int tagId, out ProfileTagDefinition definition)
=> TagLibrary.TryGetValue(tagId, out definition);
private static IReadOnlyDictionary<int, ProfileTagDefinition> CreateTagLibrary()
{
return new Dictionary<int, ProfileTagDefinition>
{
[0] = ProfileTagDefinition.FromIconAndText(
230419,
"SFW",
background: new Vector4(0.16f, 0.24f, 0.18f, 0.95f),
border: new Vector4(0.32f, 0.52f, 0.34f, 0.85f),
textColor: new Vector4(0.78f, 0.94f, 0.80f, 1f)),
[1] = ProfileTagDefinition.FromIconAndText(
230419,
"NSFW",
background: new Vector4(0.32f, 0.18f, 0.22f, 0.95f),
border: new Vector4(0.72f, 0.32f, 0.38f, 0.85f),
textColor: new Vector4(1f, 0.82f, 0.86f, 1f)),
[2] = ProfileTagDefinition.FromIconAndText(
61545,
"RP",
background: new Vector4(0.20f, 0.20f, 0.30f, 0.95f),
border: new Vector4(0.42f, 0.42f, 0.66f, 0.85f),
textColor: new Vector4(0.80f, 0.84f, 1f, 1f)),
[3] = ProfileTagDefinition.FromIconAndText(
61545,
"ERP",
background: new Vector4(0.20f, 0.20f, 0.30f, 0.95f),
border: new Vector4(0.42f, 0.42f, 0.66f, 0.85f),
textColor: new Vector4(0.80f, 0.84f, 1f, 1f)),
[4] = ProfileTagDefinition.FromIconAndText(
230420,
"No RP",
background: new Vector4(0.30f, 0.18f, 0.30f, 0.95f),
border: new Vector4(0.69f, 0.40f, 0.65f, 0.85f),
textColor: new Vector4(1f, 0.84f, 1f, 1f)),
[5] = ProfileTagDefinition.FromIconAndText(
230420,
"No ERP",
background: new Vector4(0.30f, 0.18f, 0.30f, 0.95f),
border: new Vector4(0.69f, 0.40f, 0.65f, 0.85f),
textColor: new Vector4(1f, 0.84f, 1f, 1f)),
[6] = ProfileTagDefinition.FromIconAndText(
60756,
"Venues",
background: new Vector4(0.18f, 0.24f, 0.28f, 0.95f),
border: new Vector4(0.33f, 0.55f, 0.63f, 0.85f),
textColor: new Vector4(0.78f, 0.90f, 0.97f, 1f)),
[7] = ProfileTagDefinition.FromIconAndText(
61546,
"GPose",
background: new Vector4(0.18f, 0.18f, 0.26f, 0.95f),
border: new Vector4(0.35f, 0.34f, 0.54f, 0.85f),
textColor: new Vector4(0.80f, 0.82f, 0.96f, 1f)),
[8] = ProfileTagDefinition.FromIconAndText(
60572,
"Limsa"),
[9] = ProfileTagDefinition.FromIconAndText(
60573,
"Gridania"),
[10] = ProfileTagDefinition.FromIconAndText(
60574,
"Ul'dah"),
[11] = ProfileTagDefinition.FromIconAndText(
61397,
"WU/T"),
[1001] = ProfileTagDefinition.FromIcon(61806), // PVP
[1002] = ProfileTagDefinition.FromIcon(61832), // Ultimate
[1003] = ProfileTagDefinition.FromIcon(61802), // Raids
[1004] = ProfileTagDefinition.FromIcon(61807), // Roulette
[1005] = ProfileTagDefinition.FromIcon(61816), // Crafting
[1006] = ProfileTagDefinition.FromIcon(61753), // Casual
[1007] = ProfileTagDefinition.FromIcon(61754), // Hardcore
[1008] = ProfileTagDefinition.FromIcon(61759), // Glamour
[1009] = ProfileTagDefinition.FromIcon(61760), // Mentor
// Role Tags
[2001] = ProfileTagDefinition.FromIconAndText(62581, "Tank"),
[2002] = ProfileTagDefinition.FromIconAndText(62582, "Healer"),
[2003] = ProfileTagDefinition.FromIconAndText(62583, "DPS"),
[2004] = ProfileTagDefinition.FromIconAndText(62584, "Melee DPS"),
[2005] = ProfileTagDefinition.FromIconAndText(62585, "Ranged DPS"),
[2006] = ProfileTagDefinition.FromIconAndText(62586, "Physical Ranged DPS"),
[2007] = ProfileTagDefinition.FromIconAndText(62587, "Magical Ranged DPS"),
// Misc Role Tags
[2101] = ProfileTagDefinition.FromIconAndText(62146, "All-Rounder"),
// Tank Job Tags
[2201] = ProfileTagDefinition.FromIconAndText(62119, "Paladin"),
[2202] = ProfileTagDefinition.FromIconAndText(62121, "Warrior"),
[2203] = ProfileTagDefinition.FromIconAndText(62132, "Dark Knight"),
[2204] = ProfileTagDefinition.FromIconAndText(62137, "Gunbreaker"),
// Healer Job Tags
[2301] = ProfileTagDefinition.FromIconAndText(62124, "White Mage"),
[2302] = ProfileTagDefinition.FromIconAndText(62128, "Scholar"),
[2303] = ProfileTagDefinition.FromIconAndText(62133, "Astrologian"),
[2304] = ProfileTagDefinition.FromIconAndText(62140, "Sage"),
// Melee DPS Job Tags
[2401] = ProfileTagDefinition.FromIconAndText(62120, "Monk"),
[2402] = ProfileTagDefinition.FromIconAndText(62122, "Dragoon"),
[2403] = ProfileTagDefinition.FromIconAndText(62130, "Ninja"),
[2404] = ProfileTagDefinition.FromIconAndText(62134, "Samurai"),
[2405] = ProfileTagDefinition.FromIconAndText(62139, "Reaper"),
[2406] = ProfileTagDefinition.FromIconAndText(62141, "Viper"),
// PRanged DPS Job Tags
[2501] = ProfileTagDefinition.FromIconAndText(62123, "Bard"),
[2502] = ProfileTagDefinition.FromIconAndText(62131, "Machinist"),
[2503] = ProfileTagDefinition.FromIconAndText(62138, "Dancer"),
// MRanged DPS Job Tags
[2601] = ProfileTagDefinition.FromIconAndText(62125, "Black Mage"),
[2602] = ProfileTagDefinition.FromIconAndText(62127, "Summoner"),
[2603] = ProfileTagDefinition.FromIconAndText(62135, "Red Mage"),
[2604] = ProfileTagDefinition.FromIconAndText(62142, "Pictomancer"),
[2605] = ProfileTagDefinition.FromIconAndText(62136, "Blue Mage") // this job sucks xd
};
}
}

View File

@@ -2,18 +2,21 @@ using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.LightFinder;
using LightlessSync.Services.Mediator;
using LightlessSync.UI.Models;
using LightlessSync.UI.Style;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using System;
using System.Numerics;
namespace LightlessSync.UI;
public class TopTabMenu
@@ -22,9 +25,10 @@ public class TopTabMenu
private readonly LightlessMediator _lightlessMediator;
private readonly PairManager _pairManager;
private readonly PairRequestService _pairRequestService;
private readonly DalamudUtilService _dalamudUtilService;
private readonly LightFinderService _lightFinderService;
private readonly LightFinderScannerService _lightFinderScannerService;
private readonly HashSet<string> _pendingPairRequestActions = new(StringComparer.Ordinal);
private bool _pairRequestsExpanded; // useless for now
private int _lastRequestCount;
@@ -36,15 +40,18 @@ public class TopTabMenu
private string _pairToAdd = string.Empty;
private SelectedTab _selectedTab = SelectedTab.None;
public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService)
private PairUiSnapshot? _currentSnapshot;
public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService, LightFinderService lightFinderService, LightFinderScannerService lightFinderScannerService)
{
_lightlessMediator = lightlessMediator;
_apiController = apiController;
_pairManager = pairManager;
_pairRequestService = pairRequestService;
_dalamudUtilService = dalamudUtilService;
_uiSharedService = uiSharedService;
_lightlessNotificationService = lightlessNotificationService;
_lightFinderService = lightFinderService;
_lightFinderScannerService = lightFinderScannerService;
}
private enum SelectedTab
@@ -77,130 +84,178 @@ public class TopTabMenu
_selectedTab = value;
}
}
public void Draw()
private PairUiSnapshot Snapshot => _currentSnapshot ?? throw new InvalidOperationException("Pair UI snapshot is not available outside of Draw.");
public void Draw(PairUiSnapshot snapshot)
{
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
var spacing = ImGui.GetStyle().ItemSpacing;
var buttonX = (availableWidth - (spacing.X * 4)) / 5f;
var buttonY = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Pause).Y;
var buttonSize = new Vector2(buttonX, buttonY);
var drawList = ImGui.GetWindowDrawList();
var underlineColor = ImGui.GetColorU32(UIColors.Get("LightlessPurpleActive")); // ImGui.GetColorU32(ImGuiCol.Separator);
var btncolor = ImRaii.PushColor(ImGuiCol.Button, ImGui.ColorConvertFloat4ToU32(new(0, 0, 0, 0)));
ImGuiHelpers.ScaledDummy(spacing.Y / 2f);
using (ImRaii.PushFont(UiBuilder.IconFont))
_currentSnapshot = snapshot;
try
{
var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.User.ToIconString(), buttonSize))
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
var spacing = ImGui.GetStyle().ItemSpacing;
var buttonX = (availableWidth - (spacing.X * 5)) / 6f;
var buttonY = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Pause).Y;
var buttonSize = new Vector2(buttonX, buttonY);
const float buttonBorderThickness = 12f;
var buttonRounding = ImGui.GetStyle().FrameRounding;
var drawList = ImGui.GetWindowDrawList();
var underlineColor = ImGui.GetColorU32(UIColors.Get("LightlessPurpleActive")); // ImGui.GetColorU32(ImGuiCol.Separator);
var btncolor = ImRaii.PushColor(ImGuiCol.Button, ImGui.ColorConvertFloat4ToU32(new(0, 0, 0, 0)));
ImGuiHelpers.ScaledDummy(spacing.Y / 2f);
using (ImRaii.PushFont(UiBuilder.IconFont))
{
TabSelection = TabSelection == SelectedTab.Individual ? SelectedTab.None : SelectedTab.Individual;
var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.User.ToIconString(), buttonSize))
{
TabSelection = TabSelection == SelectedTab.Individual ? SelectedTab.None : SelectedTab.Individual;
}
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.Individual)
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("Individual Pair Menu");
using (ImRaii.PushFont(UiBuilder.IconFont))
{
var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.Users.ToIconString(), buttonSize))
{
TabSelection = TabSelection == SelectedTab.Syncshell ? SelectedTab.None : SelectedTab.Syncshell;
}
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.Syncshell)
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("Syncshell Menu");
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button(FontAwesomeIcon.Comments.ToIconString(), buttonSize))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(ZoneChatUi)));
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
}
}
UiSharedService.AttachToolTip("Lightless Chat");
ImGui.SameLine();
var xAfter = ImGui.GetCursorScreenPos();
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;
}
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");
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.UserCog.ToIconString(), buttonSize))
{
TabSelection = TabSelection == SelectedTab.UserConfig ? SelectedTab.None : SelectedTab.UserConfig;
}
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.UserConfig)
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("Your User Menu");
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString(), buttonSize))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
}
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();
}
UiSharedService.AttachToolTip("Open Lightless Settings");
ImGui.NewLine();
btncolor.Dispose();
ImGuiHelpers.ScaledDummy(spacing);
if (TabSelection == SelectedTab.Individual)
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("Individual Pair Menu");
using (ImRaii.PushFont(UiBuilder.IconFont))
{
var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.Users.ToIconString(), buttonSize))
{
TabSelection = TabSelection == SelectedTab.Syncshell ? SelectedTab.None : SelectedTab.Syncshell;
DrawAddPair(availableWidth, spacing.X);
DrawGlobalIndividualButtons(availableWidth, spacing.X);
}
ImGui.SameLine();
var xAfter = ImGui.GetCursorScreenPos();
if (TabSelection == SelectedTab.Syncshell)
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("Syncshell Menu");
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.Compass.ToIconString(), buttonSize))
else if (TabSelection == SelectedTab.Syncshell)
{
TabSelection = TabSelection == SelectedTab.Lightfinder ? SelectedTab.None : SelectedTab.Lightfinder;
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);
}
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");
if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f);
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
DrawIncomingPairRequests(availableWidth);
ImGui.Separator();
DrawFilter(availableWidth, spacing.X);
}
finally
{
var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.UserCog.ToIconString(), buttonSize))
{
TabSelection = TabSelection == SelectedTab.UserConfig ? SelectedTab.None : SelectedTab.UserConfig;
}
ImGui.SameLine();
var xAfter = ImGui.GetCursorScreenPos();
if (TabSelection == SelectedTab.UserConfig)
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);
_currentSnapshot = null;
}
UiSharedService.AttachToolTip("Your User Menu");
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString(), buttonSize))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
}
ImGui.SameLine();
}
UiSharedService.AttachToolTip("Open Lightless Settings");
ImGui.NewLine();
btncolor.Dispose();
ImGuiHelpers.ScaledDummy(spacing);
if (TabSelection == SelectedTab.Individual)
{
DrawAddPair(availableWidth, spacing.X);
DrawGlobalIndividualButtons(availableWidth, spacing.X);
}
else if (TabSelection == SelectedTab.Syncshell)
{
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);
}
if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f);
DrawIncomingPairRequests(availableWidth);
ImGui.Separator();
DrawFilter(availableWidth, spacing.X);
}
private void DrawAddPair(float availableXWidth, float spacingX)
@@ -209,7 +264,7 @@ public class TopTabMenu
ImGui.SetNextItemWidth(availableXWidth - buttonSize - spacingX);
ImGui.InputTextWithHint("##otheruid", "Other players UID/Alias", ref _pairToAdd, 20);
ImGui.SameLine();
var alreadyExisting = _pairManager.DirectPairs.Exists(p => string.Equals(p.UserData.UID, _pairToAdd, StringComparison.Ordinal) || string.Equals(p.UserData.Alias, _pairToAdd, StringComparison.Ordinal));
var alreadyExisting = Snapshot.DirectPairs.Any(p => string.Equals(p.UserData.UID, _pairToAdd, StringComparison.Ordinal) || string.Equals(p.UserData.Alias, _pairToAdd, StringComparison.Ordinal));
using (ImRaii.Disabled(alreadyExisting || string.IsNullOrEmpty(_pairToAdd)))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserPlus, "Add"))
@@ -431,12 +486,23 @@ public class TopTabMenu
{
Filter = filter;
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, 10, exactSize: true, clipToElement: true, roundingOverride: ImGui.GetStyle().FrameRounding);
}
ImGui.SameLine();
using var disabled = ImRaii.Disabled(string.IsNullOrEmpty(Filter));
var disableClear = string.IsNullOrEmpty(Filter);
using var disabled = ImRaii.Disabled(disableClear);
var clearHovered = false;
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Ban, "Clear"))
{
Filter = string.Empty;
}
clearHovered = ImGui.IsItemHovered();
if (!disableClear && clearHovered)
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, 10, exactSize: true, clipToElement: true, roundingOverride: ImGui.GetStyle().FrameRounding);
}
}
private void DrawGlobalIndividualButtons(float availableXWidth, float spacingX)
@@ -666,7 +732,7 @@ public class TopTabMenu
if (ImGui.Button(FontAwesomeIcon.Check.ToIconString(), buttonSize))
{
_ = GlobalControlCountdown(10);
var bulkSyncshells = _pairManager.GroupPairs.Keys.OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase)
var bulkSyncshells = Snapshot.GroupPairs.Keys.OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Group.GID, g =>
{
var perm = g.GroupUserPermissions;
@@ -691,7 +757,8 @@ public class TopTabMenu
{
var buttonX = (availableWidth - (spacingX)) / 2f;
using (ImRaii.Disabled(_pairManager.GroupPairs.Select(k => k.Key).Distinct()
using (ImRaii.Disabled(Snapshot.GroupPairs.Keys
.Distinct()
.Count(g => string.Equals(g.OwnerUID, _apiController.UID, StringComparison.Ordinal)) >= _apiController.ServerInfo.MaxGroupsCreatedByUser))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Create new Syncshell", buttonX))
@@ -701,7 +768,7 @@ public class TopTabMenu
ImGui.SameLine();
}
using (ImRaii.Disabled(_pairManager.GroupPairs.Select(k => k.Key).Distinct().Count() >= _apiController.ServerInfo.MaxGroupsJoinedByUser))
using (ImRaii.Disabled(Snapshot.GroupPairs.Keys.Distinct().Count() >= _apiController.ServerInfo.MaxGroupsJoinedByUser))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Users, "Join existing Syncshell", buttonX))
{
@@ -716,17 +783,46 @@ public class TopTabMenu
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCirclePlus, "Lightfinder", buttonX, center: true))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(BroadcastUI)));
_lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
}
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Globe, "Syncshell Finder", buttonX, center: true))
var syncshellFinderLabel = GetSyncshellFinderLabel();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Globe, syncshellFinderLabel, buttonX, center: true))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(SyncshellFinderUI)));
}
}
private string GetSyncshellFinderLabel()
{
if (!_lightFinderService.IsBroadcasting)
return "Syncshell Finder";
string? myHashedCid = null;
try
{
var cid = _dalamudUtilService.GetCID();
myHashedCid = cid.ToString().GetHash256();
}
catch (Exception)
{
// Couldnt get own CID, log and return default table
}
var nearbyCount = _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)
{
var buttonX = (availableWidth - spacingX) / 2f;
@@ -770,7 +866,7 @@ public class TopTabMenu
if (_uiSharedService.IconTextButton(enableIcon, enableText, null, true))
{
_ = GlobalControlCountdown(10);
var bulkIndividualPairs = _pairManager.PairsWithGroups.Keys
var bulkIndividualPairs = Snapshot.PairsWithGroups.Keys
.Where(g => g.IndividualPairStatus == IndividualPairStatus.Bidirectional)
.ToDictionary(g => g.UserPair.User.UID, g =>
{
@@ -784,7 +880,7 @@ public class TopTabMenu
if (_uiSharedService.IconTextButton(disableIcon, disableText, null, true))
{
_ = GlobalControlCountdown(10);
var bulkIndividualPairs = _pairManager.PairsWithGroups.Keys
var bulkIndividualPairs = Snapshot.PairsWithGroups.Keys
.Where(g => g.IndividualPairStatus == IndividualPairStatus.Bidirectional)
.ToDictionary(g => g.UserPair.User.UID, g =>
{
@@ -808,7 +904,7 @@ public class TopTabMenu
if (_uiSharedService.IconTextButton(enableIcon, enableText, null, true))
{
_ = GlobalControlCountdown(10);
var bulkSyncshells = _pairManager.GroupPairs.Keys
var bulkSyncshells = Snapshot.GroupPairs.Keys
.OrderBy(u => u.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Group.GID, g =>
{
@@ -822,7 +918,7 @@ public class TopTabMenu
if (_uiSharedService.IconTextButton(disableIcon, disableText, null, true))
{
_ = GlobalControlCountdown(10);
var bulkSyncshells = _pairManager.GroupPairs.Keys
var bulkSyncshells = Snapshot.GroupPairs.Keys
.OrderBy(u => u.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Group.GID, g =>
{

View File

@@ -15,8 +15,11 @@ namespace LightlessSync.UI
{ "FullBlack", "#000000" },
{ "LightlessBlue", "#a6c2ff" },
{ "LightlessYellow", "#ffe97a" },
{ "LightlessYellow2", "#cfbd63" },
{ "LightlessGreen", "#7cd68a" },
{ "LightlessGreenDefault", "#468a50" },
{ "LightlessOrange", "#ffb366" },
{ "LightlessGrey", "#8f8f8f" },
{ "PairBlue", "#88a2db" },
{ "DimRed", "#d44444" },
{ "LightlessAdminText", "#ffd663" },
@@ -25,6 +28,9 @@ namespace LightlessSync.UI
{ "Lightfinder", "#ad8af5" },
{ "LightfinderEdge", "#000000" },
{ "ProfileBodyGradientTop", "#2f283fff" },
{ "ProfileBodyGradientBottom", "#372d4d00" },
};
private static LightlessConfigService? _configService;
@@ -40,7 +46,7 @@ namespace LightlessSync.UI
return HexToRgba(customColorHex);
if (!DefaultHexColors.TryGetValue(name, out var hex))
throw new ArgumentException($"Color '{name}' not found in UIColors.");
throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name));
return HexToRgba(hex);
}
@@ -48,7 +54,7 @@ namespace LightlessSync.UI
public static void Set(string name, Vector4 color)
{
if (!DefaultHexColors.ContainsKey(name))
throw new ArgumentException($"Color '{name}' not found in UIColors.");
throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name));
if (_configService != null)
{
@@ -78,7 +84,7 @@ namespace LightlessSync.UI
public static Vector4 GetDefault(string name)
{
if (!DefaultHexColors.TryGetValue(name, out var hex))
throw new ArgumentException($"Color '{name}' not found in UIColors.");
throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name));
return HexToRgba(hex);
}
@@ -96,10 +102,10 @@ namespace LightlessSync.UI
public static Vector4 HexToRgba(string hexColor)
{
hexColor = hexColor.TrimStart('#');
int r = int.Parse(hexColor.Substring(0, 2), NumberStyles.HexNumber);
int g = int.Parse(hexColor.Substring(2, 2), NumberStyles.HexNumber);
int b = int.Parse(hexColor.Substring(4, 2), NumberStyles.HexNumber);
int a = hexColor.Length == 8 ? int.Parse(hexColor.Substring(6, 2), NumberStyles.HexNumber) : 255;
int r = int.Parse(hexColor[..2], NumberStyles.HexNumber);
int g = int.Parse(hexColor[2..4], NumberStyles.HexNumber);
int b = int.Parse(hexColor[4..6], NumberStyles.HexNumber);
int a = hexColor.Length == 8 ? int.Parse(hexColor[6..8], NumberStyles.HexNumber) : 255;
return new Vector4(r / 255f, g / 255f, b / 255f, a / 255f);
}

View File

@@ -1,18 +1,21 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Gui.ContextMenu;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.Textures;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using System;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using LightlessSync.FileCache;
using LightlessSync.Interop.Ipc;
using LightlessSync.Interop.Ipc.Framework;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Localization;
@@ -24,8 +27,11 @@ using LightlessSync.Utils;
using LightlessSync.WebAPI;
using LightlessSync.WebAPI.SignalR;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
@@ -70,7 +76,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
private bool _isOneDrive = false;
private bool _isPenumbraDirectory = false;
private bool _moodlesExists = false;
private Dictionary<string, DateTime> _oauthTokenExpiry = new();
private readonly Dictionary<string, DateTime> _oauthTokenExpiry = [];
private bool _penumbraExists = false;
private bool _petNamesExists = false;
private int _serverSelectionIndex = -1;
@@ -178,15 +184,108 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
int i = 0;
double dblSByte = bytes;
while (dblSByte >= 1000 && i < suffix.Length - 1)
while (dblSByte >= 1024 && i < suffix.Length - 1)
{
dblSByte /= 1000.0;
dblSByte /= 1024.0;
i++;
}
return addSuffix ? $"{dblSByte:0.00} {suffix[i]}" : $"{dblSByte:0.00}";
}
public readonly struct TabOption<T>
{
public string Label { get; }
public T Value { get; }
public bool Enabled { get; }
public TabOption(string label, T value, bool enabled = true)
{
Label = label;
Value = value;
Enabled = enabled;
}
}
public static bool Tab<T>(string id, IReadOnlyList<TabOption<T>> options, ref T selectedValue) where T : struct
{
if (options.Count == 0)
return false;
var pushIdValue = string.IsNullOrEmpty(id)
? $"UiSharedTab_{RuntimeHelpers.GetHashCode(options):X}"
: id;
using var tabId = ImRaii.PushId(pushIdValue);
var selectedIndex = -1;
for (var i = 0; i < options.Count; i++)
{
if (!EqualityComparer<T>.Default.Equals(options[i].Value, selectedValue))
continue;
selectedIndex = i;
break;
}
if (selectedIndex == -1 || !options[selectedIndex].Enabled)
selectedIndex = GetFirstEnabledTabIndex(options);
if (selectedIndex == -1)
return false;
var changed = DrawTabsInternal(options, ref selectedIndex);
selectedValue = options[Math.Clamp(selectedIndex, 0, options.Count - 1)].Value;
return changed;
}
private static int GetFirstEnabledTabIndex<T>(IReadOnlyList<TabOption<T>> options)
{
for (var i = 0; i < options.Count; i++)
{
if (options[i].Enabled)
return i;
}
return -1;
}
private static bool DrawTabsInternal<T>(IReadOnlyList<TabOption<T>> options, ref int selectedIndex)
{
selectedIndex = Math.Clamp(selectedIndex, 0, Math.Max(0, options.Count - 1));
var style = ImGui.GetStyle();
var availableWidth = ImGui.GetContentRegionAvail().X;
var spacingX = style.ItemSpacing.X;
var buttonWidth = options.Count > 0 ? Math.Max(1f, (availableWidth - spacingX * (options.Count - 1)) / options.Count) : availableWidth;
var buttonHeight = Math.Max(ImGui.GetFrameHeight() + style.FramePadding.Y, 28f * ImGuiHelpers.GlobalScale);
var changed = false;
for (var i = 0; i < options.Count; i++)
{
if (i > 0)
ImGui.SameLine();
var tab = options[i];
var isSelected = i == selectedIndex;
using (ImRaii.Disabled(!tab.Enabled))
{
using var tabIndexId = ImRaii.PushId(i);
using var selectedButton = isSelected ? ImRaii.PushColor(ImGuiCol.Button, style.Colors[(int)ImGuiCol.TabActive]) : null;
using var selectedHover = isSelected ? ImRaii.PushColor(ImGuiCol.ButtonHovered, style.Colors[(int)ImGuiCol.TabHovered]) : null;
using var selectedActive = isSelected ? ImRaii.PushColor(ImGuiCol.ButtonActive, style.Colors[(int)ImGuiCol.TabActive]) : null;
if (ImGui.Button(tab.Label, new Vector2(buttonWidth, buttonHeight)))
{
selectedIndex = i;
changed = true;
}
}
}
return changed;
}
public static void CenterNextWindow(float width, float height, ImGuiCond cond = ImGuiCond.None)
{
var center = ImGui.GetMainViewport().GetCenter();
@@ -400,10 +499,21 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
public static bool ShiftPressed() => (GetKeyState(0xA1) & 0x8000) != 0 || (GetKeyState(0xA0) & 0x8000) != 0;
public static void TextWrapped(string text, float wrapPos = 0)
public static void TextWrapped(string text, float wrapPos = 0, Vector4? color = null)
{
ImGui.PushTextWrapPos(wrapPos);
if (color.HasValue)
{
ImGui.PushStyleColor(ImGuiCol.Text, color.Value);
}
ImGui.TextUnformatted(text);
if (color.HasValue)
{
ImGui.PopStyleColor();
}
ImGui.PopTextWrapPos();
}
@@ -475,7 +585,22 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
);
}
public void ColoredSeparator(Vector4? color = null, float thickness = 1f, float indent = 0f)
public static void AddContextMenuItem(IMenuOpenedArgs args, SeString name, char prefixChar, ushort colorMenuItem, Func<Task> onClick)
{
args.AddMenuItem(new MenuItem
{
Name = name,
PrefixChar = prefixChar,
UseDefaultPrefix = false,
PrefixColor = colorMenuItem,
OnClicked = _ =>
{
onClick();
},
});
}
public static void ColoredSeparator(Vector4? color = null, float thickness = 1f, float indent = 0f)
{
var drawList = ImGui.GetWindowDrawList();
var min = ImGui.GetCursorScreenPos();
@@ -519,8 +644,9 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
bool changed = ImGui.Checkbox(label, ref value);
var boxSize = ImGui.GetFrameHeight();
var min = pos;
var max = ImGui.GetItemRectMax();
var max = new Vector2(pos.X + boxSize, pos.Y + boxSize);
var col = ImGui.GetColorU32(borderColor ?? ImGuiColors.DalamudGrey);
ImGui.GetWindowDrawList().AddRect(min, max, col, rounding, ImDrawFlags.None, borderThickness);
@@ -945,36 +1071,36 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
ImGui.SameLine(150);
ColorText("Penumbra", GetBoolColor(_penumbraExists));
AttachToolTip($"Penumbra is " + (_penumbraExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("Penumbra", _penumbraExists, _ipcManager.Penumbra.State));
ImGui.SameLine();
ColorText("Glamourer", GetBoolColor(_glamourerExists));
AttachToolTip($"Glamourer is " + (_glamourerExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("Glamourer", _glamourerExists, _ipcManager.Glamourer.State));
ImGui.TextUnformatted("Optional Plugins:");
ImGui.SameLine(150);
ColorText("SimpleHeels", GetBoolColor(_heelsExists));
AttachToolTip($"SimpleHeels is " + (_heelsExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("SimpleHeels", _heelsExists, _ipcManager.Heels.State));
ImGui.SameLine();
ColorText("Customize+", GetBoolColor(_customizePlusExists));
AttachToolTip($"Customize+ is " + (_customizePlusExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("Customize+", _customizePlusExists, _ipcManager.CustomizePlus.State));
ImGui.SameLine();
ColorText("Honorific", GetBoolColor(_honorificExists));
AttachToolTip($"Honorific is " + (_honorificExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("Honorific", _honorificExists, _ipcManager.Honorific.State));
ImGui.SameLine();
ColorText("Moodles", GetBoolColor(_moodlesExists));
AttachToolTip($"Moodles is " + (_moodlesExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("Moodles", _moodlesExists, _ipcManager.Moodles.State));
ImGui.SameLine();
ColorText("PetNicknames", GetBoolColor(_petNamesExists));
AttachToolTip($"PetNicknames is " + (_petNamesExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("PetNicknames", _petNamesExists, _ipcManager.PetNames.State));
ImGui.SameLine();
ColorText("Brio", GetBoolColor(_brioExists));
AttachToolTip($"Brio is " + (_brioExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("Brio", _brioExists, _ipcManager.Brio.State));
if (!_penumbraExists || !_glamourerExists)
{
@@ -985,6 +1111,25 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
return true;
}
private static string BuildPluginTooltip(string pluginName, bool isAvailable, IpcConnectionState state)
{
var availability = isAvailable ? "available and up to date." : "unavailable or not up to date.";
return $"{pluginName} is {availability}{Environment.NewLine}IPC State: {DescribeIpcState(state)}";
}
private static string DescribeIpcState(IpcConnectionState state)
=> state switch
{
IpcConnectionState.Unknown => "Not evaluated yet",
IpcConnectionState.MissingPlugin => "Plugin not installed",
IpcConnectionState.VersionMismatch => "Installed version below required minimum",
IpcConnectionState.PluginDisabled => "Plugin installed but disabled",
IpcConnectionState.NotReady => "Plugin is not ready yet",
IpcConnectionState.Available => "Available",
IpcConnectionState.Error => "Error occurred while checking IPC",
_ => state.ToString()
};
public int DrawServiceSelection(bool selectOnChange = false, bool showConnect = true)
{
string[] comboEntries = _serverConfigurationManager.GetServerNames();
@@ -1067,7 +1212,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
{
using (ImRaii.Disabled(_discordOAuthUIDs == null))
{
var aliasPairs = _discordOAuthUIDs?.Result?.Select(t => new UIDAliasPair(t.Key, t.Value)).ToList() ?? [new UIDAliasPair(item.UID ?? null, null)];
var aliasPairs = _discordOAuthUIDs?.Result?.Select(t => new UidAliasPair(t.Key, t.Value)).ToList() ?? [new UidAliasPair(item.UID ?? null, null)];
var uidComboName = "UID###" + item.CharacterName + item.WorldId + serverUri + indexOffset + aliasPairs.Count;
DrawCombo(uidComboName, aliasPairs,
(v) =>
@@ -1220,6 +1365,100 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
return _textureProvider.CreateFromImageAsync(imageData).Result;
}
private static readonly (bool ItemHq, bool HiRes)[] IconLookupOrders =
[
(false, true),
(true, true),
(false, false),
(true, false)
];
public bool TryGetIcon(uint iconId, out IDalamudTextureWrap? wrap)
{
foreach (var (itemHq, hiRes) in IconLookupOrders)
{
if (TryGetIconWithLookup(iconId, itemHq, hiRes, out wrap))
return true;
}
foreach (var (itemHq, hiRes) in IconLookupOrders)
{
if (!_textureProvider.TryGetIconPath(new GameIconLookup(iconId, itemHq, hiRes), out var path) || string.IsNullOrEmpty(path))
continue;
try
{
var reference = _textureProvider.GetFromGame(path);
if (reference.TryGetWrap(out var texture, out _))
{
wrap = texture;
return true;
}
}
catch (Exception ex)
{
Logger.LogTrace(ex, "Failed to load icon {IconId} from path {Path}", iconId, path);
}
}
foreach (var hiRes in new[] { true, false })
{
var manualPath = BuildIconPath(iconId, hiRes);
if (TryLoadTextureFromPath(manualPath, iconId, out wrap))
return true;
}
wrap = null;
return false;
}
private bool TryLoadTextureFromPath(string path, uint iconId, out IDalamudTextureWrap? wrap)
{
try
{
var reference = _textureProvider.GetFromGame(path);
if (reference.TryGetWrap(out var texture, out _))
{
wrap = texture;
return true;
}
}
catch (Exception ex)
{
Logger.LogTrace(ex, "Failed to load icon {IconId} from manual path {Path}", iconId, path);
}
wrap = null;
return false;
}
private static string BuildIconPath(uint iconId, bool hiRes)
{
var folder = iconId - iconId % 1000;
var basePath = $"ui/icon/{folder:000000}/{iconId:000000}";
return hiRes ? $"{basePath}_hr1.tex" : $"{basePath}.tex";
}
private bool TryGetIconWithLookup(uint iconId, bool itemHq, bool hiRes, out IDalamudTextureWrap? wrap)
{
try
{
var icon = _textureProvider.GetFromGameIcon(new GameIconLookup(iconId, itemHq, hiRes));
if (icon.TryGetWrap(out var texture, out _))
{
wrap = texture;
return true;
}
}
catch (Exception ex)
{
Logger.LogTrace(ex, "Failed to load icon {IconId} (HQ:{ItemHq}, HR:{HiRes})", iconId, itemHq, hiRes);
}
wrap = null;
return false;
}
public void LoadLocalization(string languageCode)
{
_localization.SetupWithLangCode(languageCode);
@@ -1253,6 +1492,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
UidFont.Dispose();
GameFont.Dispose();
MediumFont.Dispose();
_discordOAuthGetCts.Dispose();
}
private static void CenterWindow(float width, float height, ImGuiCond cond = ImGuiCond.None)
@@ -1285,13 +1525,24 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
num++;
}
ImGui.PushID(text);
string displayText = text;
string idText = text;
int idSeparatorIndex = text.IndexOf("##", StringComparison.Ordinal);
if (idSeparatorIndex >= 0)
{
displayText = text[..idSeparatorIndex];
idText = text[(idSeparatorIndex + 2)..];
if (string.IsNullOrEmpty(idText))
idText = displayText;
}
ImGui.PushID(idText);
Vector2 vector;
using (IconFont.Push())
vector = ImGui.CalcTextSize(icon.ToIconString());
Vector2 vector2 = ImGui.CalcTextSize(text);
Vector2 vector2 = ImGui.CalcTextSize(displayText);
ImDrawListPtr windowDrawList = ImGui.GetWindowDrawList();
Vector2 cursorScreenPos = ImGui.GetCursorScreenPos();
float num2 = 3f * ImGuiHelpers.GlobalScale;
@@ -1316,7 +1567,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
windowDrawList.AddText(pos, ImGui.GetColorU32(ImGuiCol.Text), icon.ToIconString());
Vector2 pos2 = new Vector2(pos.X + vector.X + num2, cursorScreenPos.Y + ImGui.GetStyle().FramePadding.Y);
windowDrawList.AddText(pos2, ImGui.GetColorU32(ImGuiCol.Text), text);
windowDrawList.AddText(pos2, ImGui.GetColorU32(ImGuiCol.Text), displayText);
ImGui.PopID();
if (num > 0)
{
@@ -1325,6 +1576,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
return result;
}
public sealed record IconScaleData(Vector2 IconSize, Vector2 NormalizedIconScale, float OffsetX, float IconScaling);
private record UIDAliasPair(string? UID, string? Alias);
private sealed record UidAliasPair(string? UID, string? Alias);
}

View File

@@ -13,6 +13,8 @@ using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using Dalamud.Interface;
using LightlessSync.UI.Models;
using LightlessSync.UI.Style;
using LightlessSync.Utils;
namespace LightlessSync.UI;
@@ -25,39 +27,8 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
private ChangelogFile _changelog = new();
private CreditsFile _credits = new();
private bool _scrollToTop;
private int _selectedTab;
private bool _hasInitializedCollapsingHeaders;
private struct Particle
{
public Vector2 Position;
public Vector2 Velocity;
public float Life;
public float MaxLife;
public float Size;
public ParticleType Type;
public List<Vector2>? Trail;
public float Twinkle;
public float Depth;
public float Hue;
}
private enum ParticleType
{
TwinklingStar,
ShootingStar
}
private readonly List<Particle> _particles = [];
private float _particleSpawnTimer;
private readonly Random _random = new();
private const float _headerHeight = 150f;
private const float _particleSpawnInterval = 0.2f;
private const int _maxParticles = 50;
private const int _maxTrailLength = 50;
private const float _edgeFadeDistance = 30f;
private const float _extendedParticleHeight = 40f;
private readonly AnimatedHeader _animatedHeader = new();
public UpdateNotesUi(ILogger<UpdateNotesUi> logger,
LightlessMediator mediator,
@@ -70,21 +41,20 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
_uiShared = uiShared;
_configService = configService;
AllowClickthrough = false;
AllowPinning = false;
RespectCloseHotkey = true;
ShowCloseButton = true;
Flags = ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse |
ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove;
SizeConstraints = new WindowSizeConstraints()
{
MinimumSize = new Vector2(800, 700), MaximumSize = new Vector2(800, 700),
};
PositionCondition = ImGuiCond.Always;
WindowBuilder.For(this)
.AllowPinning(false)
.AllowClickthrough(false)
.SetFixedSize(new Vector2(800, 700))
.Apply();
LoadEmbeddedResources();
logger.LogInformation("UpdateNotesUi constructor completed successfully");
}
@@ -95,6 +65,11 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
_hasInitializedCollapsingHeaders = false;
}
public override void OnClose()
{
_animatedHeader.ClearParticles();
}
private void CenterWindow()
{
var viewport = ImGui.GetMainViewport();
@@ -117,21 +92,18 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
private void DrawHeader()
{
var windowPos = ImGui.GetWindowPos();
var windowPadding = ImGui.GetStyle().WindowPadding;
var headerWidth = (800f * ImGuiHelpers.GlobalScale) - (windowPadding.X * 2);
var headerStart = windowPos + new Vector2(windowPadding.X, windowPadding.Y);
var headerEnd = headerStart + new Vector2(headerWidth, _headerHeight);
var buttons = new List<HeaderButton>
{
new(FontAwesomeIcon.Comments, "Join our Discord", () => Util.OpenLink("https://discord.gg/dsbjcXMnhA")),
new(FontAwesomeIcon.Code, "View on Git", () => Util.OpenLink("https://git.lightless-sync.org/Lightless-Sync"))
};
var extendedParticleSize = new Vector2(headerWidth, _headerHeight + _extendedParticleHeight);
_animatedHeader.DrawWithButtons(headerWidth, "Lightless Sync", "Update Notes", buttons, _uiShared.UidFont);
DrawGradientBackground(headerStart, headerEnd);
DrawHeaderText(headerStart);
DrawHeaderButtons(headerStart, headerWidth);
DrawBottomGradient(headerStart, headerEnd, headerWidth);
ImGui.SetCursorPosY(windowPadding.Y + _headerHeight + 5);
ImGui.SetCursorPosY(windowPadding.Y + _animatedHeader.Height + 5);
ImGui.SetCursorPosX(20);
using (ImRaii.PushFont(UiBuilder.IconFont))
{
@@ -156,347 +128,8 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
}
ImGuiHelpers.ScaledDummy(3);
DrawParticleEffects(headerStart, extendedParticleSize);
}
private void DrawGradientBackground(Vector2 headerStart, Vector2 headerEnd)
{
var drawList = ImGui.GetWindowDrawList();
var darkPurple = new Vector4(0.08f, 0.05f, 0.15f, 1.0f);
var deepPurple = new Vector4(0.12f, 0.08f, 0.20f, 1.0f);
drawList.AddRectFilledMultiColor(
headerStart,
headerEnd,
ImGui.GetColorU32(darkPurple),
ImGui.GetColorU32(darkPurple),
ImGui.GetColorU32(deepPurple),
ImGui.GetColorU32(deepPurple)
);
var random = new Random(42);
for (int i = 0; i < 50; i++)
{
var starPos = headerStart + new Vector2(
(float)random.NextDouble() * (headerEnd.X - headerStart.X),
(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)));
}
}
private void DrawBottomGradient(Vector2 headerStart, Vector2 headerEnd, float width)
{
var drawList = ImGui.GetWindowDrawList();
var gradientHeight = 60f;
for (int i = 0; i < gradientHeight; i++)
{
var progress = i / gradientHeight;
var smoothProgress = progress * progress;
var r = 0.12f + (0.0f - 0.12f) * smoothProgress;
var g = 0.08f + (0.0f - 0.08f) * smoothProgress;
var b = 0.20f + (0.0f - 0.20f) * smoothProgress;
var alpha = 1f - smoothProgress;
var gradientColor = new Vector4(r, g, b, alpha);
drawList.AddLine(
new Vector2(headerStart.X, headerEnd.Y + i),
new Vector2(headerStart.X + width, headerEnd.Y + i),
ImGui.GetColorU32(gradientColor),
1f
);
}
}
private void DrawHeaderText(Vector2 headerStart)
{
var textX = 20f;
var textY = 30f;
ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY));
using (_uiShared.UidFont.Push())
{
ImGui.TextColored(new Vector4(0.95f, 0.95f, 0.95f, 1.0f), "Lightless Sync");
}
ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY + 45f));
ImGui.TextColored(UIColors.Get("LightlessBlue"), "Update Notes");
}
private void DrawHeaderButtons(Vector2 headerStart, float headerWidth)
{
var buttonSize = _uiShared.GetIconButtonSize(FontAwesomeIcon.Globe);
var spacing = 8f * ImGuiHelpers.GlobalScale;
var rightPadding = 15f * ImGuiHelpers.GlobalScale;
var topPadding = 15f * ImGuiHelpers.GlobalScale;
var buttonY = headerStart.Y + topPadding;
var gitButtonX = headerStart.X + headerWidth - rightPadding - buttonSize.X;
var discordButtonX = gitButtonX - buttonSize.X - spacing;
ImGui.SetCursorScreenPos(new Vector2(discordButtonX, buttonY));
using (ImRaii.PushColor(ImGuiCol.Button, new Vector4(0, 0, 0, 0)))
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple") with { W = 0.3f }))
using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive") with { W = 0.5f }))
{
if (_uiShared.IconButton(FontAwesomeIcon.Comments))
{
Util.OpenLink("https://discord.gg/dsbjcXMnhA");
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Join our Discord");
}
ImGui.SetCursorScreenPos(new Vector2(gitButtonX, buttonY));
if (_uiShared.IconButton(FontAwesomeIcon.Code))
{
Util.OpenLink("https://git.lightless-sync.org/Lightless-Sync");
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("View on Git");
}
}
}
private void DrawParticleEffects(Vector2 bannerStart, Vector2 bannerSize)
{
var deltaTime = ImGui.GetIO().DeltaTime;
_particleSpawnTimer += deltaTime;
if (_particleSpawnTimer > _particleSpawnInterval && _particles.Count < _maxParticles)
{
SpawnParticle(bannerSize);
_particleSpawnTimer = 0f;
}
if (_random.NextDouble() < 0.003)
{
SpawnShootingStar(bannerSize);
}
var drawList = ImGui.GetWindowDrawList();
for (int i = _particles.Count - 1; i >= 0; i--)
{
var particle = _particles[i];
var screenPos = bannerStart + particle.Position;
if (particle.Type == ParticleType.ShootingStar && particle.Trail != null)
{
particle.Trail.Insert(0, particle.Position);
if (particle.Trail.Count > _maxTrailLength)
particle.Trail.RemoveAt(particle.Trail.Count - 1);
}
if (particle.Type == ParticleType.TwinklingStar)
{
particle.Twinkle += 0.005f * particle.Depth;
}
particle.Position += particle.Velocity * deltaTime;
particle.Life -= deltaTime;
var isOutOfBounds = particle.Position.X < -50 || particle.Position.X > bannerSize.X + 50 ||
particle.Position.Y < -50 || particle.Position.Y > bannerSize.Y + 50;
if (particle.Life <= 0 || (particle.Type != ParticleType.TwinklingStar && isOutOfBounds))
{
_particles.RemoveAt(i);
continue;
}
if (particle.Type == ParticleType.TwinklingStar)
{
if (particle.Position.X < 0 || particle.Position.X > bannerSize.X)
particle.Velocity = particle.Velocity with { X = -particle.Velocity.X };
if (particle.Position.Y < 0 || particle.Position.Y > bannerSize.Y)
particle.Velocity = particle.Velocity with { Y = -particle.Velocity.Y };
}
var fadeIn = Math.Min(1f, (particle.MaxLife - particle.Life) / 20f);
var fadeOut = Math.Min(1f, particle.Life / 20f);
var lifeFade = Math.Min(fadeIn, fadeOut);
var edgeFadeX = Math.Min(
Math.Min(1f, (particle.Position.X + _edgeFadeDistance) / _edgeFadeDistance),
Math.Min(1f, (bannerSize.X - particle.Position.X + _edgeFadeDistance) / _edgeFadeDistance)
);
var edgeFadeY = Math.Min(
Math.Min(1f, (particle.Position.Y + _edgeFadeDistance) / _edgeFadeDistance),
Math.Min(1f, (bannerSize.Y - particle.Position.Y + _edgeFadeDistance) / _edgeFadeDistance)
);
var edgeFade = Math.Min(edgeFadeX, edgeFadeY);
var baseAlpha = lifeFade * edgeFade;
var finalAlpha = particle.Type == ParticleType.TwinklingStar
? baseAlpha * (0.6f + 0.4f * MathF.Sin(particle.Twinkle))
: baseAlpha;
if (particle.Type == ParticleType.ShootingStar && particle.Trail != null && particle.Trail.Count > 1)
{
var cyanColor = new Vector4(0.4f, 0.8f, 1.0f, 1.0f);
for (int t = 1; t < particle.Trail.Count; t++)
{
var trailProgress = (float)t / particle.Trail.Count;
var trailAlpha = Math.Min(1f, (1f - trailProgress) * finalAlpha * 1.8f);
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 }),
trailWidth + 4f
);
drawList.AddLine(
bannerStart + particle.Trail[t - 1],
bannerStart + particle.Trail[t],
ImGui.GetColorU32(cyanColor with { W = trailAlpha }),
trailWidth
);
}
}
else if (particle.Type == ParticleType.TwinklingStar)
{
DrawTwinklingStar(drawList, screenPos, particle.Size, particle.Hue, finalAlpha, particle.Depth);
}
_particles[i] = particle;
}
}
private void DrawTwinklingStar(ImDrawListPtr drawList, Vector2 position, float size, float hue, float alpha,
float depth)
{
var color = HslToRgb(hue, 1.0f, 0.85f);
color.W = alpha;
drawList.AddCircleFilled(position, size, ImGui.GetColorU32(color));
var glowColor = color with { W = alpha * 0.3f };
drawList.AddCircleFilled(position, size * (1.2f + depth * 0.3f), ImGui.GetColorU32(glowColor));
}
private static Vector4 HslToRgb(float h, float s, float l)
{
h = h / 360f;
float c = (1 - MathF.Abs(2 * l - 1)) * s;
float x = c * (1 - MathF.Abs((h * 6) % 2 - 1));
float m = l - c / 2;
float r, g, b;
if (h < 1f / 6f)
{
r = c;
g = x;
b = 0;
}
else if (h < 2f / 6f)
{
r = x;
g = c;
b = 0;
}
else if (h < 3f / 6f)
{
r = 0;
g = c;
b = x;
}
else if (h < 4f / 6f)
{
r = 0;
g = x;
b = c;
}
else if (h < 5f / 6f)
{
r = x;
g = 0;
b = c;
}
else
{
r = c;
g = 0;
b = x;
}
return new Vector4(r + m, g + m, b + m, 1.0f);
}
private void SpawnParticle(Vector2 bannerSize)
{
var position = new Vector2(
(float)_random.NextDouble() * bannerSize.X,
(float)_random.NextDouble() * bannerSize.Y
);
var depthLayers = new[] { 0.5f, 1.0f, 1.5f };
var depth = depthLayers[_random.Next(depthLayers.Length)];
var velocity = new Vector2(
((float)_random.NextDouble() - 0.5f) * 0.05f * depth,
((float)_random.NextDouble() - 0.5f) * 0.05f * depth
);
var isBlue = _random.NextDouble() < 0.5;
var hue = isBlue ? 220f + (float)_random.NextDouble() * 30f : 270f + (float)_random.NextDouble() * 40f;
var size = (0.5f + (float)_random.NextDouble() * 2f) * depth;
var maxLife = 120f + (float)_random.NextDouble() * 60f;
_particles.Add(new Particle
{
Position = position,
Velocity = velocity,
Life = maxLife,
MaxLife = maxLife,
Size = size,
Type = ParticleType.TwinklingStar,
Trail = null,
Twinkle = (float)_random.NextDouble() * MathF.PI * 2,
Depth = depth,
Hue = hue
});
}
private void SpawnShootingStar(Vector2 bannerSize)
{
var maxLife = 80f + (float)_random.NextDouble() * 40f;
var startX = bannerSize.X * (0.3f + (float)_random.NextDouble() * 0.6f);
var startY = -10f;
_particles.Add(new Particle
{
Position = new Vector2(startX, startY),
Velocity = new Vector2(
-50f - (float)_random.NextDouble() * 40f,
30f + (float)_random.NextDouble() * 40f
),
Life = maxLife,
MaxLife = maxLife,
Size = 2.5f,
Type = ParticleType.ShootingStar,
Trail = new List<Vector2>(),
Twinkle = 0,
Depth = 1.0f,
Hue = 270f
});
}
private void DrawTabs()
{
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 6f))
@@ -513,7 +146,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
{
if (changelogTab)
{
_selectedTab = 0;
DrawChangelog();
}
}
@@ -524,7 +156,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
{
if (creditsTab)
{
_selectedTab = 1;
DrawCredits();
}
}
@@ -558,19 +189,21 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
}
}
private void DrawCreditCategory(CreditCategory category)
private static void DrawCreditCategory(CreditCategory category)
{
DrawFeatureSection(category.Category, UIColors.Get("LightlessBlue"));
foreach (var item in category.Items)
{
ImGui.Bullet();
ImGui.SameLine();
if (!string.IsNullOrEmpty(item.Role))
{
ImGui.BulletText($"{item.Name} — {item.Role}");
ImGui.TextWrapped($"{item.Name} — {item.Role}");
}
else
{
ImGui.BulletText(item.Name);
ImGui.TextWrapped(item.Name);
}
}
@@ -623,7 +256,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
ImGui.SetScrollHereY(0);
}
ImGui.PushTextWrapPos();
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X);
foreach (var entry in _changelog.Changelog)
{
@@ -683,7 +316,9 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
foreach (var item in version.Items)
{
ImGui.BulletText(item);
ImGui.Bullet();
ImGui.SameLine();
ImGui.TextWrapped(item);
}
ImGuiHelpers.ScaledDummy(5);
@@ -745,7 +380,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
using var changelogStream = assembly.GetManifestResourceStream("LightlessSync.Changelog.changelog.yaml");
if (changelogStream != null)
{
using var reader = new StreamReader(changelogStream, Encoding.UTF8, true, 128);
using var reader = new StreamReader(changelogStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, 128);
var yaml = reader.ReadToEnd();
_changelog = deserializer.Deserialize<ChangelogFile>(yaml) ?? new();
}
@@ -754,7 +389,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
using var creditsStream = assembly.GetManifestResourceStream("LightlessSync.Changelog.credits.yaml");
if (creditsStream != null)
{
using var reader = new StreamReader(creditsStream, Encoding.UTF8, true, 128);
using var reader = new StreamReader(creditsStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, 128);
var yaml = reader.ReadToEnd();
_credits = deserializer.Deserialize<CreditsFile>(yaml) ?? new();
}

File diff suppressed because it is too large Load Diff