856 lines
31 KiB
C#
856 lines
31 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.Plugin.Services;
|
|
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.Services;
|
|
using LightlessSync.Services.LightFinder;
|
|
using LightlessSync.Services.Mediator;
|
|
using LightlessSync.UI.Services;
|
|
using LightlessSync.UI.Tags;
|
|
using LightlessSync.Utils;
|
|
using LightlessSync.WebAPI;
|
|
using Microsoft.Extensions.Logging;
|
|
using System.Numerics;
|
|
|
|
namespace LightlessSync.UI;
|
|
|
|
public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|
{
|
|
private readonly ApiController _apiController;
|
|
private readonly LightFinderService _broadcastService;
|
|
private readonly UiSharedService _uiSharedService;
|
|
private readonly LightFinderScannerService _broadcastScannerService;
|
|
private readonly PairUiService _pairUiService;
|
|
private readonly DalamudUtilService _dalamudUtilService;
|
|
|
|
private Vector4 _tagBackgroundColor = new(0.18f, 0.18f, 0.18f, 0.95f);
|
|
private Vector4 _tagBorderColor = new(0.35f, 0.35f, 0.35f, 0.4f);
|
|
|
|
private readonly List<SeStringUtils.SeStringSegment> _seResolvedSegments = new();
|
|
private readonly List<GroupJoinDto> _nearbySyncshells = [];
|
|
private List<GroupFullInfoDto> _currentSyncshells = [];
|
|
private int _selectedNearbyIndex = -1;
|
|
private int _syncshellPageIndex = 0;
|
|
private readonly HashSet<string> _recentlyJoined = new(StringComparer.Ordinal);
|
|
|
|
private GroupJoinDto? _joinDto;
|
|
private GroupJoinInfoDto? _joinInfo;
|
|
private DefaultPermissionsDto _ownPermissions = null!;
|
|
private bool _useTestSyncshells = false;
|
|
|
|
private bool _compactView = false;
|
|
private readonly LightlessProfileManager _lightlessProfileManager;
|
|
|
|
public SyncshellFinderUI(
|
|
ILogger<SyncshellFinderUI> logger,
|
|
LightlessMediator mediator,
|
|
PerformanceCollectorService performanceCollectorService,
|
|
LightFinderService broadcastService,
|
|
UiSharedService uiShared,
|
|
ApiController apiController,
|
|
LightFinderScannerService broadcastScannerService,
|
|
PairUiService pairUiService,
|
|
DalamudUtilService dalamudUtilService,
|
|
LightlessProfileManager lightlessProfileManager) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService)
|
|
{
|
|
_broadcastService = broadcastService;
|
|
_uiSharedService = uiShared;
|
|
_apiController = apiController;
|
|
_broadcastScannerService = broadcastScannerService;
|
|
_pairUiService = pairUiService;
|
|
_dalamudUtilService = dalamudUtilService;
|
|
_lightlessProfileManager = lightlessProfileManager;
|
|
|
|
IsOpen = false;
|
|
WindowBuilder.For(this)
|
|
.SetSizeConstraints(new Vector2(600, 400), new Vector2(600, 550))
|
|
.Apply();
|
|
|
|
Mediator.Subscribe<SyncshellBroadcastsUpdatedMessage>(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false));
|
|
Mediator.Subscribe<BroadcastStatusChangedMessage>(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false));
|
|
Mediator.Subscribe<UserLeftSyncshell>(this, async _ => await RefreshSyncshellsAsync(_.gid).ConfigureAwait(false));
|
|
Mediator.Subscribe<UserJoinedSyncshell>(this, async _ => await RefreshSyncshellsAsync(_.gid).ConfigureAwait(false));
|
|
}
|
|
|
|
public override async void OnOpen()
|
|
{
|
|
_ownPermissions = _apiController.DefaultPermissions.DeepClone()!;
|
|
await RefreshSyncshellsAsync().ConfigureAwait(false);
|
|
}
|
|
|
|
protected override void DrawInternal()
|
|
{
|
|
ImGui.BeginGroup();
|
|
_uiSharedService.MediumText("Nearby Syncshells", UIColors.Get("LightlessPurple"));
|
|
|
|
#if DEBUG
|
|
if (ImGui.SmallButton("Show test syncshells"))
|
|
{
|
|
_useTestSyncshells = !_useTestSyncshells;
|
|
_ = Task.Run(async () => await RefreshSyncshellsAsync().ConfigureAwait(false));
|
|
}
|
|
ImGui.SameLine();
|
|
#endif
|
|
|
|
string checkboxLabel = "Compact view";
|
|
float availWidth = ImGui.GetContentRegionAvail().X;
|
|
float checkboxWidth = ImGui.CalcTextSize(checkboxLabel).X + ImGui.GetFrameHeight();
|
|
|
|
float rightX = ImGui.GetCursorPosX() + availWidth - checkboxWidth - 4.0f;
|
|
ImGui.SetCursorPosX(rightX);
|
|
ImGui.Checkbox(checkboxLabel, ref _compactView);
|
|
ImGui.EndGroup();
|
|
|
|
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"));
|
|
ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale));
|
|
if (_nearbySyncshells.Count == 0)
|
|
{
|
|
ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted.");
|
|
|
|
if (!_broadcastService.IsBroadcasting)
|
|
{
|
|
|
|
UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"));
|
|
|
|
ImGui.TextColored(UIColors.Get("LightlessYellow"), "Lightfinder is currently disabled, to locate nearby syncshells, Lightfinder must be active.");
|
|
ImGuiHelpers.ScaledDummy(0.5f);
|
|
|
|
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10.0f);
|
|
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessPurple"));
|
|
|
|
if (ImGui.Button("Open Lightfinder", new Vector2(200 * ImGuiHelpers.GlobalScale, 0)))
|
|
{
|
|
Mediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
|
}
|
|
|
|
ImGui.PopStyleColor();
|
|
ImGui.PopStyleVar();
|
|
|
|
return;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().ToList() ?? [];
|
|
_broadcastScannerService.TryGetLocalHashedCid(out var localHashedCid);
|
|
|
|
var cardData = new List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)>();
|
|
|
|
foreach (var shell in _nearbySyncshells)
|
|
{
|
|
string broadcasterName;
|
|
|
|
if (shell?.Group == null || string.IsNullOrEmpty(shell.Group.GID))
|
|
continue;
|
|
|
|
if (_useTestSyncshells)
|
|
{
|
|
var displayName = !string.IsNullOrEmpty(shell.Group.Alias)
|
|
? shell.Group.Alias
|
|
: shell.Group.GID;
|
|
|
|
broadcasterName = $"{displayName} (Tester of TestWorld)";
|
|
}
|
|
else
|
|
{
|
|
var broadcast = broadcasts
|
|
.FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal));
|
|
|
|
if (broadcast == null)
|
|
continue;
|
|
|
|
var (name, address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID);
|
|
if (string.IsNullOrEmpty(name))
|
|
continue;
|
|
|
|
var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(address);
|
|
broadcasterName = !string.IsNullOrEmpty(worldName)
|
|
? $"{name} ({worldName})"
|
|
: name;
|
|
|
|
var isSelfBroadcast = !string.IsNullOrEmpty(localHashedCid)
|
|
&& string.Equals(broadcast.HashedCID, localHashedCid, StringComparison.Ordinal);
|
|
|
|
cardData.Add((shell, broadcasterName, isSelfBroadcast));
|
|
continue;
|
|
}
|
|
|
|
cardData.Add((shell, broadcasterName, false));
|
|
}
|
|
|
|
if (cardData.Count == 0)
|
|
{
|
|
ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted.");
|
|
return;
|
|
}
|
|
|
|
if (_compactView)
|
|
{
|
|
DrawSyncshellGrid(cardData);
|
|
}
|
|
else
|
|
{
|
|
DrawSyncshellList(cardData);
|
|
}
|
|
|
|
|
|
if (_joinDto != null && _joinInfo != null && _joinInfo.Success)
|
|
DrawConfirmation();
|
|
}
|
|
|
|
private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)> listData)
|
|
{
|
|
const int shellsPerPage = 3;
|
|
var totalPages = (int)Math.Ceiling(listData.Count / (float)shellsPerPage);
|
|
if (totalPages <= 0)
|
|
totalPages = 1;
|
|
|
|
_syncshellPageIndex = Math.Clamp(_syncshellPageIndex, 0, totalPages - 1);
|
|
|
|
var firstIndex = _syncshellPageIndex * shellsPerPage;
|
|
var lastExclusive = Math.Min(firstIndex + shellsPerPage, listData.Count);
|
|
|
|
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8.0f);
|
|
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1.0f);
|
|
|
|
for (int index = firstIndex; index < lastExclusive; index++)
|
|
{
|
|
var (shell, broadcasterName, isSelfBroadcast) = listData[index];
|
|
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
|
|
? (isSelfBroadcast ? "You" : string.Empty)
|
|
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
|
|
|
|
ImGui.PushID(shell.Group.GID);
|
|
float rowHeight = 74f * ImGuiHelpers.GlobalScale;
|
|
|
|
ImGui.BeginChild($"ShellRow##{shell.Group.GID}", new Vector2(-1, rowHeight), border: true);
|
|
|
|
var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID;
|
|
|
|
var style = ImGui.GetStyle();
|
|
float startX = ImGui.GetCursorPosX();
|
|
float regionW = ImGui.GetContentRegionAvail().X;
|
|
float rightTxtW = ImGui.CalcTextSize(broadcasterLabel).X;
|
|
|
|
_uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple"));
|
|
if (ImGui.IsItemHovered())
|
|
ImGui.SetTooltip("Click to open profile.");
|
|
if (ImGui.IsItemClicked())
|
|
{
|
|
Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group));
|
|
}
|
|
|
|
float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X;
|
|
ImGui.SameLine();
|
|
ImGui.SetCursorPosX(rightX);
|
|
ImGui.TextUnformatted(broadcasterLabel);
|
|
if (ImGui.IsItemHovered())
|
|
ImGui.SetTooltip("Broadcaster of the syncshell.");
|
|
|
|
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"));
|
|
|
|
var groupProfile = _lightlessProfileManager.GetLightlessGroupProfile(shell.Group);
|
|
|
|
IReadOnlyList<ProfileTagDefinition> groupTags =
|
|
groupProfile != null && 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;
|
|
float tagsHeight = 0f;
|
|
|
|
if (limitedTags.Count > 0)
|
|
{
|
|
(tagsWidth, tagsHeight) = RenderProfileTagsSingleRow(limitedTags, tagScale);
|
|
}
|
|
else
|
|
{
|
|
ImGui.SetCursorPosX(startX);
|
|
ImGui.TextDisabled("-- No tags set --");
|
|
ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale));
|
|
}
|
|
|
|
float btnBaselineY = rowStartLocal.Y;
|
|
float joinX = rowStartLocal.X + (tagsWidth > 0 ? tagsWidth + style.ItemSpacing.X : 0f);
|
|
|
|
ImGui.SetCursorPos(new Vector2(joinX, btnBaselineY));
|
|
DrawJoinButton(shell, isSelfBroadcast);
|
|
|
|
float btnHeight = ImGui.GetFrameHeightWithSpacing();
|
|
float rowHeightUsed = MathF.Max(tagsHeight, btnHeight);
|
|
|
|
ImGui.SetCursorPos(new Vector2(
|
|
rowStartLocal.X,
|
|
rowStartLocal.Y + rowHeightUsed));
|
|
|
|
ImGui.EndChild();
|
|
ImGui.PopID();
|
|
|
|
ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale));
|
|
}
|
|
|
|
ImGui.PopStyleVar(2);
|
|
|
|
DrawPagination(totalPages);
|
|
}
|
|
|
|
private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)> cardData)
|
|
{
|
|
const int shellsPerPage = 4;
|
|
var totalPages = (int)Math.Ceiling(cardData.Count / (float)shellsPerPage);
|
|
if (totalPages <= 0)
|
|
totalPages = 1;
|
|
|
|
_syncshellPageIndex = Math.Clamp(_syncshellPageIndex, 0, totalPages - 1);
|
|
|
|
var firstIndex = _syncshellPageIndex * shellsPerPage;
|
|
var lastExclusive = Math.Min(firstIndex + shellsPerPage, cardData.Count);
|
|
|
|
var avail = ImGui.GetContentRegionAvail();
|
|
var spacing = ImGui.GetStyle().ItemSpacing;
|
|
|
|
var cardWidth = (avail.X - spacing.X) / 2.0f;
|
|
var cardHeight = (avail.Y - spacing.Y - (ImGui.GetFrameHeightWithSpacing() * 2.0f)) / 2.0f;
|
|
cardHeight = MathF.Max(110f * ImGuiHelpers.GlobalScale, cardHeight);
|
|
|
|
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8.0f);
|
|
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1.0f);
|
|
|
|
for (int index = firstIndex; index < lastExclusive; index++)
|
|
{
|
|
var localIndex = index - firstIndex;
|
|
var (shell, broadcasterName, isSelfBroadcast) = cardData[index];
|
|
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
|
|
? (isSelfBroadcast ? "You" : string.Empty)
|
|
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
|
|
|
|
if (localIndex % 2 != 0)
|
|
ImGui.SameLine();
|
|
|
|
ImGui.PushID(shell.Group.GID);
|
|
|
|
ImGui.BeginGroup();
|
|
_ = ImGui.BeginChild("ShellCard##" + shell.Group.GID, new Vector2(cardWidth, cardHeight), border: true);
|
|
|
|
var displayName = !string.IsNullOrEmpty(shell.Group.Alias)
|
|
? shell.Group.Alias
|
|
: shell.Group.GID;
|
|
|
|
var style = ImGui.GetStyle();
|
|
float startX = ImGui.GetCursorPosX();
|
|
float availW = ImGui.GetContentRegionAvail().X;
|
|
|
|
ImGui.BeginGroup();
|
|
|
|
_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 nameRightX = ImGui.GetItemRectMax().X;
|
|
|
|
var regionMinScreen = ImGui.GetCursorScreenPos();
|
|
float regionRightX = regionMinScreen.X + availW;
|
|
|
|
float minBroadcasterX = nameRightX + style.ItemSpacing.X;
|
|
|
|
float maxBroadcasterWidth = regionRightX - minBroadcasterX;
|
|
|
|
string broadcasterToShow = broadcasterLabel;
|
|
|
|
if (!string.IsNullOrEmpty(broadcasterLabel) && maxBroadcasterWidth > 0f)
|
|
{
|
|
float bcFullWidth = ImGui.CalcTextSize(broadcasterLabel).X;
|
|
string toolTip;
|
|
|
|
if (bcFullWidth > maxBroadcasterWidth)
|
|
{
|
|
broadcasterToShow = TruncateTextToWidth(broadcasterLabel, maxBroadcasterWidth);
|
|
toolTip = broadcasterLabel + Environment.NewLine + Environment.NewLine + "Broadcaster of the syncshell.";
|
|
}
|
|
else
|
|
{
|
|
toolTip = "Broadcaster of the syncshell.";
|
|
}
|
|
|
|
float bcWidth = ImGui.CalcTextSize(broadcasterToShow).X;
|
|
|
|
float broadX = regionRightX - bcWidth;
|
|
|
|
broadX = MathF.Max(broadX, minBroadcasterX);
|
|
|
|
ImGui.SameLine();
|
|
var curPos = ImGui.GetCursorPos();
|
|
ImGui.SetCursorPos(new Vector2(broadX - regionMinScreen.X + startX, curPos.Y + 3f * ImGuiHelpers.GlobalScale));
|
|
ImGui.TextUnformatted(broadcasterToShow);
|
|
if (ImGui.IsItemHovered())
|
|
ImGui.SetTooltip(toolTip);
|
|
}
|
|
|
|
ImGui.EndGroup();
|
|
|
|
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"));
|
|
|
|
ImGui.Dummy(new Vector2(0, 6 * ImGuiHelpers.GlobalScale));
|
|
|
|
var groupProfile = _lightlessProfileManager.GetLightlessGroupProfile(shell.Group);
|
|
|
|
IReadOnlyList<ProfileTagDefinition> groupTags =
|
|
groupProfile != null && groupProfile.Tags.Count > 0
|
|
? ProfileTagService.ResolveTags(groupProfile.Tags)
|
|
: [];
|
|
|
|
float tagScale = ImGuiHelpers.GlobalScale * 0.9f;
|
|
|
|
if (groupTags.Count > 0)
|
|
{
|
|
var limitedTags = groupTags.Count > 2
|
|
? [.. groupTags.Take(2)]
|
|
: groupTags;
|
|
|
|
ImGui.SetCursorPosX(startX);
|
|
|
|
var (_, tagsHeight) = RenderProfileTagsSingleRow(limitedTags, tagScale);
|
|
|
|
ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale));
|
|
}
|
|
else
|
|
{
|
|
ImGui.SetCursorPosX(startX);
|
|
ImGui.TextDisabled("-- No tags set --");
|
|
ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale));
|
|
}
|
|
|
|
var buttonHeight = ImGui.GetFrameHeightWithSpacing();
|
|
var remainingY = ImGui.GetContentRegionAvail().Y - buttonHeight;
|
|
if (remainingY > 0)
|
|
ImGui.Dummy(new Vector2(0, remainingY));
|
|
|
|
DrawJoinButton(shell, isSelfBroadcast);
|
|
|
|
ImGui.EndChild();
|
|
ImGui.EndGroup();
|
|
|
|
ImGui.PopID();
|
|
}
|
|
|
|
ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale));
|
|
ImGui.PopStyleVar(2);
|
|
|
|
DrawPagination(totalPages);
|
|
}
|
|
|
|
private void DrawPagination(int totalPages)
|
|
{
|
|
if (totalPages > 1)
|
|
{
|
|
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"));
|
|
|
|
var style = ImGui.GetStyle();
|
|
string pageLabel = $"Page {_syncshellPageIndex + 1}/{totalPages}";
|
|
|
|
float prevWidth = ImGui.CalcTextSize("<").X + style.FramePadding.X * 2;
|
|
float nextWidth = ImGui.CalcTextSize(">").X + style.FramePadding.X * 2;
|
|
float textWidth = ImGui.CalcTextSize(pageLabel).X;
|
|
|
|
float totalWidth = prevWidth + textWidth + nextWidth + style.ItemSpacing.X * 2;
|
|
|
|
float availWidth = ImGui.GetContentRegionAvail().X;
|
|
float offsetX = (availWidth - totalWidth) * 0.5f;
|
|
|
|
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + offsetX);
|
|
|
|
if (ImGui.Button("<##PrevSyncshellPage") && _syncshellPageIndex > 0)
|
|
_syncshellPageIndex--;
|
|
|
|
ImGui.SameLine();
|
|
ImGui.Text(pageLabel);
|
|
|
|
ImGui.SameLine();
|
|
if (ImGui.Button(">##NextSyncshellPage") && _syncshellPageIndex < totalPages - 1)
|
|
_syncshellPageIndex++;
|
|
}
|
|
}
|
|
|
|
private void DrawJoinButton(GroupJoinDto shell, bool isSelfBroadcast)
|
|
{
|
|
const string visibleLabel = "Join";
|
|
var label = $"{visibleLabel}##{shell.Group.GID}";
|
|
|
|
var isAlreadyMember = _currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal));
|
|
var isRecentlyJoined = _recentlyJoined.Contains(shell.GID);
|
|
|
|
Vector2 buttonSize;
|
|
|
|
if (!_compactView)
|
|
{
|
|
var style = ImGui.GetStyle();
|
|
var textSize = ImGui.CalcTextSize(visibleLabel);
|
|
|
|
var width = textSize.X + style.FramePadding.X * 20f;
|
|
buttonSize = new Vector2(width, 30f);
|
|
|
|
float availX = ImGui.GetContentRegionAvail().X;
|
|
float curX = ImGui.GetCursorPosX();
|
|
float newX = curX + (availX - buttonSize.X);
|
|
ImGui.SetCursorPosX(newX);
|
|
}
|
|
else
|
|
{
|
|
buttonSize = new Vector2(-1, 0);
|
|
}
|
|
|
|
if (!isAlreadyMember && !isRecentlyJoined && !isSelfBroadcast)
|
|
{
|
|
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))
|
|
{
|
|
_logger.LogInformation($"Join requested for Syncshell {shell.Group.GID} ({shell.Group.Alias})");
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
var info = await _apiController.GroupJoinHashed(new GroupJoinHashedDto(
|
|
shell.Group,
|
|
shell.Password,
|
|
shell.GroupUserPreferredPermissions
|
|
)).ConfigureAwait(false);
|
|
|
|
if (info != null && info.Success)
|
|
{
|
|
_joinDto = new GroupJoinDto(shell.Group, shell.Password, shell.GroupUserPreferredPermissions);
|
|
_joinInfo = info;
|
|
_ownPermissions = _apiController.DefaultPermissions.DeepClone()!;
|
|
|
|
_logger.LogInformation($"Fetched join info for {shell.Group.GID}");
|
|
}
|
|
else
|
|
{
|
|
_logger.LogWarning($"Failed to join {shell.Group.GID}: info was null or unsuccessful");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, $"Join failed for {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(isSelfBroadcast
|
|
? "This is your own Syncshell."
|
|
: "Already a member or owner of this Syncshell.");
|
|
}
|
|
|
|
ImGui.PopStyleColor(3);
|
|
}
|
|
|
|
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} for profile tags", iconId);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void DrawConfirmation()
|
|
{
|
|
if (_joinDto != null && _joinInfo != null)
|
|
{
|
|
ImGui.Separator();
|
|
ImGui.TextUnformatted($"Join Syncshell: {_joinDto.Group.AliasOrGID} by {_joinInfo.OwnerAliasOrUID}");
|
|
ImGuiHelpers.ScaledDummy(2f);
|
|
ImGui.TextUnformatted("Suggested Syncshell Permissions:");
|
|
|
|
DrawPermissionRow("Sounds", _joinInfo.GroupPermissions.IsPreferDisableSounds(), _ownPermissions.DisableGroupSounds, v => _ownPermissions.DisableGroupSounds = v);
|
|
DrawPermissionRow("Animations", _joinInfo.GroupPermissions.IsPreferDisableAnimations(), _ownPermissions.DisableGroupAnimations, v => _ownPermissions.DisableGroupAnimations = v);
|
|
DrawPermissionRow("VFX", _joinInfo.GroupPermissions.IsPreferDisableVFX(), _ownPermissions.DisableGroupVFX, v => _ownPermissions.DisableGroupVFX = v);
|
|
|
|
ImGui.NewLine();
|
|
ImGui.NewLine();
|
|
|
|
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Plus, $"Finalize and join {_joinDto.Group.AliasOrGID}"))
|
|
{
|
|
var finalPermissions = GroupUserPreferredPermissions.NoneSet;
|
|
finalPermissions.SetDisableSounds(_ownPermissions.DisableGroupSounds);
|
|
finalPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations);
|
|
finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX);
|
|
|
|
_ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions));
|
|
|
|
_recentlyJoined.Add(_joinDto.Group.GID);
|
|
|
|
_joinDto = null;
|
|
_joinInfo = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawPermissionRow(string label, bool suggested, bool current, Action<bool> apply)
|
|
{
|
|
ImGui.AlignTextToFramePadding();
|
|
ImGui.TextUnformatted($"- {label}");
|
|
|
|
ImGui.SameLine(150 * ImGuiHelpers.GlobalScale);
|
|
ImGui.TextUnformatted("Current:");
|
|
ImGui.SameLine();
|
|
_uiSharedService.BooleanToColoredIcon(!current);
|
|
|
|
ImGui.SameLine(300 * ImGuiHelpers.GlobalScale);
|
|
ImGui.TextUnformatted("Suggested:");
|
|
ImGui.SameLine();
|
|
_uiSharedService.BooleanToColoredIcon(!suggested);
|
|
|
|
ImGui.SameLine(450 * ImGuiHelpers.GlobalScale);
|
|
using var id = ImRaii.PushId(label);
|
|
if (current != suggested)
|
|
{
|
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Apply"))
|
|
apply(suggested);
|
|
}
|
|
|
|
ImGui.NewLine();
|
|
}
|
|
|
|
private async Task RefreshSyncshellsAsync(string? gid = null)
|
|
{
|
|
var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts();
|
|
var snapshot = _pairUiService.GetSnapshot();
|
|
_currentSyncshells = [.. snapshot.GroupPairs.Keys];
|
|
|
|
_recentlyJoined.RemoveWhere(gid =>
|
|
_currentSyncshells.Exists(s => string.Equals(s.GID, gid, StringComparison.Ordinal)));
|
|
|
|
List<GroupJoinDto>? updatedList = [];
|
|
|
|
if (_useTestSyncshells)
|
|
{
|
|
updatedList = BuildTestSyncshells();
|
|
}
|
|
else
|
|
{
|
|
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();
|
|
}
|
|
|
|
private static List<GroupJoinDto> BuildTestSyncshells()
|
|
{
|
|
var testGroup1 = new GroupData("TEST-ALPHA", "Alpha Shell");
|
|
var testGroup2 = new GroupData("TEST-BETA", "Beta Shell");
|
|
var testGroup3 = new GroupData("TEST-GAMMA", "Gamma Shell");
|
|
var testGroup4 = new GroupData("TEST-DELTA", "Delta Shell");
|
|
var testGroup5 = new GroupData("TEST-CHARLIE", "Charlie Shell");
|
|
var testGroup6 = new GroupData("TEST-OMEGA", "Omega Shell");
|
|
var testGroup7 = new GroupData("TEST-POINT", "Point Shell");
|
|
var testGroup8 = new GroupData("TEST-HOTEL", "Hotel Shell");
|
|
|
|
return
|
|
[
|
|
new(testGroup1, "", GroupUserPreferredPermissions.NoneSet),
|
|
new(testGroup2, "", GroupUserPreferredPermissions.NoneSet),
|
|
new(testGroup3, "", GroupUserPreferredPermissions.NoneSet),
|
|
new(testGroup4, "", GroupUserPreferredPermissions.NoneSet),
|
|
new(testGroup5, "", GroupUserPreferredPermissions.NoneSet),
|
|
new(testGroup6, "", GroupUserPreferredPermissions.NoneSet),
|
|
new(testGroup7, "", GroupUserPreferredPermissions.NoneSet),
|
|
new(testGroup8, "", GroupUserPreferredPermissions.NoneSet),
|
|
];
|
|
}
|
|
|
|
private void ClearSyncshells()
|
|
{
|
|
if (_nearbySyncshells.Count == 0)
|
|
return;
|
|
|
|
_nearbySyncshells.Clear();
|
|
ClearSelection();
|
|
}
|
|
|
|
private void ClearSelection()
|
|
{
|
|
_selectedNearbyIndex = -1;
|
|
_syncshellPageIndex = 0;
|
|
_joinDto = null;
|
|
_joinInfo = null;
|
|
}
|
|
|
|
private string? GetSelectedGid()
|
|
{
|
|
if (_selectedNearbyIndex < 0 || _selectedNearbyIndex >= _nearbySyncshells.Count)
|
|
return null;
|
|
|
|
return _nearbySyncshells[_selectedNearbyIndex].Group.GID;
|
|
}
|
|
|
|
}
|