1739 lines
68 KiB
C#
1739 lines
68 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.PlayerData.Pairs;
|
|
using LightlessSync.Services;
|
|
using LightlessSync.Services.ActorTracking;
|
|
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 ActorObjectService _actorObjectService;
|
|
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 LightFinderPlateHandler _lightFinderPlateHandler;
|
|
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 bool _joinModalOpen = true;
|
|
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 const float AnimationSpeed = 6f;
|
|
private readonly Dictionary<string, float> _itemAlpha = new(StringComparer.Ordinal);
|
|
private readonly HashSet<string> _currentVisibleItems = new(StringComparer.Ordinal);
|
|
private readonly HashSet<string> _previousVisibleItems = new(StringComparer.Ordinal);
|
|
|
|
private enum LightfinderTab { NearbySyncshells, NearbyPlayers, BroadcastSettings, Help }
|
|
|
|
#if DEBUG
|
|
private enum LightfinderTabDebug { NearbySyncshells, NearbyPlayers, 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,
|
|
ActorObjectService actorObjectService
|
|
,
|
|
LightFinderPlateHandler lightFinderPlateHandler) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService)
|
|
{
|
|
_broadcastService = broadcastService;
|
|
_uiSharedService = uiShared;
|
|
_configService = configService;
|
|
_apiController = apiController;
|
|
_broadcastScannerService = broadcastScannerService;
|
|
_pairUiService = pairUiService;
|
|
_dalamudUtilService = dalamudUtilService;
|
|
_lightlessProfileManager = lightlessProfileManager;
|
|
_actorObjectService = actorObjectService;
|
|
|
|
_animatedHeader.Height = 85f;
|
|
_animatedHeader.EnableBottomGradient = true;
|
|
_animatedHeader.GradientHeight = 90f;
|
|
_animatedHeader.EnableParticles = _configService.Current.EnableParticleEffects;
|
|
|
|
IsOpen = false;
|
|
WindowBuilder.For(this)
|
|
.SetSizeConstraints(new Vector2(620, 85), new Vector2(700, 2000))
|
|
.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));
|
|
_lightFinderPlateHandler = lightFinderPlateHandler;
|
|
}
|
|
|
|
#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("Nearby Players", LightfinderTabDebug.NearbyPlayers),
|
|
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.NearbyPlayers:
|
|
DrawNearbyPlayersTab();
|
|
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("Nearby Players", LightfinderTab.NearbyPlayers),
|
|
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.NearbyPlayers:
|
|
DrawNearbyPlayersTab();
|
|
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("NearbySyncshells", ImGuiTableColumnFlags.WidthStretch, 1f);
|
|
ImGui.TableSetupColumn("NearbyPlayers", 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 syncshells cell
|
|
var nearbySyncshellCount = _nearbySyncshells.Count;
|
|
var nearbySyncshellColor = nearbySyncshellCount > 0 ? UIColors.Get("LightlessPurple") : infoColor;
|
|
DrawStatusCell(FontAwesomeIcon.Compass, nearbySyncshellColor, nearbySyncshellCount.ToString(), "Syncshells", infoColor, scale);
|
|
|
|
// Nearby players cell (exclude self)
|
|
string? myHashedCidForCount = null;
|
|
try { myHashedCidForCount = _dalamudUtilService.GetCID().ToString().GetHash256(); } catch { }
|
|
var nearbyPlayerCount = _broadcastScannerService.CountActiveBroadcasts(myHashedCidForCount);
|
|
var nearbyPlayerColor = nearbyPlayerCount > 0 ? UIColors.Get("LightlessBlue") : infoColor;
|
|
DrawStatusCell(FontAwesomeIcon.Users, nearbyPlayerColor, nearbyPlayerCount.ToString(), "Players", 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 Animation Helpers
|
|
|
|
private void UpdateItemAnimations(IEnumerable<string> visibleItemIds)
|
|
{
|
|
var deltaTime = ImGui.GetIO().DeltaTime;
|
|
|
|
_previousVisibleItems.Clear();
|
|
foreach (var id in _currentVisibleItems)
|
|
_previousVisibleItems.Add(id);
|
|
|
|
_currentVisibleItems.Clear();
|
|
foreach (var id in visibleItemIds)
|
|
_currentVisibleItems.Add(id);
|
|
|
|
// Fade in new items
|
|
foreach (var id in _currentVisibleItems)
|
|
{
|
|
if (!_itemAlpha.ContainsKey(id))
|
|
_itemAlpha[id] = 0f;
|
|
|
|
_itemAlpha[id] = Math.Min(1f, _itemAlpha[id] + deltaTime * AnimationSpeed);
|
|
}
|
|
|
|
// Fade out removed items
|
|
var toRemove = new List<string>();
|
|
foreach (var (id, alpha) in _itemAlpha)
|
|
{
|
|
if (!_currentVisibleItems.Contains(id))
|
|
{
|
|
_itemAlpha[id] = Math.Max(0f, alpha - deltaTime * AnimationSpeed);
|
|
if (_itemAlpha[id] <= 0.01f)
|
|
toRemove.Add(id);
|
|
}
|
|
}
|
|
|
|
foreach (var id in toRemove)
|
|
_itemAlpha.Remove(id);
|
|
}
|
|
|
|
private float GetItemAlpha(string itemId)
|
|
{
|
|
return _itemAlpha.TryGetValue(itemId, out var alpha) ? alpha : 1f;
|
|
}
|
|
|
|
#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)
|
|
{
|
|
UpdateItemAnimations([]);
|
|
ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells found.");
|
|
return;
|
|
}
|
|
|
|
// Update animations for syncshell items
|
|
UpdateItemAnimations(cardData.Select(c => $"shell_{c.Shell.Group.GID}"));
|
|
|
|
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, bool IsOwnBroadcast)> BuildSyncshellCardData()
|
|
{
|
|
string? myHashedCid = null;
|
|
try
|
|
{
|
|
var cid = _dalamudUtilService.GetCID();
|
|
myHashedCid = cid.ToString().GetHash256();
|
|
}
|
|
catch { }
|
|
|
|
var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().ToList();
|
|
|
|
var cardData = new List<(GroupJoinDto Shell, string BroadcasterName, bool IsOwnBroadcast)>();
|
|
|
|
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)", false));
|
|
continue;
|
|
}
|
|
#endif
|
|
|
|
var broadcast = broadcasts.FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal));
|
|
if (broadcast == null)
|
|
continue;
|
|
|
|
var isOwnBroadcast = !string.IsNullOrEmpty(myHashedCid) && string.Equals(broadcast.HashedCID, myHashedCid, StringComparison.Ordinal);
|
|
|
|
string broadcasterName;
|
|
if (isOwnBroadcast)
|
|
{
|
|
broadcasterName = "You";
|
|
}
|
|
else
|
|
{
|
|
var (name, address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID);
|
|
if (string.IsNullOrEmpty(name))
|
|
continue;
|
|
|
|
var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(address);
|
|
broadcasterName = !string.IsNullOrEmpty(worldName) ? $"{name} ({worldName})" : name;
|
|
}
|
|
|
|
cardData.Add((shell, broadcasterName, isOwnBroadcast));
|
|
}
|
|
|
|
return cardData;
|
|
}
|
|
|
|
private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName, bool IsOwnBroadcast)> 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, isOwnBroadcast) in listData)
|
|
{
|
|
DrawSyncshellListItem(shell, broadcasterName, isOwnBroadcast);
|
|
ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale));
|
|
}
|
|
}
|
|
ImGui.EndChild();
|
|
|
|
ImGui.PopStyleVar(2);
|
|
}
|
|
|
|
private void DrawSyncshellListItem(GroupJoinDto shell, string broadcasterName, bool isOwnBroadcast)
|
|
{
|
|
var itemId = $"shell_{shell.Group.GID}";
|
|
var alpha = GetItemAlpha(itemId);
|
|
if (alpha <= 0.01f)
|
|
return;
|
|
|
|
ImGui.PushID(shell.Group.GID);
|
|
using var alphaStyle = ImRaii.PushStyle(ImGuiStyleVar.Alpha, alpha);
|
|
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);
|
|
if (isOwnBroadcast)
|
|
ImGui.TextColored(UIColors.Get("LightlessGreen"), broadcasterName);
|
|
else
|
|
ImGui.TextUnformatted(broadcasterName);
|
|
if (ImGui.IsItemHovered())
|
|
ImGui.SetTooltip(isOwnBroadcast ? "Your broadcast" : "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, bool IsOwnBroadcast)> cardData)
|
|
{
|
|
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8f);
|
|
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f);
|
|
|
|
foreach (var (shell, _, isOwnBroadcast) in cardData)
|
|
{
|
|
DrawSyncshellCompactItem(shell, isOwnBroadcast);
|
|
ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale));
|
|
}
|
|
|
|
ImGui.PopStyleVar(2);
|
|
}
|
|
|
|
private void DrawSyncshellCompactItem(GroupJoinDto shell, bool isOwnBroadcast)
|
|
{
|
|
var itemId = $"shell_{shell.Group.GID}";
|
|
var alpha = GetItemAlpha(itemId);
|
|
if (alpha <= 0.01f)
|
|
return;
|
|
|
|
ImGui.PushID(shell.Group.GID);
|
|
using var alphaStyle = ImRaii.PushStyle(ImGuiStyleVar.Alpha, alpha);
|
|
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;
|
|
if (isOwnBroadcast)
|
|
displayName += " (You)";
|
|
var style = ImGui.GetStyle();
|
|
float availW = ImGui.GetContentRegionAvail().X;
|
|
|
|
ImGui.AlignTextToFramePadding();
|
|
_uiSharedService.MediumText(displayName, isOwnBroadcast ? UIColors.Get("LightlessGreen") : 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...");
|
|
}
|
|
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;
|
|
_joinModalOpen = true;
|
|
_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;
|
|
|
|
var scale = ImGuiHelpers.GlobalScale;
|
|
|
|
// if not already open
|
|
if (!ImGui.IsPopupOpen("JoinSyncshellModal"))
|
|
ImGui.OpenPopup("JoinSyncshellModal");
|
|
|
|
Vector2 windowPos = ImGui.GetWindowPos();
|
|
Vector2 windowSize = ImGui.GetWindowSize();
|
|
float modalWidth = Math.Min(420f * scale, windowSize.X - 40f * scale);
|
|
float modalHeight = 295f * scale;
|
|
ImGui.SetNextWindowPos(new Vector2(
|
|
windowPos.X + (windowSize.X - modalWidth) * 0.5f,
|
|
windowPos.Y + (windowSize.Y - modalHeight) * 0.5f
|
|
), ImGuiCond.Always);
|
|
ImGui.SetNextWindowSize(new Vector2(modalWidth, modalHeight));
|
|
ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 6f * ImGuiHelpers.GlobalScale);
|
|
ImGui.PushStyleColor(ImGuiCol.Border, Vector4.Zero);
|
|
|
|
using ImRaii.Color modalBorder = ImRaii.PushColor(ImGuiCol.Border, UIColors.Get("LightlessPurple").WithAlpha(0.5f));
|
|
using ImRaii.Style rounding = ImRaii.PushStyle(ImGuiStyleVar.WindowRounding, 8f * scale);
|
|
using ImRaii.Style borderSize = ImRaii.PushStyle(ImGuiStyleVar.WindowBorderSize, 2f * scale);
|
|
using ImRaii.Style padding = ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(16f * scale, 16f * scale));
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoScrollbar;
|
|
if (ImGui.BeginPopupModal("JoinSyncshellModal", ref _joinModalOpen, flags))
|
|
{
|
|
float contentWidth = ImGui.GetContentRegionAvail().X;
|
|
|
|
// Header
|
|
_uiSharedService.MediumText("Join Syncshell", UIColors.Get("LightlessPurple"));
|
|
ImGuiHelpers.ScaledDummy(2f);
|
|
ImGui.TextColored(ImGuiColors.DalamudGrey, $"{_joinDto.Group.AliasOrGID} by {_joinInfo.OwnerAliasOrUID}");
|
|
|
|
ImGuiHelpers.ScaledDummy(8f);
|
|
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault").WithAlpha(0.4f));
|
|
ImGuiHelpers.ScaledDummy(8f);
|
|
|
|
// Permissions section
|
|
ImGui.TextColored(ImGuiColors.DalamudWhite, "Permissions");
|
|
ImGuiHelpers.ScaledDummy(6f);
|
|
|
|
DrawPermissionToggleRow("Sounds", FontAwesomeIcon.VolumeUp,
|
|
_joinInfo.GroupPermissions.IsPreferDisableSounds(),
|
|
_ownPermissions.DisableGroupSounds,
|
|
v => _ownPermissions.DisableGroupSounds = v,
|
|
contentWidth);
|
|
|
|
DrawPermissionToggleRow("Animations", FontAwesomeIcon.Running,
|
|
_joinInfo.GroupPermissions.IsPreferDisableAnimations(),
|
|
_ownPermissions.DisableGroupAnimations,
|
|
v => _ownPermissions.DisableGroupAnimations = v,
|
|
contentWidth);
|
|
|
|
DrawPermissionToggleRow("VFX", FontAwesomeIcon.Magic,
|
|
_joinInfo.GroupPermissions.IsPreferDisableVFX(),
|
|
_ownPermissions.DisableGroupVFX,
|
|
v => _ownPermissions.DisableGroupVFX = v,
|
|
contentWidth);
|
|
|
|
ImGuiHelpers.ScaledDummy(12f);
|
|
|
|
// Buttons
|
|
float buttonHeight = 32f * scale;
|
|
float buttonSpacing = 8f * scale;
|
|
float joinButtonWidth = (contentWidth - buttonSpacing) * 0.65f;
|
|
float cancelButtonWidth = (contentWidth - buttonSpacing) * 0.35f;
|
|
|
|
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 6f * scale))
|
|
{
|
|
// Join button
|
|
using (ImRaii.PushColor(ImGuiCol.Button, UIColors.Get("LightlessGreen")))
|
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f)))
|
|
using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f)))
|
|
{
|
|
if (ImGui.Button($"Join Syncshell##{_joinDto.Group.GID}", new Vector2(joinButtonWidth, buttonHeight)))
|
|
{
|
|
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.CloseCurrentPopup();
|
|
}
|
|
}
|
|
|
|
ImGui.SameLine(0f, buttonSpacing);
|
|
|
|
// Cancel button
|
|
using (ImRaii.PushColor(ImGuiCol.Button, new Vector4(0.3f, 0.3f, 0.3f, 1f)))
|
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, new Vector4(0.4f, 0.4f, 0.4f, 1f)))
|
|
using (ImRaii.PushColor(ImGuiCol.ButtonActive, new Vector4(0.25f, 0.25f, 0.25f, 1f)))
|
|
{
|
|
if (ImGui.Button("Cancel", new Vector2(cancelButtonWidth, buttonHeight)))
|
|
{
|
|
_joinDto = null;
|
|
_joinInfo = null;
|
|
ImGui.CloseCurrentPopup();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle modal close via the bool ref
|
|
if (!_joinModalOpen)
|
|
{
|
|
_joinDto = null;
|
|
_joinInfo = null;
|
|
}
|
|
|
|
ImGui.EndPopup();
|
|
}
|
|
}
|
|
|
|
private void DrawPermissionToggleRow(string label, FontAwesomeIcon icon, bool suggested, bool current, Action<bool> apply, float contentWidth)
|
|
{
|
|
var scale = ImGuiHelpers.GlobalScale;
|
|
float rowHeight = 28f * scale;
|
|
bool isDifferent = current != suggested;
|
|
|
|
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 4f * scale))
|
|
using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 4f * scale))
|
|
using (ImRaii.PushColor(ImGuiCol.ChildBg, new Vector4(0.18f, 0.15f, 0.22f, 0.6f)))
|
|
{
|
|
ImGui.BeginChild($"PermRow_{label}", new Vector2(contentWidth, rowHeight), false, ImGuiWindowFlags.NoScrollbar);
|
|
|
|
float innerPadding = 8f * scale;
|
|
ImGui.SetCursorPos(new Vector2(innerPadding, (rowHeight - ImGui.GetTextLineHeight()) * 0.5f));
|
|
|
|
// Icon and label
|
|
var enabledColor = UIColors.Get("LightlessGreen");
|
|
var disabledColor = UIColors.Get("DimRed");
|
|
var currentColor = !current ? enabledColor : disabledColor;
|
|
|
|
_uiSharedService.IconText(icon, currentColor);
|
|
ImGui.SameLine(0f, 6f * scale);
|
|
ImGui.TextUnformatted(label);
|
|
|
|
// Current status
|
|
ImGui.SameLine();
|
|
float statusX = contentWidth * 0.38f;
|
|
ImGui.SetCursorPosX(statusX);
|
|
ImGui.TextColored(ImGuiColors.DalamudGrey, "Current:");
|
|
ImGui.SameLine(0f, 4f * scale);
|
|
_uiSharedService.BooleanToColoredIcon(!current, false);
|
|
|
|
// Suggested status
|
|
ImGui.SameLine();
|
|
float suggestedX = contentWidth * 0.60f;
|
|
ImGui.SetCursorPosX(suggestedX);
|
|
ImGui.TextColored(ImGuiColors.DalamudGrey, "Suggested:");
|
|
ImGui.SameLine(0f, 4f * scale);
|
|
_uiSharedService.BooleanToColoredIcon(!suggested, false);
|
|
|
|
// Apply checkmark button if different
|
|
if (isDifferent)
|
|
{
|
|
ImGui.SameLine();
|
|
float applyX = contentWidth - 26f * scale;
|
|
ImGui.SetCursorPosX(applyX);
|
|
ImGui.SetCursorPosY((rowHeight - ImGui.GetFrameHeight()) * 0.5f);
|
|
|
|
using (ImRaii.PushColor(ImGuiCol.Button, UIColors.Get("LightlessGreen").WithAlpha(0.6f)))
|
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen")))
|
|
using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreenDefault")))
|
|
{
|
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Check))
|
|
apply(suggested);
|
|
}
|
|
if (ImGui.IsItemHovered())
|
|
ImGui.SetTooltip("Apply suggested");
|
|
}
|
|
|
|
ImGui.EndChild();
|
|
}
|
|
ImGui.Dummy(new Vector2(0, 2f * scale));
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
#region Nearby Players Tab
|
|
|
|
private void DrawNearbyPlayersTab()
|
|
{
|
|
ImGui.BeginGroup();
|
|
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 (!_broadcastService.IsBroadcasting)
|
|
{
|
|
ImGui.TextColored(UIColors.Get("LightlessYellow"), "Lightfinder must be active to see nearby players.");
|
|
return;
|
|
}
|
|
|
|
string? myHashedCid = null;
|
|
try
|
|
{
|
|
var cid = _dalamudUtilService.GetCID();
|
|
myHashedCid = cid.ToString().GetHash256();
|
|
}
|
|
catch { }
|
|
|
|
var activeBroadcasts = _broadcastScannerService.GetActiveBroadcasts(myHashedCid);
|
|
|
|
if (activeBroadcasts.Count == 0)
|
|
{
|
|
ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby Lightfinder players found.");
|
|
return;
|
|
}
|
|
|
|
var playerData = BuildNearbyPlayerData(activeBroadcasts);
|
|
if (playerData.Count == 0)
|
|
{
|
|
UpdateItemAnimations([]);
|
|
ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby Lightfinder players found.");
|
|
return;
|
|
}
|
|
|
|
// Update animations for player items
|
|
UpdateItemAnimations(playerData.Select(p => $"player_{p.HashedCid}"));
|
|
|
|
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8f);
|
|
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f);
|
|
|
|
if (ImGui.BeginChild("NearbyPlayersScroll", new Vector2(-1, -1), border: false))
|
|
{
|
|
foreach (var data in playerData)
|
|
{
|
|
if (_compactView)
|
|
DrawNearbyPlayerCompactRow(data);
|
|
else
|
|
DrawNearbyPlayerRow(data);
|
|
ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale));
|
|
}
|
|
}
|
|
ImGui.EndChild();
|
|
|
|
ImGui.PopStyleVar(2);
|
|
}
|
|
|
|
private List<NearbyPlayerData> BuildNearbyPlayerData(List<KeyValuePair<string, LightFinderScannerService.BroadcastEntry>> activeBroadcasts)
|
|
{
|
|
var snapshot = _pairUiService.GetSnapshot();
|
|
var playerData = new List<NearbyPlayerData>();
|
|
|
|
foreach (var broadcast in activeBroadcasts)
|
|
{
|
|
var (name, address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.Key);
|
|
if (string.IsNullOrEmpty(name) || address == nint.Zero)
|
|
continue;
|
|
|
|
var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(address);
|
|
var pair = snapshot.PairsByUid.Values.FirstOrDefault(p =>
|
|
p.IsVisible &&
|
|
!string.IsNullOrEmpty(p.GetPlayerNameHash()) &&
|
|
string.Equals(p.GetPlayerNameHash(), broadcast.Key, StringComparison.Ordinal));
|
|
|
|
var isDirectlyPaired = pair?.IsDirectlyPaired ?? false;
|
|
var sharedGroups = pair?.UserPair?.Groups ?? [];
|
|
var sharedGroupNames = sharedGroups
|
|
.Select(gid => snapshot.GroupsByGid.TryGetValue(gid, out var g) ? g.GroupAliasOrGID : gid)
|
|
.ToList();
|
|
|
|
playerData.Add(new NearbyPlayerData(broadcast.Key, name, worldName, address, pair, isDirectlyPaired, sharedGroupNames));
|
|
}
|
|
|
|
return playerData;
|
|
}
|
|
|
|
private readonly record struct NearbyPlayerData(
|
|
string HashedCid,
|
|
string Name,
|
|
string? World,
|
|
nint Address,
|
|
Pair? Pair,
|
|
bool IsDirectlyPaired,
|
|
List<string> SharedSyncshells);
|
|
|
|
private void DrawNearbyPlayerRow(NearbyPlayerData data)
|
|
{
|
|
var itemId = $"player_{data.HashedCid}";
|
|
var alpha = GetItemAlpha(itemId);
|
|
if (alpha <= 0.01f)
|
|
return;
|
|
|
|
ImGui.PushID(data.HashedCid);
|
|
using var alphaStyle = ImRaii.PushStyle(ImGuiStyleVar.Alpha, alpha);
|
|
float rowHeight = 74f * ImGuiHelpers.GlobalScale;
|
|
|
|
ImGui.BeginChild($"PlayerRow##{data.HashedCid}", new Vector2(-1, rowHeight), border: true);
|
|
|
|
var serverName = !string.IsNullOrEmpty(data.World) ? data.World : "Unknown";
|
|
var style = ImGui.GetStyle();
|
|
float startX = ImGui.GetCursorPosX();
|
|
float regionW = ImGui.GetContentRegionAvail().X;
|
|
float rightTxtW = ImGui.CalcTextSize(serverName).X;
|
|
|
|
_uiSharedService.MediumText(data.Name, UIColors.Get("LightlessPurple"));
|
|
if (data.Pair != null)
|
|
{
|
|
if (ImGui.IsItemHovered())
|
|
ImGui.SetTooltip("Click to open profile.");
|
|
if (ImGui.IsItemClicked())
|
|
Mediator.Publish(new ProfileOpenStandaloneMessage(data.Pair));
|
|
}
|
|
|
|
float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X;
|
|
ImGui.SameLine();
|
|
ImGui.SetCursorPosX(rightX);
|
|
ImGui.TextUnformatted(serverName);
|
|
if (ImGui.IsItemHovered())
|
|
ImGui.SetTooltip("Home World");
|
|
|
|
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"));
|
|
|
|
Vector2 rowStartLocal = ImGui.GetCursorPos();
|
|
|
|
if (data.IsDirectlyPaired)
|
|
{
|
|
ImGui.SetCursorPosX(startX);
|
|
_uiSharedService.IconText(FontAwesomeIcon.UserCheck, UIColors.Get("LightlessGreen"));
|
|
ImGui.SameLine(0f, 4f * ImGuiHelpers.GlobalScale);
|
|
ImGui.TextColored(UIColors.Get("LightlessGreen"), "Direct Pair");
|
|
}
|
|
else if (data.SharedSyncshells.Count > 0)
|
|
{
|
|
ImGui.SetCursorPosX(startX);
|
|
_uiSharedService.IconText(FontAwesomeIcon.Users, UIColors.Get("LightlessPurple"));
|
|
ImGui.SameLine(0f, 4f * ImGuiHelpers.GlobalScale);
|
|
var shellText = data.SharedSyncshells.Count == 1
|
|
? data.SharedSyncshells[0]
|
|
: $"{data.SharedSyncshells.Count} shared shells";
|
|
ImGui.TextColored(UIColors.Get("LightlessPurple"), shellText);
|
|
if (data.SharedSyncshells.Count > 1 && ImGui.IsItemHovered())
|
|
ImGui.SetTooltip(string.Join("\n", data.SharedSyncshells));
|
|
}
|
|
else
|
|
{
|
|
ImGui.SetCursorPosX(startX);
|
|
_uiSharedService.IconText(FontAwesomeIcon.Wifi, UIColors.Get("LightlessBlue"));
|
|
ImGui.SameLine(0f, 4f * ImGuiHelpers.GlobalScale);
|
|
ImGui.TextColored(ImGuiColors.DalamudGrey, "Lightfinder user");
|
|
}
|
|
|
|
ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale));
|
|
|
|
DrawPlayerActionButtons(data, startX, regionW, rowStartLocal.Y, style);
|
|
|
|
ImGui.EndChild();
|
|
ImGui.PopID();
|
|
}
|
|
|
|
private void DrawNearbyPlayerCompactRow(NearbyPlayerData data)
|
|
{
|
|
var itemId = $"player_{data.HashedCid}";
|
|
var alpha = GetItemAlpha(itemId);
|
|
if (alpha <= 0.01f)
|
|
return;
|
|
|
|
ImGui.PushID(data.HashedCid);
|
|
using var alphaStyle = ImRaii.PushStyle(ImGuiStyleVar.Alpha, alpha);
|
|
float rowHeight = 36f * ImGuiHelpers.GlobalScale;
|
|
|
|
ImGui.BeginChild($"PlayerCompact##{data.HashedCid}", new Vector2(-1, rowHeight), border: true);
|
|
|
|
ImGui.AlignTextToFramePadding();
|
|
|
|
if (data.IsDirectlyPaired)
|
|
{
|
|
_uiSharedService.IconText(FontAwesomeIcon.UserCheck, UIColors.Get("LightlessGreen"));
|
|
ImGui.SameLine(0f, 4f * ImGuiHelpers.GlobalScale);
|
|
}
|
|
else if (data.SharedSyncshells.Count > 0)
|
|
{
|
|
_uiSharedService.IconText(FontAwesomeIcon.Users, UIColors.Get("LightlessPurple"));
|
|
ImGui.SameLine(0f, 4f * ImGuiHelpers.GlobalScale);
|
|
}
|
|
else
|
|
{
|
|
_uiSharedService.IconText(FontAwesomeIcon.Wifi, UIColors.Get("LightlessBlue"));
|
|
ImGui.SameLine(0f, 4f * ImGuiHelpers.GlobalScale);
|
|
}
|
|
|
|
var displayText = !string.IsNullOrEmpty(data.World) ? $"{data.Name} ({data.World})" : data.Name;
|
|
_uiSharedService.MediumText(displayText, UIColors.Get("LightlessPurple"));
|
|
if (data.Pair != null)
|
|
{
|
|
if (ImGui.IsItemHovered())
|
|
ImGui.SetTooltip("Click to open profile.");
|
|
if (ImGui.IsItemClicked())
|
|
Mediator.Publish(new ProfileOpenStandaloneMessage(data.Pair));
|
|
}
|
|
|
|
ImGui.SameLine();
|
|
DrawPlayerActionButtons(data, 0, ImGui.GetContentRegionAvail().X, ImGui.GetCursorPosY(), ImGui.GetStyle(), compact: true);
|
|
|
|
ImGui.EndChild();
|
|
ImGui.PopID();
|
|
}
|
|
|
|
private void DrawPlayerActionButtons(NearbyPlayerData data, float startX, float regionW, float rowY, ImGuiStylePtr style, bool compact = false)
|
|
{
|
|
float buttonWidth = compact ? 60f * ImGuiHelpers.GlobalScale : 80f * ImGuiHelpers.GlobalScale;
|
|
float buttonHeight = compact ? 0 : 30f;
|
|
float totalButtonsWidth = buttonWidth * 2 + style.ItemSpacing.X;
|
|
|
|
if (compact)
|
|
{
|
|
float availX = ImGui.GetContentRegionAvail().X;
|
|
float curX = ImGui.GetCursorPosX();
|
|
ImGui.SetCursorPosX(curX + availX - totalButtonsWidth);
|
|
}
|
|
else
|
|
{
|
|
ImGui.SetCursorPos(new Vector2(startX + regionW - totalButtonsWidth - style.ItemSpacing.X, rowY));
|
|
}
|
|
|
|
using (ImRaii.PushColor(ImGuiCol.Button, UIColors.Get("LightlessGreen")))
|
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f)))
|
|
using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f)))
|
|
using (ImRaii.Disabled(data.IsDirectlyPaired))
|
|
{
|
|
if (ImGui.Button($"Pair##{data.HashedCid}", new Vector2(buttonWidth, buttonHeight)))
|
|
{
|
|
_ = SendPairRequestAsync(data.HashedCid);
|
|
}
|
|
}
|
|
if (data.IsDirectlyPaired)
|
|
UiSharedService.AttachToolTip("Already directly paired with this player.");
|
|
|
|
ImGui.SameLine();
|
|
|
|
using (ImRaii.PushColor(ImGuiCol.Button, UIColors.Get("LightlessPurple")))
|
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple").WithAlpha(0.85f)))
|
|
using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurple").WithAlpha(0.75f)))
|
|
{
|
|
if (ImGui.Button($"Target##{data.HashedCid}", new Vector2(buttonWidth, buttonHeight)))
|
|
{
|
|
TargetPlayerByAddress(data.Address);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task SendPairRequestAsync(string hashedCid)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(hashedCid))
|
|
return;
|
|
|
|
try
|
|
{
|
|
await _apiController.TryPairWithContentId(hashedCid).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to send pair request to {HashedCid}", hashedCid);
|
|
}
|
|
}
|
|
|
|
private void TargetPlayerByAddress(nint address)
|
|
{
|
|
if (address == nint.Zero)
|
|
return;
|
|
|
|
_dalamudUtilService.TargetPlayerByAddress(address);
|
|
}
|
|
|
|
#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
|
|
|
|
private void DrawDebugTab()
|
|
{
|
|
#if DEBUG
|
|
if (ImGui.CollapsingHeader("LightFinder Plates", ImGuiTreeNodeFlags.DefaultOpen))
|
|
{
|
|
var h = _lightFinderPlateHandler;
|
|
|
|
var enabled = h.DebugEnabled;
|
|
if (ImGui.Checkbox("Enable LightFinder debug", ref enabled))
|
|
h.DebugEnabled = enabled;
|
|
|
|
if (h.DebugEnabled)
|
|
{
|
|
ImGui.Indent();
|
|
|
|
var disableOcc = h.DebugDisableOcclusion;
|
|
if (ImGui.Checkbox("Disable occlusion (force draw)", ref disableOcc))
|
|
h.DebugDisableOcclusion = disableOcc;
|
|
|
|
var drawUiRects = h.DebugDrawUiRects;
|
|
if (ImGui.Checkbox("Draw UI rects", ref drawUiRects))
|
|
h.DebugDrawUiRects = drawUiRects;
|
|
|
|
var drawLabelRects = h.DebugDrawLabelRects;
|
|
if (ImGui.Checkbox("Draw label rects", ref drawLabelRects))
|
|
h.DebugDrawLabelRects = drawLabelRects;
|
|
|
|
ImGui.Separator();
|
|
ImGui.TextUnformatted($"Labels last frame: {h.DebugLabelCountLastFrame}");
|
|
ImGui.TextUnformatted($"UI rects last frame: {h.DebugUiRectCountLastFrame}");
|
|
ImGui.TextUnformatted($"Occluded last frame: {h.DebugOccludedCountLastFrame}");
|
|
ImGui.TextUnformatted($"Last NamePlate frame: {h.DebugLastNameplateFrame}");
|
|
|
|
ImGui.Unindent();
|
|
}
|
|
}
|
|
|
|
ImGui.Separator();
|
|
|
|
ImGui.Text("Broadcast Cache");
|
|
|
|
if (ImGui.BeginTable("##BroadcastCacheTable", 4,
|
|
ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY,
|
|
new Vector2(-1, 225f)))
|
|
{
|
|
ImGui.TableSetupColumn("CID", ImGuiTableColumnFlags.WidthStretch);
|
|
ImGui.TableSetupColumn("IsBroadcasting", 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();
|
|
}
|
|
#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
|
|
} |