All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m9s
# Patchnotes 2.1.0 The changes in this update are more than just "patches". With a new UI, a new feature, and a bunch of bug fixes, improvements and a new member on the dev team, we thought this was more of a minor update. We would like to introduce @tsubasahane of MareCN to the team! We’re happy to work with them to bring Lightless and its features to the CN client as well as having another talented dev bring features and ideas to us. Speaking of which: # Location Sharing (Big shout out to @tsubasahane for bringing this feature) - Are you TIRED of scrambling to find the address of the venue you're in to share with your friends? We are introducing Location Sharing! An optional feature where you can share your location with direct pairs temporarily [30 minutes, 1 hour, 3 hours] minutes or until you turn it off for them. That's up to you! [#125](<#125>) [#49](<Lightless-Sync/LightlessServer#49>) - To share your location with a pair, click the three dots beside the pair and choose a duration to share with them. [#125](<#125>) [#49](<Lightless-Sync/LightlessServer#49>) - To view the location of someone who's shared with you, simply hover over the globe icon! [#125](<#125>) [#49](<Lightless-Sync/LightlessServer#49>) [1] # Model Optimization (Mesh Decimating) - This new option can automatically “simplify” incoming character meshes to help performance by reducing triangle counts. You choose how strong the reduction is (default/recommended is 80%). [#131](<#131>) - Decimation only kicks in when a mesh is above a certain triangle threshold, and only for the items that qualify for it and you selected for. [#131](<#131>) - Hair meshes is always excluded, since simplifying hair meshes is very prone to breaking. - You can find everything under Settings → Performance → Model Optimization. [#131](<#131>) + ** IF YOU HAVE USED DECIMATION IN TESTING, PLEASE CLEAR YOUR CACHE ❗ ** [2] # Animation (PAP) Validation (Safer animations) - Lightless now checks your currently animations to see if they work with your local skeleton/bone mod. If an animation matches, it’s included in what gets sent to other players. If it doesn’t, Lightless will skip it and write a warning to your log showing how many were skipped due to skeleton changes. Its defaulted to Unsafe (off). turn it on if you experience crashes from others users. [#131](<#131>) - Lightless also does the same kind of check for incoming animation files, to make sure they match the body/skeleton they were sent with. [#131](<#131>) - Because these checks can sometimes be a little picky, you can adjust how strict they are in Settings -> General -> Animation & Bones to reduce false positives. [#131](<#131>) # UI Changes (Thanks to @kyuwu for UI Changes) - The top part of the main screen has gotten a makeover. You can adjust the colors of the gradiant in the Color settings of Lightless. [#127](<#127>) [3] - Settings have gotten some changes as well to make this change more universal, and will use the same color settings. [#127](<#127>) - The particle effects of the gradient are toggleable in 'Settings -> UI -> Behavior' [#127](<#127>) - Instead of showing download/upload on bottom of Main UI, it will show VRAM usage and triangles with their optimization options next to it [#138](<#138>) # LightFinder / ShellFinder - UI Changes that follow our new design follow the color codes for the Gradient top as the main screen does. [#127](<#127>) [4] Co-authored-by: defnotken <itsdefnotken@gmail.com> Co-authored-by: azyges <aaaaaa@aaa.aaa> Co-authored-by: cake <admin@cakeandbanana.nl> Co-authored-by: Tsubasa <tsubasa@noreply.git.lightless-sync.org> Co-authored-by: choco <choco@patat.nl> Co-authored-by: celine <aaa@aaa.aaa> Co-authored-by: celine <celine@noreply.git.lightless-sync.org> Co-authored-by: Tsubasahane <wozaiha@gmail.com> Co-authored-by: cake <cake@noreply.git.lightless-sync.org> Reviewed-on: #123
1743 lines
68 KiB
C#
1743 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.ToArray())
|
|
{
|
|
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;
|
|
|
|
Vector2 windowSize = ImGui.GetWindowSize();
|
|
float modalWidth = Math.Min(420f * scale, windowSize.X - 40f * scale);
|
|
float modalHeight = 295f * scale;
|
|
Vector2 childPos = new Vector2(
|
|
(windowSize.X - modalWidth) * 0.5f,
|
|
(windowSize.Y - modalHeight) * 0.5f
|
|
);
|
|
ImGui.SetCursorPos(childPos);
|
|
|
|
using var modalBorder = ImRaii.PushColor(ImGuiCol.Border, UIColors.Get("LightlessPurple").WithAlpha(0.5f));
|
|
using var childBg = ImRaii.PushColor(ImGuiCol.ChildBg, ImGui.GetStyle().Colors[(int)ImGuiCol.WindowBg]);
|
|
using var rounding = ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 8f * scale);
|
|
using var borderSize = ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, 2f * scale);
|
|
using var padding = ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(16f * scale, 16f * scale));
|
|
|
|
if (ImGui.BeginChild("JoinSyncshellOverlay", new Vector2(modalWidth, modalHeight), true, ImGuiWindowFlags.NoScrollbar))
|
|
{
|
|
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.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.EndChild();
|
|
}
|
|
|
|
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)
|
|
{
|
|
try
|
|
{
|
|
var nearbySyncshellsSnapshot = _nearbySyncshells.ToArray();
|
|
var newIndex = Array.FindIndex(nearbySyncshellsSnapshot,
|
|
s => string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal));
|
|
if (newIndex >= 0)
|
|
{
|
|
_selectedNearbyIndex = newIndex;
|
|
return;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
ClearSelection();
|
|
}
|
|
}
|
|
|
|
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()
|
|
{
|
|
try
|
|
{
|
|
var index = _selectedNearbyIndex;
|
|
var list = _nearbySyncshells.ToArray();
|
|
if (index < 0 || index >= list.Length)
|
|
return null;
|
|
return list[index].Group.GID;
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
#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
|
|
} |