init 2
This commit is contained in:
@@ -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);
|
||||
@@ -369,7 +369,7 @@ namespace LightlessSync.UI
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
#if DEBUG
|
||||
if (ImGui.BeginTabItem("Debug"))
|
||||
{
|
||||
ImGui.Text("Broadcast Cache");
|
||||
@@ -428,7 +428,7 @@ namespace LightlessSync.UI
|
||||
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
ImGui.EndTabBar();
|
||||
}
|
||||
|
||||
@@ -795,11 +795,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 +869,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))))
|
||||
|
||||
@@ -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,6 +14,8 @@ 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;
|
||||
|
||||
@@ -26,7 +27,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
||||
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;
|
||||
@@ -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) =>
|
||||
|
||||
@@ -16,6 +16,8 @@ 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;
|
||||
@@ -38,11 +40,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;
|
||||
@@ -65,13 +68,15 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
private float _transferPartHeight;
|
||||
private bool _wasOpen;
|
||||
private float _windowContentWidth;
|
||||
private readonly SeluneBrush _seluneBrush = new();
|
||||
private const float ConnectButtonHighlightThickness = 14f;
|
||||
|
||||
public CompactUi(
|
||||
ILogger<CompactUi> logger,
|
||||
UiSharedService uiShared,
|
||||
LightlessConfigService configService,
|
||||
ApiController apiController,
|
||||
PairManager pairManager,
|
||||
PairUiService pairUiService,
|
||||
ServerConfigurationManager serverManager,
|
||||
LightlessMediator mediator,
|
||||
FileUploadManager fileTransferManager,
|
||||
@@ -87,12 +92,12 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
IpcManager ipcManager,
|
||||
BroadcastService 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) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
|
||||
{
|
||||
_uiSharedService = uiShared;
|
||||
_configService = configService;
|
||||
_apiController = apiController;
|
||||
_pairManager = pairManager;
|
||||
_pairUiService = pairUiService;
|
||||
_serverManager = serverManager;
|
||||
_fileTransferManager = fileTransferManager;
|
||||
_tagHandler = tagHandler;
|
||||
@@ -105,7 +110,8 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
_renamePairTagUi = renameTagUi;
|
||||
_ipcManager = ipcManager;
|
||||
_broadcastService = broadcastService;
|
||||
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService, lightlessNotificationService);
|
||||
_pairLedger = pairLedger;
|
||||
_tabMenu = new TopTabMenu(Mediator, _apiController, _uiSharedService, pairRequestService, dalamudUtilService, lightlessNotificationService);
|
||||
|
||||
AllowPinning = true;
|
||||
AllowClickthrough = false;
|
||||
@@ -176,6 +182,11 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
|
||||
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 +234,47 @@ 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);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -290,15 +319,17 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
: (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 +402,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);
|
||||
}
|
||||
}
|
||||
@@ -527,6 +571,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);
|
||||
|
||||
@@ -603,6 +658,20 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -675,6 +744,20 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
ImGui.TextColored(GetUidColor(), _apiController.UID);
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
var padding = new Vector2(30f * ImGuiHelpers.GlobalScale);
|
||||
Selune.RegisterHighlight(
|
||||
ImGui.GetItemRectMin() - padding,
|
||||
ImGui.GetItemRectMax() + padding,
|
||||
SeluneHighlightMode.Point,
|
||||
exactSize: true,
|
||||
clipToElement: true,
|
||||
clipPadding: padding,
|
||||
highlightColorOverride: vanityGlowColor,
|
||||
highlightAlphaOverride: 0.05f);
|
||||
}
|
||||
|
||||
bool uidFooterClicked = ImGui.IsItemClicked();
|
||||
UiSharedService.AttachToolTip("Click to copy");
|
||||
if (uidFooterClicked)
|
||||
@@ -696,28 +779,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,123 +830,215 @@ 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)));
|
||||
var allOnlineNotTaggedPairs = SortEntries(allEntries.Where(FilterNotTaggedUsers));
|
||||
var onlineNotTaggedPairs = SortEntries(filteredEntries.Where(e => FilterNotTaggedUsers(e) && FilterOnlineOrPausedSelf(e)));
|
||||
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder((_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag), onlineNotTaggedPairs, allOnlineNotTaggedPairs));
|
||||
if (allOnlineNotTaggedPairs.Count > 0)
|
||||
{
|
||||
drawFolders.Add(_drawEntityFactory.CreateTagFolder(
|
||||
_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : 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 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)));
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
private static bool FilterOfflineSyncshellUsers(PairUiEntry entry) => !entry.IsDirectlyPaired && !entry.IsOnline && !entry.SelfPermissions.IsPaused();
|
||||
|
||||
private ImmutableList<PairUiEntry> SortEntries(IEnumerable<PairUiEntry> entries)
|
||||
{
|
||||
return entries
|
||||
.OrderByDescending(e => e.IsVisible)
|
||||
.ThenByDescending(e => e.IsOnline)
|
||||
.ThenBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)
|
||||
.ToImmutableList();
|
||||
}
|
||||
|
||||
private ImmutableList<PairUiEntry> SortVisibleEntries(IEnumerable<PairUiEntry> entries)
|
||||
{
|
||||
var entryList = entries.ToList();
|
||||
return _configService.Current.VisiblePairSortMode switch
|
||||
{
|
||||
VisiblePairSortMode.VramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateVramBytes),
|
||||
VisiblePairSortMode.EffectiveVramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateEffectiveVramBytes),
|
||||
VisiblePairSortMode.TriangleCount => SortVisibleByMetric(entryList, e => e.LastAppliedDataTris),
|
||||
VisiblePairSortMode.Alphabetical => entryList
|
||||
.OrderBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)
|
||||
.ToImmutableList(),
|
||||
VisiblePairSortMode.PreferredDirectPairs => SortVisibleByPreferred(entryList),
|
||||
_ => SortEntries(entryList),
|
||||
};
|
||||
}
|
||||
|
||||
private ImmutableList<PairUiEntry> SortVisibleByMetric(IEnumerable<PairUiEntry> entries, Func<PairUiEntry, long> selector)
|
||||
{
|
||||
return entries
|
||||
.OrderByDescending(entry => selector(entry) >= 0)
|
||||
.ThenByDescending(selector)
|
||||
.ThenByDescending(entry => entry.IsOnline)
|
||||
.ThenBy(entry => AlphabeticalSortKey(entry), StringComparer.OrdinalIgnoreCase)
|
||||
.ToImmutableList();
|
||||
}
|
||||
|
||||
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)
|
||||
.ToImmutableList();
|
||||
}
|
||||
|
||||
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)
|
||||
.ToImmutableList();
|
||||
}
|
||||
|
||||
private int GroupSortWeight(PairUiEntry entry, GroupFullInfoDto group)
|
||||
{
|
||||
if (string.Equals(entry.DisplayEntry.Ident.UserId, group.OwnerUID, StringComparison.Ordinal))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (group.GroupPairUserInfos.TryGetValue(entry.DisplayEntry.Ident.UserId, out var info))
|
||||
{
|
||||
if (info.IsModerator()) return 1;
|
||||
if (info.IsPinned()) return 2;
|
||||
}
|
||||
|
||||
return entry.IsVisible ? 3 : 4;
|
||||
}
|
||||
|
||||
private ImmutableList<PairUiEntry> ResolveGroupEntries(
|
||||
IReadOnlyDictionary<string, PairUiEntry> entryLookup,
|
||||
IReadOnlyDictionary<string, Syncshell> syncshells,
|
||||
GroupFullInfoDto group,
|
||||
bool applyFilters)
|
||||
{
|
||||
if (!syncshells.TryGetValue(group.Group.GID, out var shell))
|
||||
{
|
||||
return ImmutableList<PairUiEntry>.Empty;
|
||||
}
|
||||
|
||||
var entries = shell.Users.Keys
|
||||
.Select(id => entryLookup.TryGetValue(id, out var entry) ? entry : null)
|
||||
.Where(entry => entry is not null)
|
||||
.Cast<PairUiEntry>();
|
||||
|
||||
if (applyFilters && _configService.Current.ShowOfflineUsersSeparately)
|
||||
{
|
||||
entries = entries.Where(entry => !FilterOfflineUsers(entry));
|
||||
}
|
||||
|
||||
if (applyFilters && _configService.Current.ShowSyncshellOfflineUsersSeparately)
|
||||
{
|
||||
entries = entries.Where(entry => !FilterOfflineSyncshellUsers(entry));
|
||||
}
|
||||
|
||||
return SortGroupEntries(entries, group);
|
||||
}
|
||||
|
||||
private string GetServerError()
|
||||
|
||||
@@ -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;
|
||||
using LightlessSync.UI.Style;
|
||||
|
||||
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
|
||||
@@ -110,6 +149,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 +163,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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
UiSharedService.AttachToolTip("Opens the profile for this syncshell in a new window.");
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Copy, "Copy ID", menuWidth, true))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
@@ -160,6 +168,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 +260,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));
|
||||
|
||||
@@ -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,25 @@ 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 (_id == TagHandler.CustomVisibleTag)
|
||||
{
|
||||
return DrawVisibleFilter(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 +197,89 @@ 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(GetSortLabel(mode), string.Empty, selected))
|
||||
{
|
||||
if (!selected)
|
||||
{
|
||||
_configService.Current.VisiblePairSortMode = mode;
|
||||
_configService.Save();
|
||||
_mediator.Publish(new RefreshUiMessage());
|
||||
}
|
||||
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
return buttonStart - spacingX;
|
||||
}
|
||||
|
||||
private static string GetSortLabel(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",
|
||||
};
|
||||
}
|
||||
@@ -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"))
|
||||
|
||||
@@ -12,11 +12,16 @@ 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using LightlessSync.UI;
|
||||
|
||||
namespace LightlessSync.UI.Components;
|
||||
|
||||
@@ -27,29 +32,41 @@ 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 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,
|
||||
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;
|
||||
@@ -59,6 +76,18 @@ public class DrawUserPair
|
||||
_uiSharedService = uiSharedService;
|
||||
_performanceConfigService = performanceConfigService;
|
||||
_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 +106,10 @@ public class DrawUserPair
|
||||
DrawName(posX, rightSide);
|
||||
}
|
||||
_wasHovered = ImGui.IsItemHovered();
|
||||
if (_wasHovered)
|
||||
{
|
||||
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), spanFullWidth: true);
|
||||
}
|
||||
color.Dispose();
|
||||
}
|
||||
|
||||
@@ -103,7 +136,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();
|
||||
@@ -313,6 +346,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 +415,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 +461,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 +689,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 +731,7 @@ public class DrawUserPair
|
||||
ImGui.Separator();
|
||||
}
|
||||
|
||||
if (selfIsOwner)
|
||||
if (showOwnerActions)
|
||||
{
|
||||
ImGui.TextUnformatted("Syncshell Owner Functions");
|
||||
string modText = userIsModerator ? "Demod user" : "Mod user";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,151 @@ 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,
|
||||
_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);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -17,6 +16,8 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using LightlessSync.UI.Services;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using static LightlessSync.Services.PairRequestService;
|
||||
|
||||
namespace LightlessSync.UI;
|
||||
@@ -37,7 +38,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
||||
private readonly BroadcastService _broadcastService;
|
||||
private readonly BroadcastScannerService _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,7 +58,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
||||
IDtrBar dtrBar,
|
||||
ConfigurationServiceBase<LightlessConfig> configService,
|
||||
LightlessMediator lightlessMediator,
|
||||
PairManager pairManager,
|
||||
PairUiService pairUiService,
|
||||
PairRequestService pairRequestService,
|
||||
ApiController apiController,
|
||||
ServerConfigurationManager serverManager,
|
||||
@@ -71,7 +72,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 +166,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 +255,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));
|
||||
|
||||
701
LightlessSync/UI/EditProfileUi.Group.cs
Normal file
701
LightlessSync/UI/EditProfileUi.Group.cs
Normal file
@@ -0,0 +1,701 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Components;
|
||||
using Dalamud.Interface.ImGuiFileDialog;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI.Tags;
|
||||
using LightlessSync.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
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));
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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, ImGuiWindowFlags.NoScrollbar))
|
||||
{
|
||||
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();
|
||||
|
||||
ImGui.Dummy(new Vector2(0f, 4f * scale));
|
||||
ImGui.TextColored(UIColors.Get("LightlessBlue"), "Saved Tags");
|
||||
var savedTags = _profileTagService.ResolveTags(_profileTagIds);
|
||||
if (savedTags.Count == 0)
|
||||
{
|
||||
ImGui.TextDisabled("-- No tags set --");
|
||||
}
|
||||
else
|
||||
{
|
||||
bool first = true;
|
||||
for (int i = 0; i < savedTags.Count; i++)
|
||||
{
|
||||
if (!savedTags[i].HasContent)
|
||||
continue;
|
||||
|
||||
if (!first)
|
||||
ImGui.SameLine(0f, 6f * scale);
|
||||
first = false;
|
||||
|
||||
using (ImRaii.PushId($"group-snapshot-tag-{i}"))
|
||||
DrawTagPreview(savedTags[i], scale, "##groupSnapshotTagPreview");
|
||||
}
|
||||
if (!first)
|
||||
ImGui.NewLine();
|
||||
}
|
||||
}
|
||||
|
||||
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: true,
|
||||
onPayloadPrepared: payload =>
|
||||
{
|
||||
_tagEditorSelection.Clear();
|
||||
if (payload.Length > 0)
|
||||
_tagEditorSelection.AddRange(payload);
|
||||
});
|
||||
}
|
||||
|
||||
private void DrawGroupProfileVisibilityControls()
|
||||
{
|
||||
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);
|
||||
await using var stream = new MemoryStream(fileContent);
|
||||
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);
|
||||
await using var stream = new MemoryStream(fileContent);
|
||||
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 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 ? Array.Empty<int>() : payload.ToArray();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
_groupIsNsfw = profile.IsNsfw;
|
||||
_groupIsDisabled = profile.IsDisabled;
|
||||
_groupServerIsNsfw = profile.IsNsfw;
|
||||
_groupServerIsDisabled = profile.IsDisabled;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
@@ -48,6 +56,13 @@ public class IdDisplayHandler
|
||||
using (ImRaii.PushFont(UiBuilder.MonoFont, textIsUid))
|
||||
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))
|
||||
{
|
||||
var prevState = textIsUid;
|
||||
@@ -73,6 +88,11 @@ public class IdDisplayHandler
|
||||
_editEntry = group.GID;
|
||||
_editIsUid = false;
|
||||
}
|
||||
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Middle))
|
||||
{
|
||||
_mediator.Publish(new GroupProfileOpenStandaloneMessage(group));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -97,10 +117,14 @@ 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 rowStart = ImGui.GetCursorScreenPos();
|
||||
var rowWidth = MathF.Max(editBoxWidth.Invoke(), 0f);
|
||||
var rowRightLimit = rowStart.X + rowWidth;
|
||||
|
||||
var font = textIsUid ? UiBuilder.MonoFont : ImGui.GetFont();
|
||||
|
||||
@@ -125,7 +149,6 @@ public class IdDisplayHandler
|
||||
? SeStringUtils.BuildFormattedPlayerName(playerText, textColor, glowColor)
|
||||
: SeStringUtils.BuildPlain(playerText);
|
||||
|
||||
var rowStart = ImGui.GetCursorScreenPos();
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
bool useHighlight = false;
|
||||
float highlightPadX = 0f;
|
||||
@@ -200,6 +223,8 @@ public class IdDisplayHandler
|
||||
drawList.ChannelsMerge();
|
||||
}
|
||||
|
||||
var nameRectMin = ImGui.GetItemRectMin();
|
||||
var nameRectMax = ImGui.GetItemRectMax();
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
if (!string.Equals(_lastMouseOverUid, id))
|
||||
@@ -261,12 +286,43 @@ 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 nameHeight = nameRectMax.Y - nameRectMin.Y;
|
||||
var targetPos = ImGui.GetCursorScreenPos();
|
||||
var availableWidth = MathF.Max(rowRightLimit - targetPos.X, 0f);
|
||||
var centeredY = nameRectMin.Y + MathF.Max((nameHeight - compactHeight) * 0.5f, 0f);
|
||||
float verticalOffset = 1f * ImGuiHelpers.GlobalScale;
|
||||
centeredY += verticalOffset;
|
||||
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 +402,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 +477,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();
|
||||
}
|
||||
}
|
||||
|
||||
25
LightlessSync/UI/Models/PairDisplayEntry.cs
Normal file
25
LightlessSync/UI/Models/PairDisplayEntry.cs
Normal 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;
|
||||
}
|
||||
30
LightlessSync/UI/Models/PairUiEntry.cs
Normal file
30
LightlessSync/UI/Models/PairUiEntry.cs
Normal 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;
|
||||
}
|
||||
24
LightlessSync/UI/Models/PairUiSnapshot.cs
Normal file
24
LightlessSync/UI/Models/PairUiSnapshot.cs
Normal 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>());
|
||||
}
|
||||
11
LightlessSync/UI/Models/VisiblePairSortMode.cs
Normal file
11
LightlessSync/UI/Models/VisiblePairSortMode.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace LightlessSync.UI.Models;
|
||||
|
||||
public enum VisiblePairSortMode
|
||||
{
|
||||
Default = 0,
|
||||
Alphabetical = 1,
|
||||
VramUsage = 2,
|
||||
EffectiveVramUsage = 3,
|
||||
TriangleCount = 4,
|
||||
PreferredDirectPairs = 5,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
84
LightlessSync/UI/ProfileEditorLayoutCoordinator.cs
Normal file
84
LightlessSync/UI/ProfileEditorLayoutCoordinator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,30 @@
|
||||
{
|
||||
SFW = 0,
|
||||
NSFW = 1,
|
||||
|
||||
RP = 2,
|
||||
ERP = 3,
|
||||
Venues = 4,
|
||||
Gpose = 5
|
||||
No_RP = 4,
|
||||
No_ERP = 5,
|
||||
|
||||
Venues = 6,
|
||||
Gpose = 7,
|
||||
|
||||
Limsa = 8,
|
||||
Gridania = 9,
|
||||
Ul_dah = 10,
|
||||
|
||||
WUT = 11,
|
||||
|
||||
PVP = 1001,
|
||||
Ultimate = 1002,
|
||||
Raids = 1003,
|
||||
Roulette = 1004,
|
||||
Crafting = 1005,
|
||||
Casual = 1006,
|
||||
Hardcore = 1007,
|
||||
Glamour = 1008,
|
||||
Mentor = 1009,
|
||||
|
||||
}
|
||||
}
|
||||
228
LightlessSync/UI/Services/PairUiService.cs
Normal file
228
LightlessSync/UI/Services/PairUiService.cs
Normal file
@@ -0,0 +1,228 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility;
|
||||
@@ -16,8 +17,10 @@ using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.UI.Services;
|
||||
using LightlessSync.UI.Style;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.UtilsEnum.Enum;
|
||||
@@ -25,10 +28,12 @@ using LightlessSync.WebAPI;
|
||||
using LightlessSync.WebAPI.Files;
|
||||
using LightlessSync.WebAPI.Files.Models;
|
||||
using LightlessSync.WebAPI.SignalR.Utils;
|
||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||
using Microsoft.AspNetCore.Http.Connections;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
@@ -37,6 +42,9 @@ using System.Net.Http.Json;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using FfxivCharacter = FFXIVClientStructs.FFXIV.Client.Game.Character.Character;
|
||||
using FfxivCharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase;
|
||||
|
||||
namespace LightlessSync.UI;
|
||||
|
||||
@@ -54,7 +62,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
private readonly FileUploadManager _fileTransferManager;
|
||||
private readonly FileTransferOrchestrator _fileTransferOrchestrator;
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly ActorObjectService _actorObjectService;
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly PerformanceCollectorService _performanceCollector;
|
||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||
@@ -94,7 +103,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
|
||||
public SettingsUi(ILogger<SettingsUi> logger,
|
||||
UiSharedService uiShared, LightlessConfigService configService, UiThemeConfigService themeConfigService,
|
||||
PairManager pairManager,
|
||||
PairUiService pairUiService,
|
||||
ServerConfigurationManager serverConfigurationManager,
|
||||
PlayerPerformanceConfigService playerPerformanceConfigService,
|
||||
PairProcessingLimiter pairProcessingLimiter,
|
||||
@@ -106,12 +115,13 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
IpcManager ipcManager, CacheMonitor cacheMonitor,
|
||||
DalamudUtilService dalamudUtilService, HttpClient httpClient,
|
||||
NameplateService nameplateService,
|
||||
NameplateHandler nameplateHandler) : base(logger, mediator, "Lightless Sync Settings",
|
||||
NameplateHandler nameplateHandler,
|
||||
ActorObjectService actorObjectService) : base(logger, mediator, "Lightless Sync Settings",
|
||||
performanceCollector)
|
||||
{
|
||||
_configService = configService;
|
||||
_themeConfigService = themeConfigService;
|
||||
_pairManager = pairManager;
|
||||
_pairUiService = pairUiService;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||
_pairProcessingLimiter = pairProcessingLimiter;
|
||||
@@ -128,13 +138,15 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
_uiShared = uiShared;
|
||||
_nameplateService = nameplateService;
|
||||
_nameplateHandler = nameplateHandler;
|
||||
_actorObjectService = actorObjectService;
|
||||
AllowClickthrough = false;
|
||||
AllowPinning = true;
|
||||
_validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v);
|
||||
|
||||
SizeConstraints = new WindowSizeConstraints()
|
||||
{
|
||||
MinimumSize = new Vector2(800, 400), MaximumSize = new Vector2(800, 2000),
|
||||
MinimumSize = new Vector2(850f, 400f),
|
||||
MaximumSize = new Vector2(850f, 2000f),
|
||||
};
|
||||
|
||||
TitleBarButtons = new()
|
||||
@@ -449,6 +461,74 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTextureDownscaleCounters()
|
||||
{
|
||||
HashSet<Pair> trackedPairs = new();
|
||||
|
||||
var snapshot = _pairUiService.GetSnapshot();
|
||||
|
||||
foreach (var pair in snapshot.DirectPairs)
|
||||
{
|
||||
trackedPairs.Add(pair);
|
||||
}
|
||||
|
||||
foreach (var group in snapshot.GroupPairs.Values)
|
||||
{
|
||||
foreach (var pair in group)
|
||||
{
|
||||
trackedPairs.Add(pair);
|
||||
}
|
||||
}
|
||||
|
||||
long totalOriginalBytes = 0;
|
||||
long totalEffectiveBytes = 0;
|
||||
var hasData = false;
|
||||
|
||||
foreach (var pair in trackedPairs)
|
||||
{
|
||||
if (!pair.IsVisible)
|
||||
continue;
|
||||
|
||||
var original = pair.LastAppliedApproximateVRAMBytes;
|
||||
var effective = pair.LastAppliedApproximateEffectiveVRAMBytes;
|
||||
|
||||
if (original >= 0)
|
||||
{
|
||||
hasData = true;
|
||||
totalOriginalBytes += original;
|
||||
}
|
||||
|
||||
if (effective >= 0)
|
||||
{
|
||||
hasData = true;
|
||||
totalEffectiveBytes += effective;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasData)
|
||||
{
|
||||
ImGui.TextDisabled("VRAM usage has not been calculated yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
var savedBytes = Math.Max(0L, totalOriginalBytes - totalEffectiveBytes);
|
||||
var originalText = UiSharedService.ByteToString(totalOriginalBytes, addSuffix: true);
|
||||
var effectiveText = UiSharedService.ByteToString(totalEffectiveBytes, addSuffix: true);
|
||||
var savedText = UiSharedService.ByteToString(savedBytes, addSuffix: true);
|
||||
|
||||
ImGui.TextUnformatted($"Total VRAM usage (original): {originalText}");
|
||||
ImGui.TextUnformatted($"Total VRAM usage (effective): {effectiveText}");
|
||||
|
||||
if (savedBytes > 0)
|
||||
{
|
||||
UiSharedService.ColorText($"VRAM saved by downscaling: {savedText}", UIColors.Get("LightlessGreen"));
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted($"VRAM saved by downscaling: {savedText}");
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawThemeVectorRow(MainStyle.StyleVector2Option option)
|
||||
{
|
||||
ImGui.TableNextRow();
|
||||
@@ -1383,6 +1463,22 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
_logger.LogWarning(ex, $"Could not delete file {file} because it is in use.");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var directory in Directory.GetDirectories(_configService.Current.CacheFolder))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(directory, recursive: true);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not delete directory {Directory} because it is in use.", directory);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not delete directory {Directory} due to access restrictions.", directory);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1422,8 +1518,9 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.StickyNote, "Export all your user notes to clipboard"))
|
||||
{
|
||||
ImGui.SetClipboardText(UiSharedService.GetNotes(_pairManager.DirectPairs
|
||||
.UnionBy(_pairManager.GroupPairs.SelectMany(p => p.Value), p => p.UserData,
|
||||
var snapshot = _pairUiService.GetSnapshot();
|
||||
ImGui.SetClipboardText(UiSharedService.GetNotes(snapshot.DirectPairs
|
||||
.UnionBy(snapshot.GroupPairs.SelectMany(p => p.Value), p => p.UserData,
|
||||
UserDataComparer.Instance).ToList()));
|
||||
}
|
||||
|
||||
@@ -2388,6 +2485,22 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
_uiShared.DrawHelpText(
|
||||
"Will show a performance indicator when players exceed defined thresholds in Lightless UI." +
|
||||
Environment.NewLine + "Will use warning thresholds.");
|
||||
|
||||
using (ImRaii.Disabled(!showPerformanceIndicator))
|
||||
{
|
||||
using var indent = ImRaii.PushIndent();
|
||||
bool showCompactStats = _playerPerformanceConfigService.Current.ShowPerformanceUsageNextToName;
|
||||
if (ImGui.Checkbox("Show performance stats next to alias", ref showCompactStats))
|
||||
{
|
||||
_playerPerformanceConfigService.Current.ShowPerformanceUsageNextToName = showCompactStats;
|
||||
_playerPerformanceConfigService.Save();
|
||||
}
|
||||
|
||||
_uiShared.DrawHelpText(
|
||||
"Adds a text with approx. VRAM usage and triangle count to the right of pairs alias." +
|
||||
Environment.NewLine + "Requires performance indicator to be enabled.");
|
||||
}
|
||||
|
||||
bool warnOnExceedingThresholds = _playerPerformanceConfigService.Current.WarnOnExceedingThresholds;
|
||||
if (ImGui.Checkbox("Warn on loading in players exceeding performance thresholds",
|
||||
ref warnOnExceedingThresholds))
|
||||
@@ -2552,6 +2665,102 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
ImGui.TreePop();
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
if (_uiShared.MediumTreeNode("Texture Optimization", UIColors.Get("LightlessYellow")))
|
||||
{
|
||||
_uiShared.MediumText("Warning", UIColors.Get("DimRed"));
|
||||
_uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"),
|
||||
new SeStringUtils.RichTextEntry("Texture compression and downscaling is potentially a "),
|
||||
new SeStringUtils.RichTextEntry("destructive", UIColors.Get("DimRed"), true),
|
||||
new SeStringUtils.RichTextEntry(" process and may cause broken or incorrect character appearances."));
|
||||
|
||||
_uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"),
|
||||
new SeStringUtils.RichTextEntry("This feature is encouraged to help "),
|
||||
new SeStringUtils.RichTextEntry("lower-end systems with limited VRAM", UIColors.Get("LightlessYellow"), true),
|
||||
new SeStringUtils.RichTextEntry(" and for use in "),
|
||||
new SeStringUtils.RichTextEntry("performance-critical scenarios", UIColors.Get("LightlessYellow"), true),
|
||||
new SeStringUtils.RichTextEntry("."));
|
||||
|
||||
_uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"),
|
||||
new SeStringUtils.RichTextEntry("Runtime downscaling "),
|
||||
new SeStringUtils.RichTextEntry("MAY", UIColors.Get("DimRed"), true),
|
||||
new SeStringUtils.RichTextEntry(" cause higher load on the system when processing downloads."));
|
||||
|
||||
_uiShared.DrawNoteLine("!!! ", UIColors.Get("DimRed"),
|
||||
new SeStringUtils.RichTextEntry("When enabled, we cannot provide support for appearance issues caused by this setting!", UIColors.Get("DimRed"), true));
|
||||
|
||||
var textureConfig = _playerPerformanceConfigService.Current;
|
||||
var trimNonIndex = textureConfig.EnableNonIndexTextureMipTrim;
|
||||
if (ImGui.Checkbox("Trim mip levels for textures", ref trimNonIndex))
|
||||
{
|
||||
textureConfig.EnableNonIndexTextureMipTrim = trimNonIndex;
|
||||
_playerPerformanceConfigService.Save();
|
||||
}
|
||||
_uiShared.DrawHelpText("When enabled, Lightless will remove high-resolution mip levels from textures (not index) that exceed the size limit and are not compressed with any kind compression.");
|
||||
|
||||
var downscaleIndex = textureConfig.EnableIndexTextureDownscale;
|
||||
if (ImGui.Checkbox("Downscale index textures above limit", ref downscaleIndex))
|
||||
{
|
||||
textureConfig.EnableIndexTextureDownscale = downscaleIndex;
|
||||
_playerPerformanceConfigService.Save();
|
||||
}
|
||||
_uiShared.DrawHelpText("Controls whether Lightless reduces index textures that exceed the size limit.");
|
||||
|
||||
var dimensionOptions = new[] { 512, 1024, 2048, 4096 };
|
||||
var optionLabels = dimensionOptions.Select(static value => value.ToString()).ToArray();
|
||||
var currentDimension = textureConfig.TextureDownscaleMaxDimension;
|
||||
var selectedIndex = Array.IndexOf(dimensionOptions, currentDimension);
|
||||
if (selectedIndex < 0)
|
||||
{
|
||||
selectedIndex = Array.IndexOf(dimensionOptions, 2048);
|
||||
}
|
||||
|
||||
ImGui.SetNextItemWidth(140 * ImGuiHelpers.GlobalScale);
|
||||
if (ImGui.Combo("Maximum texture dimension", ref selectedIndex, optionLabels, optionLabels.Length))
|
||||
{
|
||||
textureConfig.TextureDownscaleMaxDimension = dimensionOptions[selectedIndex];
|
||||
_playerPerformanceConfigService.Save();
|
||||
}
|
||||
_uiShared.DrawHelpText($"Textures above this size will be reduced until their largest dimension is at or below the limit. Block-compressed textures are skipped when \"Only downscale uncompressed\" is enabled.{UiSharedService.TooltipSeparator}Default: 2048");
|
||||
|
||||
var keepOriginalTextures = textureConfig.KeepOriginalTextureFiles;
|
||||
if (ImGui.Checkbox("Keep original texture files", ref keepOriginalTextures))
|
||||
{
|
||||
textureConfig.KeepOriginalTextureFiles = keepOriginalTextures;
|
||||
_playerPerformanceConfigService.Save();
|
||||
}
|
||||
_uiShared.DrawHelpText("When disabled, Lightless removes the original texture after a downscaled copy is created.");
|
||||
ImGui.SameLine();
|
||||
_uiShared.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("If disabled, saved + effective VRAM usage information will not work.", UIColors.Get("LightlessYellow")));
|
||||
|
||||
if (!textureConfig.EnableNonIndexTextureMipTrim && !textureConfig.EnableIndexTextureDownscale)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("Both trimming and downscale are disabled. Lightless will keep original textures regardless of size.", UIColors.Get("DimRed"));
|
||||
}
|
||||
|
||||
ImGui.Dummy(new Vector2(5));
|
||||
|
||||
_uiShared.ColoredSeparator(UIColors.Get("DimRed"), 3f);
|
||||
var onlyUncompressed = textureConfig.OnlyDownscaleUncompressedTextures;
|
||||
if (ImGui.Checkbox("Only downscale uncompressed textures", ref onlyUncompressed))
|
||||
{
|
||||
textureConfig.OnlyDownscaleUncompressedTextures = onlyUncompressed;
|
||||
_playerPerformanceConfigService.Save();
|
||||
}
|
||||
_uiShared.DrawHelpText("If disabled, compressed textures will be targeted for downscaling too.");
|
||||
_uiShared.ColoredSeparator(UIColors.Get("DimRed"), 3f);
|
||||
|
||||
ImGui.Dummy(new Vector2(5));
|
||||
|
||||
DrawTextureDownscaleCounters();
|
||||
|
||||
ImGui.Dummy(new Vector2(5));
|
||||
|
||||
_uiShared.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f);
|
||||
ImGui.TreePop();
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
ImGui.Dummy(new Vector2(10));
|
||||
|
||||
@@ -3511,7 +3720,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
// Lightless notification locations
|
||||
var lightlessLocations = GetLightlessNotificationLocations();
|
||||
var downloadLocations = GetDownloadNotificationLocations();
|
||||
|
||||
|
||||
if (ImGui.BeginTable("##NotificationLocationTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit))
|
||||
{
|
||||
ImGui.TableSetupColumn("Notification Type", ImGuiTableColumnFlags.WidthFixed, 200f * ImGuiHelpers.GlobalScale);
|
||||
@@ -3674,7 +3883,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
|
||||
ImGui.EndTable();
|
||||
}
|
||||
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Clear All Notifications"))
|
||||
{
|
||||
@@ -3792,7 +4001,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.TextUnformatted("Size & Layout");
|
||||
|
||||
|
||||
float notifWidth = _configService.Current.NotificationWidth;
|
||||
if (ImGui.SliderFloat("Notification Width", ref notifWidth, 250f, 600f, "%.0f"))
|
||||
{
|
||||
@@ -3825,7 +4034,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.TextUnformatted("Position");
|
||||
|
||||
|
||||
var currentCorner = _configService.Current.NotificationCorner;
|
||||
if (ImGui.BeginCombo("Notification Position", GetNotificationCornerLabel(currentCorner)))
|
||||
{
|
||||
@@ -3843,7 +4052,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
ImGui.EndCombo();
|
||||
}
|
||||
_uiShared.DrawHelpText("Choose which corner of the screen notifications appear in.");
|
||||
|
||||
|
||||
int offsetY = _configService.Current.NotificationOffsetY;
|
||||
if (ImGui.SliderInt("Vertical Offset", ref offsetY, -2500, 2500))
|
||||
{
|
||||
@@ -4136,7 +4345,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
|
||||
ImGui.Separator();
|
||||
// Location descriptions removed - information is now inline with each setting
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4256,7 +4465,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
ImGui.TableSetColumnIndex(2);
|
||||
var availableWidth = ImGui.GetContentRegionAvail().X;
|
||||
var buttonWidth = (availableWidth - ImGui.GetStyle().ItemSpacing.X * 2) / 3;
|
||||
|
||||
|
||||
// Play button
|
||||
using var playId = ImRaii.PushId($"Play_{typeIndex}");
|
||||
using (ImRaii.Disabled(isDisabled))
|
||||
@@ -4277,7 +4486,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
UiSharedService.AttachToolTip("Test this sound");
|
||||
|
||||
|
||||
// Disable toggle button
|
||||
ImGui.SameLine();
|
||||
using var disableId = ImRaii.PushId($"Disable_{typeIndex}");
|
||||
@@ -4285,11 +4494,11 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
var icon = isDisabled ? FontAwesomeIcon.VolumeOff : FontAwesomeIcon.VolumeUp;
|
||||
var color = isDisabled ? UIColors.Get("DimRed") : UIColors.Get("LightlessGreen");
|
||||
|
||||
|
||||
ImGui.PushStyleColor(ImGuiCol.Button, color);
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, color * new Vector4(1.2f, 1.2f, 1.2f, 1f));
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonActive, color * new Vector4(0.8f, 0.8f, 0.8f, 1f));
|
||||
|
||||
|
||||
if (ImGui.Button(icon.ToIconString(), new Vector2(buttonWidth, 0)))
|
||||
{
|
||||
bool newDisabled = !isDisabled;
|
||||
@@ -4303,16 +4512,16 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
_configService.Save();
|
||||
}
|
||||
|
||||
|
||||
ImGui.PopStyleColor(3);
|
||||
}
|
||||
UiSharedService.AttachToolTip(isDisabled ? "Sound is disabled - click to enable" : "Sound is enabled - click to disable");
|
||||
|
||||
|
||||
// Reset button
|
||||
ImGui.SameLine();
|
||||
using var resetId = ImRaii.PushId($"Reset_{typeIndex}");
|
||||
bool isDefault = currentSoundId == defaultSoundId;
|
||||
|
||||
|
||||
using (ImRaii.Disabled(isDefault))
|
||||
{
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
@@ -4337,6 +4546,4 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
ImGui.EndTable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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),
|
||||
|
||||
1006
LightlessSync/UI/Style/Selune.cs
Normal file
1006
LightlessSync/UI/Style/Selune.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,6 @@
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.ImGuiFileDialog;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using LightlessSync.API.Data;
|
||||
@@ -10,14 +9,13 @@ using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI.Handlers;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.WebAPI;
|
||||
using LightlessSync.UI.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
@@ -30,35 +28,28 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
private readonly bool _isModerator = false;
|
||||
private readonly bool _isOwner = false;
|
||||
private readonly List<string> _oneTimeInvites = [];
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly LightlessProfileManager _lightlessProfileManager;
|
||||
private readonly FileDialogManager _fileDialogManager;
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private List<BannedGroupUserDto> _bannedUsers = [];
|
||||
private LightlessGroupProfileData? _profileData = null;
|
||||
private bool _adjustedForScollBarsLocalProfile = false;
|
||||
private bool _adjustedForScollBarsOnlineProfile = false;
|
||||
private string _descriptionText = string.Empty;
|
||||
private IDalamudTextureWrap? _pfpTextureWrap;
|
||||
private string _profileDescription = string.Empty;
|
||||
private byte[] _profileImage = [];
|
||||
private bool _showFileDialogError = false;
|
||||
private int _multiInvites;
|
||||
private string _newPassword;
|
||||
private bool _pwChangeSuccess;
|
||||
private Task<int>? _pruneTestTask;
|
||||
private Task<int>? _pruneTask;
|
||||
private int _pruneDays = 14;
|
||||
private List<int> _selectedTags = [];
|
||||
|
||||
public SyncshellAdminUI(ILogger<SyncshellAdminUI> logger, LightlessMediator mediator, ApiController apiController,
|
||||
UiSharedService uiSharedService, PairManager pairManager, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, LightlessProfileManager lightlessProfileManager, FileDialogManager fileDialogManager)
|
||||
UiSharedService uiSharedService, PairUiService pairUiService, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, LightlessProfileManager lightlessProfileManager, FileDialogManager fileDialogManager)
|
||||
: base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService)
|
||||
{
|
||||
GroupFullInfo = groupFullInfo;
|
||||
_apiController = apiController;
|
||||
_uiSharedService = uiSharedService;
|
||||
_pairManager = pairManager;
|
||||
_pairUiService = pairUiService;
|
||||
_lightlessProfileManager = lightlessProfileManager;
|
||||
_fileDialogManager = fileDialogManager;
|
||||
|
||||
@@ -68,14 +59,6 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
_multiInvites = 30;
|
||||
_pwChangeSuccess = true;
|
||||
IsOpen = true;
|
||||
Mediator.Subscribe<ClearProfileGroupDataMessage>(this, (msg) =>
|
||||
{
|
||||
if (msg.GroupData == null || string.Equals(msg.GroupData.AliasOrGID, GroupFullInfo.Group.AliasOrGID, StringComparison.Ordinal))
|
||||
{
|
||||
_pfpTextureWrap?.Dispose();
|
||||
_pfpTextureWrap = null;
|
||||
}
|
||||
});
|
||||
SizeConstraints = new WindowSizeConstraints()
|
||||
{
|
||||
MinimumSize = new(700, 500),
|
||||
@@ -90,10 +73,13 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
if (!_isModerator && !_isOwner) return;
|
||||
|
||||
_logger.LogTrace("Drawing Syncshell Admin UI for {group}", GroupFullInfo.GroupAliasOrGID);
|
||||
GroupFullInfo = _pairManager.Groups[GroupFullInfo.Group];
|
||||
var snapshot = _pairUiService.GetSnapshot();
|
||||
if (snapshot.GroupsByGid.TryGetValue(GroupFullInfo.Group.GID, out var updatedInfo))
|
||||
{
|
||||
GroupFullInfo = updatedInfo;
|
||||
}
|
||||
|
||||
_profileData = _lightlessProfileManager.GetLightlessGroupProfile(GroupFullInfo.Group);
|
||||
GetTagsFromProfile();
|
||||
|
||||
using var id = ImRaii.PushId("syncshell_admin_" + GroupFullInfo.GID);
|
||||
using (_uiSharedService.UidFont.Push())
|
||||
@@ -215,179 +201,47 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
private void DrawProfile()
|
||||
{
|
||||
var profileTab = ImRaii.TabItem("Profile");
|
||||
if (!profileTab)
|
||||
return;
|
||||
|
||||
if (profileTab)
|
||||
if (_profileData != null)
|
||||
{
|
||||
if (_uiSharedService.MediumTreeNode("Current Profile", UIColors.Get("LightlessPurple")))
|
||||
if (!string.Equals(_profileDescription, _profileData.Description, StringComparison.Ordinal))
|
||||
{
|
||||
ImGui.Dummy(new Vector2(5));
|
||||
|
||||
if (!_profileImage.SequenceEqual(_profileData.ImageData.Value))
|
||||
{
|
||||
_profileImage = _profileData.ImageData.Value;
|
||||
_pfpTextureWrap?.Dispose();
|
||||
_pfpTextureWrap = _uiSharedService.LoadImage(_profileImage);
|
||||
}
|
||||
|
||||
if (!string.Equals(_profileDescription, _profileData.Description, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_profileDescription = _profileData.Description;
|
||||
_descriptionText = _profileDescription;
|
||||
}
|
||||
|
||||
if (_pfpTextureWrap != null)
|
||||
{
|
||||
ImGui.Image(_pfpTextureWrap.Handle, ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height));
|
||||
}
|
||||
|
||||
var spacing = ImGui.GetStyle().ItemSpacing.X;
|
||||
ImGuiHelpers.ScaledRelativeSameLine(256, spacing);
|
||||
using (_uiSharedService.GameFont.Push())
|
||||
{
|
||||
var descriptionTextSize = ImGui.CalcTextSize(_profileData.Description, wrapWidth: 256f);
|
||||
var childFrame = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 256);
|
||||
if (descriptionTextSize.Y > childFrame.Y)
|
||||
{
|
||||
_adjustedForScollBarsOnlineProfile = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_adjustedForScollBarsOnlineProfile = false;
|
||||
}
|
||||
childFrame = childFrame with
|
||||
{
|
||||
X = childFrame.X + (_adjustedForScollBarsOnlineProfile ? ImGui.GetStyle().ScrollbarSize : 0),
|
||||
};
|
||||
if (ImGui.BeginChildFrame(101, childFrame))
|
||||
{
|
||||
UiSharedService.TextWrapped(_profileData.Description);
|
||||
}
|
||||
ImGui.EndChildFrame();
|
||||
ImGui.TreePop();
|
||||
}
|
||||
var nsfw = _profileData.IsNsfw;
|
||||
ImGui.BeginDisabled();
|
||||
ImGui.Checkbox("Is NSFW", ref nsfw);
|
||||
ImGui.EndDisabled();
|
||||
_profileDescription = _profileData.Description;
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
UiSharedService.TextWrapped("Preview the Syncshell profile in a standalone window.");
|
||||
|
||||
if (_uiSharedService.MediumTreeNode("Profile Settings", UIColors.Get("LightlessPurple")))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.AddressCard, "Open Syncshell Profile"))
|
||||
{
|
||||
ImGui.Dummy(new Vector2(5));
|
||||
ImGui.TextUnformatted($"Profile Picture:");
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture"))
|
||||
{
|
||||
_fileDialogManager.OpenFileDialog("Select new Profile picture", ".png", (success, file) =>
|
||||
{
|
||||
if (!success) return;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
var fileContent = await File.ReadAllBytesAsync(file).ConfigureAwait(false);
|
||||
MemoryStream ms = new(fileContent);
|
||||
await using (ms.ConfigureAwait(false))
|
||||
{
|
||||
var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false);
|
||||
if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
_showFileDialogError = true;
|
||||
return;
|
||||
}
|
||||
using var image = Image.Load<Rgba32>(fileContent);
|
||||
|
||||
if (image.Width > 512 || image.Height > 512 || (fileContent.Length > 2000 * 1024))
|
||||
{
|
||||
_showFileDialogError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_showFileDialogError = false;
|
||||
await _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, Convert.ToBase64String(fileContent), BannerBase64: null, IsNsfw: null, IsDisabled: null))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
UiSharedService.AttachToolTip("Select and upload a new profile picture");
|
||||
ImGui.SameLine();
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture"))
|
||||
{
|
||||
_ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: null, IsDisabled: null));
|
||||
}
|
||||
UiSharedService.AttachToolTip("Clear your currently uploaded profile picture");
|
||||
if (_showFileDialogError)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size", ImGuiColors.DalamudRed);
|
||||
}
|
||||
ImGui.Separator();
|
||||
ImGui.TextUnformatted($"Tags:");
|
||||
var childFrameLocal = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 200);
|
||||
|
||||
var allCategoryIndexes = Enum.GetValues<ProfileTags>()
|
||||
.Cast<int>()
|
||||
.ToList();
|
||||
|
||||
foreach(int tag in allCategoryIndexes)
|
||||
{
|
||||
using (ImRaii.PushId($"tag-{tag}")) DrawTag(tag);
|
||||
}
|
||||
ImGui.Separator();
|
||||
var widthTextBox = 400;
|
||||
var posX = ImGui.GetCursorPosX();
|
||||
ImGui.TextUnformatted($"Description {_descriptionText.Length}/1500");
|
||||
ImGui.SetCursorPosX(posX);
|
||||
ImGuiHelpers.ScaledRelativeSameLine(widthTextBox, ImGui.GetStyle().ItemSpacing.X);
|
||||
ImGui.TextUnformatted("Preview (approximate)");
|
||||
using (_uiSharedService.GameFont.Push())
|
||||
ImGui.InputTextMultiline("##description", ref _descriptionText, 1500, ImGuiHelpers.ScaledVector2(widthTextBox, 200));
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
using (_uiSharedService.GameFont.Push())
|
||||
{
|
||||
var descriptionTextSizeLocal = ImGui.CalcTextSize(_descriptionText, wrapWidth: 256f);
|
||||
if (descriptionTextSizeLocal.Y > childFrameLocal.Y)
|
||||
{
|
||||
_adjustedForScollBarsLocalProfile = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_adjustedForScollBarsLocalProfile = false;
|
||||
}
|
||||
childFrameLocal = childFrameLocal with
|
||||
{
|
||||
X = childFrameLocal.X + (_adjustedForScollBarsLocalProfile ? ImGui.GetStyle().ScrollbarSize : 0),
|
||||
};
|
||||
if (ImGui.BeginChildFrame(102, childFrameLocal))
|
||||
{
|
||||
UiSharedService.TextWrapped(_descriptionText);
|
||||
}
|
||||
ImGui.EndChildFrame();
|
||||
}
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description"))
|
||||
{
|
||||
_ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: _descriptionText, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: null, IsDisabled: null));
|
||||
}
|
||||
UiSharedService.AttachToolTip("Sets your profile description text");
|
||||
ImGui.SameLine();
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description"))
|
||||
{
|
||||
_ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: null, IsDisabled: null));
|
||||
}
|
||||
UiSharedService.AttachToolTip("Clears your profile description text");
|
||||
ImGui.Separator();
|
||||
ImGui.TextUnformatted($"Profile Options:");
|
||||
var isNsfw = _profileData.IsNsfw;
|
||||
if (ImGui.Checkbox("Profile is NSFW", ref isNsfw))
|
||||
{
|
||||
_ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: isNsfw, IsDisabled: null));
|
||||
}
|
||||
_uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON");
|
||||
ImGui.TreePop();
|
||||
Mediator.Publish(new GroupProfileOpenStandaloneMessage(GroupFullInfo));
|
||||
}
|
||||
UiSharedService.AttachToolTip("Opens the standalone Syncshell profile window for this group.");
|
||||
|
||||
ImGuiHelpers.ScaledDummy(2f);
|
||||
ImGui.TextDisabled("Profile Flags");
|
||||
ImGui.BulletText(_profileData.IsNsfw ? "Marked as NSFW" : "Marked as SFW");
|
||||
ImGui.BulletText(_profileData.IsDisabled ? "Profile disabled for viewers" : "Profile active");
|
||||
|
||||
ImGuiHelpers.ScaledDummy(2f);
|
||||
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
|
||||
ImGuiHelpers.ScaledDummy(2f);
|
||||
|
||||
UiSharedService.TextWrapped("Open the syncshell profile editor to update images, description, tags, and visibility settings.");
|
||||
ImGuiHelpers.ScaledDummy(2f);
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserEdit, "Open Syncshell Profile Editor"))
|
||||
{
|
||||
Mediator.Publish(new OpenGroupProfileEditorMessage(GroupFullInfo));
|
||||
}
|
||||
UiSharedService.AttachToolTip("Launches the editor window and associated live preview for this syncshell.");
|
||||
}
|
||||
else
|
||||
{
|
||||
UiSharedService.TextWrapped("Profile information is loading...");
|
||||
}
|
||||
|
||||
profileTab.Dispose();
|
||||
}
|
||||
|
||||
@@ -398,7 +252,8 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
{
|
||||
if (_uiSharedService.MediumTreeNode("User List & Administration", UIColors.Get("LightlessPurple")))
|
||||
{
|
||||
if (!_pairManager.GroupPairs.TryGetValue(GroupFullInfo, out var pairs))
|
||||
var snapshot = _pairUiService.GetSnapshot();
|
||||
if (!snapshot.GroupPairs.TryGetValue(GroupFullInfo, out var pairs))
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("No users found in this Syncshell", ImGuiColors.DalamudYellow);
|
||||
}
|
||||
@@ -734,37 +589,8 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
}
|
||||
inviteTab.Dispose();
|
||||
}
|
||||
private void DrawTag(int tag)
|
||||
{
|
||||
var HasTag = _selectedTags.Contains(tag);
|
||||
var tagName = (ProfileTags)tag;
|
||||
|
||||
if (ImGui.Checkbox(tagName.ToString(), ref HasTag))
|
||||
{
|
||||
if (HasTag)
|
||||
{
|
||||
_selectedTags.Add(tag);
|
||||
_ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: _selectedTags.ToArray(), PictureBase64: null, BannerBase64: null, IsNsfw: null, IsDisabled: null));
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedTags.Remove(tag);
|
||||
_ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: _selectedTags.ToArray(), PictureBase64: null, BannerBase64: null, IsNsfw: null, IsDisabled: null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void GetTagsFromProfile()
|
||||
{
|
||||
if (_profileData != null)
|
||||
{
|
||||
_selectedTags = [.. _profileData.Tags];
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnClose()
|
||||
{
|
||||
Mediator.Publish(new RemoveWindowMessage(this));
|
||||
_pfpTextureWrap?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,16 @@ 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.Mediator;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI;
|
||||
using LightlessSync.UI.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LightlessSync.UI;
|
||||
|
||||
@@ -23,7 +26,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
private readonly BroadcastService _broadcastService;
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private readonly BroadcastScannerService _broadcastScannerService;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
|
||||
private readonly List<GroupJoinDto> _nearbySyncshells = [];
|
||||
@@ -43,14 +46,14 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
UiSharedService uiShared,
|
||||
ApiController apiController,
|
||||
BroadcastScannerService broadcastScannerService,
|
||||
PairManager pairManager,
|
||||
PairUiService pairUiService,
|
||||
DalamudUtilService dalamudUtilService) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService)
|
||||
{
|
||||
_broadcastService = broadcastService;
|
||||
_uiSharedService = uiShared;
|
||||
_apiController = apiController;
|
||||
_broadcastScannerService = broadcastScannerService;
|
||||
_pairManager = pairManager;
|
||||
_pairUiService = pairUiService;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
|
||||
IsOpen = false;
|
||||
@@ -266,7 +269,8 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
private async Task RefreshSyncshellsAsync()
|
||||
{
|
||||
var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts();
|
||||
_currentSyncshells = [.. _pairManager.GroupPairs.Select(g => g.Key)];
|
||||
var snapshot = _pairUiService.GetSnapshot();
|
||||
_currentSyncshells = snapshot.GroupPairs.Keys.ToList();
|
||||
|
||||
_recentlyJoined.RemoveWhere(gid => _currentSyncshells.Any(s => string.Equals(s.GID, gid, StringComparison.Ordinal)));
|
||||
|
||||
|
||||
30
LightlessSync/UI/Tags/ProfileTagDefinition.cs
Normal file
30
LightlessSync/UI/Tags/ProfileTagDefinition.cs
Normal 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);
|
||||
}
|
||||
226
LightlessSync/UI/Tags/ProfileTagRenderer.cs
Normal file
226
LightlessSync/UI/Tags/ProfileTagRenderer.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
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;
|
||||
using System.Collections.Generic;
|
||||
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;
|
||||
}
|
||||
}
|
||||
131
LightlessSync/UI/Tags/ProfileTagService.cs
Normal file
131
LightlessSync/UI/Tags/ProfileTagService.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using LightlessSync.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
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 IReadOnlyDictionary<int, ProfileTagDefinition> GetTagLibrary()
|
||||
=> TagLibrary;
|
||||
|
||||
public 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()
|
||||
{
|
||||
var dictionary = new Dictionary<int, ProfileTagDefinition>
|
||||
{
|
||||
[(int)ProfileTags.SFW] = 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)),
|
||||
|
||||
[(int)ProfileTags.NSFW] = 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)),
|
||||
|
||||
|
||||
[(int)ProfileTags.RP] = 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)),
|
||||
|
||||
[(int)ProfileTags.ERP] = 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)),
|
||||
|
||||
[(int)ProfileTags.No_RP] = 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)),
|
||||
|
||||
[(int)ProfileTags.No_ERP] = 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)),
|
||||
|
||||
|
||||
[(int)ProfileTags.Venues] = 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)),
|
||||
|
||||
[(int)ProfileTags.Gpose] = 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)),
|
||||
|
||||
|
||||
[(int)ProfileTags.Limsa] = ProfileTagDefinition.FromIconAndText(
|
||||
60572,
|
||||
"Limsa"),
|
||||
|
||||
[(int)ProfileTags.Gridania] = ProfileTagDefinition.FromIconAndText(
|
||||
60573,
|
||||
"Gridania"),
|
||||
|
||||
[(int)ProfileTags.Ul_dah] = ProfileTagDefinition.FromIconAndText(
|
||||
60574,
|
||||
"Ul'dah"),
|
||||
|
||||
|
||||
[(int)ProfileTags.WUT] = ProfileTagDefinition.FromIconAndText(
|
||||
61397,
|
||||
"WU/T"),
|
||||
|
||||
|
||||
[(int)ProfileTags.PVP] = ProfileTagDefinition.FromIcon(61806),
|
||||
[(int)ProfileTags.Ultimate] = ProfileTagDefinition.FromIcon(61832),
|
||||
[(int)ProfileTags.Raids] = ProfileTagDefinition.FromIcon(61802),
|
||||
[(int)ProfileTags.Roulette] = ProfileTagDefinition.FromIcon(61807),
|
||||
[(int)ProfileTags.Crafting] = ProfileTagDefinition.FromIcon(61816),
|
||||
[(int)ProfileTags.Casual] = ProfileTagDefinition.FromIcon(61753),
|
||||
[(int)ProfileTags.Hardcore] = ProfileTagDefinition.FromIcon(61754),
|
||||
[(int)ProfileTags.Glamour] = ProfileTagDefinition.FromIcon(61759),
|
||||
[(int)ProfileTags.Mentor] = ProfileTagDefinition.FromIcon(61760)
|
||||
|
||||
};
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
@@ -10,8 +11,12 @@ using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.UI.Models;
|
||||
using LightlessSync.UI.Style;
|
||||
using LightlessSync.WebAPI;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
|
||||
|
||||
namespace LightlessSync.UI;
|
||||
@@ -22,7 +27,6 @@ public class TopTabMenu
|
||||
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly PairRequestService _pairRequestService;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly HashSet<string> _pendingPairRequestActions = new(StringComparer.Ordinal);
|
||||
@@ -36,11 +40,12 @@ 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)
|
||||
{
|
||||
_lightlessMediator = lightlessMediator;
|
||||
_apiController = apiController;
|
||||
_pairManager = pairManager;
|
||||
_pairRequestService = pairRequestService;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_uiSharedService = uiSharedService;
|
||||
@@ -77,34 +82,46 @@ 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);
|
||||
}
|
||||
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");
|
||||
UiSharedService.AttachToolTip("Individual Pair Menu");
|
||||
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
{
|
||||
@@ -113,6 +130,10 @@ public class TopTabMenu
|
||||
{
|
||||
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)
|
||||
@@ -122,6 +143,20 @@ public class TopTabMenu
|
||||
}
|
||||
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("Zone Chat");
|
||||
ImGui.SameLine();
|
||||
|
||||
ImGui.SameLine();
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
{
|
||||
@@ -130,6 +165,10 @@ public class TopTabMenu
|
||||
{
|
||||
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();
|
||||
@@ -148,6 +187,10 @@ public class TopTabMenu
|
||||
{
|
||||
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();
|
||||
@@ -166,6 +209,10 @@ public class TopTabMenu
|
||||
{
|
||||
_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");
|
||||
@@ -196,12 +243,18 @@ public class TopTabMenu
|
||||
|
||||
if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f);
|
||||
|
||||
|
||||
DrawIncomingPairRequests(availableWidth);
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
DrawFilter(availableWidth, spacing.X);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_currentSnapshot = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawAddPair(float availableXWidth, float spacingX)
|
||||
{
|
||||
@@ -209,7 +262,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 +484,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 +730,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 +755,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 +766,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))
|
||||
{
|
||||
@@ -770,7 +835,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 +849,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 +873,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 +887,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 =>
|
||||
{
|
||||
|
||||
@@ -15,7 +15,9 @@ namespace LightlessSync.UI
|
||||
{ "FullBlack", "#000000" },
|
||||
{ "LightlessBlue", "#a6c2ff" },
|
||||
{ "LightlessYellow", "#ffe97a" },
|
||||
{ "LightlessYellow2", "#cfbd63" },
|
||||
{ "LightlessGreen", "#7cd68a" },
|
||||
{ "LightlessGreenDefault", "#468a50" },
|
||||
{ "LightlessOrange", "#ffb366" },
|
||||
{ "PairBlue", "#88a2db" },
|
||||
{ "DimRed", "#d44444" },
|
||||
@@ -25,6 +27,9 @@ namespace LightlessSync.UI
|
||||
|
||||
{ "Lightfinder", "#ad8af5" },
|
||||
{ "LightfinderEdge", "#000000" },
|
||||
|
||||
{ "ProfileBodyGradientTop", "#2f283fff" },
|
||||
{ "ProfileBodyGradientBottom", "#372d4d00" },
|
||||
};
|
||||
|
||||
private static LightlessConfigService? _configService;
|
||||
|
||||
@@ -4,6 +4,7 @@ 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;
|
||||
@@ -400,10 +401,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();
|
||||
}
|
||||
|
||||
@@ -519,8 +531,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);
|
||||
@@ -1220,6 +1233,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);
|
||||
@@ -1285,13 +1392,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 +1434,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)
|
||||
{
|
||||
|
||||
1101
LightlessSync/UI/ZoneChatUi.cs
Normal file
1101
LightlessSync/UI/ZoneChatUi.cs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user