using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using LightlessSync.API.Data.Enum; using LightlessSync.API.Dto; using LightlessSync.API.Dto.Group; using LightlessSync.LightlessConfiguration; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Utils; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; using LightlessSync.API.Data.Extensions; using System.Numerics; namespace LightlessSync.UI; public class SyncshellFinderUI : WindowMediatorSubscriberBase { private readonly ApiController _apiController; private readonly LightlessConfigService _configService; private readonly BroadcastService _broadcastService; private readonly UiSharedService _uiSharedService; private readonly NameplateService _nameplateService; private readonly List _nearbySyncshells = new(); private int _selectedNearbyIndex = -1; private GroupJoinDto? _joinDto; private GroupJoinInfoDto? _joinInfo; private DefaultPermissionsDto _ownPermissions = null!; public SyncshellFinderUI( ILogger logger, LightlessMediator mediator, PerformanceCollectorService performanceCollectorService, BroadcastService broadcastService, LightlessConfigService configService, UiSharedService uiShared, ApiController apiController, NameplateService nameplateService ) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService) { _broadcastService = broadcastService; _uiSharedService = uiShared; _configService = configService; _apiController = apiController; _nameplateService = nameplateService; IsOpen = false; SizeConstraints = new() { MinimumSize = new(600, 400), MaximumSize = new(600, 400) }; Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync()); } public override async void OnOpen() { _ownPermissions = _apiController.DefaultPermissions.DeepClone()!; await RefreshSyncshellsAsync(); } protected override void DrawInternal() { _uiSharedService.MediumText("Nearby Syncshells", UIColors.Get("PairBlue")); _uiSharedService.ColoredSeparator(UIColors.Get("PairBlue")); 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("LightlessYellow2")); if (ImGui.Button("Open Lightfinder", new Vector2(200 * ImGuiHelpers.GlobalScale, 0))) { Mediator.Publish(new UiToggleMessage(typeof(BroadcastUI))); } ImGui.PopStyleColor(); ImGui.PopStyleVar(); return; } return; } if (ImGui.BeginTable("##NearbySyncshellsTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg)) { ImGui.TableSetupColumn("Alias", ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn("GID", ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn("Join", ImGuiTableColumnFlags.WidthFixed, 80f * ImGuiHelpers.GlobalScale); ImGui.TableHeadersRow(); for (int i = 0; i < _nearbySyncshells.Count; i++) { var shell = _nearbySyncshells[i]; ImGui.TableNextRow(); ImGui.TableNextColumn(); ImGui.TextUnformatted(shell.Group.Alias ?? "(No Alias)"); ImGui.TableNextColumn(); ImGui.TextUnformatted(shell.Group.GID); ImGui.TableNextColumn(); var label = $"Join##{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)); if (ImGui.Button(label)) { _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}"); } }); } ImGui.PopStyleColor(3); } ImGui.EndTable(); } if (_joinDto != null && _joinInfo != null && _joinInfo.Success) DrawConfirmation(); } private void DrawConfirmation() { 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)); _joinDto = null; _joinInfo = null; } } private void DrawPermissionRow(string label, bool suggested, bool current, Action 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() { var syncshellBroadcasts = _nameplateService.GetActiveSyncshellBroadcasts(); if (syncshellBroadcasts.Count == 0) { ClearSyncshells(); return; } List updatedList; try { var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts); updatedList = groups?.ToList() ?? new(); } catch (Exception ex) { _logger.LogError(ex, "Failed to refresh broadcasted syncshells."); return; } var currentGids = _nearbySyncshells.Select(s => s.Group.GID).ToHashSet(); var newGids = updatedList.Select(s => s.Group.GID).ToHashSet(); if (currentGids.SetEquals(newGids)) return; var previousGid = GetSelectedGid(); _nearbySyncshells.Clear(); _nearbySyncshells.AddRange(updatedList); if (previousGid != null) { var newIndex = _nearbySyncshells.FindIndex(s => s.Group.GID == previousGid); if (newIndex >= 0) { _selectedNearbyIndex = newIndex; return; } } ClearSelection(); } private void ClearSyncshells() { if (_nearbySyncshells.Count == 0) return; _nearbySyncshells.Clear(); ClearSelection(); } private void ClearSelection() { _selectedNearbyIndex = -1; _joinDto = null; _joinInfo = null; } private string? GetSelectedGid() { if (_selectedNearbyIndex < 0 || _selectedNearbyIndex >= _nearbySyncshells.Count) return null; return _nearbySyncshells[_selectedNearbyIndex].Group.GID; } protected override void Dispose(bool disposing) { base.Dispose(disposing); } }