1189 lines
46 KiB
C#
1189 lines
46 KiB
C#
using Dalamud.Bindings.ImGui;
|
|
using Dalamud.Interface;
|
|
using Dalamud.Interface.Colors;
|
|
using Dalamud.Interface.Textures.TextureWraps;
|
|
using Dalamud.Interface.Utility;
|
|
using Dalamud.Interface.Utility.Raii;
|
|
using Dalamud.Utility;
|
|
using LightlessSync.API.Data;
|
|
using LightlessSync.API.Data.Enum;
|
|
using LightlessSync.API.Data.Extensions;
|
|
using LightlessSync.API.Dto;
|
|
using LightlessSync.API.Dto.Group;
|
|
using LightlessSync.LightlessConfiguration;
|
|
using LightlessSync.Services;
|
|
using LightlessSync.Services.LightFinder;
|
|
using LightlessSync.Services.Mediator;
|
|
using LightlessSync.UI.Services;
|
|
using LightlessSync.UI.Style;
|
|
using LightlessSync.UI.Tags;
|
|
using LightlessSync.Utils;
|
|
using LightlessSync.WebAPI;
|
|
using Microsoft.Extensions.Logging;
|
|
using System.Numerics;
|
|
|
|
namespace LightlessSync.UI;
|
|
|
|
public class LightFinderUI : WindowMediatorSubscriberBase
|
|
{
|
|
#region Services
|
|
|
|
private readonly ApiController _apiController;
|
|
private readonly DalamudUtilService _dalamudUtilService;
|
|
private readonly LightFinderScannerService _broadcastScannerService;
|
|
private readonly LightFinderService _broadcastService;
|
|
private readonly LightlessConfigService _configService;
|
|
private readonly LightlessProfileManager _lightlessProfileManager;
|
|
private readonly PairUiService _pairUiService;
|
|
private readonly UiSharedService _uiSharedService;
|
|
|
|
#endregion
|
|
|
|
#region UI Components
|
|
|
|
private readonly AnimatedHeader _animatedHeader = new();
|
|
private readonly List<SeStringUtils.SeStringSegment> _seResolvedSegments = new();
|
|
private readonly Vector4 _tagBackgroundColor = new(0.18f, 0.18f, 0.18f, 0.95f);
|
|
private readonly Vector4 _tagBorderColor = new(0.35f, 0.35f, 0.35f, 0.4f);
|
|
|
|
#endregion
|
|
|
|
#region State
|
|
|
|
private IReadOnlyList<GroupFullInfoDto> _allSyncshells = Array.Empty<GroupFullInfoDto>();
|
|
private bool _compactView;
|
|
private List<GroupFullInfoDto> _currentSyncshells = [];
|
|
private GroupJoinDto? _joinDto;
|
|
private GroupJoinInfoDto? _joinInfo;
|
|
private readonly List<GroupJoinDto> _nearbySyncshells = [];
|
|
private DefaultPermissionsDto _ownPermissions = null!;
|
|
private readonly HashSet<string> _recentlyJoined = new(StringComparer.Ordinal);
|
|
private int _selectedNearbyIndex = -1;
|
|
private readonly List<(string Label, string? GID, bool IsAvailable)> _syncshellOptions = new();
|
|
private LightfinderTab _selectedTab = LightfinderTab.NearbySyncshells;
|
|
private string _userUid = string.Empty;
|
|
|
|
private enum LightfinderTab { NearbySyncshells, BroadcastSettings, Help }
|
|
|
|
#if DEBUG
|
|
private enum LightfinderTabDebug { NearbySyncshells, BroadcastSettings, Help, Debug }
|
|
private LightfinderTabDebug _selectedTabDebug = LightfinderTabDebug.NearbySyncshells;
|
|
#endif
|
|
|
|
#if DEBUG
|
|
private bool _useTestSyncshells;
|
|
#endif
|
|
|
|
#endregion
|
|
|
|
#region Constructor
|
|
|
|
public LightFinderUI(
|
|
ILogger<LightFinderUI> logger,
|
|
LightlessMediator mediator,
|
|
PerformanceCollectorService performanceCollectorService,
|
|
LightFinderService broadcastService,
|
|
LightlessConfigService configService,
|
|
UiSharedService uiShared,
|
|
ApiController apiController,
|
|
LightFinderScannerService broadcastScannerService,
|
|
PairUiService pairUiService,
|
|
DalamudUtilService dalamudUtilService,
|
|
LightlessProfileManager lightlessProfileManager
|
|
) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService)
|
|
{
|
|
_broadcastService = broadcastService;
|
|
_uiSharedService = uiShared;
|
|
_configService = configService;
|
|
_apiController = apiController;
|
|
_broadcastScannerService = broadcastScannerService;
|
|
_pairUiService = pairUiService;
|
|
_dalamudUtilService = dalamudUtilService;
|
|
_lightlessProfileManager = lightlessProfileManager;
|
|
|
|
_animatedHeader.Height = 100f;
|
|
_animatedHeader.EnableBottomGradient = true;
|
|
_animatedHeader.GradientHeight = 120f;
|
|
_animatedHeader.EnableParticles = _configService.Current.EnableParticleEffects;
|
|
|
|
IsOpen = false;
|
|
WindowBuilder.For(this)
|
|
.SetSizeConstraints(new Vector2(620, 85), new Vector2(700, 600))
|
|
.Apply();
|
|
|
|
Mediator.Subscribe<SyncshellBroadcastsUpdatedMessage>(this, async _ => await RefreshNearbySyncshellsAsync().ConfigureAwait(false));
|
|
Mediator.Subscribe<BroadcastStatusChangedMessage>(this, async _ => await RefreshNearbySyncshellsAsync().ConfigureAwait(false));
|
|
Mediator.Subscribe<UserLeftSyncshell>(this, async _ => await RefreshNearbySyncshellsAsync(_.gid).ConfigureAwait(false));
|
|
Mediator.Subscribe<UserJoinedSyncshell>(this, async _ => await RefreshNearbySyncshellsAsync(_.gid).ConfigureAwait(false));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Lifecycle
|
|
|
|
public override void OnOpen()
|
|
{
|
|
_userUid = _apiController.UID;
|
|
_ownPermissions = _apiController.DefaultPermissions.DeepClone()!;
|
|
_ = RefreshSyncshellsAsync();
|
|
_ = RefreshNearbySyncshellsAsync();
|
|
}
|
|
|
|
public override void OnClose()
|
|
{
|
|
_animatedHeader.ClearParticles();
|
|
ClearSelection();
|
|
base.OnClose();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Main Drawing
|
|
|
|
protected override void DrawInternal()
|
|
{
|
|
var contentWidth = ImGui.GetContentRegionAvail().X;
|
|
_animatedHeader.Draw(contentWidth, (_, _) => { });
|
|
|
|
if (!_broadcastService.IsLightFinderAvailable)
|
|
{
|
|
ImGui.TextColored(UIColors.Get("LightlessYellow"), "This server doesn't support Lightfinder.");
|
|
ImGuiHelpers.ScaledDummy(2f);
|
|
}
|
|
|
|
DrawStatusPanel();
|
|
ImGuiHelpers.ScaledDummy(4f);
|
|
|
|
#if DEBUG
|
|
var debugTabOptions = new List<UiSharedService.TabOption<LightfinderTabDebug>>
|
|
{
|
|
new("Nearby Syncshells", LightfinderTabDebug.NearbySyncshells),
|
|
new("Broadcast", LightfinderTabDebug.BroadcastSettings),
|
|
new("Help", LightfinderTabDebug.Help),
|
|
new("Debug", LightfinderTabDebug.Debug)
|
|
};
|
|
UiSharedService.Tab("LightfinderTabs", debugTabOptions, ref _selectedTabDebug);
|
|
|
|
ImGuiHelpers.ScaledDummy(4f);
|
|
|
|
switch (_selectedTabDebug)
|
|
{
|
|
case LightfinderTabDebug.NearbySyncshells:
|
|
DrawNearbySyncshellsTab();
|
|
break;
|
|
case LightfinderTabDebug.BroadcastSettings:
|
|
DrawBroadcastSettingsTab();
|
|
break;
|
|
case LightfinderTabDebug.Help:
|
|
DrawHelpTab();
|
|
break;
|
|
case LightfinderTabDebug.Debug:
|
|
DrawDebugTab();
|
|
break;
|
|
}
|
|
#else
|
|
var tabOptions = new List<UiSharedService.TabOption<LightfinderTab>>
|
|
{
|
|
new("Nearby Syncshells", LightfinderTab.NearbySyncshells),
|
|
new("Broadcast", LightfinderTab.BroadcastSettings),
|
|
new("Help", LightfinderTab.Help)
|
|
};
|
|
UiSharedService.Tab("LightfinderTabs", tabOptions, ref _selectedTab);
|
|
|
|
ImGuiHelpers.ScaledDummy(4f);
|
|
|
|
switch (_selectedTab)
|
|
{
|
|
case LightfinderTab.NearbySyncshells:
|
|
DrawNearbySyncshellsTab();
|
|
break;
|
|
case LightfinderTab.BroadcastSettings:
|
|
DrawBroadcastSettingsTab();
|
|
break;
|
|
case LightfinderTab.Help:
|
|
DrawHelpTab();
|
|
break;
|
|
}
|
|
#endif
|
|
|
|
if (_joinDto != null && _joinInfo != null && _joinInfo.Success)
|
|
DrawJoinConfirmation();
|
|
}
|
|
|
|
private void DrawStatusPanel()
|
|
{
|
|
var scale = ImGuiHelpers.GlobalScale;
|
|
var isBroadcasting = _broadcastService.IsBroadcasting;
|
|
var cooldown = _broadcastService.RemainingCooldown;
|
|
var isOnCooldown = cooldown.HasValue && cooldown.Value.TotalSeconds > 0;
|
|
|
|
var accent = isBroadcasting ? UIColors.Get("LightlessGreen") : UIColors.Get("LightlessPurple");
|
|
var accentBg = new Vector4(accent.X, accent.Y, accent.Z, 0.16f);
|
|
var accentBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.32f);
|
|
var infoColor = ImGuiColors.DalamudGrey;
|
|
|
|
var summaryHeight = MathF.Max(ImGui.GetTextLineHeightWithSpacing() * 2.4f, 46f * scale);
|
|
float buttonWidth = 130 * scale;
|
|
|
|
using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f * scale))
|
|
using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize)))
|
|
using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(12f * scale, 6f * scale)))
|
|
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(18f * scale, 4f * scale)))
|
|
using (ImRaii.PushColor(ImGuiCol.ChildBg, UiSharedService.Color(accentBg)))
|
|
using (ImRaii.PushColor(ImGuiCol.Border, UiSharedService.Color(accentBorder)))
|
|
using (var child = ImRaii.Child("StatusPanel", new Vector2(-1f, summaryHeight), true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse))
|
|
{
|
|
if (child)
|
|
{
|
|
using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(8f * scale, 4f * scale)))
|
|
{
|
|
if (ImGui.BeginTable("StatusPanelTable", 6, ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.NoBordersInBody))
|
|
{
|
|
ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.WidthStretch, 1f);
|
|
ImGui.TableSetupColumn("Time", ImGuiTableColumnFlags.WidthStretch, 1f);
|
|
ImGui.TableSetupColumn("NearbyPlayers", ImGuiTableColumnFlags.WidthStretch, 1f);
|
|
ImGui.TableSetupColumn("NearbySyncshells", ImGuiTableColumnFlags.WidthStretch, 1f);
|
|
ImGui.TableSetupColumn("Broadcasting", ImGuiTableColumnFlags.WidthStretch, 1f);
|
|
ImGui.TableSetupColumn("Button", ImGuiTableColumnFlags.WidthFixed, buttonWidth + 16f * scale);
|
|
|
|
ImGui.TableNextRow();
|
|
|
|
// Status cell
|
|
var statusColor = isBroadcasting ? UIColors.Get("LightlessGreen") : UIColors.Get("DimRed");
|
|
var statusText = isBroadcasting ? "Active" : "Inactive";
|
|
var statusIcon = isBroadcasting ? FontAwesomeIcon.CheckCircle : FontAwesomeIcon.TimesCircle;
|
|
DrawStatusCell(statusIcon, statusColor, statusText, "Status", infoColor, scale);
|
|
|
|
// Time remaining Cooldown cell
|
|
string timeValue;
|
|
string timeSub;
|
|
Vector4 timeColor;
|
|
if (isOnCooldown)
|
|
{
|
|
timeValue = $"{Math.Ceiling(cooldown!.Value.TotalSeconds)}s";
|
|
timeSub = "Cooldown";
|
|
timeColor = UIColors.Get("DimRed");
|
|
}
|
|
else if (isBroadcasting && _broadcastService.RemainingTtl is { } remaining && remaining > TimeSpan.Zero)
|
|
{
|
|
timeValue = $"{remaining:hh\\:mm\\:ss}";
|
|
timeSub = "Time left";
|
|
timeColor = UIColors.Get("LightlessYellow");
|
|
}
|
|
else
|
|
{
|
|
timeValue = "--:--:--";
|
|
timeSub = "Time left";
|
|
timeColor = infoColor;
|
|
}
|
|
DrawStatusCell(FontAwesomeIcon.Clock, timeColor, timeValue, timeSub, infoColor, scale);
|
|
|
|
// Nearby players cell
|
|
var nearbyPlayerCount = _broadcastScannerService.CountActiveBroadcasts();
|
|
var nearbyPlayerColor = nearbyPlayerCount > 0 ? UIColors.Get("LightlessBlue") : infoColor;
|
|
DrawStatusCell(FontAwesomeIcon.Users, nearbyPlayerColor, nearbyPlayerCount.ToString(), "Players", infoColor, scale);
|
|
|
|
// Nearby syncshells cell
|
|
var nearbySyncshellCount = _nearbySyncshells.Count;
|
|
var nearbySyncshellColor = nearbySyncshellCount > 0 ? UIColors.Get("LightlessPurple") : infoColor;
|
|
DrawStatusCell(FontAwesomeIcon.Compass, nearbySyncshellColor, nearbySyncshellCount.ToString(), "Syncshells", infoColor, scale);
|
|
|
|
// Broadcasting syncshell cell
|
|
var isBroadcastingSyncshell = _configService.Current.SyncshellFinderEnabled && isBroadcasting;
|
|
var broadcastSyncshellColor = isBroadcastingSyncshell ? UIColors.Get("LightlessGreen") : infoColor;
|
|
var broadcastSyncshellText = isBroadcastingSyncshell ? "Yes" : "No";
|
|
var broadcastSyncshellIcon = FontAwesomeIcon.Wifi;
|
|
DrawStatusCell(broadcastSyncshellIcon, broadcastSyncshellColor, broadcastSyncshellText, "Broadcasting", infoColor, scale);
|
|
|
|
// Enable/Disable button cell - right aligned
|
|
ImGui.TableNextColumn();
|
|
|
|
float cellWidth = ImGui.GetContentRegionAvail().X;
|
|
float offsetX = cellWidth - buttonWidth;
|
|
if (offsetX > 0)
|
|
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + offsetX);
|
|
|
|
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 6f * scale))
|
|
{
|
|
Vector4 buttonColor;
|
|
if (isOnCooldown)
|
|
buttonColor = UIColors.Get("DimRed");
|
|
else if (isBroadcasting)
|
|
buttonColor = UIColors.Get("LightlessGreen");
|
|
else
|
|
buttonColor = UIColors.Get("LightlessPurple");
|
|
|
|
using (ImRaii.PushColor(ImGuiCol.Button, buttonColor))
|
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, buttonColor.WithAlpha(0.85f)))
|
|
using (ImRaii.PushColor(ImGuiCol.ButtonActive, buttonColor.WithAlpha(0.75f)))
|
|
using (ImRaii.Disabled(isOnCooldown || !_broadcastService.IsLightFinderAvailable))
|
|
{
|
|
string buttonText = isBroadcasting ? "Disable" : "Enable";
|
|
if (ImGui.Button(buttonText, new Vector2(buttonWidth, 0)))
|
|
_broadcastService.ToggleBroadcast();
|
|
}
|
|
}
|
|
|
|
ImGui.EndTable();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawStatusCell(FontAwesomeIcon icon, Vector4 iconColor, string mainText, string subText, Vector4 subColor, float scale)
|
|
{
|
|
ImGui.TableNextColumn();
|
|
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(6f * scale, 2f * scale)))
|
|
using (ImRaii.Group())
|
|
{
|
|
_uiSharedService.IconText(icon, iconColor);
|
|
ImGui.SameLine(0f, 6f * scale);
|
|
using (ImRaii.PushColor(ImGuiCol.Text, iconColor))
|
|
{
|
|
ImGui.TextUnformatted(mainText);
|
|
}
|
|
using (ImRaii.PushColor(ImGuiCol.Text, subColor))
|
|
{
|
|
ImGui.TextUnformatted(subText);
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Nearby Syncshells Tab
|
|
|
|
private void DrawNearbySyncshellsTab()
|
|
{
|
|
ImGui.BeginGroup();
|
|
|
|
#if DEBUG
|
|
if (ImGui.SmallButton("Test Data"))
|
|
{
|
|
_useTestSyncshells = !_useTestSyncshells;
|
|
_ = Task.Run(async () => await RefreshNearbySyncshellsAsync().ConfigureAwait(false));
|
|
}
|
|
ImGui.SameLine();
|
|
#endif
|
|
|
|
string checkboxLabel = "Compact";
|
|
float checkboxWidth = ImGui.CalcTextSize(checkboxLabel).X + ImGui.GetFrameHeight() + 8f;
|
|
float availWidth = ImGui.GetContentRegionAvail().X;
|
|
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + availWidth - checkboxWidth);
|
|
ImGui.Checkbox(checkboxLabel, ref _compactView);
|
|
ImGui.EndGroup();
|
|
|
|
ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale));
|
|
|
|
if (_nearbySyncshells.Count == 0)
|
|
{
|
|
DrawNoSyncshellsMessage();
|
|
return;
|
|
}
|
|
|
|
var cardData = BuildSyncshellCardData();
|
|
if (cardData.Count == 0)
|
|
{
|
|
ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells found.");
|
|
return;
|
|
}
|
|
|
|
if (_compactView)
|
|
DrawSyncshellGrid(cardData);
|
|
else
|
|
DrawSyncshellList(cardData);
|
|
}
|
|
|
|
private void DrawNoSyncshellsMessage()
|
|
{
|
|
ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted.");
|
|
|
|
if (!_broadcastService.IsBroadcasting)
|
|
{
|
|
ImGuiHelpers.ScaledDummy(4f);
|
|
UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"));
|
|
ImGuiHelpers.ScaledDummy(2f);
|
|
|
|
ImGui.TextColored(UIColors.Get("LightlessYellow"), "Lightfinder must be active to find nearby syncshells.");
|
|
}
|
|
}
|
|
|
|
private List<(GroupJoinDto Shell, string BroadcasterName)> BuildSyncshellCardData()
|
|
{
|
|
string? myHashedCid = null;
|
|
try
|
|
{
|
|
var cid = _dalamudUtilService.GetCID();
|
|
myHashedCid = cid.ToString().GetHash256();
|
|
}
|
|
catch { }
|
|
|
|
var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts()
|
|
.Where(b => !string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal))
|
|
.ToList();
|
|
|
|
var cardData = new List<(GroupJoinDto Shell, string BroadcasterName)>();
|
|
|
|
foreach (var shell in _nearbySyncshells)
|
|
{
|
|
if (shell?.Group == null || string.IsNullOrEmpty(shell.Group.GID))
|
|
continue;
|
|
|
|
#if DEBUG
|
|
if (_useTestSyncshells)
|
|
{
|
|
var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID;
|
|
cardData.Add((shell, $"{displayName} (Test World)"));
|
|
continue;
|
|
}
|
|
#endif
|
|
|
|
var broadcast = broadcasts.FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal));
|
|
if (broadcast == null)
|
|
continue;
|
|
|
|
var (name, address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID);
|
|
if (string.IsNullOrEmpty(name))
|
|
continue;
|
|
|
|
var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(address);
|
|
var broadcasterName = !string.IsNullOrEmpty(worldName) ? $"{name} ({worldName})" : name;
|
|
|
|
cardData.Add((shell, broadcasterName));
|
|
}
|
|
|
|
return cardData;
|
|
}
|
|
|
|
private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName)> listData)
|
|
{
|
|
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8f);
|
|
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f);
|
|
|
|
if (ImGui.BeginChild("SyncshellListScroll", new Vector2(-1, -1), border: false))
|
|
{
|
|
foreach (var (shell, broadcasterName) in listData)
|
|
{
|
|
DrawSyncshellListItem(shell, broadcasterName);
|
|
ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale));
|
|
}
|
|
}
|
|
ImGui.EndChild();
|
|
|
|
ImGui.PopStyleVar(2);
|
|
}
|
|
|
|
private void DrawSyncshellListItem(GroupJoinDto shell, string broadcasterName)
|
|
{
|
|
ImGui.PushID(shell.Group.GID);
|
|
float rowHeight = 74f * ImGuiHelpers.GlobalScale;
|
|
|
|
ImGui.BeginChild($"ShellRow##{shell.Group.GID}", new Vector2(-1, rowHeight), border: true);
|
|
|
|
var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID;
|
|
var style = ImGui.GetStyle();
|
|
float startX = ImGui.GetCursorPosX();
|
|
float regionW = ImGui.GetContentRegionAvail().X;
|
|
float rightTxtW = ImGui.CalcTextSize(broadcasterName).X;
|
|
|
|
_uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple"));
|
|
if (ImGui.IsItemHovered())
|
|
ImGui.SetTooltip("Click to open profile.");
|
|
if (ImGui.IsItemClicked())
|
|
Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group));
|
|
|
|
float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X;
|
|
ImGui.SameLine();
|
|
ImGui.SetCursorPosX(rightX);
|
|
ImGui.TextUnformatted(broadcasterName);
|
|
if (ImGui.IsItemHovered())
|
|
ImGui.SetTooltip("Broadcaster");
|
|
|
|
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"));
|
|
|
|
var groupProfile = _lightlessProfileManager.GetLightlessGroupProfile(shell.Group);
|
|
IReadOnlyList<ProfileTagDefinition> groupTags = groupProfile?.Tags.Count > 0
|
|
? ProfileTagService.ResolveTags(groupProfile.Tags)
|
|
: [];
|
|
|
|
var limitedTags = groupTags.Count > 3 ? [.. groupTags.Take(3)] : groupTags;
|
|
float tagScale = ImGuiHelpers.GlobalScale * 0.9f;
|
|
|
|
Vector2 rowStartLocal = ImGui.GetCursorPos();
|
|
float tagsWidth = 0f;
|
|
|
|
if (limitedTags.Count > 0)
|
|
(tagsWidth, _) = RenderProfileTagsSingleRow(limitedTags, tagScale);
|
|
else
|
|
{
|
|
ImGui.SetCursorPosX(startX);
|
|
ImGui.TextDisabled("No tags");
|
|
ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale));
|
|
}
|
|
|
|
float joinX = rowStartLocal.X + (tagsWidth > 0 ? tagsWidth + style.ItemSpacing.X : 0f);
|
|
ImGui.SetCursorPos(new Vector2(joinX, rowStartLocal.Y));
|
|
DrawJoinButton(shell, false);
|
|
|
|
ImGui.EndChild();
|
|
ImGui.PopID();
|
|
}
|
|
|
|
private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName)> cardData)
|
|
{
|
|
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8f);
|
|
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f);
|
|
|
|
foreach (var (shell, _) in cardData)
|
|
{
|
|
DrawSyncshellCompactItem(shell);
|
|
ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale));
|
|
}
|
|
|
|
ImGui.PopStyleVar(2);
|
|
}
|
|
|
|
private void DrawSyncshellCompactItem(GroupJoinDto shell)
|
|
{
|
|
ImGui.PushID(shell.Group.GID);
|
|
float rowHeight = 36f * ImGuiHelpers.GlobalScale;
|
|
|
|
ImGui.BeginChild($"ShellCompact##{shell.Group.GID}", new Vector2(-1, rowHeight), border: true);
|
|
|
|
var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID;
|
|
var style = ImGui.GetStyle();
|
|
float availW = ImGui.GetContentRegionAvail().X;
|
|
|
|
ImGui.AlignTextToFramePadding();
|
|
_uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple"));
|
|
if (ImGui.IsItemHovered())
|
|
ImGui.SetTooltip("Click to open profile.");
|
|
if (ImGui.IsItemClicked())
|
|
Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group));
|
|
|
|
ImGui.SameLine();
|
|
DrawJoinButton(shell, false);
|
|
|
|
ImGui.EndChild();
|
|
ImGui.PopID();
|
|
}
|
|
|
|
private void DrawJoinButton(GroupJoinDto shell, bool fullWidth)
|
|
{
|
|
const string visibleLabel = "Join";
|
|
var label = $"{visibleLabel}##{shell.Group.GID}";
|
|
|
|
var isAlreadyMember = _currentSyncshells.Exists(g => string.Equals(g.GID, shell.Group.GID, StringComparison.Ordinal));
|
|
var isRecentlyJoined = _recentlyJoined.Contains(shell.Group.GID);
|
|
var isOwnBroadcast = _configService.Current.SyncshellFinderEnabled
|
|
&& _broadcastService.IsBroadcasting
|
|
&& string.Equals(_configService.Current.SelectedFinderSyncshell, shell.Group.GID, StringComparison.Ordinal);
|
|
|
|
Vector2 buttonSize;
|
|
if (fullWidth)
|
|
{
|
|
buttonSize = new Vector2(-1, 0);
|
|
}
|
|
else
|
|
{
|
|
var textSize = ImGui.CalcTextSize(visibleLabel);
|
|
var width = textSize.X + ImGui.GetStyle().FramePadding.X * 20f;
|
|
buttonSize = new Vector2(width, 30f);
|
|
|
|
float availX = ImGui.GetContentRegionAvail().X;
|
|
float curX = ImGui.GetCursorPosX();
|
|
ImGui.SetCursorPosX(curX + availX - buttonSize.X);
|
|
}
|
|
|
|
if (isOwnBroadcast)
|
|
{
|
|
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessPurple"));
|
|
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple").WithAlpha(0.85f));
|
|
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurple").WithAlpha(0.75f));
|
|
|
|
using (ImRaii.Disabled())
|
|
ImGui.Button(label, buttonSize);
|
|
|
|
UiSharedService.AttachToolTip("You can't join your own Syncshell, silly! That's like trying to high-five yourself.");
|
|
}
|
|
else if (!isAlreadyMember && !isRecentlyJoined)
|
|
{
|
|
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"));
|
|
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f));
|
|
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f));
|
|
|
|
if (ImGui.Button(label, buttonSize))
|
|
{
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
var info = await _apiController.GroupJoinHashed(new GroupJoinHashedDto(
|
|
shell.Group, shell.Password, shell.GroupUserPreferredPermissions
|
|
)).ConfigureAwait(false);
|
|
|
|
if (info?.Success == true)
|
|
{
|
|
_joinDto = new GroupJoinDto(shell.Group, shell.Password, shell.GroupUserPreferredPermissions);
|
|
_joinInfo = info;
|
|
_ownPermissions = _apiController.DefaultPermissions.DeepClone()!;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Join failed for {GID}", shell.Group.GID);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("DimRed"));
|
|
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("DimRed").WithAlpha(0.85f));
|
|
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("DimRed").WithAlpha(0.75f));
|
|
|
|
using (ImRaii.Disabled())
|
|
ImGui.Button(label, buttonSize);
|
|
|
|
UiSharedService.AttachToolTip("Already a member of this Syncshell.");
|
|
}
|
|
|
|
ImGui.PopStyleColor(3);
|
|
}
|
|
|
|
|
|
private void DrawJoinConfirmation()
|
|
{
|
|
if (_joinDto == null || _joinInfo == null) return;
|
|
|
|
ImGui.Separator();
|
|
ImGui.TextUnformatted($"Join Syncshell: {_joinDto.Group.AliasOrGID} by {_joinInfo.OwnerAliasOrUID}");
|
|
ImGuiHelpers.ScaledDummy(2f);
|
|
ImGui.TextUnformatted("Suggested Permissions:");
|
|
|
|
DrawPermissionRow("Sounds", _joinInfo.GroupPermissions.IsPreferDisableSounds(), _ownPermissions.DisableGroupSounds, v => _ownPermissions.DisableGroupSounds = v);
|
|
DrawPermissionRow("Animations", _joinInfo.GroupPermissions.IsPreferDisableAnimations(), _ownPermissions.DisableGroupAnimations, v => _ownPermissions.DisableGroupAnimations = v);
|
|
DrawPermissionRow("VFX", _joinInfo.GroupPermissions.IsPreferDisableVFX(), _ownPermissions.DisableGroupVFX, v => _ownPermissions.DisableGroupVFX = v);
|
|
|
|
ImGui.NewLine();
|
|
|
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, $"Finalize and join {_joinDto.Group.AliasOrGID}"))
|
|
{
|
|
var finalPermissions = GroupUserPreferredPermissions.NoneSet;
|
|
finalPermissions.SetDisableSounds(_ownPermissions.DisableGroupSounds);
|
|
finalPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations);
|
|
finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX);
|
|
|
|
_ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions));
|
|
_recentlyJoined.Add(_joinDto.Group.GID);
|
|
|
|
_joinDto = null;
|
|
_joinInfo = null;
|
|
}
|
|
|
|
ImGui.SameLine();
|
|
if (ImGui.Button("Cancel"))
|
|
{
|
|
_joinDto = null;
|
|
_joinInfo = null;
|
|
}
|
|
}
|
|
|
|
private void DrawPermissionRow(string label, bool suggested, bool current, Action<bool> apply)
|
|
{
|
|
ImGui.AlignTextToFramePadding();
|
|
ImGui.TextUnformatted($"- {label}");
|
|
|
|
ImGui.SameLine(120 * ImGuiHelpers.GlobalScale);
|
|
ImGui.Text("Current:");
|
|
ImGui.SameLine();
|
|
_uiSharedService.BooleanToColoredIcon(!current);
|
|
|
|
ImGui.SameLine(240 * ImGuiHelpers.GlobalScale);
|
|
ImGui.Text("Suggested:");
|
|
ImGui.SameLine();
|
|
_uiSharedService.BooleanToColoredIcon(!suggested);
|
|
|
|
ImGui.SameLine(380 * ImGuiHelpers.GlobalScale);
|
|
using var id = ImRaii.PushId(label);
|
|
if (current != suggested)
|
|
{
|
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Apply"))
|
|
apply(suggested);
|
|
}
|
|
|
|
ImGui.NewLine();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Broadcast Settings Tab
|
|
|
|
private void DrawBroadcastSettingsTab()
|
|
{
|
|
_uiSharedService.MediumText("Syncshell Broadcasting", UIColors.Get("PairBlue"));
|
|
ImGuiHelpers.ScaledDummy(2f);
|
|
|
|
ImGui.PushTextWrapPos();
|
|
ImGui.TextColored(ImGuiColors.DalamudGrey, "Broadcast your Syncshell to nearby Lightfinder users. They can then join directly from the Nearby Syncshells tab.");
|
|
ImGui.PopTextWrapPos();
|
|
|
|
ImGuiHelpers.ScaledDummy(4f);
|
|
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
|
|
ImGuiHelpers.ScaledDummy(4f);
|
|
|
|
bool isBroadcasting = _broadcastService.IsBroadcasting;
|
|
|
|
if (isBroadcasting)
|
|
{
|
|
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"),
|
|
new SeStringUtils.RichTextEntry("Settings can only be changed while Lightfinder is disabled.", UIColors.Get("LightlessYellow")));
|
|
ImGuiHelpers.ScaledDummy(4f);
|
|
}
|
|
|
|
if (isBroadcasting)
|
|
ImGui.BeginDisabled();
|
|
|
|
bool shellFinderEnabled = _configService.Current.SyncshellFinderEnabled;
|
|
if (ImGui.Checkbox("Enable Syncshell Broadcasting", ref shellFinderEnabled))
|
|
{
|
|
_configService.Current.SyncshellFinderEnabled = shellFinderEnabled;
|
|
_configService.Save();
|
|
}
|
|
UiSharedService.AttachToolTip("When enabled and Lightfinder is active, your selected Syncshell will be visible to nearby users.");
|
|
|
|
ImGuiHelpers.ScaledDummy(4f);
|
|
|
|
ImGui.Text("Select Syncshell to broadcast:");
|
|
|
|
var selectedGid = _configService.Current.SelectedFinderSyncshell;
|
|
var currentOption = _syncshellOptions.FirstOrDefault(o => string.Equals(o.GID, selectedGid, StringComparison.Ordinal));
|
|
var preview = currentOption.Label ?? "Select a Syncshell...";
|
|
|
|
ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale);
|
|
if (ImGui.BeginCombo("##SyncshellDropdown", preview))
|
|
{
|
|
foreach (var (label, gid, available) in _syncshellOptions)
|
|
{
|
|
bool isSelected = string.Equals(gid, selectedGid, StringComparison.Ordinal);
|
|
|
|
if (!available)
|
|
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
|
|
|
if (ImGui.Selectable(label, isSelected))
|
|
{
|
|
_configService.Current.SelectedFinderSyncshell = gid;
|
|
_configService.Save();
|
|
_ = RefreshSyncshellsAsync();
|
|
}
|
|
|
|
if (!available && ImGui.IsItemHovered())
|
|
ImGui.SetTooltip("This Syncshell is not available on the current service.");
|
|
|
|
if (!available)
|
|
ImGui.PopStyleColor();
|
|
|
|
if (isSelected)
|
|
ImGui.SetItemDefaultFocus();
|
|
}
|
|
|
|
ImGui.EndCombo();
|
|
}
|
|
|
|
if (isBroadcasting)
|
|
ImGui.EndDisabled();
|
|
|
|
ImGui.SameLine();
|
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Sync))
|
|
_ = RefreshSyncshellsAsync();
|
|
UiSharedService.AttachToolTip("Refresh Syncshell list");
|
|
|
|
ImGuiHelpers.ScaledDummy(8f);
|
|
|
|
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8f);
|
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Cog, "Advanced Settings"))
|
|
Mediator.Publish(new OpenLightfinderSettingsMessage());
|
|
ImGui.PopStyleVar();
|
|
UiSharedService.AttachToolTip("Open Lightfinder settings in the Settings window.");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Help Tab
|
|
|
|
private void DrawHelpTab()
|
|
{
|
|
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(4, 4));
|
|
|
|
_uiSharedService.MediumText("What is Lightfinder?", UIColors.Get("PairBlue"));
|
|
ImGui.PushTextWrapPos();
|
|
ImGui.TextColored(ImGuiColors.DalamudGrey, "Lightfinder lets other Lightless users know you use Lightless. While enabled, you and others can see each other via a nameplate label.");
|
|
ImGui.PopTextWrapPos();
|
|
|
|
ImGuiHelpers.ScaledDummy(6f);
|
|
|
|
_uiSharedService.MediumText("Pairing", UIColors.Get("PairBlue"));
|
|
ImGui.PushTextWrapPos();
|
|
ImGui.TextColored(ImGuiColors.DalamudGrey, "Pairing can be initiated via the right-click context menu on another player. The process requires mutual confirmation from both users.");
|
|
ImGui.PopTextWrapPos();
|
|
|
|
ImGuiHelpers.ScaledDummy(2f);
|
|
|
|
_uiSharedService.DrawNoteLine("", UIColors.Get("LightlessGreen"),
|
|
new SeStringUtils.RichTextEntry("If Lightfinder is "),
|
|
new SeStringUtils.RichTextEntry("ENABLED", UIColors.Get("LightlessGreen"), true),
|
|
new SeStringUtils.RichTextEntry(", the receiving user will get notified about pair requests."));
|
|
|
|
_uiSharedService.DrawNoteLine("", UIColors.Get("DimRed"),
|
|
new SeStringUtils.RichTextEntry("If Lightfinder is "),
|
|
new SeStringUtils.RichTextEntry("DISABLED", UIColors.Get("DimRed"), true),
|
|
new SeStringUtils.RichTextEntry(", pair requests will NOT be visible to the recipient."));
|
|
|
|
ImGuiHelpers.ScaledDummy(6f);
|
|
|
|
_uiSharedService.MediumText("Privacy", UIColors.Get("PairBlue"));
|
|
|
|
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"),
|
|
new SeStringUtils.RichTextEntry("Lightfinder is entirely "),
|
|
new SeStringUtils.RichTextEntry("opt-in", UIColors.Get("LightlessYellow"), true),
|
|
new SeStringUtils.RichTextEntry(" and does not share personal data with other users."));
|
|
|
|
ImGui.PushTextWrapPos();
|
|
ImGui.TextColored(ImGuiColors.DalamudGrey, "All identifying information remains private to the server. Use Lightfinder when you're okay with being visible to other users.");
|
|
ImGui.PopTextWrapPos();
|
|
|
|
ImGuiHelpers.ScaledDummy(6f);
|
|
|
|
_uiSharedService.MediumText("Syncshell Broadcasting", UIColors.Get("PairBlue"));
|
|
ImGui.PushTextWrapPos();
|
|
ImGui.TextColored(ImGuiColors.DalamudGrey, "You can broadcast a Syncshell you own or moderate to nearby Lightfinder users. Configure this in the Broadcast Settings tab.");
|
|
ImGui.PopTextWrapPos();
|
|
|
|
ImGui.PopStyleVar();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#if DEBUG
|
|
#region Debug Tab
|
|
|
|
private void DrawDebugTab()
|
|
{
|
|
ImGui.Text("Broadcast Cache");
|
|
|
|
if (ImGui.BeginTable("##BroadcastCacheTable", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, new Vector2(-1, 200f)))
|
|
{
|
|
ImGui.TableSetupColumn("CID", ImGuiTableColumnFlags.WidthStretch);
|
|
ImGui.TableSetupColumn("Broadcasting", ImGuiTableColumnFlags.WidthStretch);
|
|
ImGui.TableSetupColumn("Expires In", ImGuiTableColumnFlags.WidthStretch);
|
|
ImGui.TableSetupColumn("Syncshell GID", ImGuiTableColumnFlags.WidthStretch);
|
|
ImGui.TableHeadersRow();
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
foreach (var (cid, entry) in _broadcastScannerService.BroadcastCache)
|
|
{
|
|
ImGui.TableNextRow();
|
|
|
|
ImGui.TableNextColumn();
|
|
ImGui.TextUnformatted(cid.Truncate(12));
|
|
if (ImGui.IsItemHovered())
|
|
ImGui.SetTooltip(cid);
|
|
|
|
ImGui.TableNextColumn();
|
|
var colorBroadcast = entry.IsBroadcasting ? UIColors.Get("LightlessGreen") : UIColors.Get("DimRed");
|
|
ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, ImGui.GetColorU32(colorBroadcast));
|
|
ImGui.TextUnformatted(entry.IsBroadcasting.ToString());
|
|
|
|
ImGui.TableNextColumn();
|
|
var remaining = entry.ExpiryTime - now;
|
|
var colorTtl = remaining <= TimeSpan.Zero ? UIColors.Get("DimRed")
|
|
: remaining < TimeSpan.FromSeconds(10) ? UIColors.Get("LightlessYellow")
|
|
: (Vector4?)null;
|
|
|
|
if (colorTtl != null)
|
|
ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, ImGui.GetColorU32(colorTtl.Value));
|
|
|
|
ImGui.TextUnformatted(remaining > TimeSpan.Zero ? remaining.ToString("hh\\:mm\\:ss") : "Expired");
|
|
|
|
ImGui.TableNextColumn();
|
|
ImGui.TextUnformatted(entry.GID ?? "-");
|
|
}
|
|
|
|
ImGui.EndTable();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
#endif
|
|
|
|
#region Data Refresh
|
|
|
|
private async Task RefreshSyncshellsAsync()
|
|
{
|
|
if (!_apiController.IsConnected)
|
|
{
|
|
_allSyncshells = [];
|
|
RebuildSyncshellDropdownOptions();
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
_allSyncshells = await _apiController.GroupsGetAll().ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to fetch Syncshells.");
|
|
_allSyncshells = [];
|
|
}
|
|
|
|
RebuildSyncshellDropdownOptions();
|
|
}
|
|
|
|
private void RebuildSyncshellDropdownOptions()
|
|
{
|
|
var selectedGid = _configService.Current.SelectedFinderSyncshell;
|
|
var allSyncshells = _allSyncshells ?? [];
|
|
var filteredSyncshells = allSyncshells
|
|
.Where(g => string.Equals(g.OwnerUID, _userUid, StringComparison.Ordinal) || g.GroupUserInfo.IsModerator())
|
|
.ToList();
|
|
|
|
_syncshellOptions.Clear();
|
|
_syncshellOptions.Add(("None", null, true));
|
|
|
|
var addedGids = new HashSet<string>(StringComparer.Ordinal);
|
|
|
|
foreach (var shell in filteredSyncshells)
|
|
{
|
|
var label = shell.GroupAliasOrGID ?? shell.GID;
|
|
_syncshellOptions.Add((label, shell.GID, true));
|
|
addedGids.Add(shell.GID);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(selectedGid) && !addedGids.Contains(selectedGid))
|
|
{
|
|
var matching = allSyncshells.FirstOrDefault(g => string.Equals(g.GID, selectedGid, StringComparison.Ordinal));
|
|
if (matching != null)
|
|
{
|
|
var label = matching.GroupAliasOrGID ?? matching.GID;
|
|
_syncshellOptions.Add((label, matching.GID, true));
|
|
addedGids.Add(matching.GID);
|
|
}
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(selectedGid) && !addedGids.Contains(selectedGid))
|
|
_syncshellOptions.Add(($"[Unavailable] {selectedGid}", selectedGid, false));
|
|
}
|
|
|
|
private async Task RefreshNearbySyncshellsAsync(string? gid = null)
|
|
{
|
|
var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts();
|
|
var snapshot = _pairUiService.GetSnapshot();
|
|
_currentSyncshells = [.. snapshot.GroupPairs.Keys];
|
|
|
|
_recentlyJoined.RemoveWhere(g => _currentSyncshells.Exists(s => string.Equals(s.GID, g, StringComparison.Ordinal)));
|
|
|
|
List<GroupJoinDto>? updatedList = [];
|
|
|
|
#if DEBUG
|
|
if (_useTestSyncshells)
|
|
{
|
|
updatedList = BuildTestSyncshells();
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
if (syncshellBroadcasts.Count == 0)
|
|
{
|
|
ClearSyncshells();
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts).ConfigureAwait(false);
|
|
updatedList = groups?.DistinctBy(g => g.Group.GID).ToList();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to refresh broadcasted syncshells.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (updatedList == null || updatedList.Count == 0)
|
|
{
|
|
ClearSyncshells();
|
|
return;
|
|
}
|
|
|
|
if (gid != null && _recentlyJoined.Contains(gid))
|
|
_recentlyJoined.Clear();
|
|
|
|
var previousGid = GetSelectedGid();
|
|
|
|
_nearbySyncshells.Clear();
|
|
_nearbySyncshells.AddRange(updatedList);
|
|
|
|
if (previousGid != null)
|
|
{
|
|
var newIndex = _nearbySyncshells.FindIndex(s => string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal));
|
|
if (newIndex >= 0)
|
|
{
|
|
_selectedNearbyIndex = newIndex;
|
|
return;
|
|
}
|
|
}
|
|
|
|
ClearSelection();
|
|
}
|
|
|
|
#if DEBUG
|
|
private static List<GroupJoinDto> BuildTestSyncshells()
|
|
{
|
|
return
|
|
[
|
|
new(new GroupData("TEST-ALPHA", "Alpha Shell"), "", GroupUserPreferredPermissions.NoneSet),
|
|
new(new GroupData("TEST-BETA", "Beta Shell"), "", GroupUserPreferredPermissions.NoneSet),
|
|
new(new GroupData("TEST-GAMMA", "Gamma Shell"), "", GroupUserPreferredPermissions.NoneSet),
|
|
new(new GroupData("TEST-DELTA", "Delta Shell"), "", GroupUserPreferredPermissions.NoneSet),
|
|
new(new GroupData("TEST-EPSILON", "Epsilon Shell"), "", GroupUserPreferredPermissions.NoneSet),
|
|
new(new GroupData("TEST-ZETA", "Zeta Shell"), "", GroupUserPreferredPermissions.NoneSet),
|
|
new(new GroupData("TEST-ETA", "Eta Shell"), "", GroupUserPreferredPermissions.NoneSet),
|
|
new(new GroupData("TEST-THETA", "Theta Shell"), "", GroupUserPreferredPermissions.NoneSet),
|
|
new(new GroupData("TEST-IOTA", "Iota Shell"), "", GroupUserPreferredPermissions.NoneSet),
|
|
new(new GroupData("TEST-KAPPA", "Kappa Shell"), "", GroupUserPreferredPermissions.NoneSet),
|
|
];
|
|
}
|
|
#endif
|
|
|
|
private void ClearSyncshells()
|
|
{
|
|
if (_nearbySyncshells.Count == 0) return;
|
|
_nearbySyncshells.Clear();
|
|
ClearSelection();
|
|
}
|
|
|
|
private void ClearSelection()
|
|
{
|
|
_selectedNearbyIndex = -1;
|
|
_joinDto = null;
|
|
_joinInfo = null;
|
|
}
|
|
|
|
private string? GetSelectedGid()
|
|
{
|
|
if (_selectedNearbyIndex < 0 || _selectedNearbyIndex >= _nearbySyncshells.Count)
|
|
return null;
|
|
return _nearbySyncshells[_selectedNearbyIndex].Group.GID;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helpers
|
|
|
|
private (float widthUsed, float rowHeight) RenderProfileTagsSingleRow(IReadOnlyList<ProfileTagDefinition> tags, float scale)
|
|
{
|
|
if (tags == null || tags.Count == 0)
|
|
return (0f, 0f);
|
|
|
|
var drawList = ImGui.GetWindowDrawList();
|
|
var style = ImGui.GetStyle();
|
|
var defaultTextColorU32 = ImGui.GetColorU32(ImGuiCol.Text);
|
|
|
|
var baseLocal = ImGui.GetCursorPos();
|
|
var baseScreen = ImGui.GetCursorScreenPos();
|
|
float availableWidth = ImGui.GetContentRegionAvail().X;
|
|
if (availableWidth <= 0f)
|
|
availableWidth = 1f;
|
|
|
|
float cursorLocalX = baseLocal.X;
|
|
float cursorScreenX = baseScreen.X;
|
|
float rowHeight = 0f;
|
|
|
|
for (int i = 0; i < tags.Count; i++)
|
|
{
|
|
var tag = tags[i];
|
|
if (!tag.HasContent)
|
|
continue;
|
|
|
|
var tagSize = ProfileTagRenderer.MeasureTag(tag, scale, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _seResolvedSegments, GetIconWrap, _logger);
|
|
|
|
float tagWidth = tagSize.X;
|
|
float tagHeight = tagSize.Y;
|
|
|
|
if (cursorLocalX > baseLocal.X && cursorLocalX + tagWidth > baseLocal.X + availableWidth)
|
|
break;
|
|
|
|
var tagScreenPos = new Vector2(cursorScreenX, baseScreen.Y);
|
|
ImGui.SetCursorScreenPos(tagScreenPos);
|
|
ImGui.InvisibleButton($"##profileTagInline_{i}", tagSize);
|
|
|
|
ProfileTagRenderer.RenderTag(tag, tagScreenPos, scale, drawList, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _seResolvedSegments, GetIconWrap, _logger);
|
|
|
|
cursorLocalX += tagWidth + style.ItemSpacing.X;
|
|
cursorScreenX += tagWidth + style.ItemSpacing.X;
|
|
rowHeight = MathF.Max(rowHeight, tagHeight);
|
|
}
|
|
|
|
ImGui.SetCursorPos(new Vector2(baseLocal.X, baseLocal.Y + rowHeight));
|
|
|
|
float widthUsed = cursorLocalX - baseLocal.X;
|
|
return (widthUsed, rowHeight);
|
|
}
|
|
|
|
private static string TruncateTextToWidth(string text, float maxWidth)
|
|
{
|
|
if (string.IsNullOrEmpty(text))
|
|
return text;
|
|
|
|
const string ellipsis = "...";
|
|
float ellipsisWidth = ImGui.CalcTextSize(ellipsis).X;
|
|
|
|
if (maxWidth <= ellipsisWidth)
|
|
return ellipsis;
|
|
|
|
int low = 0;
|
|
int high = text.Length;
|
|
string best = ellipsis;
|
|
|
|
while (low <= high)
|
|
{
|
|
int mid = (low + high) / 2;
|
|
string candidate = string.Concat(text.AsSpan(0, mid), ellipsis);
|
|
float width = ImGui.CalcTextSize(candidate).X;
|
|
|
|
if (width <= maxWidth)
|
|
{
|
|
best = candidate;
|
|
low = mid + 1;
|
|
}
|
|
else
|
|
{
|
|
high = mid - 1;
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|
|
|
|
private IDalamudTextureWrap? GetIconWrap(uint iconId)
|
|
{
|
|
try
|
|
{
|
|
if (_uiSharedService.TryGetIcon(iconId, out var wrap) && wrap != null)
|
|
return wrap;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogDebug(ex, "Failed to resolve icon {IconId}", iconId);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
#endregion
|
|
}
|