This commit is contained in:
2025-11-25 07:14:59 +09:00
parent 9c794137c1
commit ef592032b3
111 changed files with 20622 additions and 3476 deletions

View File

@@ -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();
}

View File

@@ -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))))

View File

@@ -7,7 +7,6 @@ using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Dto.CharaData;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.CharaData;
using LightlessSync.Services.CharaData.Models;
@@ -15,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) =>

View File

@@ -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()

View File

@@ -1,9 +1,11 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.UI.Handlers;
using LightlessSync.UI.Models;
using System.Collections.Immutable;
using LightlessSync.UI;
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));
}
}

View File

@@ -5,9 +5,9 @@ using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.Group;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator;
using LightlessSync.UI.Handlers;
using LightlessSync.UI.Models;
using LightlessSync.WebAPI;
using System.Collections.Immutable;
@@ -22,7 +22,7 @@ public class DrawFolderGroup : DrawFolderBase
private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi;
public DrawFolderGroup(string id, GroupFullInfoDto groupFullInfoDto, ApiController apiController,
IImmutableList<DrawUserPair> drawPairs, IImmutableList<Pair> allPairs, TagHandler tagHandler, IdDisplayHandler idDisplayHandler,
IImmutableList<DrawUserPair> drawPairs, IImmutableList<PairUiEntry> allPairs, TagHandler tagHandler, IdDisplayHandler idDisplayHandler,
LightlessMediator lightlessMediator, UiSharedService uiSharedService, SelectTagForSyncshellUi selectTagForSyncshellUi) :
base(id, drawPairs, allPairs, tagHandler, uiSharedService)
{
@@ -35,6 +35,7 @@ public class DrawFolderGroup : DrawFolderBase
protected override bool RenderIfEmpty => true;
protected override bool RenderMenu => true;
protected override bool EnableRowClick => false;
private bool IsModerator => IsOwner || _groupFullInfoDto.GroupUserInfo.IsModerator();
private bool IsOwner => string.Equals(_groupFullInfoDto.OwnerUID, _apiController.UID, StringComparison.Ordinal);
private bool IsPinned => _groupFullInfoDto.GroupUserInfo.IsPinned();
@@ -87,6 +88,13 @@ public class DrawFolderGroup : DrawFolderBase
ImGui.Separator();
ImGui.TextUnformatted("General Syncshell Actions");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.AddressCard, "Open Syncshell Profile", menuWidth, true))
{
ImGui.CloseCurrentPopup();
_lightlessMediator.Publish(new GroupProfileOpenStandaloneMessage(_groupFullInfoDto));
}
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));

View File

