2.1.0 (#123)
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m9s
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
This commit was merged in pull request #123.
This commit is contained in:
@@ -1,855 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user