diff --git a/LightlessSync/UI/LightFinderUI.cs b/LightlessSync/UI/LightFinderUI.cs index ed55d4f..9cb7891 100644 --- a/LightlessSync/UI/LightFinderUI.cs +++ b/LightlessSync/UI/LightFinderUI.cs @@ -58,6 +58,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase private List _currentSyncshells = []; private GroupJoinDto? _joinDto; private GroupJoinInfoDto? _joinInfo; + private bool _joinModalOpen = true; private readonly List _nearbySyncshells = []; private DefaultPermissionsDto _ownPermissions = null!; private readonly HashSet _recentlyJoined = new(StringComparer.Ordinal); @@ -66,6 +67,11 @@ public class LightFinderUI : WindowMediatorSubscriberBase private LightfinderTab _selectedTab = LightfinderTab.NearbySyncshells; private string _userUid = string.Empty; + private const float AnimationSpeed = 6f; + private readonly Dictionary _itemAlpha = new(StringComparer.Ordinal); + private readonly HashSet _currentVisibleItems = new(StringComparer.Ordinal); + private readonly HashSet _previousVisibleItems = new(StringComparer.Ordinal); + private enum LightfinderTab { NearbySyncshells, NearbyPlayers, BroadcastSettings, Help } #if DEBUG @@ -106,14 +112,14 @@ public class LightFinderUI : WindowMediatorSubscriberBase _lightlessProfileManager = lightlessProfileManager; _actorObjectService = actorObjectService; - _animatedHeader.Height = 100f; + _animatedHeader.Height = 85f; _animatedHeader.EnableBottomGradient = true; - _animatedHeader.GradientHeight = 120f; + _animatedHeader.GradientHeight = 90f; _animatedHeader.EnableParticles = _configService.Current.EnableParticleEffects; IsOpen = false; WindowBuilder.For(this) - .SetSizeConstraints(new Vector2(620, 85), new Vector2(700, 600)) + .SetSizeConstraints(new Vector2(620, 85), new Vector2(700, 2000)) .Apply(); Mediator.Subscribe(this, async _ => await RefreshNearbySyncshellsAsync().ConfigureAwait(false)); @@ -367,6 +373,52 @@ public class LightFinderUI : WindowMediatorSubscriberBase #endregion + #region Animation Helpers + + private void UpdateItemAnimations(IEnumerable visibleItemIds) + { + var deltaTime = ImGui.GetIO().DeltaTime; + + _previousVisibleItems.Clear(); + foreach (var id in _currentVisibleItems) + _previousVisibleItems.Add(id); + + _currentVisibleItems.Clear(); + foreach (var id in visibleItemIds) + _currentVisibleItems.Add(id); + + // Fade in new items + foreach (var id in _currentVisibleItems) + { + if (!_itemAlpha.ContainsKey(id)) + _itemAlpha[id] = 0f; + + _itemAlpha[id] = Math.Min(1f, _itemAlpha[id] + deltaTime * AnimationSpeed); + } + + // Fade out removed items + var toRemove = new List(); + foreach (var (id, alpha) in _itemAlpha) + { + if (!_currentVisibleItems.Contains(id)) + { + _itemAlpha[id] = Math.Max(0f, alpha - deltaTime * AnimationSpeed); + if (_itemAlpha[id] <= 0.01f) + toRemove.Add(id); + } + } + + foreach (var id in toRemove) + _itemAlpha.Remove(id); + } + + private float GetItemAlpha(string itemId) + { + return _itemAlpha.TryGetValue(itemId, out var alpha) ? alpha : 1f; + } + + #endregion + #region Nearby Syncshells Tab private void DrawNearbySyncshellsTab() @@ -400,10 +452,14 @@ public class LightFinderUI : WindowMediatorSubscriberBase var cardData = BuildSyncshellCardData(); if (cardData.Count == 0) { + UpdateItemAnimations([]); ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells found."); return; } + // Update animations for syncshell items + UpdateItemAnimations(cardData.Select(c => $"shell_{c.Shell.Group.GID}")); + if (_compactView) DrawSyncshellGrid(cardData); else @@ -499,7 +555,13 @@ public class LightFinderUI : WindowMediatorSubscriberBase private void DrawSyncshellListItem(GroupJoinDto shell, string broadcasterName, bool isOwnBroadcast) { + var itemId = $"shell_{shell.Group.GID}"; + var alpha = GetItemAlpha(itemId); + if (alpha <= 0.01f) + return; + ImGui.PushID(shell.Group.GID); + using var alphaStyle = ImRaii.PushStyle(ImGuiStyleVar.Alpha, alpha); float rowHeight = 74f * ImGuiHelpers.GlobalScale; ImGui.BeginChild($"ShellRow##{shell.Group.GID}", new Vector2(-1, rowHeight), border: true); @@ -572,7 +634,13 @@ public class LightFinderUI : WindowMediatorSubscriberBase private void DrawSyncshellCompactItem(GroupJoinDto shell, bool isOwnBroadcast) { + var itemId = $"shell_{shell.Group.GID}"; + var alpha = GetItemAlpha(itemId); + if (alpha <= 0.01f) + return; + ImGui.PushID(shell.Group.GID); + using var alphaStyle = ImRaii.PushStyle(ImGuiStyleVar.Alpha, alpha); float rowHeight = 36f * ImGuiHelpers.GlobalScale; ImGui.BeginChild($"ShellCompact##{shell.Group.GID}", new Vector2(-1, rowHeight), border: true); @@ -655,6 +723,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase { _joinDto = new GroupJoinDto(shell.Group, shell.Password, shell.GroupUserPreferredPermissions); _joinInfo = info; + _joinModalOpen = true; _ownPermissions = _apiController.DefaultPermissions.DeepClone()!; } } @@ -685,65 +754,188 @@ public class LightFinderUI : WindowMediatorSubscriberBase { if (_joinDto == null || _joinInfo == null) return; - ImGui.Separator(); - ImGui.TextUnformatted($"Join Syncshell: {_joinDto.Group.AliasOrGID} by {_joinInfo.OwnerAliasOrUID}"); - ImGuiHelpers.ScaledDummy(2f); - ImGui.TextUnformatted("Suggested Permissions:"); + var scale = ImGuiHelpers.GlobalScale; + + // if not already open + if (!ImGui.IsPopupOpen("JoinSyncshellModal")) + ImGui.OpenPopup("JoinSyncshellModal"); - 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(); - - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, $"Finalize and join {_joinDto.Group.AliasOrGID}")) + Vector2 windowPos = ImGui.GetWindowPos(); + Vector2 windowSize = ImGui.GetWindowSize(); + float modalWidth = Math.Min(420f * scale, windowSize.X - 40f * scale); + float modalHeight = 295f * scale; + ImGui.SetNextWindowPos(new Vector2( + windowPos.X + (windowSize.X - modalWidth) * 0.5f, + windowPos.Y + (windowSize.Y - modalHeight) * 0.5f + ), ImGuiCond.Always); + ImGui.SetNextWindowSize(new Vector2(modalWidth, modalHeight)); + ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 6f * ImGuiHelpers.GlobalScale); + ImGui.PushStyleColor(ImGuiCol.Border, Vector4.Zero); + + using ImRaii.Color modalBorder = ImRaii.PushColor(ImGuiCol.Border, UIColors.Get("LightlessPurple").WithAlpha(0.5f)); + using ImRaii.Style rounding = ImRaii.PushStyle(ImGuiStyleVar.WindowRounding, 8f * scale); + using ImRaii.Style borderSize = ImRaii.PushStyle(ImGuiStyleVar.WindowBorderSize, 2f * scale); + using ImRaii.Style padding = ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(16f * scale, 16f * scale)); + + ImGuiWindowFlags flags = ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoScrollbar; + if (ImGui.BeginPopupModal("JoinSyncshellModal", ref _joinModalOpen, flags)) { - var finalPermissions = GroupUserPreferredPermissions.NoneSet; - finalPermissions.SetDisableSounds(_ownPermissions.DisableGroupSounds); - finalPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations); - finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX); + float contentWidth = ImGui.GetContentRegionAvail().X; - _ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions)); - _recentlyJoined.Add(_joinDto.Group.GID); + // Header + _uiSharedService.MediumText("Join Syncshell", UIColors.Get("LightlessPurple")); + ImGuiHelpers.ScaledDummy(2f); + ImGui.TextColored(ImGuiColors.DalamudGrey, $"{_joinDto.Group.AliasOrGID} by {_joinInfo.OwnerAliasOrUID}"); - _joinDto = null; - _joinInfo = null; - } + ImGuiHelpers.ScaledDummy(8f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault").WithAlpha(0.4f)); + ImGuiHelpers.ScaledDummy(8f); - ImGui.SameLine(); - if (ImGui.Button("Cancel")) - { - _joinDto = null; - _joinInfo = null; + // Permissions section + ImGui.TextColored(ImGuiColors.DalamudWhite, "Permissions"); + ImGuiHelpers.ScaledDummy(6f); + + DrawPermissionToggleRow("Sounds", FontAwesomeIcon.VolumeUp, + _joinInfo.GroupPermissions.IsPreferDisableSounds(), + _ownPermissions.DisableGroupSounds, + v => _ownPermissions.DisableGroupSounds = v, + contentWidth); + + DrawPermissionToggleRow("Animations", FontAwesomeIcon.Running, + _joinInfo.GroupPermissions.IsPreferDisableAnimations(), + _ownPermissions.DisableGroupAnimations, + v => _ownPermissions.DisableGroupAnimations = v, + contentWidth); + + DrawPermissionToggleRow("VFX", FontAwesomeIcon.Magic, + _joinInfo.GroupPermissions.IsPreferDisableVFX(), + _ownPermissions.DisableGroupVFX, + v => _ownPermissions.DisableGroupVFX = v, + contentWidth); + + ImGuiHelpers.ScaledDummy(12f); + + // Buttons + float buttonHeight = 32f * scale; + float buttonSpacing = 8f * scale; + float joinButtonWidth = (contentWidth - buttonSpacing) * 0.65f; + float cancelButtonWidth = (contentWidth - buttonSpacing) * 0.35f; + + using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 6f * scale)) + { + // Join button + using (ImRaii.PushColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"))) + using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f))) + using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f))) + { + if (ImGui.Button($"Join Syncshell##{_joinDto.Group.GID}", new Vector2(joinButtonWidth, buttonHeight))) + { + var finalPermissions = GroupUserPreferredPermissions.NoneSet; + finalPermissions.SetDisableSounds(_ownPermissions.DisableGroupSounds); + finalPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations); + finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX); + + _ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions)); + _recentlyJoined.Add(_joinDto.Group.GID); + + _joinDto = null; + _joinInfo = null; + ImGui.CloseCurrentPopup(); + } + } + + ImGui.SameLine(0f, buttonSpacing); + + // Cancel button + using (ImRaii.PushColor(ImGuiCol.Button, new Vector4(0.3f, 0.3f, 0.3f, 1f))) + using (ImRaii.PushColor(ImGuiCol.ButtonHovered, new Vector4(0.4f, 0.4f, 0.4f, 1f))) + using (ImRaii.PushColor(ImGuiCol.ButtonActive, new Vector4(0.25f, 0.25f, 0.25f, 1f))) + { + if (ImGui.Button("Cancel", new Vector2(cancelButtonWidth, buttonHeight))) + { + _joinDto = null; + _joinInfo = null; + ImGui.CloseCurrentPopup(); + } + } + } + + // Handle modal close via the bool ref + if (!_joinModalOpen) + { + _joinDto = null; + _joinInfo = null; + } + + ImGui.EndPopup(); } } - private void DrawPermissionRow(string label, bool suggested, bool current, Action apply) + private void DrawPermissionToggleRow(string label, FontAwesomeIcon icon, bool suggested, bool current, Action apply, float contentWidth) { - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted($"- {label}"); - - ImGui.SameLine(120 * ImGuiHelpers.GlobalScale); - ImGui.Text("Current:"); - ImGui.SameLine(); - _uiSharedService.BooleanToColoredIcon(!current); - - ImGui.SameLine(240 * ImGuiHelpers.GlobalScale); - ImGui.Text("Suggested:"); - ImGui.SameLine(); - _uiSharedService.BooleanToColoredIcon(!suggested); - - ImGui.SameLine(380 * ImGuiHelpers.GlobalScale); - using var id = ImRaii.PushId(label); - if (current != suggested) + var scale = ImGuiHelpers.GlobalScale; + float rowHeight = 28f * scale; + bool isDifferent = current != suggested; + + using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 4f * scale)) + using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 4f * scale)) + using (ImRaii.PushColor(ImGuiCol.ChildBg, new Vector4(0.18f, 0.15f, 0.22f, 0.6f))) { - if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Apply")) - apply(suggested); - } + ImGui.BeginChild($"PermRow_{label}", new Vector2(contentWidth, rowHeight), false, ImGuiWindowFlags.NoScrollbar); + + float innerPadding = 8f * scale; + ImGui.SetCursorPos(new Vector2(innerPadding, (rowHeight - ImGui.GetTextLineHeight()) * 0.5f)); - ImGui.NewLine(); + // Icon and label + var enabledColor = UIColors.Get("LightlessGreen"); + var disabledColor = UIColors.Get("DimRed"); + var currentColor = !current ? enabledColor : disabledColor; + + _uiSharedService.IconText(icon, currentColor); + ImGui.SameLine(0f, 6f * scale); + ImGui.TextUnformatted(label); + + // Current status + ImGui.SameLine(); + float statusX = contentWidth * 0.38f; + ImGui.SetCursorPosX(statusX); + ImGui.TextColored(ImGuiColors.DalamudGrey, "Current:"); + ImGui.SameLine(0f, 4f * scale); + _uiSharedService.BooleanToColoredIcon(!current, false); + + // Suggested status + ImGui.SameLine(); + float suggestedX = contentWidth * 0.60f; + ImGui.SetCursorPosX(suggestedX); + ImGui.TextColored(ImGuiColors.DalamudGrey, "Suggested:"); + ImGui.SameLine(0f, 4f * scale); + _uiSharedService.BooleanToColoredIcon(!suggested, false); + + // Apply checkmark button if different + if (isDifferent) + { + ImGui.SameLine(); + float applyX = contentWidth - 26f * scale; + ImGui.SetCursorPosX(applyX); + ImGui.SetCursorPosY((rowHeight - ImGui.GetFrameHeight()) * 0.5f); + + using (ImRaii.PushColor(ImGuiCol.Button, UIColors.Get("LightlessGreen").WithAlpha(0.6f))) + using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen"))) + using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreenDefault"))) + { + if (_uiSharedService.IconButton(FontAwesomeIcon.Check)) + apply(suggested); + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Apply suggested"); + } + + ImGui.EndChild(); + } + ImGui.Dummy(new Vector2(0, 2f * scale)); } + #endregion #region Nearby Players Tab @@ -785,10 +977,14 @@ public class LightFinderUI : WindowMediatorSubscriberBase var playerData = BuildNearbyPlayerData(activeBroadcasts); if (playerData.Count == 0) { + UpdateItemAnimations([]); ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby Lightfinder players found."); return; } + // Update animations for player items + UpdateItemAnimations(playerData.Select(p => $"player_{p.HashedCid}")); + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8f); ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f); @@ -848,7 +1044,13 @@ public class LightFinderUI : WindowMediatorSubscriberBase private void DrawNearbyPlayerRow(NearbyPlayerData data) { + var itemId = $"player_{data.HashedCid}"; + var alpha = GetItemAlpha(itemId); + if (alpha <= 0.01f) + return; + ImGui.PushID(data.HashedCid); + using var alphaStyle = ImRaii.PushStyle(ImGuiStyleVar.Alpha, alpha); float rowHeight = 74f * ImGuiHelpers.GlobalScale; ImGui.BeginChild($"PlayerRow##{data.HashedCid}", new Vector2(-1, rowHeight), border: true); @@ -903,7 +1105,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase ImGui.SetCursorPosX(startX); _uiSharedService.IconText(FontAwesomeIcon.Wifi, UIColors.Get("LightlessBlue")); ImGui.SameLine(0f, 4f * ImGuiHelpers.GlobalScale); - ImGui.TextColored(ImGuiColors.DalamudGrey, "Lightfinder User"); + ImGui.TextColored(ImGuiColors.DalamudGrey, "Lightfinder sir ma'am or whatever"); } ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale)); @@ -916,7 +1118,13 @@ public class LightFinderUI : WindowMediatorSubscriberBase private void DrawNearbyPlayerCompactRow(NearbyPlayerData data) { + var itemId = $"player_{data.HashedCid}"; + var alpha = GetItemAlpha(itemId); + if (alpha <= 0.01f) + return; + ImGui.PushID(data.HashedCid); + using var alphaStyle = ImRaii.PushStyle(ImGuiStyleVar.Alpha, alpha); float rowHeight = 36f * ImGuiHelpers.GlobalScale; ImGui.BeginChild($"PlayerCompact##{data.HashedCid}", new Vector2(-1, rowHeight), border: true);