@@ -1,11 +1,18 @@
using Dalamud.Bindings.ImGui;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.LightlessConfiguration;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Handlers;
using LightlessSync.UI.Models;
using LightlessSync.WebAPI;
using System.Collections.Immutable;
namespace LightlessSync.UI.Components;
@@ -14,14 +21,30 @@ public class DrawFolderTag : DrawFolderBase
private readonly ApiController _apiController;
private readonly SelectPairForTagUi _selectPairForTagUi;
private readonly RenamePairTagUi _renameTagUi;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly LightlessConfigService _configService;
private readonly LightlessMediator _mediator;
public DrawFolderTag(string id, IImmutableList<DrawUserPair> drawPairs, IImmutableList<Pair> allPairs,
TagHandler tagHandler, ApiController apiController, SelectPairForTagUi selectPairForTagUi, RenamePairTagUi renameTagUi, UiSharedService uiSharedService)
public DrawFolderTag(
string id,
IImmutableList<DrawUserPair> drawPairs,
IImmutableList<PairUiEntry> allPairs,
TagHandler tagHandler,
ApiController apiController,
SelectPairForTagUi selectPairForTagUi,
RenamePairTagUi renameTagUi,
UiSharedService uiSharedService,
ServerConfigurationManager serverConfigurationManager,
LightlessConfigService configService,
LightlessMediator mediator)
: base(id, drawPairs, allPairs, tagHandler, uiSharedService)
{
_apiController = apiController;
_selectPairForTagUi = selectPairForTagUi;
_renameTagUi = renameTagUi;
_serverConfigurationManager = serverConfigurationManager;
_configService = configService;
_mediator = mediator;
}
protected override bool RenderIfEmpty => _id switch
@@ -86,15 +109,18 @@ public class DrawFolderTag : DrawFolderBase
if (RenderCount)
{
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemSpacing.X / 2f }))
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemSpacing.X / 2f }))
{
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("[" + OnlinePairs.ToString() + "]");
ImGui.TextUnformatted($"[{OnlinePairs}]");
}
UiSharedService.AttachToolTip(OnlinePairs + " online" + Environment.NewLine + TotalPairs + " total");
UiSharedService.AttachToolTip($"{OnlinePairs} online{Environment.NewLine}{TotalPairs} total");
}
ImGui.SameLine();
return ImGui.GetCursorPosX();
}
@@ -102,19 +128,24 @@ public class DrawFolderTag : DrawFolderBase
protected override void DrawMenu(float menuWidth)
{
ImGui.TextUnformatted("Group Menu");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Users, "Select Pairs", menuWidth, isInPopup: true))
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Users, "Select Pairs", menuWidth, true))
{
_selectPairForTagUi.Open(_id);
}
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Edit, "Rename Pair Group", menuWidth, isInPopup: true))
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Edit, "Rename Pair Group", menuWidth, true))
{
_renameTagUi.Open(_id);
}
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Pair Group", menuWidth, isInPopup: true) && UiSharedService.CtrlPressed())
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Pair Group", menuWidth, true) &&
UiSharedService.CtrlPressed())
{
_tagHandler.RemovePairTag(_id);
}
UiSharedService.AttachToolTip("Hold CTRL to remove this Group permanently." + Environment.NewLine +
UiSharedService.AttachToolTip(
"Hold CTRL to remove this Group permanently." + Environment.NewLine +
"Note: this will not unpair with users in this Group.");
}
@@ -122,7 +153,7 @@ public class DrawFolderTag : DrawFolderBase
{
ImGui.AlignTextToFramePadding();
string name = _id switch
var name = _id switch
{
TagHandler.CustomUnpairedTag => "One-sided Individual Pairs",
TagHandler.CustomOnlineTag => "Online / Paused by you",
@@ -138,16 +169,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",
};
}

View File

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

View File

@@ -12,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";

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,21 @@
using LightlessSync.API.Dto.Group;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.Group;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Factories;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Components;
using LightlessSync.UI.Handlers;
using LightlessSync.UI.Models;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging;
using System.Collections.Immutable;
namespace LightlessSync.UI;
@@ -19,6 +26,7 @@ public class DrawEntityFactory
private readonly LightlessMediator _mediator;
private readonly SelectPairForTagUi _selectPairForTagUi;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly LightlessConfigService _configService;
private readonly UiSharedService _uiSharedService;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly CharaDataManager _charaDataManager;
@@ -29,13 +37,28 @@ public class DrawEntityFactory
private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi;
private readonly TagHandler _tagHandler;
private readonly IdDisplayHandler _uidDisplayHandler;
private readonly PairLedger _pairLedger;
private readonly PairFactory _pairFactory;
public DrawEntityFactory(ILogger<DrawEntityFactory> logger, ApiController apiController, IdDisplayHandler uidDisplayHandler,
SelectTagForPairUi selectTagForPairUi, RenamePairTagUi renamePairTagUi, LightlessMediator mediator,
TagHandler tagHandler, SelectPairForTagUi selectPairForTagUi,
ServerConfigurationManager serverConfigurationManager, UiSharedService uiSharedService,
PlayerPerformanceConfigService playerPerformanceConfigService, CharaDataManager charaDataManager,
SelectTagForSyncshellUi selectTagForSyncshellUi, RenameSyncshellTagUi renameSyncshellTagUi, SelectSyncshellForTagUi selectSyncshellForTagUi)
public DrawEntityFactory(
ILogger<DrawEntityFactory> logger,
ApiController apiController,
IdDisplayHandler uidDisplayHandler,
SelectTagForPairUi selectTagForPairUi,
RenamePairTagUi renamePairTagUi,
LightlessMediator mediator,
TagHandler tagHandler,
SelectPairForTagUi selectPairForTagUi,
ServerConfigurationManager serverConfigurationManager,
LightlessConfigService configService,
UiSharedService uiSharedService,
PlayerPerformanceConfigService playerPerformanceConfigService,
CharaDataManager charaDataManager,
SelectTagForSyncshellUi selectTagForSyncshellUi,
RenameSyncshellTagUi renameSyncshellTagUi,
SelectSyncshellForTagUi selectSyncshellForTagUi,
PairLedger pairLedger,
PairFactory pairFactory)
{
_logger = logger;
_apiController = apiController;
@@ -46,44 +69,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);
}
}

