Files
LightlessClient/LightlessSync/UI/TopTabMenu.cs
defnotken 72a62b7449
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m9s
2.1.0 (#123)
# 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
2026-01-20 19:43:00 +00:00

910 lines
39 KiB
C#

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.LightFinder;
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;
public class TopTabMenu
{
private readonly ApiController _apiController;
private readonly LightlessMediator _lightlessMediator;
private readonly PairRequestService _pairRequestService;
private readonly DalamudUtilService _dalamudUtilService;
private readonly LightFinderService _lightFinderService;
private readonly LightFinderScannerService _lightFinderScannerService;
private readonly HashSet<string> _pendingPairRequestActions = new(StringComparer.Ordinal);
private bool _pairRequestsExpanded; // useless for now
private int _lastRequestCount;
private readonly UiSharedService _uiSharedService;
private readonly NotificationService _lightlessNotificationService;
private string _filter = string.Empty;
private int _globalControlCountdown = 0;
private float _pairRequestsHeight = 150f;
private string _pairToAdd = string.Empty;
private SelectedTab _selectedTab = SelectedTab.None;
private PairUiSnapshot? _currentSnapshot;
public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService, LightFinderService lightFinderService, LightFinderScannerService lightFinderScannerService)
{
_lightlessMediator = lightlessMediator;
_apiController = apiController;
_pairRequestService = pairRequestService;
_dalamudUtilService = dalamudUtilService;
_uiSharedService = uiSharedService;
_lightlessNotificationService = lightlessNotificationService;
_lightFinderService = lightFinderService;
_lightFinderScannerService = lightFinderScannerService;
}
private enum SelectedTab
{
None,
Individual,
Syncshell,
Lightfinder,
UserConfig,
Settings
}
public string Filter
{
get => _filter;
private set
{
if (!string.Equals(_filter, value, StringComparison.OrdinalIgnoreCase))
{
_lightlessMediator.Publish(new RefreshUiMessage());
}
_filter = value;
}
}
private SelectedTab TabSelection
{
get => _selectedTab; set
{
_selectedTab = value;
}
}
private PairUiSnapshot Snapshot => _currentSnapshot ?? throw new InvalidOperationException("Pair UI snapshot is not available outside of Draw.");
public void Draw(PairUiSnapshot snapshot)
{
_currentSnapshot = snapshot;
try
{
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
var spacing = ImGui.GetStyle().ItemSpacing;
var buttonX = (availableWidth - (spacing.X * 5)) / 6f;
var buttonY = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Pause).Y;
var buttonSize = new Vector2(buttonX, buttonY);
const float buttonBorderThickness = 12f;
var buttonRounding = ImGui.GetStyle().FrameRounding;
var drawList = ImGui.GetWindowDrawList();
var underlineColor = ImGui.GetColorU32(UIColors.Get("LightlessPurpleActive")); // ImGui.GetColorU32(ImGuiCol.Separator);
var btncolor = ImRaii.PushColor(ImGuiCol.Button, ImGui.ColorConvertFloat4ToU32(new(0, 0, 0, 0)));
ImGuiHelpers.ScaledDummy(spacing.Y / 2f);
using (ImRaii.PushFont(UiBuilder.IconFont))
{
var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.User.ToIconString(), buttonSize))
{
TabSelection = TabSelection == SelectedTab.Individual ? SelectedTab.None : SelectedTab.Individual;
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
}
ImGui.SameLine();
var xAfter = ImGui.GetCursorScreenPos();
if (TabSelection == SelectedTab.Individual)
drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y },
xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X },
underlineColor, 2);
}
UiSharedService.AttachToolTip("Individual Pair Menu");
using (ImRaii.PushFont(UiBuilder.IconFont))
{
var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.Users.ToIconString(), buttonSize))
{
TabSelection = TabSelection == SelectedTab.Syncshell ? SelectedTab.None : SelectedTab.Syncshell;
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
}
ImGui.SameLine();
var xAfter = ImGui.GetCursorScreenPos();
if (TabSelection == SelectedTab.Syncshell)
drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y },
xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X },
underlineColor, 2);
}
UiSharedService.AttachToolTip("Syncshell Menu");
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button(FontAwesomeIcon.Comments.ToIconString(), buttonSize))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(ZoneChatUi)));
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
}
}
UiSharedService.AttachToolTip("Lightless Chat");
ImGui.SameLine();
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button(FontAwesomeIcon.Compass.ToIconString(), buttonSize))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
}
}
var nearbyCount = GetNearbySyncshellCount();
if (nearbyCount > 0)
{
var buttonMax = ImGui.GetItemRectMax();
var badgeRadius = 8f * ImGuiHelpers.GlobalScale;
var badgeCenter = new Vector2(buttonMax.X - badgeRadius * 1.3f, buttonMax.Y - buttonSize.Y + badgeRadius * 0.5f);
var badgeText = nearbyCount > 99 ? "99+" : nearbyCount.ToString();
var textSize = ImGui.CalcTextSize(badgeText);
drawList.AddCircleFilled(badgeCenter, badgeRadius + 1f, ImGui.GetColorU32(new Vector4(0, 0, 0, 0.6f)));
drawList.AddCircleFilled(badgeCenter, badgeRadius, ImGui.GetColorU32(UIColors.Get("LightlessPurple")));
var textPos = new Vector2(badgeCenter.X - textSize.X * 0.45f, badgeCenter.Y - textSize.Y * 0.55f);
drawList.AddText(textPos, ImGui.GetColorU32(new Vector4(1, 1, 1, 1)), badgeText);
}
UiSharedService.AttachToolTip(nearbyCount > 0 ? $"Lightfinder ({nearbyCount} nearby)" : "Open Lightfinder");
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.UserCog.ToIconString(), buttonSize))
{
TabSelection = TabSelection == SelectedTab.UserConfig ? SelectedTab.None : SelectedTab.UserConfig;
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
}
ImGui.SameLine();
var xAfter = ImGui.GetCursorScreenPos();
if (TabSelection == SelectedTab.UserConfig)
drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y },
xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X },
underlineColor, 2);
}
UiSharedService.AttachToolTip("Your User Menu");
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString(), buttonSize))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
}
ImGui.SameLine();
}
UiSharedService.AttachToolTip("Open Lightless Settings");
ImGui.NewLine();
btncolor.Dispose();
ImGuiHelpers.ScaledDummy(spacing);
if (TabSelection == SelectedTab.Individual)
{
DrawAddPair(availableWidth, spacing.X);
DrawGlobalIndividualButtons(availableWidth, spacing.X);
}
else if (TabSelection == SelectedTab.Syncshell)
{
DrawSyncshellMenu(availableWidth, spacing.X);
DrawGlobalSyncshellButtons(availableWidth, spacing.X);
}
else if (TabSelection == SelectedTab.UserConfig)
{
DrawUserConfig(availableWidth, spacing.X);
}
if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f);
DrawIncomingPairRequests(availableWidth);
ImGui.Separator();
DrawFilter(availableWidth, spacing.X);
}
finally
{
_currentSnapshot = null;
}
}
private void DrawAddPair(float availableXWidth, float spacingX)
{
var buttonSize = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.UserPlus, "Add");
ImGui.SetNextItemWidth(availableXWidth - buttonSize - spacingX);
ImGui.InputTextWithHint("##otheruid", "Other players UID/Alias", ref _pairToAdd, 20);
ImGui.SameLine();
var alreadyExisting = Snapshot.DirectPairs.Any(p => string.Equals(p.UserData.UID, _pairToAdd, StringComparison.Ordinal) || string.Equals(p.UserData.Alias, _pairToAdd, StringComparison.Ordinal));
using (ImRaii.Disabled(alreadyExisting || string.IsNullOrEmpty(_pairToAdd)))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserPlus, "Add"))
{
_ = _apiController.UserAddPair(new(new(_pairToAdd)));
_pairToAdd = string.Empty;
}
}
UiSharedService.AttachToolTip("Pair with " + (_pairToAdd.IsNullOrEmpty() ? "other user" : _pairToAdd));
}
private void DrawIncomingPairRequests(float availableWidth)
{
var requests = _pairRequestService.GetActiveRequests();
var count = requests.Count;
if (count == 0)
{
_pairRequestsExpanded = false;
_lastRequestCount = 0;
return;
}
if (count > _lastRequestCount)
{
_pairRequestsExpanded = true;
}
_lastRequestCount = count;
var label = $"Incoming Pair Requests - {count}##IncomingPairRequestsHeader";
using (ImRaii.PushColor(ImGuiCol.Header, UIColors.Get("LightlessPurple")))
using (ImRaii.PushColor(ImGuiCol.HeaderHovered, UIColors.Get("LightlessPurpleActive")))
using (ImRaii.PushColor(ImGuiCol.HeaderActive, UIColors.Get("LightlessPurple")))
{
bool open = ImGui.CollapsingHeader(label, ImGuiTreeNodeFlags.DefaultOpen);
_pairRequestsExpanded = open;
if (ImGui.IsItemHovered())
UiSharedService.AttachToolTip("Expand to view incoming pair requests.");
if (open)
{
var lineHeight = ImGui.GetTextLineHeightWithSpacing();
//var desiredHeight = Math.Clamp(count * lineHeight * 2f, 130f * ImGuiHelpers.GlobalScale, 185f * ImGuiHelpers.GlobalScale); we use resize bar instead
ImGui.SetCursorPosX(ImGui.GetCursorPosX() - 2f);
using (ImRaii.PushColor(ImGuiCol.ChildBg, UIColors.Get("LightlessPurple")))
using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f))
{
if (ImGui.BeginChild("##IncomingPairRequestsOuter", new Vector2(availableWidth + 5f, _pairRequestsHeight), true))
{
var defaultChildBg = ImGui.GetStyle().Colors[(int)ImGuiCol.WindowBg];
using (ImRaii.PushColor(ImGuiCol.ChildBg, defaultChildBg))
using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 5f))
using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(6, 6)))
{
if (ImGui.BeginChild("##IncomingPairRequestsInner", new Vector2(0, 0), true))
{
using (ImRaii.PushColor(ImGuiCol.TableBorderStrong, ImGui.GetStyle().Colors[(int)ImGuiCol.Border]))
using (ImRaii.PushColor(ImGuiCol.TableBorderLight, ImGui.GetStyle().Colors[(int)ImGuiCol.Border]))
{
DrawPairRequestList(requests);
}
}
ImGui.EndChild();
}
}
ImGui.EndChild();
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("ButtonDefault"));
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple"));
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive"));
ImGui.Button("##resizeHandle", new Vector2(availableWidth, 4f));
ImGui.PopStyleColor(3);
if (ImGui.IsItemActive())
{
_pairRequestsHeight += ImGui.GetIO().MouseDelta.Y;
_pairRequestsHeight = Math.Clamp(_pairRequestsHeight, 100f, 300f);
}
}
}
}
}
private void DrawPairRequestList(IReadOnlyList<PairRequestService.PairRequestDisplay> requests)
{
float playerColWidth = 207f * ImGuiHelpers.GlobalScale;
float receivedColWidth = 73f * ImGuiHelpers.GlobalScale;
float actionsColWidth = 50f * ImGuiHelpers.GlobalScale;
ImGui.Separator();
ImGui.TextUnformatted("Player");
ImGui.SameLine(playerColWidth + 2f);
ImGui.TextUnformatted("Received");
ImGui.SameLine(playerColWidth + receivedColWidth + 12f);
ImGui.TextUnformatted("Actions");
ImGui.Separator();
foreach (var request in requests)
{
ImGui.BeginGroup();
var label = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName;
ImGui.TextUnformatted(label.Truncate(26));
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
ImGui.TextUnformatted(label);
ImGui.EndTooltip();
}
ImGui.SameLine(playerColWidth);
ImGui.TextUnformatted(GetRelativeTime(request.ReceivedAt));
ImGui.SameLine(playerColWidth + receivedColWidth);
DrawPairRequestActions(request);
ImGui.EndGroup();
}
}
private void DrawPairRequestActions(PairRequestService.PairRequestDisplay request)
{
using var id = ImRaii.PushId(request.HashedCid);
var label = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName;
var inFlight = _pendingPairRequestActions.Contains(request.HashedCid);
using (ImRaii.Disabled(inFlight))
{
if (_uiSharedService.IconButton(FontAwesomeIcon.Check))
{
_ = AcceptPairRequestAsync(request);
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Accept request");
ImGui.SameLine();
if (_uiSharedService.IconButton(FontAwesomeIcon.Times))
{
RejectPairRequest(request.HashedCid, label);
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Decline request");
}
}
private static string GetRelativeTime(DateTime receivedAt)
{
var delta = DateTime.UtcNow - receivedAt;
if (delta <= TimeSpan.FromSeconds(10))
{
return "Just now";
}
if (delta.TotalMinutes >= 1)
{
return $"{Math.Floor(delta.TotalMinutes)}m {delta.Seconds:D2}s ago";
}
return $"{delta.Seconds}s ago";
}
private async Task AcceptPairRequestAsync(PairRequestService.PairRequestDisplay request)
{
if (!_pendingPairRequestActions.Add(request.HashedCid))
{
return;
}
try
{
var myCidHash = _dalamudUtilService.GetCID().ToString().GetHash256();
await _apiController.TryPairWithContentId(request.HashedCid).ConfigureAwait(false);
_pairRequestService.RemoveRequest(request.HashedCid);
var display = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName;
_lightlessMediator.Publish(new NotificationMessage(
"Pair request accepted",
$"Sent a pair request back to {display}.",
NotificationType.Info,
TimeSpan.FromSeconds(3)));
}
catch (Exception ex)
{
_lightlessMediator.Publish(new NotificationMessage(
"Failed to accept pair request",
ex.Message,
NotificationType.Error,
TimeSpan.FromSeconds(5)));
}
finally
{
_pendingPairRequestActions.Remove(request.HashedCid);
}
}
private void RejectPairRequest(string hashedCid, string playerName)
{
if (!_pairRequestService.RemoveRequest(hashedCid))
{
return;
}
_lightlessMediator.Publish(new NotificationMessage("Pair request declined", "Declined " + playerName + "'s pending pair request.",
NotificationType.Info,
TimeSpan.FromSeconds(3)));
}
private void DrawFilter(float availableWidth, float spacingX)
{
var buttonSize = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.Ban, "Clear");
ImGui.SetNextItemWidth(availableWidth - buttonSize - spacingX);
string filter = Filter;
if (ImGui.InputTextWithHint("##filter", "Filter for UID/notes", ref filter, 255))
{
Filter = filter;
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, 10, exactSize: true, clipToElement: true, roundingOverride: ImGui.GetStyle().FrameRounding);
}
ImGui.SameLine();
var disableClear = string.IsNullOrEmpty(Filter);
using var disabled = ImRaii.Disabled(disableClear);
var clearHovered = false;
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Ban, "Clear"))
{
Filter = string.Empty;
}
clearHovered = ImGui.IsItemHovered();
if (!disableClear && clearHovered)
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, 10, exactSize: true, clipToElement: true, roundingOverride: ImGui.GetStyle().FrameRounding);
}
}
private void DrawGlobalIndividualButtons(float availableXWidth, float spacingX)
{
var buttonX = (availableXWidth - (spacingX * 3)) / 4f;
var buttonY = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Pause).Y;
var buttonSize = new Vector2(buttonX, buttonY);
using (ImRaii.PushFont(UiBuilder.IconFont))
{
using var disabled = ImRaii.Disabled(_globalControlCountdown > 0);
if (ImGui.Button(FontAwesomeIcon.Pause.ToIconString(), buttonSize))
{
ImGui.OpenPopup("Individual Pause");
}
}
UiSharedService.AttachToolTip("Globally resume or pause all individual pairs." + UiSharedService.TooltipSeparator
+ (_globalControlCountdown > 0 ? UiSharedService.TooltipSeparator + "Available again in " + _globalControlCountdown + " seconds." : string.Empty));
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
using var disabled = ImRaii.Disabled(_globalControlCountdown > 0);
if (ImGui.Button(FontAwesomeIcon.VolumeUp.ToIconString(), buttonSize))
{
ImGui.OpenPopup("Individual Sounds");
}
}
UiSharedService.AttachToolTip("Globally enable or disable sound sync with all individual pairs."
+ (_globalControlCountdown > 0 ? UiSharedService.TooltipSeparator + "Available again in " + _globalControlCountdown + " seconds." : string.Empty));
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
using var disabled = ImRaii.Disabled(_globalControlCountdown > 0);
if (ImGui.Button(FontAwesomeIcon.Running.ToIconString(), buttonSize))
{
ImGui.OpenPopup("Individual Animations");
}
}
UiSharedService.AttachToolTip("Globally enable or disable animation sync with all individual pairs." + UiSharedService.TooltipSeparator
+ (_globalControlCountdown > 0 ? UiSharedService.TooltipSeparator + "Available again in " + _globalControlCountdown + " seconds." : string.Empty));
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
using var disabled = ImRaii.Disabled(_globalControlCountdown > 0);
if (ImGui.Button(FontAwesomeIcon.Sun.ToIconString(), buttonSize))
{
ImGui.OpenPopup("Individual VFX");
}
}
UiSharedService.AttachToolTip("Globally enable or disable VFX sync with all individual pairs." + UiSharedService.TooltipSeparator
+ (_globalControlCountdown > 0 ? UiSharedService.TooltipSeparator + "Available again in " + _globalControlCountdown + " seconds." : string.Empty));
PopupIndividualSetting("Individual Pause", "Unpause all individuals", "Pause all individuals",
FontAwesomeIcon.Play, FontAwesomeIcon.Pause,
(perm) =>
{
perm.SetPaused(false);
return perm;
},
(perm) =>
{
perm.SetPaused(true);
return perm;
});
PopupIndividualSetting("Individual Sounds", "Enable sounds for all individuals", "Disable sounds for all individuals",
FontAwesomeIcon.VolumeUp, FontAwesomeIcon.VolumeMute,
(perm) =>
{
perm.SetDisableSounds(false);
return perm;
},
(perm) =>
{
perm.SetDisableSounds(true);
return perm;
});
PopupIndividualSetting("Individual Animations", "Enable animations for all individuals", "Disable animations for all individuals",
FontAwesomeIcon.Running, FontAwesomeIcon.Stop,
(perm) =>
{
perm.SetDisableAnimations(false);
return perm;
},
(perm) =>
{
perm.SetDisableAnimations(true);
return perm;
});
PopupIndividualSetting("Individual VFX", "Enable VFX for all individuals", "Disable VFX for all individuals",
FontAwesomeIcon.Sun, FontAwesomeIcon.Circle,
(perm) =>
{
perm.SetDisableVFX(false);
return perm;
},
(perm) =>
{
perm.SetDisableVFX(true);
return perm;
});
}
private void DrawGlobalSyncshellButtons(float availableXWidth, float spacingX)
{
var buttonX = (availableXWidth - (spacingX * 4)) / 5f;
var buttonY = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Pause).Y;
var buttonSize = new Vector2(buttonX, buttonY);
using (ImRaii.PushFont(UiBuilder.IconFont))
{
using var disabled = ImRaii.Disabled(_globalControlCountdown > 0);
if (ImGui.Button(FontAwesomeIcon.Pause.ToIconString(), buttonSize))
{
ImGui.OpenPopup("Syncshell Pause");
}
}
UiSharedService.AttachToolTip("Globally resume or pause all syncshells." + UiSharedService.TooltipSeparator
+ "Note: This will not affect users with preferred permissions in syncshells."
+ (_globalControlCountdown > 0 ? UiSharedService.TooltipSeparator + "Available again in " + _globalControlCountdown + " seconds." : string.Empty));
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
using var disabled = ImRaii.Disabled(_globalControlCountdown > 0);
if (ImGui.Button(FontAwesomeIcon.VolumeUp.ToIconString(), buttonSize))
{
ImGui.OpenPopup("Syncshell Sounds");
}
}
UiSharedService.AttachToolTip("Globally enable or disable sound sync with all syncshells." + UiSharedService.TooltipSeparator
+ "Note: This will not affect users with preferred permissions in syncshells."
+ (_globalControlCountdown > 0 ? UiSharedService.TooltipSeparator + "Available again in " + _globalControlCountdown + " seconds." : string.Empty));
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
using var disabled = ImRaii.Disabled(_globalControlCountdown > 0);
if (ImGui.Button(FontAwesomeIcon.Running.ToIconString(), buttonSize))
{
ImGui.OpenPopup("Syncshell Animations");
}
}
UiSharedService.AttachToolTip("Globally enable or disable animation sync with all syncshells." + UiSharedService.TooltipSeparator
+ "Note: This will not affect users with preferred permissions in syncshells."
+ (_globalControlCountdown > 0 ? UiSharedService.TooltipSeparator + "Available again in " + _globalControlCountdown + " seconds." : string.Empty));
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
using var disabled = ImRaii.Disabled(_globalControlCountdown > 0);
if (ImGui.Button(FontAwesomeIcon.Sun.ToIconString(), buttonSize))
{
ImGui.OpenPopup("Syncshell VFX");
}
}
UiSharedService.AttachToolTip("Globally enable or disable VFX sync with all syncshells." + UiSharedService.TooltipSeparator
+ "Note: This will not affect users with preferred permissions in syncshells."
+ (_globalControlCountdown > 0 ? UiSharedService.TooltipSeparator + "Available again in " + _globalControlCountdown + " seconds." : string.Empty));
PopupSyncshellSetting("Syncshell Pause", "Unpause all syncshells", "Pause all syncshells",
FontAwesomeIcon.Play, FontAwesomeIcon.Pause,
(perm) =>
{
perm.SetPaused(false);
return perm;
},
(perm) =>
{
perm.SetPaused(true);
return perm;
});
PopupSyncshellSetting("Syncshell Sounds", "Enable sounds for all syncshells", "Disable sounds for all syncshells",
FontAwesomeIcon.VolumeUp, FontAwesomeIcon.VolumeMute,
(perm) =>
{
perm.SetDisableSounds(false);
return perm;
},
(perm) =>
{
perm.SetDisableSounds(true);
return perm;
});
PopupSyncshellSetting("Syncshell Animations", "Enable animations for all syncshells", "Disable animations for all syncshells",
FontAwesomeIcon.Running, FontAwesomeIcon.Stop,
(perm) =>
{
perm.SetDisableAnimations(false);
return perm;
},
(perm) =>
{
perm.SetDisableAnimations(true);
return perm;
});
PopupSyncshellSetting("Syncshell VFX", "Enable VFX for all syncshells", "Disable VFX for all syncshells",
FontAwesomeIcon.Sun, FontAwesomeIcon.Circle,
(perm) =>
{
perm.SetDisableVFX(false);
return perm;
},
(perm) =>
{
perm.SetDisableVFX(true);
return perm;
});
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
using var disabled = ImRaii.Disabled(_globalControlCountdown > 0 || !UiSharedService.CtrlPressed());
if (ImGui.Button(FontAwesomeIcon.Check.ToIconString(), buttonSize))
{
_ = GlobalControlCountdown(10);
var bulkSyncshells = Snapshot.GroupPairs.Keys.OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Group.GID, g =>
{
var perm = g.GroupUserPermissions;
perm.SetDisableSounds(g.GroupPermissions.IsPreferDisableSounds());
perm.SetDisableAnimations(g.GroupPermissions.IsPreferDisableAnimations());
perm.SetDisableVFX(g.GroupPermissions.IsPreferDisableVFX());
return perm;
}, StringComparer.Ordinal);
_ = _apiController.SetBulkPermissions(new(new(StringComparer.Ordinal), bulkSyncshells)).ConfigureAwait(false);
}
}
UiSharedService.AttachToolTip("Globally align syncshell permissions to suggested syncshell permissions." + UiSharedService.TooltipSeparator
+ "Note: This will not affect users with preferred permissions in syncshells." + Environment.NewLine
+ "Note: If multiple users share one syncshell the permissions to that user will be set to " + Environment.NewLine
+ "the ones of the last applied syncshell in alphabetical order." + UiSharedService.TooltipSeparator
+ "Hold CTRL to enable this button"
+ (_globalControlCountdown > 0 ? UiSharedService.TooltipSeparator + "Available again in " + _globalControlCountdown + " seconds." : string.Empty));
}
private void DrawSyncshellMenu(float availableWidth, float spacingX)
{
var buttonX = (availableWidth - (spacingX)) / 2f;
using (ImRaii.Disabled(Snapshot.GroupPairs.Keys
.Distinct()
.Count(g => string.Equals(g.OwnerUID, _apiController.UID, StringComparison.Ordinal)) >= _apiController.ServerInfo.MaxGroupsCreatedByUser))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Create new Syncshell", buttonX))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(CreateSyncshellUI)));
}
ImGui.SameLine();
}
using (ImRaii.Disabled(Snapshot.GroupPairs.Keys.Distinct().Count() >= _apiController.ServerInfo.MaxGroupsJoinedByUser))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Users, "Join existing Syncshell", buttonX))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(JoinSyncshellUI)));
}
}
}
private int GetNearbySyncshellCount()
{
if (!_lightFinderService.IsBroadcasting)
return 0;
var myHashedCid = _dalamudUtilService.GetCID().ToString().GetHash256();
return _lightFinderScannerService
.GetActiveSyncshellBroadcasts()
.Where(b =>
!string.IsNullOrEmpty(b.GID) &&
!string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal))
.Select(b => b.GID!)
.Distinct(StringComparer.Ordinal)
.Count();
}
private void DrawUserConfig(float availableWidth, float spacingX)
{
var buttonX = (availableWidth - spacingX) / 2f;
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserCircle, "Edit Lightless Profile", buttonX))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(EditProfileUi)));
}
UiSharedService.AttachToolTip("Edit your Lightless Profile");
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, "Chara Data Analysis", buttonX))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
}
UiSharedService.AttachToolTip("View and analyze your generated character data");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Running, "Character Data Hub", availableWidth))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(CharaDataHubUi)));
}
}
private async Task GlobalControlCountdown(int countdown)
{
#if DEBUG
return;
#endif
_globalControlCountdown = countdown;
while (_globalControlCountdown > 0)
{
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
_globalControlCountdown--;
}
}
private void PopupIndividualSetting(string popupTitle, string enableText, string disableText,
FontAwesomeIcon enableIcon, FontAwesomeIcon disableIcon,
Func<UserPermissions, UserPermissions> actEnable, Func<UserPermissions, UserPermissions> actDisable)
{
if (ImGui.BeginPopup(popupTitle))
{
if (_uiSharedService.IconTextButton(enableIcon, enableText, null, true))
{
_ = GlobalControlCountdown(10);
var bulkIndividualPairs = Snapshot.PairsWithGroups.Keys
.Where(g => g.IndividualPairStatus == IndividualPairStatus.Bidirectional)
.ToDictionary(g => g.UserPair.User.UID, g =>
{
return actEnable(g.UserPair.OwnPermissions);
}, StringComparer.Ordinal);
_ = _apiController.SetBulkPermissions(new(bulkIndividualPairs, new(StringComparer.Ordinal))).ConfigureAwait(false);
ImGui.CloseCurrentPopup();
}
if (_uiSharedService.IconTextButton(disableIcon, disableText, null, true))
{
_ = GlobalControlCountdown(10);
var bulkIndividualPairs = Snapshot.PairsWithGroups.Keys
.Where(g => g.IndividualPairStatus == IndividualPairStatus.Bidirectional)
.ToDictionary(g => g.UserPair.User.UID, g =>
{
return actDisable(g.UserPair.OwnPermissions);
}, StringComparer.Ordinal);
_ = _apiController.SetBulkPermissions(new(bulkIndividualPairs, new(StringComparer.Ordinal))).ConfigureAwait(false);
ImGui.CloseCurrentPopup();
}
ImGui.EndPopup();
}
}
private void PopupSyncshellSetting(string popupTitle, string enableText, string disableText,
FontAwesomeIcon enableIcon, FontAwesomeIcon disableIcon,
Func<GroupUserPreferredPermissions, GroupUserPreferredPermissions> actEnable,
Func<GroupUserPreferredPermissions, GroupUserPreferredPermissions> actDisable)
{
if (ImGui.BeginPopup(popupTitle))
{
if (_uiSharedService.IconTextButton(enableIcon, enableText, null, true))
{
_ = GlobalControlCountdown(10);
var bulkSyncshells = Snapshot.GroupPairs.Keys
.OrderBy(u => u.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Group.GID, g =>
{
return actEnable(g.GroupUserPermissions);
}, StringComparer.Ordinal);
_ = _apiController.SetBulkPermissions(new(new(StringComparer.Ordinal), bulkSyncshells)).ConfigureAwait(false);
ImGui.CloseCurrentPopup();
}
if (_uiSharedService.IconTextButton(disableIcon, disableText, null, true))
{
_ = GlobalControlCountdown(10);
var bulkSyncshells = Snapshot.GroupPairs.Keys
.OrderBy(u => u.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Group.GID, g =>
{
return actDisable(g.GroupUserPermissions);
}, StringComparer.Ordinal);
_ = _apiController.SetBulkPermissions(new(new(StringComparer.Ordinal), bulkSyncshells)).ConfigureAwait(false);
ImGui.CloseCurrentPopup();
}
ImGui.EndPopup();
}
}
}