diff --git a/LightlessSync/Changelog/changelog.yaml b/LightlessSync/Changelog/changelog.yaml index 43b0c79..18a0a8c 100644 --- a/LightlessSync/Changelog/changelog.yaml +++ b/LightlessSync/Changelog/changelog.yaml @@ -1,11 +1,80 @@ -tagline: "Lightless Sync v1.12.4" -subline: "Bugfixes and various improvements across Lightless" +tagline: "Lightless Sync v2.0.0" +subline: "LIGHTLESS IS EVOLVING!!" changelog: + - name: "v2.0.0" + tagline: "Thank you for 4 months!" + date: "December 2025" + # be sure to set this every new version + isCurrent: true + versions: + - number: "Lightless Chat" + icon: "" + items: + - "Chat has been added to the top of the main UI. It will work in certain Zones or in Syncshells!" + - "You will only be able to use the chat feature after enabling it and accepting the rules. If you're not interested, don't use it!" + - "Breaking the rules may result in a mute or ban from chat. Serious offenses may result in a ban from the Lightless service altogether." + - "You can right click the offender in the chat and report them within the chat, reports will be reviewed asap." + - "Syncshells can enforce their own chat rules and moderate their own chat. This however does not apply to serious offenses." + - "Your name in chat will not be shown unless you are paired with the person OR you are in the same syncshell. Otherwise, you will be anonymous." + - "Refer to #release-notes in the Discord for more information. Feel free to ask questions in the Discord as well." + - number: "Changes to LightFinder" + icon: "" + items: + - "We have recieve quite a bit of reports of users crashing due to how Nameplates are handled across various plugins. As a result, we have moved the LightFinder icon and text to Imgui." + - "This should resolve the crashing issues, however, it may not look as nice as before. We are looking into ways to improve the Imgui experience in the future." + - "We will always prioritize stability and safety over visuals." + - "Refer to #release-notes in the Discord for an example of the error." + - number: "User Profiles, ShellFinder, Syncshells, Syncshell Profiles" + icon: "" + items: + - "Both User Profiles and Syncshell Profiles have been revamped for 2.0.0." + - "We have added profile tags to both Users and Syncshells that will show when a profile is being viewed" + - "Syncshell Admin Panel has been reworked to make it a friendlier experience" + - "Syncshell Moderators can now also broadcast on ShellFinder" + - "ShellFinder has been revamped to be more visually friends and also show more information (Tags) about the Syncshell" + - "Syncshells has an auto-prune feature now that will remove inactive members after a set amount of time, options available are 1, 3, 7, and 14 days that runs in 1 hour intervals" + - "IF YOUR SYNCSHELL IS NSFW, PLEASE MARK IT AS NSFW!" + - "Refer to #release-notes in the Discord for pretty pictures or try it yourself!." + - number: "Texture Optimization" + icon: "" + items: + - "In 2.0.0, we've added the option for Texture Optimization to improve the performance of scenarios such as overwhelmingly big " + - "NOTE: ALL OF THESE ARE OPTIONAL AND DISABLED BY DEFAULT" + - "Within Texture Optimization, you will be able to safely downscale all textures of new downloads around you." + - "This downscale DOES NOT APPLY to DIRECT PAIRS or those who've updated their preferred settings to not be downscaled" + - "The first time this is enabled, you may experience some lag or frame drops, but in the long run, it will help performance." + - "This can be found in Lightless Settings > Performance > Texture Optimization" + - "Like a broken record, please refer to #release-notes in the Discord for more information." + - number: "Character Analysis - The big scary UI no one knew about" + icon: "" + items: + - "We have made the Character Analysis UI more user friendly. This includes a revamp of the look and functionality" + - "You can now see more information about your character and how it affects performance" + - "It will show you the Textures tab by default with an option for \"Other file types\"" + - "You can now choose if you want to BC7/BC5/BC4/BC3/BC1 compress a certain texture." + - "The UI will give you a recommendation on what BC compression to use based on the file." + - "Shows a small preview of what the texture looks like with some general info about it." + - "Shows you how much VRAM you would take up." + - "This can be found in Lightless Settings > Performance > Character Analysis" + - number: "Performance" + icon: "" + items: + - "Moved to the internal object table to have improved overall plugin performance." + - "Compactor is now running on a multi-threaded level instead of single-threaded; This should increase the speed of compacting files." + - "Penumbra Collections are now only made when people are visible, reducing the load on boot-up when having many Syncshells in your list." + - "Pairing system has been revamped to make pausing and unpausing faster, and loading people should be faster as well." + - number: "Miscellaneous Changes and Bugfixes" + icon: "" + items: + - "UI has been updated to look more modern" + - "We have started on file compression for Linux with the option for BTRFS or ZFS but it's not very great yet and will release later." + - "Nameplate colours now use sigs to client structs as an alternative to the Nameplate Handler, also preventing crashes on that from our end." + - "Notifications now work with the \"Enable multi-monitor windows\" settings of Dalamud." + - "Fixed a bug where nothing above the notifications was clickable in certain cases." + - "Added a check that prevents small messages from going below 0 resulting in an ArgumentOutOfRangeException." - name: "v1.12.4" tagline: "Preparation for future features" date: "November 11th 2025" - # be sure to set this every new version - isCurrent: true versions: - number: "Syncshells" icon: "" diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index 8930cb6..b46cccc 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -3,7 +3,7 @@ - 1.12.4 + 2.0.0 https://github.com/Light-Public-Syncshells/LightlessClient diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 2cf8bdf..d8e5ee7 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -467,7 +467,8 @@ public sealed class Plugin : IDalamudPlugin sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), - sp.GetRequiredService())); + sp.GetRequiredService(), + sp.GetRequiredService())); services.AddScoped(); services.AddScoped(); diff --git a/LightlessSync/Services/Chat/ZoneChatService.cs b/LightlessSync/Services/Chat/ZoneChatService.cs index edb3a86..8e86b49 100644 --- a/LightlessSync/Services/Chat/ZoneChatService.cs +++ b/LightlessSync/Services/Chat/ZoneChatService.cs @@ -12,11 +12,11 @@ namespace LightlessSync.Services.Chat; public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedService { private const int MaxMessageHistory = 150; - internal const int MaxOutgoingLength = 400; + internal const int MaxOutgoingLength = 200; private const int MaxUnreadCount = 999; private const string ZoneUnavailableMessage = "Zone chat is only available in major cities."; private const string ZoneChannelKey = "zone"; - private const int MaxReportReasonLength = 500; + private const int MaxReportReasonLength = 100; private const int MaxReportContextLength = 1000; private readonly ApiController _apiController; diff --git a/LightlessSync/Services/ContextMenuService.cs b/LightlessSync/Services/ContextMenuService.cs index 740f52b..53bbb45 100644 --- a/LightlessSync/Services/ContextMenuService.cs +++ b/LightlessSync/Services/ContextMenuService.cs @@ -218,7 +218,7 @@ internal class ContextMenuService : IHostedService return; } - var senderCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetBlake3Hash(); + var senderCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address); _logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid); diff --git a/LightlessSync/UI/DownloadUi.cs b/LightlessSync/UI/DownloadUi.cs index c54cf46..b960b46 100644 --- a/LightlessSync/UI/DownloadUi.cs +++ b/LightlessSync/UI/DownloadUi.cs @@ -560,9 +560,12 @@ public class DownloadUi : WindowMediatorSubscriberBase { foreach (var p in perPlayer) { - boxHeight += lineHeight + spacingY; + boxHeight += lineHeight + spacingY; - if (_configService.Current.ShowPlayerSpeedBarsTransferWindow && p.DlProg > 0) + var showBar = _configService.Current.ShowPlayerSpeedBarsTransferWindow + && p.TransferredBytes > 0; + + if (showBar) { boxHeight += perPlayerBarHeight + spacingY; } @@ -630,46 +633,23 @@ public class DownloadUi : WindowMediatorSubscriberBase ); cursor.Y += lineHeight * 1.4f + spacingY; - if (_configService.Current.ShowPlayerLinesTransferWindow) + var orderedPlayers = perPlayer.OrderByDescending(p => p.TotalBytes).ToList(); + + foreach (var p in orderedPlayers) { - var orderedPlayers = perPlayer.OrderByDescending(p => p.TotalBytes).ToList(); + var hasSpeed = p.SpeedBytesPerSecond > 0; + var playerSpeedText = hasSpeed + ? $"{UiSharedService.ByteToString((long)p.SpeedBytesPerSecond)}/s" + : "-"; - foreach (var p in orderedPlayers) + var showBar = _configService.Current.ShowPlayerSpeedBarsTransferWindow + && p.TransferredBytes > 0; + + var labelLine = + $"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}] {p.TransferredFiles}/{p.TotalFiles}"; + + if (!showBar) { - var hasSpeed = p.SpeedBytesPerSecond > 0; - var playerSpeedText = hasSpeed - ? $"{UiSharedService.ByteToString((long)p.SpeedBytesPerSecond)}/s" - : "-"; - - // Label line for the player - var labelLine = - $"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}] {p.TransferredFiles}/{p.TotalFiles}"; - - // State flags - var isDownloading = p.DlProg > 0; - var isDecompressing = p.DlDecomp > 0 - || (!isDownloading && p.TotalBytes > 0 && p.TransferredBytes >= p.TotalBytes); - - - var showBar = _configService.Current.ShowPlayerSpeedBarsTransferWindow - && (isDownloading || isDecompressing); - - if (!showBar) - { - UiSharedService.DrawOutlinedFont( - drawList, - labelLine, - cursor, - UiSharedService.Color(255, 255, 255, _transferBoxTransparency), - UiSharedService.Color(0, 0, 0, _transferBoxTransparency), - 1 - ); - - cursor.Y += lineHeight + spacingY; - continue; - } - - // Top label line (only name + W/Q/P/D + files) UiSharedService.DrawOutlinedFont( drawList, labelLine, @@ -678,82 +658,90 @@ public class DownloadUi : WindowMediatorSubscriberBase UiSharedService.Color(0, 0, 0, _transferBoxTransparency), 1 ); + cursor.Y += lineHeight + spacingY; - - // Bar background - var barBgMin = new Vector2(boxMin.X + padding, cursor.Y); - var barBgMax = new Vector2(boxMax.X - padding, cursor.Y + perPlayerBarHeight); - - drawList.AddRectFilled( - barBgMin, - barBgMax, - UiSharedService.Color(40, 40, 40, _transferBoxTransparency), - 3f - ); - - float ratio = 0f; - if (isDownloading && p.TotalBytes > 0) - { - ratio = (float)p.TransferredBytes / p.TotalBytes; - } - else if (isDecompressing) - { - ratio = 1f; - } - - if (ratio < 0f) ratio = 0f; - if (ratio > 1f) ratio = 1f; - - var fillX = barBgMin.X + (barBgMax.X - barBgMin.X) * ratio; - var barFillMax = new Vector2(fillX, barBgMax.Y); - - drawList.AddRectFilled( - barBgMin, - barFillMax, - UiSharedService.Color(UIColors.Get("LightlessPurple")), - 3f - ); - - string barText; - - if (isDownloading) - { - var bytesInside = - $"{UiSharedService.ByteToString(p.TransferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(p.TotalBytes)}"; - - barText = hasSpeed - ? $"{bytesInside} @ {playerSpeedText}" - : bytesInside; - } - else if (isDecompressing) - { - barText = "Decompressing..."; - } - else - { - barText = string.Empty; - } - - if (!string.IsNullOrEmpty(barText)) - { - var barTextSize = ImGui.CalcTextSize(barText); - var barTextPos = new Vector2( - barBgMin.X + ((barBgMax.X - barBgMin.X) - barTextSize.X) / 2f - 1, - barBgMin.Y + ((perPlayerBarHeight - barTextSize.Y) / 2f) - 1 - ); - - UiSharedService.DrawOutlinedFont( - drawList, - barText, - barTextPos, - UiSharedService.Color(255, 255, 255, _transferBoxTransparency), - UiSharedService.Color(0, 0, 0, _transferBoxTransparency), - 1 - ); - } - - cursor.Y += perPlayerBarHeight + spacingY; + continue; } + + UiSharedService.DrawOutlinedFont( + drawList, + labelLine, + cursor, + UiSharedService.Color(255, 255, 255, _transferBoxTransparency), + UiSharedService.Color(0, 0, 0, _transferBoxTransparency), + 1 + ); + cursor.Y += lineHeight + spacingY; + + // Bar background + var barBgMin = new Vector2(boxMin.X + padding, cursor.Y); + var barBgMax = new Vector2(boxMax.X - padding, cursor.Y + perPlayerBarHeight); + + drawList.AddRectFilled( + barBgMin, + barBgMax, + UiSharedService.Color(40, 40, 40, _transferBoxTransparency), + 3f + ); + + // Fill based on Progress of download + float ratio = 0f; + if (p.TotalBytes > 0) + ratio = (float)p.TransferredBytes / p.TotalBytes; + + if (ratio < 0f) ratio = 0f; + if (ratio > 1f) ratio = 1f; + + var fillX = barBgMin.X + (barBgMax.X - barBgMin.X) * ratio; + var barFillMax = new Vector2(fillX, barBgMax.Y); + + drawList.AddRectFilled( + barBgMin, + barFillMax, + UiSharedService.Color(UIColors.Get("LightlessPurple")), + 3f + ); + + // Text inside bar: downloading vs decompressing + string barText; + + var isDecompressing = p.DlDecomp > 0 && p.TransferredBytes >= p.TotalBytes && p.TotalBytes > 0; + + if (isDecompressing) + { + // Keep bar full, static text showing decompressing + barText = "Decompressing..."; + } + else + { + var bytesInside = + $"{UiSharedService.ByteToString(p.TransferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(p.TotalBytes)}"; + + barText = hasSpeed + ? $"{bytesInside} @ {playerSpeedText}" + : bytesInside; + } + + if (!string.IsNullOrEmpty(barText)) + { + var barTextSize = ImGui.CalcTextSize(barText); + + var barTextPos = new Vector2( + barBgMin.X + ((barBgMax.X - barBgMin.X) - barTextSize.X) / 2f - 1, + barBgMin.Y + ((perPlayerBarHeight - barTextSize.Y) / 2f) - 1 + ); + + UiSharedService.DrawOutlinedFont( + drawList, + barText, + barTextPos, + UiSharedService.Color(255, 255, 255, _transferBoxTransparency), + UiSharedService.Color(0, 0, 0, _transferBoxTransparency), + 1 + ); + } + + cursor.Y += perPlayerBarHeight + spacingY; } } diff --git a/LightlessSync/UI/LightFinderUI.cs b/LightlessSync/UI/LightFinderUI.cs index fa88475..ca74bc9 100644 --- a/LightlessSync/UI/LightFinderUI.cs +++ b/LightlessSync/UI/LightFinderUI.cs @@ -2,6 +2,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; +using Dalamud.Utility; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; using LightlessSync.LightlessConfiguration; diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index 7374203..2f215a1 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -1,21 +1,24 @@ 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 LightlessSync.UI.Services; using Microsoft.Extensions.Logging; using System.Numerics; -using LightlessSync.Services.LightFinder; namespace LightlessSync.UI; @@ -28,6 +31,10 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase 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 _seResolvedSegments = new(); private readonly List _nearbySyncshells = []; private List _currentSyncshells = []; private int _selectedNearbyIndex = -1; @@ -40,6 +47,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase private bool _useTestSyncshells = false; private bool _compactView = false; + private readonly LightlessProfileManager _lightlessProfileManager; public SyncshellFinderUI( ILogger logger, @@ -50,7 +58,8 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase ApiController apiController, LightFinderScannerService broadcastScannerService, PairUiService pairUiService, - DalamudUtilService dalamudUtilService) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService) + DalamudUtilService dalamudUtilService, + LightlessProfileManager lightlessProfileManager) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService) { _broadcastService = broadcastService; _uiSharedService = uiShared; @@ -58,11 +67,12 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase _broadcastScannerService = broadcastScannerService; _pairUiService = pairUiService; _dalamudUtilService = dalamudUtilService; + _lightlessProfileManager = lightlessProfileManager; IsOpen = false; WindowBuilder.For(this) - .SetSizeConstraints(new Vector2(600, 400), new Vector2(600, 550)) - .Apply(); + .SetSizeConstraints(new Vector2(600, 400), new Vector2(600, 550)) + .Apply(); Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false)); Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false)); @@ -80,7 +90,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase { ImGui.BeginGroup(); _uiSharedService.MediumText("Nearby Syncshells", UIColors.Get("LightlessPurple")); - + #if DEBUG if (ImGui.SmallButton("Show test syncshells")) { @@ -92,7 +102,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase string checkboxLabel = "Compact view"; float availWidth = ImGui.GetContentRegionAvail().X; - float checkboxWidth = ImGui.CalcTextSize(checkboxLabel).X + ImGui.GetFrameHeight(); + float checkboxWidth = ImGui.CalcTextSize(checkboxLabel).X + ImGui.GetFrameHeight(); float rightX = ImGui.GetCursorPosX() + availWidth - checkboxWidth - 4.0f; ImGui.SetCursorPosX(rightX); @@ -130,20 +140,34 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase return; } + string? myHashedCid = null; + try + { + var cid = _dalamudUtilService.GetCID(); + myHashedCid = cid.ToString().GetHash256(); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to get CID, not excluding own broadcast."); + } + var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().Where(b => !string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal)).ToList() ?? []; + var cardData = new List<(GroupJoinDto Shell, string BroadcasterName)>(); - var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts(); 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 = $"Tester of {displayName}"; + broadcasterName = $"{displayName} (Tester of TestWorld)"; } else { @@ -206,7 +230,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase var (shell, broadcasterName) = listData[index]; ImGui.PushID(shell.Group.GID); - float rowHeight = 90f * ImGuiHelpers.GlobalScale; + float rowHeight = 74f * ImGuiHelpers.GlobalScale; ImGui.BeginChild($"ShellRow##{shell.Group.GID}", new Vector2(-1, rowHeight), border: true); @@ -234,10 +258,48 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault")); - ImGui.Dummy(new Vector2(0, 6 * ImGuiHelpers.GlobalScale)); + var groupProfile = _lightlessProfileManager.GetLightlessGroupProfile(shell.Group); + IReadOnlyList 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); + float btnHeight = ImGui.GetFrameHeightWithSpacing(); + float rowHeightUsed = MathF.Max(tagsHeight, btnHeight); + + ImGui.SetCursorPos(new Vector2( + rowStartLocal.X, + rowStartLocal.Y + rowHeightUsed)); + ImGui.EndChild(); ImGui.PopID(); @@ -311,10 +373,39 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase ImGui.SetTooltip("Broadcaster of the syncshell."); ImGui.EndGroup(); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault")); ImGui.Dummy(new Vector2(0, 6 * ImGuiHelpers.GlobalScale)); + var groupProfile = _lightlessProfileManager.GetLightlessGroupProfile(shell.Group); + + IReadOnlyList 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) @@ -338,7 +429,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase { if (totalPages > 1) { - UiSharedService.ColoredSeparator(UIColors.Get("PairBlue")); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault")); var style = ImGui.GetStyle(); string pageLabel = $"Page {_syncshellPageIndex + 1}/{totalPages}"; @@ -371,10 +462,6 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase const string visibleLabel = "Join"; var label = $"{visibleLabel}##{shell.Group.GID}"; - 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)); - var isAlreadyMember = _currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal)); var isRecentlyJoined = _recentlyJoined.Contains(shell.GID); @@ -386,7 +473,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase var textSize = ImGui.CalcTextSize(visibleLabel); var width = textSize.X + style.FramePadding.X * 20f; - buttonSize = new Vector2(width, 0); + buttonSize = new Vector2(width, 30f); float availX = ImGui.GetContentRegionAvail().X; float curX = ImGui.GetCursorPosX(); @@ -400,6 +487,9 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase 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)) { _logger.LogInformation($"Join requested for Syncshell {shell.Group.GID} ({shell.Group.Alias})"); @@ -436,6 +526,10 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase } 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); @@ -446,6 +540,72 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase ImGui.PopStyleColor(3); } + + private (float widthUsed, float rowHeight) RenderProfileTagsSingleRow(IReadOnlyList 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 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) @@ -470,9 +630,9 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX); _ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions)); - + _recentlyJoined.Add(_joinDto.Group.GID); - + _joinDto = null; _joinInfo = null; } diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index 16f3ea0..cc69a5d 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -1,19 +1,20 @@ -using System; using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; +using Dalamud.Plugin.Services; using Dalamud.Utility; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.LightlessConfiguration.Models; using LightlessSync.Services; -using LightlessSync.Services.Mediator; using LightlessSync.Services.LightFinder; -using LightlessSync.Utils; +using LightlessSync.Services.Mediator; using LightlessSync.UI.Models; using LightlessSync.UI.Style; +using LightlessSync.Utils; using LightlessSync.WebAPI; +using System; using System.Numerics; namespace LightlessSync.UI; @@ -799,9 +800,22 @@ public class TopTabMenu if (!_lightFinderService.IsBroadcasting) return "Syncshell Finder"; + string? myHashedCid = null; + try + { + var cid = _dalamudUtilService.GetCID(); + myHashedCid = cid.ToString().GetHash256(); + } + catch (Exception) + { + // Couldnt get own CID, log and return default table + } + var nearbyCount = _lightFinderScannerService .GetActiveSyncshellBroadcasts() - .Where(b => !string.IsNullOrEmpty(b.GID)) + .Where(b => + !string.IsNullOrEmpty(b.GID) && + !string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal)) .Select(b => b.GID!) .Distinct(StringComparer.Ordinal) .Count();