View File

@@ -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));

View 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

View File

@@ -10,13 +10,16 @@ using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Style;
using LightlessSync.Utils;
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Text;
namespace LightlessSync.UI.Handlers;
public class IdDisplayHandler
{
private readonly LightlessConfigService _lightlessConfigService;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly LightlessMediator _mediator;
private readonly ServerConfigurationManager _serverManager;
private readonly Dictionary<string, bool> _showIdForEntry = new(StringComparer.Ordinal);
@@ -30,11 +33,16 @@ public class IdDisplayHandler
private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f);
private float _highlightBoost;
public IdDisplayHandler(LightlessMediator mediator, ServerConfigurationManager serverManager, LightlessConfigService lightlessConfigService)
public IdDisplayHandler(
LightlessMediator mediator,
ServerConfigurationManager serverManager,
LightlessConfigService lightlessConfigService,
PlayerPerformanceConfigService playerPerformanceConfigService)
{
_mediator = mediator;
_serverManager = serverManager;
_lightlessConfigService = lightlessConfigService;
_playerPerformanceConfigService = playerPerformanceConfigService;
}
public void DrawGroupText(string id, GroupFullInfoDto group, float textPosX, Func<float> editBoxWidth)
@@ -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();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
}
}

View 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);
}
}

View File

@@ -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

View File

@@ -38,7 +38,7 @@ internal static class MainStyle
new("color.border", "Border", () => Rgba(65, 65, 65, 255), ImGuiCol.Border),
new("color.borderShadow", "Border Shadow", () => Rgba(0, 0, 0, 150), ImGuiCol.BorderShadow),
new("color.frameBg", "Frame Background", () => Rgba(40, 40, 40, 255), ImGuiCol.FrameBg),
new("color.frameBgHovered", "Frame Background (Hover)", () => Rgba(50, 50, 50, 255), ImGuiCol.FrameBgHovered),
new("color.frameBgHovered", "Frame Background (Hover)", () => Rgba(50, 50, 50, 100), ImGuiCol.FrameBgHovered),
new("color.frameBgActive", "Frame Background (Active)", () => Rgba(30, 30, 30, 255), ImGuiCol.FrameBgActive),
new("color.titleBg", "Title Background", () => Rgba(24, 24, 24, 232), ImGuiCol.TitleBg),
new("color.titleBgActive", "Title Background (Active)", () => Rgba(30, 30, 30, 255), ImGuiCol.TitleBgActive),

File diff suppressed because it is too large Load Diff

View File

@@ -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();
}
}

View File

@@ -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)));

View File

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

View File

@@ -0,0 +1,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;
}
}

View 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;
}
}

View File

@@ -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 =>
{

View File

@@ -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;

View File

@@ -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)
{

File diff suppressed because it is too large Load Diff