rebuild project & update internal names

This commit is contained in:
Abelfreyja
2025-08-22 13:07:48 +09:00
parent 3d9bf49d7f
commit 7d3de5361a
190 changed files with 851 additions and 851 deletions

View File

@@ -0,0 +1,194 @@
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Dto.CharaData;
using LightlessSync.MareConfiguration.Models;
using LightlessSync.Services.CharaData.Models;
using System.Text;
namespace LightlessSync.UI;
internal sealed partial class CharaDataHubUi
{
private static string GetAccessTypeString(AccessTypeDto dto) => dto switch
{
AccessTypeDto.AllPairs => "All Pairs",
AccessTypeDto.ClosePairs => "Direct Pairs",
AccessTypeDto.Individuals => "Specified",
AccessTypeDto.Public => "Everyone"
};
private static string GetShareTypeString(ShareTypeDto dto) => dto switch
{
ShareTypeDto.Private => "Code Only",
ShareTypeDto.Shared => "Shared"
};
private static string GetWorldDataTooltipText(PoseEntryExtended poseEntry)
{
if (!poseEntry.HasWorldData) return "This Pose has no world data attached.";
return poseEntry.WorldDataDescriptor;
}
private void GposeMetaInfoAction(Action<CharaDataMetaInfoExtendedDto?> gposeActionDraw, string actionDescription, CharaDataMetaInfoExtendedDto? dto, bool hasValidGposeTarget, bool isSpawning)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine(actionDescription);
bool isDisabled = false;
void AddErrorStart(StringBuilder sb)
{
sb.Append(UiSharedService.TooltipSeparator);
sb.AppendLine("Cannot execute:");
}
if (dto == null)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- No metainfo present");
isDisabled = true;
}
if (!dto?.CanBeDownloaded ?? false)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Character is not downloadable");
isDisabled = true;
}
if (!_uiSharedService.IsInGpose)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires to be in GPose");
isDisabled = true;
}
if (!hasValidGposeTarget && !isSpawning)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires a valid GPose target");
isDisabled = true;
}
if (isSpawning && !_charaDataManager.BrioAvailable)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires Brio to be installed.");
isDisabled = true;
}
using (ImRaii.Group())
{
using var dis = ImRaii.Disabled(isDisabled);
gposeActionDraw.Invoke(dto);
}
if (sb.Length > 0)
{
UiSharedService.AttachToolTip(sb.ToString());
}
}
private void GposePoseAction(Action poseActionDraw, string poseDescription, bool hasValidGposeTarget)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine(poseDescription);
bool isDisabled = false;
void AddErrorStart(StringBuilder sb)
{
sb.Append(UiSharedService.TooltipSeparator);
sb.AppendLine("Cannot execute:");
}
if (!_uiSharedService.IsInGpose)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires to be in GPose");
isDisabled = true;
}
if (!hasValidGposeTarget)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires a valid GPose target");
isDisabled = true;
}
if (!_charaDataManager.BrioAvailable)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires Brio to be installed.");
isDisabled = true;
}
using (ImRaii.Group())
{
using var dis = ImRaii.Disabled(isDisabled);
poseActionDraw.Invoke();
}
if (sb.Length > 0)
{
UiSharedService.AttachToolTip(sb.ToString());
}
}
private void SetWindowSizeConstraints(bool? inGposeTab = null)
{
SizeConstraints = new()
{
MinimumSize = new((inGposeTab ?? false) ? 400 : 1000, 500),
MaximumSize = new((inGposeTab ?? false) ? 400 : 1000, 2000)
};
}
private void UpdateFilteredFavorites()
{
_ = Task.Run(async () =>
{
if (_charaDataManager.DownloadMetaInfoTask != null)
{
await _charaDataManager.DownloadMetaInfoTask.ConfigureAwait(false);
}
Dictionary<string, (CharaDataFavorite, CharaDataMetaInfoExtendedDto?, bool)> newFiltered = [];
foreach (var favorite in _configService.Current.FavoriteCodes)
{
var uid = favorite.Key.Split(":")[0];
var note = _serverConfigurationManager.GetNoteForUid(uid) ?? string.Empty;
bool hasMetaInfo = _charaDataManager.TryGetMetaInfo(favorite.Key, out var metaInfo);
bool addFavorite =
(string.IsNullOrEmpty(_filterCodeNote)
|| (note.Contains(_filterCodeNote, StringComparison.OrdinalIgnoreCase)
|| uid.Contains(_filterCodeNote, StringComparison.OrdinalIgnoreCase)))
&& (string.IsNullOrEmpty(_filterDescription)
|| (favorite.Value.CustomDescription.Contains(_filterDescription, StringComparison.OrdinalIgnoreCase)
|| (metaInfo != null && metaInfo!.Description.Contains(_filterDescription, StringComparison.OrdinalIgnoreCase))))
&& (!_filterPoseOnly
|| (metaInfo != null && metaInfo!.HasPoses))
&& (!_filterWorldOnly
|| (metaInfo != null && metaInfo!.HasWorldData));
if (addFavorite)
{
newFiltered[favorite.Key] = (favorite.Value, metaInfo, hasMetaInfo);
}
}
_filteredFavorites = newFiltered;
});
}
private void UpdateFilteredItems()
{
if (_charaDataManager.GetSharedWithYouTask == null)
{
_filteredDict = _charaDataManager.SharedWithYouData
.SelectMany(k => k.Value)
.Where(k =>
(!_sharedWithYouDownloadableFilter || k.CanBeDownloaded)
&& (string.IsNullOrEmpty(_sharedWithYouDescriptionFilter) || k.Description.Contains(_sharedWithYouDescriptionFilter, StringComparison.OrdinalIgnoreCase)))
.GroupBy(k => k.Uploader)
.ToDictionary(k =>
{
var note = _serverConfigurationManager.GetNoteForUid(k.Key.UID);
if (note == null) return k.Key.AliasOrUID;
return $"{note} ({k.Key.AliasOrUID})";
}, k => k.ToList(), StringComparer.OrdinalIgnoreCase)
.Where(k => (string.IsNullOrEmpty(_sharedWithYouOwnerFilter) || k.Key.Contains(_sharedWithYouOwnerFilter, StringComparison.OrdinalIgnoreCase)))
.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase).ToDictionary();
}
}
}

View File

@@ -0,0 +1,227 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.Services.CharaData.Models;
namespace LightlessSync.UI;
internal sealed partial class CharaDataHubUi
{
private string _joinLobbyId = string.Empty;
private void DrawGposeTogether()
{
if (!_charaDataManager.BrioAvailable)
{
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("BRIO IS MANDATORY FOR GPOSE TOGETHER.", ImGuiColors.DalamudRed);
ImGuiHelpers.ScaledDummy(5);
}
if (!_uiSharedService.ApiController.IsConnected)
{
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("CANNOT USE GPOSE TOGETHER WHILE DISCONNECTED FROM THE SERVER.", ImGuiColors.DalamudRed);
ImGuiHelpers.ScaledDummy(5);
}
_uiSharedService.BigText("GPose Together");
DrawHelpFoldout("GPose together is a way to do multiplayer GPose sessions and collaborations." + UiSharedService.DoubleNewLine
+ "GPose together requires Brio to function. Only Brio is also supported for the actual posing interactions. Attempting to pose using other tools will lead to conflicts and exploding characters." + UiSharedService.DoubleNewLine
+ "To use GPose together you either create or join a GPose Together Lobby. After you and other people have joined, make sure that everyone is on the same map. "
+ "It is not required for you to be on the same server, DC or instance. Users that are on the same map will be drawn as moving purple wisps in the overworld, so you can easily find each other." + UiSharedService.DoubleNewLine
+ "Once you are close to each other you can initiate GPose. You must either assign or spawn characters for each of the lobby users. Their own poses and positions to their character will be automatically applied." + Environment.NewLine
+ "Pose and location data during GPose are updated approximately every 10-20s.");
using var disabled = ImRaii.Disabled(!_charaDataManager.BrioAvailable || !_uiSharedService.ApiController.IsConnected);
UiSharedService.DistanceSeparator();
_uiSharedService.BigText("Lobby Controls");
if (string.IsNullOrEmpty(_charaDataGposeTogetherManager.CurrentGPoseLobbyId))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Create New GPose Together Lobby"))
{
_charaDataGposeTogetherManager.CreateNewLobby();
}
ImGuiHelpers.ScaledDummy(5);
UiSharedService.ScaledNextItemWidth(250);
ImGui.InputTextWithHint("##lobbyId", "GPose Lobby Id", ref _joinLobbyId, 30);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Join GPose Together Lobby"))
{
_charaDataGposeTogetherManager.JoinGPoseLobby(_joinLobbyId);
_joinLobbyId = string.Empty;
}
if (!string.IsNullOrEmpty(_charaDataGposeTogetherManager.LastGPoseLobbyId)
&& _uiSharedService.IconTextButton(FontAwesomeIcon.LongArrowAltRight, $"Rejoin Last Lobby {_charaDataGposeTogetherManager.LastGPoseLobbyId}"))
{
_charaDataGposeTogetherManager.JoinGPoseLobby(_charaDataGposeTogetherManager.LastGPoseLobbyId);
}
}
else
{
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("GPose Lobby");
ImGui.SameLine();
UiSharedService.ColorTextWrapped(_charaDataGposeTogetherManager.CurrentGPoseLobbyId, ImGuiColors.ParsedGreen);
ImGui.SameLine();
if (_uiSharedService.IconButton(FontAwesomeIcon.Clipboard))
{
ImGui.SetClipboardText(_charaDataGposeTogetherManager.CurrentGPoseLobbyId);
}
UiSharedService.AttachToolTip("Copy Lobby ID to clipboard.");
using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowLeft, "Leave GPose Lobby"))
{
_charaDataGposeTogetherManager.LeaveGPoseLobby();
}
}
UiSharedService.AttachToolTip("Leave the current GPose lobby." + UiSharedService.TooltipSeparator + "Hold CTRL and click to leave.");
}
UiSharedService.DistanceSeparator();
using (ImRaii.Disabled(string.IsNullOrEmpty(_charaDataGposeTogetherManager.CurrentGPoseLobbyId)))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowUp, "Send Updated Character Data"))
{
_ = _charaDataGposeTogetherManager.PushCharacterDownloadDto();
}
UiSharedService.AttachToolTip("This will send your current appearance, pose and world data to all users in the lobby.");
if (!_uiSharedService.IsInGpose)
{
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("Assigning users to characters is only available in GPose.", ImGuiColors.DalamudYellow, 300);
}
UiSharedService.DistanceSeparator();
ImGui.TextUnformatted("Users In Lobby");
var gposeCharas = _dalamudUtilService.GetGposeCharactersFromObjectTable();
var self = _dalamudUtilService.GetPlayerCharacter();
gposeCharas = gposeCharas.Where(c => c != null && !string.Equals(c.Name.TextValue, self.Name.TextValue, StringComparison.Ordinal)).ToList();
using (ImRaii.Child("charaChild", new(0, 0), false, ImGuiWindowFlags.AlwaysAutoResize))
{
ImGuiHelpers.ScaledDummy(3);
if (!_charaDataGposeTogetherManager.UsersInLobby.Any() && !string.IsNullOrEmpty(_charaDataGposeTogetherManager.CurrentGPoseLobbyId))
{
UiSharedService.DrawGroupedCenteredColorText("No other users in current GPose lobby", ImGuiColors.DalamudYellow);
}
else
{
foreach (var user in _charaDataGposeTogetherManager.UsersInLobby)
{
DrawLobbyUser(user, gposeCharas);
}
}
}
}
}
private void DrawLobbyUser(GposeLobbyUserData user,
IEnumerable<Dalamud.Game.ClientState.Objects.Types.ICharacter?> gposeCharas)
{
using var id = ImRaii.PushId(user.UserData.UID);
using var indent = ImRaii.PushIndent(5f);
var sameMapAndServer = _charaDataGposeTogetherManager.IsOnSameMapAndServer(user);
var width = ImGui.GetContentRegionAvail().X - 5;
UiSharedService.DrawGrouped(() =>
{
var availWidth = ImGui.GetContentRegionAvail().X;
ImGui.AlignTextToFramePadding();
var note = _serverConfigurationManager.GetNoteForUid(user.UserData.UID);
var userText = note == null ? user.UserData.AliasOrUID : $"{note} ({user.UserData.AliasOrUID})";
UiSharedService.ColorText(userText, ImGuiColors.ParsedGreen);
var buttonsize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.ArrowRight).X;
var buttonsize2 = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus).X;
ImGui.SameLine();
ImGui.SetCursorPosX(availWidth - (buttonsize + buttonsize2 + ImGui.GetStyle().ItemSpacing.X));
using (ImRaii.Disabled(!_uiSharedService.IsInGpose || user.CharaData == null || user.Address == nint.Zero))
{
if (_uiSharedService.IconButton(FontAwesomeIcon.ArrowRight))
{
_ = _charaDataGposeTogetherManager.ApplyCharaData(user);
}
}
UiSharedService.AttachToolTip("Apply newly received character data to selected actor." + UiSharedService.TooltipSeparator + "Note: If the button is grayed out, the latest data has already been applied.");
ImGui.SameLine();
using (ImRaii.Disabled(!_uiSharedService.IsInGpose || user.CharaData == null || sameMapAndServer.SameEverything))
{
if (_uiSharedService.IconButton(FontAwesomeIcon.Plus))
{
_ = _charaDataGposeTogetherManager.SpawnAndApplyData(user);
}
}
UiSharedService.AttachToolTip("Spawn new actor, apply character data and and assign it to this user." + UiSharedService.TooltipSeparator + "Note: If the button is grayed out, " +
"the user has not sent any character data or you are on the same map, server and instance. If the latter is the case, join a group with that user and assign the character to them.");
using (ImRaii.Group())
{
UiSharedService.ColorText("Map Info", ImGuiColors.DalamudGrey);
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.ExternalLinkSquareAlt, ImGuiColors.DalamudGrey);
}
UiSharedService.AttachToolTip(user.WorldDataDescriptor + UiSharedService.TooltipSeparator);
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.Map, sameMapAndServer.SameMap ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed);
if (ImGui.IsItemClicked(ImGuiMouseButton.Left) && user.WorldData != null)
{
_dalamudUtilService.SetMarkerAndOpenMap(new(user.WorldData.Value.PositionX, user.WorldData.Value.PositionY, user.WorldData.Value.PositionZ), user.Map);
}
UiSharedService.AttachToolTip((sameMapAndServer.SameMap ? "You are on the same map." : "You are not on the same map.") + UiSharedService.TooltipSeparator
+ "Note: Click to open the users location on your map." + Environment.NewLine
+ "Note: For GPose synchronization to work properly, you must be on the same map.");
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.Globe, sameMapAndServer.SameServer ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed);
UiSharedService.AttachToolTip((sameMapAndServer.SameMap ? "You are on the same server." : "You are not on the same server.") + UiSharedService.TooltipSeparator
+ "Note: GPose synchronization is not dependent on the current server, but you will have to spawn a character for the other lobby users.");
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.Running, sameMapAndServer.SameEverything ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed);
UiSharedService.AttachToolTip(sameMapAndServer.SameEverything ? "You are in the same instanced area." : "You are not the same instanced area." + UiSharedService.TooltipSeparator +
"Note: Users not in your instance, but on the same map, will be drawn as floating wisps." + Environment.NewLine
+ "Note: GPose synchronization is not dependent on the current instance, but you will have to spawn a character for the other lobby users.");
using (ImRaii.Disabled(!_uiSharedService.IsInGpose))
{
UiSharedService.ScaledNextItemWidth(200);
using (var combo = ImRaii.Combo("##character", string.IsNullOrEmpty(user.AssociatedCharaName) ? "No character assigned" : CharaName(user.AssociatedCharaName)))
{
if (combo)
{
foreach (var chara in gposeCharas)
{
if (chara == null) continue;
if (ImGui.Selectable(CharaName(chara.Name.TextValue), chara.Address == user.Address))
{
user.AssociatedCharaName = chara.Name.TextValue;
user.Address = chara.Address;
}
}
}
}
ImGui.SameLine();
using (ImRaii.Disabled(user.Address == nint.Zero))
{
if (_uiSharedService.IconButton(FontAwesomeIcon.Trash))
{
user.AssociatedCharaName = string.Empty;
user.Address = nint.Zero;
}
}
UiSharedService.AttachToolTip("Unassign Actor for this user");
if (_uiSharedService.IsInGpose && user.Address == nint.Zero)
{
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudRed);
UiSharedService.AttachToolTip("No valid character assigned for this user. Pose data will not be applied.");
}
}
}, 5, width);
ImGuiHelpers.ScaledDummy(5);
}
}

View File

@@ -0,0 +1,979 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Utility;
using Dalamud.Interface;
using LightlessSync.API.Dto.CharaData;
using LightlessSync.Services.CharaData.Models;
using System.Numerics;
namespace LightlessSync.UI;
internal sealed partial class CharaDataHubUi
{
private string _createDescFilter = string.Empty;
private string _createCodeFilter = string.Empty;
private bool _createOnlyShowFav = false;
private bool _createOnlyShowNotDownloadable = false;
private void DrawEditCharaData(CharaDataFullExtendedDto? dataDto)
{
using var imguiid = ImRaii.PushId(dataDto?.Id ?? "NoData");
if (dataDto == null)
{
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("Select an entry above to edit its data.", ImGuiColors.DalamudYellow);
return;
}
var updateDto = _charaDataManager.GetUpdateDto(dataDto.Id);
if (updateDto == null)
{
UiSharedService.DrawGroupedCenteredColorText("Something went awfully wrong and there's no update DTO. Try updating Character Data via the button above.", ImGuiColors.DalamudYellow);
return;
}
int otherUpdates = 0;
foreach (var item in _charaDataManager.OwnCharaData.Values.Where(v => !string.Equals(v.Id, dataDto.Id, StringComparison.Ordinal)))
{
if (_charaDataManager.GetUpdateDto(item.Id)?.HasChanges ?? false)
{
otherUpdates++;
}
}
bool canUpdate = updateDto.HasChanges;
if (canUpdate || otherUpdates > 0 || (!_charaDataManager.CharaUpdateTask?.IsCompleted ?? false))
{
ImGuiHelpers.ScaledDummy(5);
}
var indent = ImRaii.PushIndent(10f);
if (canUpdate || _charaDataManager.UploadTask != null)
{
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGrouped(() =>
{
if (canUpdate)
{
ImGui.AlignTextToFramePadding();
UiSharedService.ColorTextWrapped("Warning: You have unsaved changes!", ImGuiColors.DalamudRed);
ImGui.SameLine();
using (ImRaii.Disabled(_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleUp, "Save to Server"))
{
_charaDataManager.UploadCharaData(dataDto.Id);
}
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Undo, "Undo all changes"))
{
updateDto.UndoChanges();
}
}
if (_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted)
{
UiSharedService.ColorTextWrapped("Updating data on server, please wait.", ImGuiColors.DalamudYellow);
}
}
if (!_charaDataManager.UploadTask?.IsCompleted ?? false)
{
DisableDisabled(() =>
{
if (_charaDataManager.UploadProgress != null)
{
UiSharedService.ColorTextWrapped(_charaDataManager.UploadProgress.Value ?? string.Empty, ImGuiColors.DalamudYellow);
}
if ((!_charaDataManager.UploadTask?.IsCompleted ?? false) && _uiSharedService.IconTextButton(FontAwesomeIcon.Ban, "Cancel Upload"))
{
_charaDataManager.CancelUpload();
}
else if (_charaDataManager.UploadTask?.IsCompleted ?? false)
{
var color = UiSharedService.GetBoolColor(_charaDataManager.UploadTask.Result.Success);
UiSharedService.ColorTextWrapped(_charaDataManager.UploadTask.Result.Output, color);
}
});
}
else if (_charaDataManager.UploadTask?.IsCompleted ?? false)
{
var color = UiSharedService.GetBoolColor(_charaDataManager.UploadTask.Result.Success);
UiSharedService.ColorTextWrapped(_charaDataManager.UploadTask.Result.Output, color);
}
});
}
if (otherUpdates > 0)
{
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGrouped(() =>
{
ImGui.AlignTextToFramePadding();
UiSharedService.ColorTextWrapped($"You have {otherUpdates} other entries with unsaved changes.", ImGuiColors.DalamudYellow);
ImGui.SameLine();
using (ImRaii.Disabled(_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowAltCircleUp, "Save all to server"))
{
_charaDataManager.UploadAllCharaData();
}
}
});
}
indent.Dispose();
if (canUpdate || otherUpdates > 0 || (!_charaDataManager.CharaUpdateTask?.IsCompleted ?? false))
{
ImGuiHelpers.ScaledDummy(5);
}
using var child = ImRaii.Child("editChild", new(0, 0), false, ImGuiWindowFlags.AlwaysAutoResize);
DrawEditCharaDataGeneral(dataDto, updateDto);
ImGuiHelpers.ScaledDummy(5);
DrawEditCharaDataAccessAndSharing(updateDto);
ImGuiHelpers.ScaledDummy(5);
DrawEditCharaDataAppearance(dataDto, updateDto);
ImGuiHelpers.ScaledDummy(5);
DrawEditCharaDataPoses(updateDto);
}
private void DrawEditCharaDataAccessAndSharing(CharaDataExtendedUpdateDto updateDto)
{
_uiSharedService.BigText("Access and Sharing");
UiSharedService.ScaledNextItemWidth(200);
var dtoAccessType = updateDto.AccessType;
if (ImGui.BeginCombo("Access Restrictions", GetAccessTypeString(dtoAccessType)))
{
foreach (var accessType in Enum.GetValues(typeof(AccessTypeDto)).Cast<AccessTypeDto>())
{
if (ImGui.Selectable(GetAccessTypeString(accessType), accessType == dtoAccessType))
{
updateDto.AccessType = accessType;
}
}
ImGui.EndCombo();
}
_uiSharedService.DrawHelpText("You can control who has access to your character data based on the access restrictions." + UiSharedService.TooltipSeparator
+ "Specified: Only people and syncshells you directly specify in 'Specific Individuals / Syncshells' can access this character data" + Environment.NewLine
+ "Direct Pairs: Only people you have directly paired can access this character data" + Environment.NewLine
+ "All Pairs: All people you have paired can access this character data" + Environment.NewLine
+ "Everyone: Everyone can access this character data" + UiSharedService.TooltipSeparator
+ "Note: To access your character data the person in question requires to have the code. Exceptions for 'Shared' data, see 'Sharing' below." + Environment.NewLine
+ "Note: For 'Direct' and 'All Pairs' the pause state plays a role. Paused people will not be able to access your character data." + Environment.NewLine
+ "Note: Directly specified Individuals or Syncshells in the 'Specific Individuals / Syncshells' list will be able to access your character data regardless of pause or pair state.");
DrawSpecific(updateDto);
UiSharedService.ScaledNextItemWidth(200);
var dtoShareType = updateDto.ShareType;
if (ImGui.BeginCombo("Sharing", GetShareTypeString(dtoShareType)))
{
foreach (var shareType in Enum.GetValues(typeof(ShareTypeDto)).Cast<ShareTypeDto>())
{
if (ImGui.Selectable(GetShareTypeString(shareType), shareType == dtoShareType))
{
updateDto.ShareType = shareType;
}
}
ImGui.EndCombo();
}
_uiSharedService.DrawHelpText("This regulates how you want to distribute this character data." + UiSharedService.TooltipSeparator
+ "Code Only: People require to have the code to download this character data" + Environment.NewLine
+ "Shared: People that are allowed through 'Access Restrictions' will have this character data entry displayed in 'Shared with You' (it can also be accessed through the code)" + UiSharedService.TooltipSeparator
+ "Note: Shared with Access Restriction 'Everyone' is the same as shared with Access Restriction 'All Pairs', it will not show up for everyone but just your pairs.");
ImGuiHelpers.ScaledDummy(10f);
}
private void DrawEditCharaDataAppearance(CharaDataFullExtendedDto dataDto, CharaDataExtendedUpdateDto updateDto)
{
_uiSharedService.BigText("Appearance");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Set Appearance to Current Appearance"))
{
_charaDataManager.SetAppearanceData(dataDto.Id);
}
_uiSharedService.DrawHelpText("This will overwrite the appearance data currently stored in this Character Data entry with your current appearance.");
ImGui.SameLine();
using (ImRaii.Disabled(dataDto.HasMissingFiles || !updateDto.IsAppearanceEqual || _charaDataManager.DataApplicationTask != null))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.CheckCircle, "Preview Saved Apperance on Self"))
{
_charaDataManager.ApplyDataToSelf(dataDto);
}
}
_uiSharedService.DrawHelpText("This will download and apply the saved character data to yourself. Once loaded it will automatically revert itself within 15 seconds." + UiSharedService.TooltipSeparator
+ "Note: Weapons will not be displayed correctly unless using the same job as the saved data.");
ImGui.TextUnformatted("Contains Glamourer Data");
ImGui.SameLine();
bool hasGlamourerdata = !string.IsNullOrEmpty(updateDto.GlamourerData);
UiSharedService.ScaledSameLine(200);
_uiSharedService.BooleanToColoredIcon(hasGlamourerdata, false);
ImGui.TextUnformatted("Contains Files");
var hasFiles = (updateDto.FileGamePaths ?? []).Any() || (dataDto.OriginalFiles.Any());
UiSharedService.ScaledSameLine(200);
_uiSharedService.BooleanToColoredIcon(hasFiles, false);
if (hasFiles && updateDto.IsAppearanceEqual)
{
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(20, 1);
ImGui.SameLine();
var pos = ImGui.GetCursorPosX();
ImGui.NewLine();
ImGui.SameLine(pos);
ImGui.TextUnformatted($"{dataDto.FileGamePaths.DistinctBy(k => k.HashOrFileSwap).Count()} unique file hashes (original upload: {dataDto.OriginalFiles.DistinctBy(k => k.HashOrFileSwap).Count()} file hashes)");
ImGui.NewLine();
ImGui.SameLine(pos);
ImGui.TextUnformatted($"{dataDto.FileGamePaths.Count} associated game paths");
ImGui.NewLine();
ImGui.SameLine(pos);
ImGui.TextUnformatted($"{dataDto.FileSwaps!.Count} file swaps");
ImGui.NewLine();
ImGui.SameLine(pos);
if (!dataDto.HasMissingFiles)
{
UiSharedService.ColorTextWrapped("All files to download this character data are present on the server", ImGuiColors.HealerGreen);
}
else
{
UiSharedService.ColorTextWrapped($"{dataDto.MissingFiles.DistinctBy(k => k.HashOrFileSwap).Count()} files to download this character data are missing on the server", ImGuiColors.DalamudRed);
ImGui.NewLine();
ImGui.SameLine(pos);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleUp, "Attempt to upload missing files and restore Character Data"))
{
_charaDataManager.UploadMissingFiles(dataDto.Id);
}
}
}
else if (hasFiles && !updateDto.IsAppearanceEqual)
{
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(20, 1);
ImGui.SameLine();
UiSharedService.ColorTextWrapped("New data was set. It may contain files that require to be uploaded (will happen on Saving to server)", ImGuiColors.DalamudYellow);
}
ImGui.TextUnformatted("Contains Manipulation Data");
bool hasManipData = !string.IsNullOrEmpty(updateDto.ManipulationData);
UiSharedService.ScaledSameLine(200);
_uiSharedService.BooleanToColoredIcon(hasManipData, false);
ImGui.TextUnformatted("Contains Customize+ Data");
ImGui.SameLine();
bool hasCustomizeData = !string.IsNullOrEmpty(updateDto.CustomizeData);
UiSharedService.ScaledSameLine(200);
_uiSharedService.BooleanToColoredIcon(hasCustomizeData, false);
}
private void DrawEditCharaDataGeneral(CharaDataFullExtendedDto dataDto, CharaDataExtendedUpdateDto updateDto)
{
_uiSharedService.BigText("General");
string code = dataDto.FullId;
using (ImRaii.Disabled())
{
UiSharedService.ScaledNextItemWidth(200);
ImGui.InputText("##CharaDataCode", ref code, 255, ImGuiInputTextFlags.ReadOnly);
}
ImGui.SameLine();
ImGui.TextUnformatted("Chara Data Code");
ImGui.SameLine();
if (_uiSharedService.IconButton(FontAwesomeIcon.Copy))
{
ImGui.SetClipboardText(code);
}
UiSharedService.AttachToolTip("Copy Code to Clipboard");
string creationTime = dataDto.CreatedDate.ToLocalTime().ToString();
string updateTime = dataDto.UpdatedDate.ToLocalTime().ToString();
string downloadCount = dataDto.DownloadCount.ToString();
using (ImRaii.Disabled())
{
UiSharedService.ScaledNextItemWidth(200);
ImGui.InputText("##CreationDate", ref creationTime, 255, ImGuiInputTextFlags.ReadOnly);
}
ImGui.SameLine();
ImGui.TextUnformatted("Creation Date");
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(20);
ImGui.SameLine();
using (ImRaii.Disabled())
{
UiSharedService.ScaledNextItemWidth(200);
ImGui.InputText("##LastUpdate", ref updateTime, 255, ImGuiInputTextFlags.ReadOnly);
}
ImGui.SameLine();
ImGui.TextUnformatted("Last Update Date");
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(23);
ImGui.SameLine();
using (ImRaii.Disabled())
{
UiSharedService.ScaledNextItemWidth(50);
ImGui.InputText("##DlCount", ref downloadCount, 255, ImGuiInputTextFlags.ReadOnly);
}
ImGui.SameLine();
ImGui.TextUnformatted("Download Count");
string description = updateDto.Description;
UiSharedService.ScaledNextItemWidth(735);
if (ImGui.InputText("##Description", ref description, 200))
{
updateDto.Description = description;
}
ImGui.SameLine();
ImGui.TextUnformatted("Description");
_uiSharedService.DrawHelpText("Description for this Character Data." + UiSharedService.TooltipSeparator
+ "Note: the description will be visible to anyone who can access this character data. See 'Access Restrictions' and 'Sharing' below.");
var expiryDate = updateDto.ExpiryDate;
bool isExpiring = expiryDate != DateTime.MaxValue;
if (ImGui.Checkbox("Expires", ref isExpiring))
{
updateDto.SetExpiry(isExpiring);
}
_uiSharedService.DrawHelpText("If expiration is enabled, the uploaded character data will be automatically deleted from the server at the specified date.");
using (ImRaii.Disabled(!isExpiring))
{
ImGui.SameLine();
UiSharedService.ScaledNextItemWidth(100);
if (ImGui.BeginCombo("Year", expiryDate.Year.ToString()))
{
for (int year = DateTime.UtcNow.Year; year < DateTime.UtcNow.Year + 4; year++)
{
if (ImGui.Selectable(year.ToString(), year == expiryDate.Year))
{
updateDto.SetExpiry(year, expiryDate.Month, expiryDate.Day);
}
}
ImGui.EndCombo();
}
ImGui.SameLine();
int daysInMonth = DateTime.DaysInMonth(expiryDate.Year, expiryDate.Month);
UiSharedService.ScaledNextItemWidth(100);
if (ImGui.BeginCombo("Month", expiryDate.Month.ToString()))
{
for (int month = 1; month <= 12; month++)
{
if (ImGui.Selectable(month.ToString(), month == expiryDate.Month))
{
updateDto.SetExpiry(expiryDate.Year, month, expiryDate.Day);
}
}
ImGui.EndCombo();
}
ImGui.SameLine();
UiSharedService.ScaledNextItemWidth(100);
if (ImGui.BeginCombo("Day", expiryDate.Day.ToString()))
{
for (int day = 1; day <= daysInMonth; day++)
{
if (ImGui.Selectable(day.ToString(), day == expiryDate.Day))
{
updateDto.SetExpiry(expiryDate.Year, expiryDate.Month, day);
}
}
ImGui.EndCombo();
}
}
ImGuiHelpers.ScaledDummy(5);
using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Character Data"))
{
_ = _charaDataManager.DeleteCharaData(dataDto);
SelectedDtoId = string.Empty;
}
}
if (!UiSharedService.CtrlPressed())
{
UiSharedService.AttachToolTip("Hold CTRL and click to delete the current data. This operation is irreversible.");
}
}
private void DrawEditCharaDataPoses(CharaDataExtendedUpdateDto updateDto)
{
_uiSharedService.BigText("Poses");
var poseCount = updateDto.PoseList.Count();
using (ImRaii.Disabled(poseCount >= maxPoses))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add new Pose"))
{
updateDto.AddPose();
}
}
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, poseCount == maxPoses))
ImGui.TextUnformatted($"{poseCount}/{maxPoses} poses attached");
ImGuiHelpers.ScaledDummy(5);
using var indent = ImRaii.PushIndent(10f);
int poseNumber = 1;
if (!_uiSharedService.IsInGpose && _charaDataManager.BrioAvailable)
{
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data you need to be in GPose.", ImGuiColors.DalamudYellow);
ImGuiHelpers.ScaledDummy(5);
}
else if (!_charaDataManager.BrioAvailable)
{
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data Brio requires to be installed.", ImGuiColors.DalamudRed);
ImGuiHelpers.ScaledDummy(5);
}
foreach (var pose in updateDto.PoseList)
{
ImGui.AlignTextToFramePadding();
using var id = ImRaii.PushId("pose" + poseNumber);
ImGui.TextUnformatted(poseNumber.ToString());
if (pose.Id == null)
{
UiSharedService.ScaledSameLine(50);
_uiSharedService.IconText(FontAwesomeIcon.Plus, ImGuiColors.DalamudYellow);
UiSharedService.AttachToolTip("This pose has not been added to the server yet. Save changes to upload this Pose data.");
}
bool poseHasChanges = updateDto.PoseHasChanges(pose);
if (poseHasChanges)
{
UiSharedService.ScaledSameLine(50);
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudYellow);
UiSharedService.AttachToolTip("This pose has changes that have not been saved to the server yet.");
}
UiSharedService.ScaledSameLine(75);
if (pose.Description == null && pose.WorldData == null && pose.PoseData == null)
{
UiSharedService.ColorText("Pose scheduled for deletion", ImGuiColors.DalamudYellow);
}
else
{
var desc = pose.Description;
if (ImGui.InputTextWithHint("##description", "Description", ref desc, 100))
{
pose.Description = desc;
updateDto.UpdatePoseList();
}
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete"))
{
updateDto.RemovePose(pose);
}
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(10, 1);
ImGui.SameLine();
bool hasPoseData = !string.IsNullOrEmpty(pose.PoseData);
_uiSharedService.IconText(FontAwesomeIcon.Running, UiSharedService.GetBoolColor(hasPoseData));
UiSharedService.AttachToolTip(hasPoseData
? "This Pose entry has pose data attached"
: "This Pose entry has no pose data attached");
ImGui.SameLine();
using (ImRaii.Disabled(!_uiSharedService.IsInGpose || !(_charaDataManager.AttachingPoseTask?.IsCompleted ?? true) || !_charaDataManager.BrioAvailable))
{
using var poseid = ImRaii.PushId("poseSet" + poseNumber);
if (_uiSharedService.IconButton(FontAwesomeIcon.Plus))
{
_charaDataManager.AttachPoseData(pose, updateDto);
}
UiSharedService.AttachToolTip("Apply current pose data to pose");
}
ImGui.SameLine();
using (ImRaii.Disabled(!hasPoseData))
{
using var poseid = ImRaii.PushId("poseDelete" + poseNumber);
if (_uiSharedService.IconButton(FontAwesomeIcon.Trash))
{
pose.PoseData = string.Empty;
updateDto.UpdatePoseList();
}
UiSharedService.AttachToolTip("Delete current pose data from pose");
}
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(10, 1);
ImGui.SameLine();
var worldData = pose.WorldData;
bool hasWorldData = (worldData ?? default) != default;
_uiSharedService.IconText(FontAwesomeIcon.Globe, UiSharedService.GetBoolColor(hasWorldData));
var tooltipText = !hasWorldData ? "This Pose has no world data attached." : "This Pose has world data attached.";
if (hasWorldData)
{
tooltipText += UiSharedService.TooltipSeparator + "Click to show location on map";
}
UiSharedService.AttachToolTip(tooltipText);
if (hasWorldData && ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
_dalamudUtilService.SetMarkerAndOpenMap(position: new Vector3(worldData.Value.PositionX, worldData.Value.PositionY, worldData.Value.PositionZ),
_dalamudUtilService.MapData.Value[worldData.Value.LocationInfo.MapId].Map);
}
ImGui.SameLine();
using (ImRaii.Disabled(!_uiSharedService.IsInGpose || !(_charaDataManager.AttachingPoseTask?.IsCompleted ?? true) || !_charaDataManager.BrioAvailable))
{
using var worldId = ImRaii.PushId("worldSet" + poseNumber);
if (_uiSharedService.IconButton(FontAwesomeIcon.Plus))
{
_charaDataManager.AttachWorldData(pose, updateDto);
}
UiSharedService.AttachToolTip("Apply current world position data to pose");
}
ImGui.SameLine();
using (ImRaii.Disabled(!hasWorldData))
{
using var worldId = ImRaii.PushId("worldDelete" + poseNumber);
if (_uiSharedService.IconButton(FontAwesomeIcon.Trash))
{
pose.WorldData = default(WorldData);
updateDto.UpdatePoseList();
}
UiSharedService.AttachToolTip("Delete current world position data from pose");
}
}
if (poseHasChanges)
{
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Undo, "Undo"))
{
updateDto.RevertDeletion(pose);
}
}
poseNumber++;
}
}
private void DrawMcdOnline()
{
_uiSharedService.BigText("Mare Character Data Online");
DrawHelpFoldout("In this tab you can create, view and edit your own Mare Character Data that is stored on the server." + Environment.NewLine + Environment.NewLine
+ "Mare Character Data Online functions similar to the previous MCDF standard for exporting your character, except that you do not have to send a file to the other person but solely a code." + Environment.NewLine + Environment.NewLine
+ "There would be a bit too much to explain here on what you can do here in its entirety, however, all elements in this tab have help texts attached what they are used for. Please review them carefully." + Environment.NewLine + Environment.NewLine
+ "Be mindful that when you share your Character Data with other people there is a chance that, with the help of unsanctioned 3rd party plugins, your appearance could be stolen irreversibly, just like when using MCDF.");
ImGuiHelpers.ScaledDummy(5);
using (ImRaii.Disabled((!_charaDataManager.GetAllDataTask?.IsCompleted ?? false)
|| (_charaDataManager.DataGetTimeoutTask != null && !_charaDataManager.DataGetTimeoutTask.IsCompleted)))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleDown, "Download your Character Data from Server"))
{
_ = _charaDataManager.GetAllData(_disposalCts.Token);
}
}
if (_charaDataManager.DataGetTimeoutTask != null && !_charaDataManager.DataGetTimeoutTask.IsCompleted)
{
UiSharedService.AttachToolTip("You can only refresh all character data from server every minute. Please wait.");
}
using (var table = ImRaii.Table("Own Character Data", 12, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollY,
new Vector2(ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X, 140 * ImGuiHelpers.GlobalScale)))
{
if (table)
{
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 18 * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 18 * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Code");
ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Created");
ImGui.TableSetupColumn("Updated");
ImGui.TableSetupColumn("Download Count", ImGuiTableColumnFlags.WidthFixed, 18 * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Downloadable", ImGuiTableColumnFlags.WidthFixed, 18 * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Files", ImGuiTableColumnFlags.WidthFixed, 32 * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Glamourer", ImGuiTableColumnFlags.WidthFixed, 18 * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Customize+", ImGuiTableColumnFlags.WidthFixed, 18 * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Expires", ImGuiTableColumnFlags.WidthFixed, 18 * ImGuiHelpers.GlobalScale);
ImGui.TableSetupScrollFreeze(0, 2);
ImGui.TableHeadersRow();
ImGui.TableNextColumn();
ImGui.Dummy(new(0, 0));
ImGui.TableNextColumn();
ImGui.Checkbox("###createOnlyShowfav", ref _createOnlyShowFav);
UiSharedService.AttachToolTip("Filter by favorites");
ImGui.TableNextColumn();
var x1 = ImGui.GetContentRegionAvail().X;
ImGui.SetNextItemWidth(x1);
ImGui.InputTextWithHint("###createFilterCode", "Filter by code", ref _createCodeFilter, 200);
ImGui.TableNextColumn();
var x2 = ImGui.GetContentRegionAvail().X;
ImGui.SetNextItemWidth(x2);
ImGui.InputTextWithHint("###createFilterDesc", "Filter by description", ref _createDescFilter, 200);
ImGui.TableNextColumn();
ImGui.Dummy(new(0, 0));
ImGui.TableNextColumn();
ImGui.Dummy(new(0, 0));
ImGui.TableNextColumn();
ImGui.Dummy(new(0, 0));
ImGui.TableNextColumn();
ImGui.Checkbox("###createShowNotDl", ref _createOnlyShowNotDownloadable);
UiSharedService.AttachToolTip("Filter by not downloadable");
ImGui.TableNextColumn();
ImGui.Dummy(new(0, 0));
ImGui.TableNextColumn();
ImGui.Dummy(new(0, 0));
ImGui.TableNextColumn();
ImGui.Dummy(new(0, 0));
ImGui.TableNextColumn();
ImGui.Dummy(new(0, 0));
foreach (var entry in _charaDataManager.OwnCharaData.Values
.Where(v =>
{
bool show = true;
if (!string.IsNullOrWhiteSpace(_createCodeFilter))
{
show &= v.FullId.Contains(_createCodeFilter, StringComparison.OrdinalIgnoreCase);
}
if (!string.IsNullOrWhiteSpace(_createDescFilter))
{
show &= v.Description.Contains(_createDescFilter, StringComparison.OrdinalIgnoreCase);
}
if (_createOnlyShowFav)
{
show &= _configService.Current.FavoriteCodes.ContainsKey(v.FullId);
}
if (_createOnlyShowNotDownloadable)
{
show &= !(!v.HasMissingFiles && !string.IsNullOrEmpty(v.GlamourerData));
}
return show;
}).OrderBy(b => b.CreatedDate))
{
var uDto = _charaDataManager.GetUpdateDto(entry.Id);
ImGui.TableNextColumn();
if (string.Equals(entry.Id, SelectedDtoId, StringComparison.Ordinal))
_uiSharedService.IconText(FontAwesomeIcon.CaretRight);
ImGui.TableNextColumn();
DrawAddOrRemoveFavorite(entry);
ImGui.TableNextColumn();
var idText = entry.FullId;
if (uDto?.HasChanges ?? false)
{
UiSharedService.ColorText(idText, ImGuiColors.DalamudYellow);
UiSharedService.AttachToolTip("This entry has unsaved changes");
}
else
{
ImGui.TextUnformatted(idText);
}
if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id;
ImGui.TableNextColumn();
ImGui.TextUnformatted(entry.Description);
if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id;
UiSharedService.AttachToolTip(entry.Description);
ImGui.TableNextColumn();
ImGui.TextUnformatted(entry.CreatedDate.ToLocalTime().ToString());
if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id;
ImGui.TableNextColumn();
ImGui.TextUnformatted(entry.UpdatedDate.ToLocalTime().ToString());
if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id;
ImGui.TableNextColumn();
ImGui.TextUnformatted(entry.DownloadCount.ToString());
if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id;
ImGui.TableNextColumn();
bool isDownloadable = !entry.HasMissingFiles
&& !string.IsNullOrEmpty(entry.GlamourerData);
_uiSharedService.BooleanToColoredIcon(isDownloadable, false);
if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id;
UiSharedService.AttachToolTip(isDownloadable ? "Can be downloaded by others" : "Cannot be downloaded: Has missing files or data, please review this entry manually");
ImGui.TableNextColumn();
var count = entry.FileGamePaths.Concat(entry.FileSwaps).Count();
ImGui.TextUnformatted(count.ToString());
if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id;
UiSharedService.AttachToolTip(count == 0 ? "No File data attached" : "Has File data attached");
ImGui.TableNextColumn();
bool hasGlamourerData = !string.IsNullOrEmpty(entry.GlamourerData);
_uiSharedService.BooleanToColoredIcon(hasGlamourerData, false);
if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id;
UiSharedService.AttachToolTip(string.IsNullOrEmpty(entry.GlamourerData) ? "No Glamourer data attached" : "Has Glamourer data attached");
ImGui.TableNextColumn();
bool hasCustomizeData = !string.IsNullOrEmpty(entry.CustomizeData);
_uiSharedService.BooleanToColoredIcon(hasCustomizeData, false);
if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id;
UiSharedService.AttachToolTip(string.IsNullOrEmpty(entry.CustomizeData) ? "No Customize+ data attached" : "Has Customize+ data attached");
ImGui.TableNextColumn();
FontAwesomeIcon eIcon = FontAwesomeIcon.None;
if (!Equals(DateTime.MaxValue, entry.ExpiryDate))
eIcon = FontAwesomeIcon.Clock;
_uiSharedService.IconText(eIcon, ImGuiColors.DalamudYellow);
if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id;
if (eIcon != FontAwesomeIcon.None)
{
UiSharedService.AttachToolTip($"This entry will expire on {entry.ExpiryDate.ToLocalTime()}");
}
}
}
}
using (ImRaii.Disabled(!_charaDataManager.Initialized || _charaDataManager.DataCreationTask != null || _charaDataManager.OwnCharaData.Count == _charaDataManager.MaxCreatableCharaData))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "New Character Data Entry"))
{
_charaDataManager.CreateCharaDataEntry(_closalCts.Token);
_selectNewEntry = true;
}
}
if (_charaDataManager.DataCreationTask != null)
{
UiSharedService.AttachToolTip("You can only create new character data every few seconds. Please wait.");
}
if (!_charaDataManager.Initialized)
{
UiSharedService.AttachToolTip("Please use the button \"Get Own Chara Data\" once before you can add new data entries.");
}
if (_charaDataManager.Initialized)
{
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
UiSharedService.TextWrapped($"Chara Data Entries on Server: {_charaDataManager.OwnCharaData.Count}/{_charaDataManager.MaxCreatableCharaData}");
if (_charaDataManager.OwnCharaData.Count == _charaDataManager.MaxCreatableCharaData)
{
ImGui.AlignTextToFramePadding();
UiSharedService.ColorTextWrapped("You have reached the maximum Character Data entries and cannot create more.", ImGuiColors.DalamudYellow);
}
}
if (_charaDataManager.DataCreationTask != null && !_charaDataManager.DataCreationTask.IsCompleted)
{
UiSharedService.ColorTextWrapped("Creating new character data entry on server...", ImGuiColors.DalamudYellow);
}
else if (_charaDataManager.DataCreationTask != null && _charaDataManager.DataCreationTask.IsCompleted)
{
var color = _charaDataManager.DataCreationTask.Result.Success ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed;
UiSharedService.ColorTextWrapped(_charaDataManager.DataCreationTask.Result.Output, color);
}
ImGuiHelpers.ScaledDummy(10);
ImGui.Separator();
var charaDataEntries = _charaDataManager.OwnCharaData.Count;
if (charaDataEntries != _dataEntries && _selectNewEntry && _charaDataManager.OwnCharaData.Any())
{
SelectedDtoId = _charaDataManager.OwnCharaData.OrderBy(o => o.Value.CreatedDate).Last().Value.Id;
_selectNewEntry = false;
}
_dataEntries = _charaDataManager.OwnCharaData.Count;
_ = _charaDataManager.OwnCharaData.TryGetValue(SelectedDtoId, out var dto);
DrawEditCharaData(dto);
}
bool _selectNewEntry = false;
int _dataEntries = 0;
private void DrawSpecific(CharaDataExtendedUpdateDto updateDto)
{
UiSharedService.DrawTree("Access for Specific Individuals / Syncshells", () =>
{
using (ImRaii.PushId("user"))
{
using (ImRaii.Group())
{
InputComboHybrid("##AliasToAdd", "##AliasToAddPicker", ref _specificIndividualAdd, _pairManager.PairsWithGroups.Keys,
static pair => (pair.UserData.UID, pair.UserData.Alias, pair.UserData.AliasOrUID, pair.GetNote()));
ImGui.SameLine();
using (ImRaii.Disabled(string.IsNullOrEmpty(_specificIndividualAdd)
|| updateDto.UserList.Any(f => string.Equals(f.UID, _specificIndividualAdd, StringComparison.Ordinal) || string.Equals(f.Alias, _specificIndividualAdd, StringComparison.Ordinal))))
{
if (_uiSharedService.IconButton(FontAwesomeIcon.Plus))
{
updateDto.AddUserToList(_specificIndividualAdd);
_specificIndividualAdd = string.Empty;
}
}
ImGui.SameLine();
ImGui.TextUnformatted("UID/Vanity UID to Add");
_uiSharedService.DrawHelpText("Users added to this list will be able to access this character data regardless of your pause or pair state with them." + UiSharedService.TooltipSeparator
+ "Note: Mistyped entries will be automatically removed on updating data to server.");
using (var lb = ImRaii.ListBox("Allowed Individuals", new(200 * ImGuiHelpers.GlobalScale, 200 * ImGuiHelpers.GlobalScale)))
{
foreach (var user in updateDto.UserList)
{
var userString = string.IsNullOrEmpty(user.Alias) ? user.UID : $"{user.Alias} ({user.UID})";
if (ImGui.Selectable(userString, string.Equals(user.UID, _selectedSpecificUserIndividual, StringComparison.Ordinal)))
{
_selectedSpecificUserIndividual = user.UID;
}
}
}
using (ImRaii.Disabled(string.IsNullOrEmpty(_selectedSpecificUserIndividual)))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Remove selected User"))
{
updateDto.RemoveUserFromList(_selectedSpecificUserIndividual);
_selectedSpecificUserIndividual = string.Empty;
}
}
using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ExclamationTriangle, "Apply current Allowed Individuals to all MCDO entries"))
{
foreach (var own in _charaDataManager.OwnCharaData.Values.Where(k => !string.Equals(k.Id, updateDto.Id, StringComparison.Ordinal)))
{
var otherUpdateDto = _charaDataManager.GetUpdateDto(own.Id);
if (otherUpdateDto == null) continue;
foreach (var user in otherUpdateDto.UserList.Select(k => k.UID).Concat(otherUpdateDto.AllowedUsers ?? []).Distinct(StringComparer.Ordinal).ToList())
{
otherUpdateDto.RemoveUserFromList(user);
}
foreach (var user in updateDto.UserList.Select(k => k.UID).Concat(updateDto.AllowedUsers ?? []).Distinct(StringComparer.Ordinal).ToList())
{
otherUpdateDto.AddUserToList(user);
}
}
}
}
UiSharedService.AttachToolTip("This will apply the current list of allowed specific individuals to ALL of your MCDO entries." + UiSharedService.TooltipSeparator
+ "Hold CTRL to enable.");
}
}
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(20);
ImGui.SameLine();
using (ImRaii.PushId("group"))
{
using (ImRaii.Group())
{
InputComboHybrid("##GroupAliasToAdd", "##GroupAliasToAddPicker", ref _specificGroupAdd, _pairManager.Groups.Keys,
group => (group.GID, group.Alias, group.AliasOrGID, _serverConfigurationManager.GetNoteForGid(group.GID)));
ImGui.SameLine();
using (ImRaii.Disabled(string.IsNullOrEmpty(_specificGroupAdd)
|| updateDto.GroupList.Any(f => string.Equals(f.GID, _specificGroupAdd, StringComparison.Ordinal) || string.Equals(f.Alias, _specificGroupAdd, StringComparison.Ordinal))))
{
if (_uiSharedService.IconButton(FontAwesomeIcon.Plus))
{
updateDto.AddGroupToList(_specificGroupAdd);
_specificGroupAdd = string.Empty;
}
}
ImGui.SameLine();
ImGui.TextUnformatted("GID/Vanity GID to Add");
_uiSharedService.DrawHelpText("Users in Syncshells added to this list will be able to access this character data regardless of your pause or pair state with them." + UiSharedService.TooltipSeparator
+ "Note: Mistyped entries will be automatically removed on updating data to server.");
using (var lb = ImRaii.ListBox("Allowed Syncshells", new(200 * ImGuiHelpers.GlobalScale, 200 * ImGuiHelpers.GlobalScale)))
{
foreach (var group in updateDto.GroupList)
{
var userString = string.IsNullOrEmpty(group.Alias) ? group.GID : $"{group.Alias} ({group.GID})";
if (ImGui.Selectable(userString, string.Equals(group.GID, _selectedSpecificGroupIndividual, StringComparison.Ordinal)))
{
_selectedSpecificGroupIndividual = group.GID;
}
}
}
using (ImRaii.Disabled(string.IsNullOrEmpty(_selectedSpecificGroupIndividual)))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Remove selected Syncshell"))
{
updateDto.RemoveGroupFromList(_selectedSpecificGroupIndividual);
_selectedSpecificGroupIndividual = string.Empty;
}
}
using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ExclamationTriangle, "Apply current Allowed Syncshells to all MCDO entries"))
{
foreach (var own in _charaDataManager.OwnCharaData.Values.Where(k => !string.Equals(k.Id, updateDto.Id, StringComparison.Ordinal)))
{
var otherUpdateDto = _charaDataManager.GetUpdateDto(own.Id);
if (otherUpdateDto == null) continue;
foreach (var group in otherUpdateDto.GroupList.Select(k => k.GID).Concat(otherUpdateDto.AllowedGroups ?? []).Distinct(StringComparer.Ordinal).ToList())
{
otherUpdateDto.RemoveGroupFromList(group);
}
foreach (var group in updateDto.GroupList.Select(k => k.GID).Concat(updateDto.AllowedGroups ?? []).Distinct(StringComparer.Ordinal).ToList())
{
otherUpdateDto.AddGroupToList(group);
}
}
}
}
UiSharedService.AttachToolTip("This will apply the current list of allowed specific syncshells to ALL of your MCDO entries." + UiSharedService.TooltipSeparator
+ "Hold CTRL to enable.");
}
}
ImGui.Separator();
ImGuiHelpers.ScaledDummy(5);
});
}
private void InputComboHybrid<T>(string inputId, string comboId, ref string value, IEnumerable<T> comboEntries,
Func<T, (string Id, string? Alias, string AliasOrId, string? Note)> parseEntry)
{
const float ComponentWidth = 200;
UiSharedService.ScaledNextItemWidth(ComponentWidth - ImGui.GetFrameHeight());
ImGui.InputText(inputId, ref value, 20);
ImGui.SameLine(0.0f, 0.0f);
using var combo = ImRaii.Combo(comboId, string.Empty, ImGuiComboFlags.NoPreview | ImGuiComboFlags.PopupAlignLeft);
if (!combo)
{
return;
}
if (_openComboHybridEntries is null || !string.Equals(_openComboHybridId, comboId, StringComparison.Ordinal))
{
var valueSnapshot = value;
_openComboHybridEntries = comboEntries
.Select(parseEntry)
.Where(entry => entry.Id.Contains(valueSnapshot, StringComparison.OrdinalIgnoreCase)
|| (entry.Alias is not null && entry.Alias.Contains(valueSnapshot, StringComparison.OrdinalIgnoreCase))
|| (entry.Note is not null && entry.Note.Contains(valueSnapshot, StringComparison.OrdinalIgnoreCase)))
.OrderBy(entry => entry.Note is null ? entry.AliasOrId : $"{entry.Note} ({entry.AliasOrId})", StringComparer.OrdinalIgnoreCase)
.ToArray();
_openComboHybridId = comboId;
}
_comboHybridUsedLastFrame = true;
// Is there a better way to handle this?
var width = ComponentWidth - 2 * ImGui.GetStyle().FramePadding.X - (_openComboHybridEntries.Length > 8 ? ImGui.GetStyle().ScrollbarSize : 0);
foreach (var (id, alias, aliasOrId, note) in _openComboHybridEntries)
{
var selected = !string.IsNullOrEmpty(value)
&& (string.Equals(id, value, StringComparison.Ordinal) || string.Equals(alias, value, StringComparison.Ordinal));
using var font = ImRaii.PushFont(UiBuilder.MonoFont, note is null);
if (ImGui.Selectable(note is null ? aliasOrId : $"{note} ({aliasOrId})", selected, ImGuiSelectableFlags.None, new(width, 0)))
{
value = aliasOrId;
}
}
}
}

View File

@@ -0,0 +1,207 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Utility;
using Dalamud.Interface;
using System.Numerics;
namespace LightlessSync.UI;
internal partial class CharaDataHubUi
{
private void DrawNearbyPoses()
{
_uiSharedService.BigText("Poses Nearby");
DrawHelpFoldout("This tab will show you all Shared World Poses nearby you." + Environment.NewLine + Environment.NewLine
+ "Shared World Poses are poses in character data that have world data attached to them and are set to shared. "
+ "This means that all data that is in 'Shared with You' that has a pose with world data attached to it will be shown here if you are nearby." + Environment.NewLine
+ "By default all poses that are shared will be shown. Poses taken in housing areas will by default only be shown on the correct server and location." + Environment.NewLine + Environment.NewLine
+ "Shared World Poses will appear in the world as floating wisps, as well as in the list below. You can mouse over a Shared World Pose in the list for it to get highlighted in the world." + Environment.NewLine + Environment.NewLine
+ "You can apply Shared World Poses to yourself or spawn the associated character to pose with them." + Environment.NewLine + Environment.NewLine
+ "You can adjust the filter and change further settings in the 'Settings & Filter' foldout.");
UiSharedService.DrawTree("Settings & Filters", () =>
{
string filterByUser = _charaDataNearbyManager.UserNoteFilter;
if (ImGui.InputTextWithHint("##filterbyuser", "Filter by User", ref filterByUser, 50))
{
_charaDataNearbyManager.UserNoteFilter = filterByUser;
}
bool onlyCurrent = _configService.Current.NearbyOwnServerOnly;
if (ImGui.Checkbox("Only show Poses on current server", ref onlyCurrent))
{
_configService.Current.NearbyOwnServerOnly = onlyCurrent;
_configService.Save();
}
_uiSharedService.DrawHelpText("Toggling this off will show you the location of all shared Poses with World Data from all Servers");
bool showOwn = _configService.Current.NearbyShowOwnData;
if (ImGui.Checkbox("Also show your own data", ref showOwn))
{
_configService.Current.NearbyShowOwnData = showOwn;
_configService.Save();
}
_uiSharedService.DrawHelpText("Toggling this on will also show you the location of your own Poses");
bool ignoreHousing = _configService.Current.NearbyIgnoreHousingLimitations;
if (ImGui.Checkbox("Ignore Housing Limitations", ref ignoreHousing))
{
_configService.Current.NearbyIgnoreHousingLimitations = ignoreHousing;
_configService.Save();
}
_uiSharedService.DrawHelpText("This will display all poses in their location regardless of housing limitations. (Ignoring Ward, Plot, Room)" + UiSharedService.TooltipSeparator
+ "Note: Poses that utilize housing props, furniture, etc. will not be displayed correctly if not spawned in the right location.");
bool showWisps = _configService.Current.NearbyDrawWisps;
if (ImGui.Checkbox("Show Pose Wisps in the overworld", ref showWisps))
{
_configService.Current.NearbyDrawWisps = showWisps;
_configService.Save();
}
_uiSharedService.DrawHelpText("When enabled, Mare will draw floating wisps where other's poses are in the world.");
int poseDetectionDistance = _configService.Current.NearbyDistanceFilter;
UiSharedService.ScaledNextItemWidth(100);
if (ImGui.SliderInt("Detection Distance", ref poseDetectionDistance, 5, 1000))
{
_configService.Current.NearbyDistanceFilter = poseDetectionDistance;
_configService.Save();
}
_uiSharedService.DrawHelpText("This setting allows you to change the maximum distance in which poses will be shown. Set it to the maximum if you want to see all poses on the current map.");
bool alwaysShow = _configService.Current.NearbyShowAlways;
if (ImGui.Checkbox("Keep active outside Poses Nearby tab", ref alwaysShow))
{
_configService.Current.NearbyShowAlways = alwaysShow;
_configService.Save();
}
_uiSharedService.DrawHelpText("This will allow Mare to continue the calculation of position of wisps etc. active outside of the 'Poses Nearby' tab." + UiSharedService.TooltipSeparator
+ "Note: The wisps etc. will disappear during combat and performing.");
});
if (!_uiSharedService.IsInGpose)
{
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("Spawning and applying pose data is only available in GPose.", ImGuiColors.DalamudYellow);
ImGuiHelpers.ScaledDummy(5);
}
DrawUpdateSharedDataButton();
UiSharedService.DistanceSeparator();
using var child = ImRaii.Child("nearbyPosesChild", new(0, 0), false, ImGuiWindowFlags.AlwaysAutoResize);
ImGuiHelpers.ScaledDummy(3f);
using var indent = ImRaii.PushIndent(5f);
if (_charaDataNearbyManager.NearbyData.Count == 0)
{
UiSharedService.DrawGroupedCenteredColorText("No Shared World Poses found nearby.", ImGuiColors.DalamudYellow);
}
bool wasAnythingHovered = false;
int i = 0;
foreach (var pose in _charaDataNearbyManager.NearbyData.OrderBy(v => v.Value.Distance))
{
using var poseId = ImRaii.PushId("nearbyPose" + (i++));
var pos = ImGui.GetCursorPos();
var circleDiameter = 60f;
var circleOriginX = ImGui.GetWindowContentRegionMax().X - circleDiameter - pos.X;
float circleOffsetY = 0;
UiSharedService.DrawGrouped(() =>
{
string? userNote = _serverConfigurationManager.GetNoteForUid(pose.Key.MetaInfo.Uploader.UID);
var noteText = pose.Key.MetaInfo.IsOwnData ? "YOU" : (userNote == null ? pose.Key.MetaInfo.Uploader.AliasOrUID : $"{userNote} ({pose.Key.MetaInfo.Uploader.AliasOrUID})");
ImGui.TextUnformatted("Pose by");
ImGui.SameLine();
UiSharedService.ColorText(noteText, ImGuiColors.ParsedGreen);
using (ImRaii.Group())
{
UiSharedService.ColorText("Character Data Description", ImGuiColors.DalamudGrey);
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.ExternalLinkAlt, ImGuiColors.DalamudGrey);
}
UiSharedService.AttachToolTip(pose.Key.MetaInfo.Description);
UiSharedService.ColorText("Description", ImGuiColors.DalamudGrey);
ImGui.SameLine();
UiSharedService.TextWrapped(pose.Key.Description ?? "No Pose Description was set", circleOriginX);
var posAfterGroup = ImGui.GetCursorPos();
var groupHeightCenter = (posAfterGroup.Y - pos.Y) / 2;
circleOffsetY = (groupHeightCenter - circleDiameter / 2);
if (circleOffsetY < 0) circleOffsetY = 0;
ImGui.SetCursorPos(new Vector2(circleOriginX, pos.Y));
ImGui.Dummy(new Vector2(circleDiameter, circleDiameter));
UiSharedService.AttachToolTip("Click to open corresponding map and set map marker" + UiSharedService.TooltipSeparator
+ pose.Key.WorldDataDescriptor);
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
_dalamudUtilService.SetMarkerAndOpenMap(pose.Key.Position, pose.Key.Map);
}
ImGui.SetCursorPos(posAfterGroup);
if (_uiSharedService.IsInGpose)
{
GposePoseAction(() =>
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Apply Pose"))
{
_charaDataManager.ApplyFullPoseDataToGposeTarget(pose.Key);
}
}, $"Apply pose and position to {CharaName(_gposeTarget)}", _hasValidGposeTarget);
ImGui.SameLine();
GposeMetaInfoAction((_) =>
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Spawn and Pose"))
{
_charaDataManager.SpawnAndApplyWorldTransform(pose.Key.MetaInfo, pose.Key);
}
}, "Spawn actor and apply pose and position", pose.Key.MetaInfo, _hasValidGposeTarget, true);
}
});
if (ImGui.IsItemHovered())
{
wasAnythingHovered = true;
_nearbyHovered = pose.Key;
}
var drawList = ImGui.GetWindowDrawList();
var circleRadius = circleDiameter / 2f;
var windowPos = ImGui.GetWindowPos();
var scrollX = ImGui.GetScrollX();
var scrollY = ImGui.GetScrollY();
var circleCenter = new Vector2(windowPos.X + circleOriginX + circleRadius - scrollX, windowPos.Y + pos.Y + circleRadius + circleOffsetY - scrollY);
var rads = pose.Value.Direction * (Math.PI / 180);
float halfConeAngleRadians = 15f * (float)Math.PI / 180f;
Vector2 baseDir1 = new Vector2((float)Math.Sin(rads - halfConeAngleRadians), -(float)Math.Cos(rads - halfConeAngleRadians));
Vector2 baseDir2 = new Vector2((float)Math.Sin(rads + halfConeAngleRadians), -(float)Math.Cos(rads + halfConeAngleRadians));
Vector2 coneBase1 = circleCenter + baseDir1 * circleRadius;
Vector2 coneBase2 = circleCenter + baseDir2 * circleRadius;
// Draw the cone as a filled triangle
drawList.AddTriangleFilled(circleCenter, coneBase1, coneBase2, UiSharedService.Color(ImGuiColors.ParsedGreen));
drawList.AddCircle(circleCenter, circleDiameter / 2, UiSharedService.Color(ImGuiColors.DalamudWhite), 360, 2);
var distance = pose.Value.Distance.ToString("0.0") + "y";
var textSize = ImGui.CalcTextSize(distance);
drawList.AddText(new Vector2(circleCenter.X - textSize.X / 2, circleCenter.Y + textSize.Y / 3f), UiSharedService.Color(ImGuiColors.DalamudWhite), distance);
ImGuiHelpers.ScaledDummy(3);
}
if (!wasAnythingHovered) _nearbyHovered = null;
_charaDataNearbyManager.SetHoveredVfx(_nearbyHovered);
}
private void DrawUpdateSharedDataButton()
{
using (ImRaii.Disabled(_charaDataManager.GetAllDataTask != null
|| (_charaDataManager.GetSharedWithYouTimeoutTask != null && !_charaDataManager.GetSharedWithYouTimeoutTask.IsCompleted)))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleDown, "Update Data Shared With You"))
{
_ = _charaDataManager.GetAllSharedData(_disposalCts.Token).ContinueWith(u => UpdateFilteredItems());
}
}
if (_charaDataManager.GetSharedWithYouTimeoutTask != null && !_charaDataManager.GetSharedWithYouTimeoutTask.IsCompleted)
{
UiSharedService.AttachToolTip("You can only refresh all character data from server every minute. Please wait.");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,640 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Utility;
using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.Group;
using LightlessSync.Interop.Ipc;
using LightlessSync.MareConfiguration;
using LightlessSync.PlayerData.Handlers;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Components;
using LightlessSync.UI.Handlers;
using LightlessSync.WebAPI;
using LightlessSync.WebAPI.Files;
using LightlessSync.WebAPI.Files.Models;
using LightlessSync.WebAPI.SignalR.Utils;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Globalization;
using System.Numerics;
using System.Reflection;
namespace LightlessSync.UI;
public class CompactUi : WindowMediatorSubscriberBase
{
private readonly ApiController _apiController;
private readonly MareConfigService _configService;
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
private readonly DrawEntityFactory _drawEntityFactory;
private readonly FileUploadManager _fileTransferManager;
private readonly PairManager _pairManager;
private readonly SelectTagForPairUi _selectGroupForPairUi;
private readonly SelectPairForTagUi _selectPairsForGroupUi;
private readonly IpcManager _ipcManager;
private readonly ServerConfigurationManager _serverManager;
private readonly TopTabMenu _tabMenu;
private readonly TagHandler _tagHandler;
private readonly UiSharedService _uiSharedService;
private List<IDrawFolder> _drawFolders;
private Pair? _lastAddedUser;
private string _lastAddedUserComment = string.Empty;
private Vector2 _lastPosition = Vector2.One;
private Vector2 _lastSize = Vector2.One;
private int _secretKeyIdx = -1;
private bool _showModalForUserAddition;
private float _transferPartHeight;
private bool _wasOpen;
private float _windowContentWidth;
public CompactUi(ILogger<CompactUi> logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager,
ServerConfigurationManager serverManager, MareMediator mediator, FileUploadManager fileTransferManager,
TagHandler tagHandler, DrawEntityFactory drawEntityFactory, SelectTagForPairUi selectTagForPairUi, SelectPairForTagUi selectPairForTagUi,
PerformanceCollectorService performanceCollectorService, IpcManager ipcManager)
: base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
{
_uiSharedService = uiShared;
_configService = configService;
_apiController = apiController;
_pairManager = pairManager;
_serverManager = serverManager;
_fileTransferManager = fileTransferManager;
_tagHandler = tagHandler;
_drawEntityFactory = drawEntityFactory;
_selectGroupForPairUi = selectTagForPairUi;
_selectPairsForGroupUi = selectPairForTagUi;
_ipcManager = ipcManager;
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService);
AllowPinning = false;
AllowClickthrough = false;
TitleBarButtons = new()
{
new TitleBarButton()
{
Icon = FontAwesomeIcon.Cog,
Click = (msg) =>
{
Mediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
},
IconOffset = new(2,1),
ShowTooltip = () =>
{
ImGui.BeginTooltip();
ImGui.Text("Open Mare Settings");
ImGui.EndTooltip();
}
},
new TitleBarButton()
{
Icon = FontAwesomeIcon.Book,
Click = (msg) =>
{
Mediator.Publish(new UiToggleMessage(typeof(EventViewerUI)));
},
IconOffset = new(2,1),
ShowTooltip = () =>
{
ImGui.BeginTooltip();
ImGui.Text("Open Mare Event Viewer");
ImGui.EndTooltip();
}
}
};
_drawFolders = GetDrawFolders().ToList();
#if DEBUG
string dev = "Dev Build";
var ver = Assembly.GetExecutingAssembly().GetName().Version!;
WindowName = $"Mare Synchronos {dev} ({ver.Major}.{ver.Minor}.{ver.Build})###LightlessSyncMainUI";
Toggle();
#else
var ver = Assembly.GetExecutingAssembly().GetName().Version;
WindowName = "Mare Synchronos " + ver.Major + "." + ver.Minor + "." + ver.Build + "###LightlessSyncMainUI";
#endif
Mediator.Subscribe<SwitchToMainUiMessage>(this, (_) => IsOpen = true);
Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) => IsOpen = false);
Mediator.Subscribe<CutsceneStartMessage>(this, (_) => UiSharedService_GposeStart());
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd());
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
Mediator.Subscribe<RefreshUiMessage>(this, (msg) => _drawFolders = GetDrawFolders().ToList());
Flags |= ImGuiWindowFlags.NoDocking;
SizeConstraints = new WindowSizeConstraints()
{
MinimumSize = new Vector2(375, 400),
MaximumSize = new Vector2(375, 2000),
};
}
protected override void DrawInternal()
{
_windowContentWidth = UiSharedService.GetWindowContentRegionWidth();
if (!_apiController.IsCurrentVersion)
{
var ver = _apiController.CurrentClientVersion;
var unsupported = "UNSUPPORTED VERSION";
using (_uiSharedService.UidFont.Push())
{
var uidTextSize = ImGui.CalcTextSize(unsupported);
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X + ImGui.GetWindowContentRegionMin().X) / 2 - uidTextSize.X / 2);
ImGui.AlignTextToFramePadding();
ImGui.TextColored(ImGuiColors.DalamudRed, unsupported);
}
UiSharedService.ColorTextWrapped($"Your Mare Synchronos installation is out of date, the current version is {ver.Major}.{ver.Minor}.{ver.Build}. " +
$"It is highly recommended to keep Mare Synchronos up to date. Open /xlplugins and update the plugin.", ImGuiColors.DalamudRed);
}
if (!_ipcManager.Initialized)
{
var unsupported = "MISSING ESSENTIAL PLUGINS";
using (_uiSharedService.UidFont.Push())
{
var uidTextSize = ImGui.CalcTextSize(unsupported);
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X + ImGui.GetWindowContentRegionMin().X) / 2 - uidTextSize.X / 2);
ImGui.AlignTextToFramePadding();
ImGui.TextColored(ImGuiColors.DalamudRed, unsupported);
}
var penumAvailable = _ipcManager.Penumbra.APIAvailable;
var glamAvailable = _ipcManager.Glamourer.APIAvailable;
UiSharedService.ColorTextWrapped($"One or more Plugins essential for Mare operation are unavailable. Enable or update following plugins:", ImGuiColors.DalamudRed);
using var indent = ImRaii.PushIndent(10f);
if (!penumAvailable)
{
UiSharedService.TextWrapped("Penumbra");
_uiSharedService.BooleanToColoredIcon(penumAvailable, true);
}
if (!glamAvailable)
{
UiSharedService.TextWrapped("Glamourer");
_uiSharedService.BooleanToColoredIcon(glamAvailable, true);
}
ImGui.Separator();
}
using (ImRaii.PushId("header")) DrawUIDHeader();
ImGui.Separator();
using (ImRaii.PushId("serverstatus")) DrawServerStatus();
ImGui.Separator();
if (_apiController.ServerState is ServerState.Connected)
{
using (ImRaii.PushId("global-topmenu")) _tabMenu.Draw();
using (ImRaii.PushId("pairlist")) DrawPairs();
ImGui.Separator();
float pairlistEnd = ImGui.GetCursorPosY();
using (ImRaii.PushId("transfers")) DrawTransfers();
_transferPartHeight = ImGui.GetCursorPosY() - pairlistEnd - ImGui.GetTextLineHeight();
using (ImRaii.PushId("group-user-popup")) _selectPairsForGroupUi.Draw(_pairManager.DirectPairs);
using (ImRaii.PushId("grouping-popup")) _selectGroupForPairUi.Draw();
}
if (_configService.Current.OpenPopupOnAdd && _pairManager.LastAddedUser != null)
{
_lastAddedUser = _pairManager.LastAddedUser;
_pairManager.LastAddedUser = null;
ImGui.OpenPopup("Set Notes for New User");
_showModalForUserAddition = true;
_lastAddedUserComment = string.Empty;
}
if (ImGui.BeginPopupModal("Set Notes for New User", ref _showModalForUserAddition, UiSharedService.PopupWindowFlags))
{
if (_lastAddedUser == null)
{
_showModalForUserAddition = false;
}
else
{
UiSharedService.TextWrapped($"You have successfully added {_lastAddedUser.UserData.AliasOrUID}. Set a local note for the user in the field below:");
ImGui.InputTextWithHint("##noteforuser", $"Note for {_lastAddedUser.UserData.AliasOrUID}", ref _lastAddedUserComment, 100);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Note"))
{
_serverManager.SetNoteForUid(_lastAddedUser.UserData.UID, _lastAddedUserComment);
_lastAddedUser = null;
_lastAddedUserComment = string.Empty;
_showModalForUserAddition = false;
}
}
UiSharedService.SetScaledWindowSize(275);
ImGui.EndPopup();
}
var pos = ImGui.GetWindowPos();
var size = ImGui.GetWindowSize();
if (_lastSize != size || _lastPosition != pos)
{
_lastSize = size;
_lastPosition = pos;
Mediator.Publish(new CompactUiChange(_lastSize, _lastPosition));
}
}
private void DrawPairs()
{
var ySize = _transferPartHeight == 0
? 1
: (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y
+ ImGui.GetTextLineHeight() - ImGui.GetStyle().WindowPadding.Y - ImGui.GetStyle().WindowBorderSize) - _transferPartHeight - ImGui.GetCursorPosY();
ImGui.BeginChild("list", new Vector2(_windowContentWidth, ySize), border: false);
foreach (var item in _drawFolders)
{
item.Draw();
}
ImGui.EndChild();
}
private void DrawServerStatus()
{
var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Link);
var userCount = _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture);
var userSize = ImGui.CalcTextSize(userCount);
var textSize = ImGui.CalcTextSize("Users Online");
#if DEBUG
string shardConnection = $"Shard: {_apiController.ServerInfo.ShardName}";
#else
string shardConnection = string.Equals(_apiController.ServerInfo.ShardName, "Main", StringComparison.OrdinalIgnoreCase) ? string.Empty : $"Shard: {_apiController.ServerInfo.ShardName}";
#endif
var shardTextSize = ImGui.CalcTextSize(shardConnection);
var printShard = !string.IsNullOrEmpty(_apiController.ServerInfo.ShardName) && shardConnection != string.Empty;
if (_apiController.ServerState is ServerState.Connected)
{
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth()) / 2 - (userSize.X + textSize.X) / 2 - ImGui.GetStyle().ItemSpacing.X / 2);
if (!printShard) ImGui.AlignTextToFramePadding();
ImGui.TextColored(ImGuiColors.ParsedGreen, userCount);
ImGui.SameLine();
if (!printShard) ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Users Online");
}
else
{
ImGui.AlignTextToFramePadding();
ImGui.TextColored(ImGuiColors.DalamudRed, "Not connected to any server");
}
if (printShard)
{
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetStyle().ItemSpacing.Y);
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth()) / 2 - shardTextSize.X / 2);
ImGui.TextUnformatted(shardConnection);
}
ImGui.SameLine();
if (printShard)
{
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((userSize.Y + textSize.Y) / 2 + shardTextSize.Y) / 2 - ImGui.GetStyle().ItemSpacing.Y + buttonSize.Y / 2);
}
bool isConnectingOrConnected = _apiController.ServerState is ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting;
var color = UiSharedService.GetBoolColor(!isConnectingOrConnected);
var connectedIcon = isConnectingOrConnected ? FontAwesomeIcon.Unlink : FontAwesomeIcon.Link;
ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X);
if (printShard)
{
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((userSize.Y + textSize.Y) / 2 + shardTextSize.Y) / 2 - ImGui.GetStyle().ItemSpacing.Y + buttonSize.Y / 2);
}
if (_apiController.ServerState is not (ServerState.Reconnecting or ServerState.Disconnecting))
{
using (ImRaii.PushColor(ImGuiCol.Text, color))
{
if (_uiSharedService.IconButton(connectedIcon))
{
if (isConnectingOrConnected && !_serverManager.CurrentServer.FullPause)
{
_serverManager.CurrentServer.FullPause = true;
_serverManager.Save();
}
else if (!isConnectingOrConnected && _serverManager.CurrentServer.FullPause)
{
_serverManager.CurrentServer.FullPause = false;
_serverManager.Save();
}
_ = _apiController.CreateConnectionsAsync();
}
}
UiSharedService.AttachToolTip(isConnectingOrConnected ? "Disconnect from " + _serverManager.CurrentServer.ServerName : "Connect to " + _serverManager.CurrentServer.ServerName);
}
}
private void DrawTransfers()
{
var currentUploads = _fileTransferManager.CurrentUploads.ToList();
ImGui.AlignTextToFramePadding();
_uiSharedService.IconText(FontAwesomeIcon.Upload);
ImGui.SameLine(35 * ImGuiHelpers.GlobalScale);
if (currentUploads.Any())
{
var totalUploads = currentUploads.Count;
var doneUploads = currentUploads.Count(c => c.IsTransferred);
var totalUploaded = currentUploads.Sum(c => c.Transferred);
var totalToUpload = currentUploads.Sum(c => c.Total);
ImGui.TextUnformatted($"{doneUploads}/{totalUploads}");
var uploadText = $"({UiSharedService.ByteToString(totalUploaded)}/{UiSharedService.ByteToString(totalToUpload)})";
var textSize = ImGui.CalcTextSize(uploadText);
ImGui.SameLine(_windowContentWidth - textSize.X);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(uploadText);
}
else
{
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("No uploads in progress");
}
var currentDownloads = _currentDownloads.SelectMany(d => d.Value.Values).ToList();
ImGui.AlignTextToFramePadding();
_uiSharedService.IconText(FontAwesomeIcon.Download);
ImGui.SameLine(35 * ImGuiHelpers.GlobalScale);
if (currentDownloads.Any())
{
var totalDownloads = currentDownloads.Sum(c => c.TotalFiles);
var doneDownloads = currentDownloads.Sum(c => c.TransferredFiles);
var totalDownloaded = currentDownloads.Sum(c => c.TransferredBytes);
var totalToDownload = currentDownloads.Sum(c => c.TotalBytes);
ImGui.TextUnformatted($"{doneDownloads}/{totalDownloads}");
var downloadText =
$"({UiSharedService.ByteToString(totalDownloaded)}/{UiSharedService.ByteToString(totalToDownload)})";
var textSize = ImGui.CalcTextSize(downloadText);
ImGui.SameLine(_windowContentWidth - textSize.X);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(downloadText);
}
else
{
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("No downloads in progress");
}
}
private void DrawUIDHeader()
{
var uidText = GetUidText();
using (_uiSharedService.UidFont.Push())
{
var uidTextSize = ImGui.CalcTextSize(uidText);
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) / 2 - (uidTextSize.X / 2));
ImGui.TextColored(GetUidColor(), uidText);
}
if (_apiController.ServerState is ServerState.Connected)
{
if (ImGui.IsItemClicked())
{
ImGui.SetClipboardText(_apiController.DisplayName);
}
UiSharedService.AttachToolTip("Click to copy");
if (!string.Equals(_apiController.DisplayName, _apiController.UID, StringComparison.Ordinal))
{
var origTextSize = ImGui.CalcTextSize(_apiController.UID);
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) / 2 - (origTextSize.X / 2));
ImGui.TextColored(GetUidColor(), _apiController.UID);
if (ImGui.IsItemClicked())
{
ImGui.SetClipboardText(_apiController.UID);
}
UiSharedService.AttachToolTip("Click to copy");
}
}
else
{
UiSharedService.ColorTextWrapped(GetServerError(), GetUidColor());
}
}
private IEnumerable<IDrawFolder> GetDrawFolders()
{
List<IDrawFolder> drawFolders = [];
var allPairs = _pairManager.PairsWithGroups
.ToDictionary(k => k.Key, k => k.Value);
var filteredPairs = allPairs
.Where(p =>
{
if (_tabMenu.Filter.IsNullOrEmpty()) return true;
return p.Key.UserData.AliasOrUID.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) ||
(p.Key.GetNote()?.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) ?? false) ||
(p.Key.PlayerName?.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) ?? false);
})
.ToDictionary(k => k.Key, k => k.Value);
string? AlphabeticalSort(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
=> (_configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(u.Key.PlayerName)
? (_configService.Current.PreferNotesOverNamesForVisible ? u.Key.GetNote() : u.Key.PlayerName)
: (u.Key.GetNote() ?? u.Key.UserData.AliasOrUID));
bool FilterOnlineOrPausedSelf(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
=> (u.Key.IsOnline || (!u.Key.IsOnline && !_configService.Current.ShowOfflineUsersSeparately)
|| u.Key.UserPair.OwnPermissions.IsPaused());
Dictionary<Pair, List<GroupFullInfoDto>> BasicSortedDictionary(IEnumerable<KeyValuePair<Pair, List<GroupFullInfoDto>>> u)
=> u.OrderByDescending(u => u.Key.IsVisible)
.ThenByDescending(u => u.Key.IsOnline)
.ThenBy(AlphabeticalSort, StringComparer.OrdinalIgnoreCase)
.ToDictionary(u => u.Key, u => u.Value);
ImmutableList<Pair> ImmutablePairList(IEnumerable<KeyValuePair<Pair, List<GroupFullInfoDto>>> u)
=> u.Select(k => k.Key).ToImmutableList();
bool FilterVisibleUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
=> u.Key.IsVisible
&& (_configService.Current.ShowSyncshellUsersInVisible || !(!_configService.Current.ShowSyncshellUsersInVisible && !u.Key.IsDirectlyPaired));
bool FilterTagusers(KeyValuePair<Pair, List<GroupFullInfoDto>> u, string tag)
=> u.Key.IsDirectlyPaired && !u.Key.IsOneSidedPair && _tagHandler.HasTag(u.Key.UserData.UID, tag);
bool FilterGroupUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u, GroupFullInfoDto group)
=> u.Value.Exists(g => string.Equals(g.GID, group.GID, StringComparison.Ordinal));
bool FilterNotTaggedUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
=> u.Key.IsDirectlyPaired && !u.Key.IsOneSidedPair && !_tagHandler.HasAnyTag(u.Key.UserData.UID);
bool FilterOfflineUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
=> ((u.Key.IsDirectlyPaired && _configService.Current.ShowSyncshellOfflineUsersSeparately)
|| !_configService.Current.ShowSyncshellOfflineUsersSeparately)
&& (!u.Key.IsOneSidedPair || u.Value.Any()) && !u.Key.IsOnline && !u.Key.UserPair.OwnPermissions.IsPaused();
bool FilterOfflineSyncshellUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
=> (!u.Key.IsDirectlyPaired && !u.Key.IsOnline && !u.Key.UserPair.OwnPermissions.IsPaused());
if (_configService.Current.ShowVisibleUsersSeparately)
{
var allVisiblePairs = ImmutablePairList(allPairs
.Where(FilterVisibleUsers));
var filteredVisiblePairs = BasicSortedDictionary(filteredPairs
.Where(FilterVisibleUsers));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomVisibleTag, filteredVisiblePairs, allVisiblePairs));
}
List<IDrawFolder> groupFolders = new();
foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase))
{
var allGroupPairs = ImmutablePairList(allPairs
.Where(u => FilterGroupUsers(u, group)));
var filteredGroupPairs = filteredPairs
.Where(u => FilterGroupUsers(u, group) && FilterOnlineOrPausedSelf(u))
.OrderByDescending(u => u.Key.IsOnline)
.ThenBy(u =>
{
if (string.Equals(u.Key.UserData.UID, group.OwnerUID, StringComparison.Ordinal)) return 0;
if (group.GroupPairUserInfos.TryGetValue(u.Key.UserData.UID, out var info))
{
if (info.IsModerator()) return 1;
if (info.IsPinned()) return 2;
}
return u.Key.IsVisible ? 3 : 4;
})
.ThenBy(AlphabeticalSort, StringComparer.OrdinalIgnoreCase)
.ToDictionary(k => k.Key, k => k.Value);
groupFolders.Add(_drawEntityFactory.CreateDrawGroupFolder(group, filteredGroupPairs, allGroupPairs));
}
if (_configService.Current.GroupUpSyncshells)
drawFolders.Add(new DrawGroupedGroupFolder(groupFolders, _tagHandler, _uiSharedService));
else
drawFolders.AddRange(groupFolders);
var tags = _tagHandler.GetAllTagsSorted();
foreach (var tag in tags)
{
var allTagPairs = ImmutablePairList(allPairs
.Where(u => FilterTagusers(u, tag)));
var filteredTagPairs = BasicSortedDictionary(filteredPairs
.Where(u => FilterTagusers(u, tag) && FilterOnlineOrPausedSelf(u)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(tag, filteredTagPairs, allTagPairs));
}
var allOnlineNotTaggedPairs = ImmutablePairList(allPairs
.Where(FilterNotTaggedUsers));
var onlineNotTaggedPairs = BasicSortedDictionary(filteredPairs
.Where(u => FilterNotTaggedUsers(u) && FilterOnlineOrPausedSelf(u)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder((_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag),
onlineNotTaggedPairs, allOnlineNotTaggedPairs));
if (_configService.Current.ShowOfflineUsersSeparately)
{
var allOfflinePairs = ImmutablePairList(allPairs
.Where(FilterOfflineUsers));
var filteredOfflinePairs = BasicSortedDictionary(filteredPairs
.Where(FilterOfflineUsers));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineTag, filteredOfflinePairs, allOfflinePairs));
if (_configService.Current.ShowSyncshellOfflineUsersSeparately)
{
var allOfflineSyncshellUsers = ImmutablePairList(allPairs
.Where(FilterOfflineSyncshellUsers));
var filteredOfflineSyncshellUsers = BasicSortedDictionary(filteredPairs
.Where(FilterOfflineSyncshellUsers));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineSyncshellTag,
filteredOfflineSyncshellUsers,
allOfflineSyncshellUsers));
}
}
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomUnpairedTag,
BasicSortedDictionary(filteredPairs.Where(u => u.Key.IsOneSidedPair)),
ImmutablePairList(allPairs.Where(u => u.Key.IsOneSidedPair))));
return drawFolders;
}
private string GetServerError()
{
return _apiController.ServerState switch
{
ServerState.Connecting => "Attempting to connect to the server.",
ServerState.Reconnecting => "Connection to server interrupted, attempting to reconnect to the server.",
ServerState.Disconnected => "You are currently disconnected from the Mare Synchronos server.",
ServerState.Disconnecting => "Disconnecting from the server",
ServerState.Unauthorized => "Server Response: " + _apiController.AuthFailureMessage,
ServerState.Offline => "Your selected Mare Synchronos server is currently offline.",
ServerState.VersionMisMatch =>
"Your plugin or the server you are connecting to is out of date. Please update your plugin now. If you already did so, contact the server provider to update their server to the latest version.",
ServerState.RateLimited => "You are rate limited for (re)connecting too often. Disconnect, wait 10 minutes and try again.",
ServerState.Connected => string.Empty,
ServerState.NoSecretKey => "You have no secret key set for this current character. Open Settings -> Service Settings and set a secret key for the current character. You can reuse the same secret key for multiple characters.",
ServerState.MultiChara => "Your Character Configuration has multiple characters configured with same name and world. You will not be able to connect until you fix this issue. Remove the duplicates from the configuration in Settings -> Service Settings -> Character Management and reconnect manually after.",
ServerState.OAuthMisconfigured => "OAuth2 is enabled but not fully configured, verify in the Settings -> Service Settings that you have OAuth2 connected and, importantly, a UID assigned to your current character.",
ServerState.OAuthLoginTokenStale => "Your OAuth2 login token is stale and cannot be used to renew. Go to the Settings -> Service Settings and unlink then relink your OAuth2 configuration.",
ServerState.NoAutoLogon => "This character has automatic login into Mare disabled. Press the connect button to connect to Mare.",
_ => string.Empty
};
}
private Vector4 GetUidColor()
{
return _apiController.ServerState switch
{
ServerState.Connecting => ImGuiColors.DalamudYellow,
ServerState.Reconnecting => ImGuiColors.DalamudRed,
ServerState.Connected => ImGuiColors.ParsedGreen,
ServerState.Disconnected => ImGuiColors.DalamudYellow,
ServerState.Disconnecting => ImGuiColors.DalamudYellow,
ServerState.Unauthorized => ImGuiColors.DalamudRed,
ServerState.VersionMisMatch => ImGuiColors.DalamudRed,
ServerState.Offline => ImGuiColors.DalamudRed,
ServerState.RateLimited => ImGuiColors.DalamudYellow,
ServerState.NoSecretKey => ImGuiColors.DalamudYellow,
ServerState.MultiChara => ImGuiColors.DalamudYellow,
ServerState.OAuthMisconfigured => ImGuiColors.DalamudRed,
ServerState.OAuthLoginTokenStale => ImGuiColors.DalamudRed,
ServerState.NoAutoLogon => ImGuiColors.DalamudYellow,
_ => ImGuiColors.DalamudRed
};
}
private string GetUidText()
{
return _apiController.ServerState switch
{
ServerState.Reconnecting => "Reconnecting",
ServerState.Connecting => "Connecting",
ServerState.Disconnected => "Disconnected",
ServerState.Disconnecting => "Disconnecting",
ServerState.Unauthorized => "Unauthorized",
ServerState.VersionMisMatch => "Version mismatch",
ServerState.Offline => "Unavailable",
ServerState.RateLimited => "Rate Limited",
ServerState.NoSecretKey => "No Secret Key",
ServerState.MultiChara => "Duplicate Characters",
ServerState.OAuthMisconfigured => "Misconfigured OAuth2",
ServerState.OAuthLoginTokenStale => "Stale OAuth2",
ServerState.NoAutoLogon => "Auto Login disabled",
ServerState.Connected => _apiController.DisplayName,
_ => string.Empty
};
}
private void UiSharedService_GposeEnd()
{
IsOpen = _wasOpen;
}
private void UiSharedService_GposeStart()
{
_wasOpen = IsOpen;
IsOpen = false;
}
}

View File

@@ -0,0 +1,129 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.UI.Handlers;
using System.Collections.Immutable;
namespace LightlessSync.UI.Components;
public abstract class DrawFolderBase : IDrawFolder
{
public IImmutableList<DrawUserPair> DrawPairs { get; init; }
protected readonly string _id;
protected readonly IImmutableList<Pair> _allPairs;
protected readonly TagHandler _tagHandler;
protected readonly UiSharedService _uiSharedService;
private float _menuWidth = -1;
public int OnlinePairs => DrawPairs.Count(u => u.Pair.IsOnline);
public int TotalPairs => _allPairs.Count;
private bool _wasHovered = false;
protected DrawFolderBase(string id, IImmutableList<DrawUserPair> drawPairs,
IImmutableList<Pair> allPairs, TagHandler tagHandler, UiSharedService uiSharedService)
{
_id = id;
DrawPairs = drawPairs;
_allPairs = allPairs;
_tagHandler = tagHandler;
_uiSharedService = uiSharedService;
}
protected abstract bool RenderIfEmpty { get; }
protected abstract bool RenderMenu { get; }
public void Draw()
{
if (!RenderIfEmpty && !DrawPairs.Any()) return;
using var id = ImRaii.PushId("folder_" + _id);
var color = ImRaii.PushColor(ImGuiCol.ChildBg, ImGui.GetColorU32(ImGuiCol.FrameBgHovered), _wasHovered);
using (ImRaii.Child("folder__" + _id, new System.Numerics.Vector2(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetCursorPosX(), ImGui.GetFrameHeight())))
{
// draw opener
var icon = _tagHandler.IsTagOpen(_id) ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight;
ImGui.AlignTextToFramePadding();
_uiSharedService.IconText(icon);
if (ImGui.IsItemClicked())
{
_tagHandler.SetTagOpen(_id, !_tagHandler.IsTagOpen(_id));
}
ImGui.SameLine();
var leftSideEnd = DrawIcon();
ImGui.SameLine();
var rightSideStart = DrawRightSideInternal();
// draw name
ImGui.SameLine(leftSideEnd);
DrawName(rightSideStart - leftSideEnd);
}
_wasHovered = ImGui.IsItemHovered();
color.Dispose();
ImGui.Separator();
// if opened draw content
if (_tagHandler.IsTagOpen(_id))
{
using var indent = ImRaii.PushIndent(_uiSharedService.GetIconSize(FontAwesomeIcon.EllipsisV).X + ImGui.GetStyle().ItemSpacing.X, false);
if (DrawPairs.Any())
{
foreach (var item in DrawPairs)
{
item.DrawPairedClient();
}
}
else
{
ImGui.TextUnformatted("No users (online)");
}
ImGui.Separator();
}
}
protected abstract float DrawIcon();
protected abstract void DrawMenu(float menuWidth);
protected abstract void DrawName(float width);
protected abstract float DrawRightSide(float currentRightSideX);
private float DrawRightSideInternal()
{
var barButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.EllipsisV);
var spacingX = ImGui.GetStyle().ItemSpacing.X;
var windowEndX = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth();
// Flyout Menu
var rightSideStart = windowEndX - (RenderMenu ? (barButtonSize.X + spacingX) : spacingX);
if (RenderMenu)
{
ImGui.SameLine(windowEndX - barButtonSize.X);
if (_uiSharedService.IconButton(FontAwesomeIcon.EllipsisV))
{
ImGui.OpenPopup("User Flyout Menu");
}
if (ImGui.BeginPopup("User Flyout Menu"))
{
using (ImRaii.PushId($"buttons-{_id}")) DrawMenu(_menuWidth);
_menuWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
ImGui.EndPopup();
}
else
{
_menuWidth = 0;
}
}
return DrawRightSide(rightSideStart);
}
}

View File

@@ -0,0 +1,244 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.Group;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator;
using LightlessSync.UI.Handlers;
using LightlessSync.WebAPI;
using System.Collections.Immutable;
namespace LightlessSync.UI.Components;
public class DrawFolderGroup : DrawFolderBase
{
private readonly ApiController _apiController;
private readonly GroupFullInfoDto _groupFullInfoDto;
private readonly IdDisplayHandler _idDisplayHandler;
private readonly MareMediator _mareMediator;
public DrawFolderGroup(string id, GroupFullInfoDto groupFullInfoDto, ApiController apiController,
IImmutableList<DrawUserPair> drawPairs, IImmutableList<Pair> allPairs, TagHandler tagHandler, IdDisplayHandler idDisplayHandler,
MareMediator mareMediator, UiSharedService uiSharedService) :
base(id, drawPairs, allPairs, tagHandler, uiSharedService)
{
_groupFullInfoDto = groupFullInfoDto;
_apiController = apiController;
_idDisplayHandler = idDisplayHandler;
_mareMediator = mareMediator;
}
protected override bool RenderIfEmpty => true;
protected override bool RenderMenu => true;
private bool IsModerator => IsOwner || _groupFullInfoDto.GroupUserInfo.IsModerator();
private bool IsOwner => string.Equals(_groupFullInfoDto.OwnerUID, _apiController.UID, StringComparison.Ordinal);
private bool IsPinned => _groupFullInfoDto.GroupUserInfo.IsPinned();
protected override float DrawIcon()
{
ImGui.AlignTextToFramePadding();
_uiSharedService.IconText(_groupFullInfoDto.GroupPermissions.IsDisableInvites() ? FontAwesomeIcon.Lock : FontAwesomeIcon.Users);
if (_groupFullInfoDto.GroupPermissions.IsDisableInvites())
{
UiSharedService.AttachToolTip("Syncshell " + _groupFullInfoDto.GroupAliasOrGID + " is closed for invites");
}
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemSpacing.X / 2f }))
{
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("[" + OnlinePairs.ToString() + "]");
}
UiSharedService.AttachToolTip(OnlinePairs + " online" + Environment.NewLine + TotalPairs + " total");
ImGui.SameLine();
if (IsOwner)
{
ImGui.AlignTextToFramePadding();
_uiSharedService.IconText(FontAwesomeIcon.Crown);
UiSharedService.AttachToolTip("You are the owner of " + _groupFullInfoDto.GroupAliasOrGID);
}
else if (IsModerator)
{
ImGui.AlignTextToFramePadding();
_uiSharedService.IconText(FontAwesomeIcon.UserShield);
UiSharedService.AttachToolTip("You are a moderator in " + _groupFullInfoDto.GroupAliasOrGID);
}
else if (IsPinned)
{
ImGui.AlignTextToFramePadding();
_uiSharedService.IconText(FontAwesomeIcon.Thumbtack);
UiSharedService.AttachToolTip("You are pinned in " + _groupFullInfoDto.GroupAliasOrGID);
}
ImGui.SameLine();
return ImGui.GetCursorPosX();
}
protected override void DrawMenu(float menuWidth)
{
ImGui.TextUnformatted("Syncshell Menu (" + _groupFullInfoDto.GroupAliasOrGID + ")");
ImGui.Separator();
ImGui.TextUnformatted("General Syncshell Actions");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Copy, "Copy ID", menuWidth, true))
{
ImGui.CloseCurrentPopup();
ImGui.SetClipboardText(_groupFullInfoDto.GroupAliasOrGID);
}
UiSharedService.AttachToolTip("Copy Syncshell ID to Clipboard");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.StickyNote, "Copy Notes", menuWidth, true))
{
ImGui.CloseCurrentPopup();
ImGui.SetClipboardText(UiSharedService.GetNotes(DrawPairs.Select(k => k.Pair).ToList()));
}
UiSharedService.AttachToolTip("Copies all your notes for all users in this Syncshell to the clipboard." + Environment.NewLine + "They can be imported via Settings -> General -> Notes -> Import notes from clipboard");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleLeft, "Leave Syncshell", menuWidth, true) && UiSharedService.CtrlPressed())
{
_ = _apiController.GroupLeave(_groupFullInfoDto);
ImGui.CloseCurrentPopup();
}
UiSharedService.AttachToolTip("Hold CTRL and click to leave this Syncshell" + (!string.Equals(_groupFullInfoDto.OwnerUID, _apiController.UID, StringComparison.Ordinal)
? string.Empty : Environment.NewLine + "WARNING: This action is irreversible" + Environment.NewLine + "Leaving an owned Syncshell will transfer the ownership to a random person in the Syncshell."));
ImGui.Separator();
ImGui.TextUnformatted("Permission Settings");
var perm = _groupFullInfoDto.GroupUserPermissions;
bool disableSounds = perm.IsDisableSounds();
bool disableAnims = perm.IsDisableAnimations();
bool disableVfx = perm.IsDisableVFX();
if ((_groupFullInfoDto.GroupPermissions.IsPreferDisableAnimations() != disableAnims
|| _groupFullInfoDto.GroupPermissions.IsPreferDisableSounds() != disableSounds
|| _groupFullInfoDto.GroupPermissions.IsPreferDisableVFX() != disableVfx)
&& _uiSharedService.IconTextButton(FontAwesomeIcon.Check, "Align with suggested permissions", menuWidth, true))
{
perm.SetDisableVFX(_groupFullInfoDto.GroupPermissions.IsPreferDisableVFX());
perm.SetDisableSounds(_groupFullInfoDto.GroupPermissions.IsPreferDisableSounds());
perm.SetDisableAnimations(_groupFullInfoDto.GroupPermissions.IsPreferDisableAnimations());
_ = _apiController.GroupChangeIndividualPermissionState(new(_groupFullInfoDto.Group, new(_apiController.UID), perm));
ImGui.CloseCurrentPopup();
}
if (_uiSharedService.IconTextButton(disableSounds ? FontAwesomeIcon.VolumeUp : FontAwesomeIcon.VolumeOff, disableSounds ? "Enable Sound Sync" : "Disable Sound Sync", menuWidth, true))
{
perm.SetDisableSounds(!disableSounds);
_ = _apiController.GroupChangeIndividualPermissionState(new(_groupFullInfoDto.Group, new(_apiController.UID), perm));
ImGui.CloseCurrentPopup();
}
if (_uiSharedService.IconTextButton(disableAnims ? FontAwesomeIcon.Running : FontAwesomeIcon.Stop, disableAnims ? "Enable Animation Sync" : "Disable Animation Sync", menuWidth, true))
{
perm.SetDisableAnimations(!disableAnims);
_ = _apiController.GroupChangeIndividualPermissionState(new(_groupFullInfoDto.Group, new(_apiController.UID), perm));
ImGui.CloseCurrentPopup();
}
if (_uiSharedService.IconTextButton(disableVfx ? FontAwesomeIcon.Sun : FontAwesomeIcon.Circle, disableVfx ? "Enable VFX Sync" : "Disable VFX Sync", menuWidth, true))
{
perm.SetDisableVFX(!disableVfx);
_ = _apiController.GroupChangeIndividualPermissionState(new(_groupFullInfoDto.Group, new(_apiController.UID), perm));
ImGui.CloseCurrentPopup();
}
if (IsModerator || IsOwner)
{
ImGui.Separator();
ImGui.TextUnformatted("Syncshell Admin Functions");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Cog, "Open Admin Panel", menuWidth, true))
{
ImGui.CloseCurrentPopup();
_mareMediator.Publish(new OpenSyncshellAdminPanel(_groupFullInfoDto));
}
}
}
protected override void DrawName(float width)
{
_idDisplayHandler.DrawGroupText(_id, _groupFullInfoDto, ImGui.GetCursorPosX(), () => width);
}
protected override float DrawRightSide(float currentRightSideX)
{
var spacingX = ImGui.GetStyle().ItemSpacing.X;
FontAwesomeIcon pauseIcon = _groupFullInfoDto.GroupUserPermissions.IsPaused() ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
var pauseButtonSize = _uiSharedService.GetIconButtonSize(pauseIcon);
var userCogButtonSize = _uiSharedService.GetIconSize(FontAwesomeIcon.UsersCog);
var individualSoundsDisabled = _groupFullInfoDto.GroupUserPermissions.IsDisableSounds();
var individualAnimDisabled = _groupFullInfoDto.GroupUserPermissions.IsDisableAnimations();
var individualVFXDisabled = _groupFullInfoDto.GroupUserPermissions.IsDisableVFX();
var infoIconPosDist = currentRightSideX - pauseButtonSize.X - spacingX;
ImGui.SameLine(infoIconPosDist - userCogButtonSize.X);
ImGui.AlignTextToFramePadding();
_uiSharedService.IconText(FontAwesomeIcon.UsersCog, (_groupFullInfoDto.GroupPermissions.IsPreferDisableAnimations() != individualAnimDisabled
|| _groupFullInfoDto.GroupPermissions.IsPreferDisableSounds() != individualSoundsDisabled
|| _groupFullInfoDto.GroupPermissions.IsPreferDisableVFX() != individualVFXDisabled) ? ImGuiColors.DalamudYellow : null);
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
ImGui.TextUnformatted("Syncshell Permissions");
ImGuiHelpers.ScaledDummy(2f);
_uiSharedService.BooleanToColoredIcon(!individualSoundsDisabled, inline: false);
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Sound Sync");
_uiSharedService.BooleanToColoredIcon(!individualAnimDisabled, inline: false);
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Animation Sync");
_uiSharedService.BooleanToColoredIcon(!individualVFXDisabled, inline: false);
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("VFX Sync");
ImGui.Separator();
ImGuiHelpers.ScaledDummy(2f);
ImGui.TextUnformatted("Suggested Permissions");
ImGuiHelpers.ScaledDummy(2f);
_uiSharedService.BooleanToColoredIcon(!_groupFullInfoDto.GroupPermissions.IsPreferDisableSounds(), inline: false);
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Sound Sync");
_uiSharedService.BooleanToColoredIcon(!_groupFullInfoDto.GroupPermissions.IsPreferDisableAnimations(), inline: false);
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Animation Sync");
_uiSharedService.BooleanToColoredIcon(!_groupFullInfoDto.GroupPermissions.IsPreferDisableVFX(), inline: false);
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("VFX Sync");
ImGui.EndTooltip();
}
ImGui.SameLine();
if (_uiSharedService.IconButton(pauseIcon))
{
var perm = _groupFullInfoDto.GroupUserPermissions;
perm.SetPaused(!perm.IsPaused());
_ = _apiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(_groupFullInfoDto.Group, new(_apiController.UID), perm));
}
return currentRightSideX;
}
}

View File

@@ -0,0 +1,190 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data.Extensions;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.UI.Handlers;
using LightlessSync.WebAPI;
using System.Collections.Immutable;
namespace LightlessSync.UI.Components;
public class DrawFolderTag : DrawFolderBase
{
private readonly ApiController _apiController;
private readonly SelectPairForTagUi _selectPairForTagUi;
public DrawFolderTag(string id, IImmutableList<DrawUserPair> drawPairs, IImmutableList<Pair> allPairs,
TagHandler tagHandler, ApiController apiController, SelectPairForTagUi selectPairForTagUi, UiSharedService uiSharedService)
: base(id, drawPairs, allPairs, tagHandler, uiSharedService)
{
_apiController = apiController;
_selectPairForTagUi = selectPairForTagUi;
}
protected override bool RenderIfEmpty => _id switch
{
TagHandler.CustomUnpairedTag => false,
TagHandler.CustomOnlineTag => false,
TagHandler.CustomOfflineTag => false,
TagHandler.CustomVisibleTag => false,
TagHandler.CustomAllTag => true,
TagHandler.CustomOfflineSyncshellTag => false,
_ => true,
};
protected override bool RenderMenu => _id switch
{
TagHandler.CustomUnpairedTag => false,
TagHandler.CustomOnlineTag => false,
TagHandler.CustomOfflineTag => false,
TagHandler.CustomVisibleTag => false,
TagHandler.CustomAllTag => false,
TagHandler.CustomOfflineSyncshellTag => false,
_ => true,
};
private bool RenderPause => _id switch
{
TagHandler.CustomUnpairedTag => false,
TagHandler.CustomOnlineTag => false,
TagHandler.CustomOfflineTag => false,
TagHandler.CustomVisibleTag => false,
TagHandler.CustomAllTag => false,
TagHandler.CustomOfflineSyncshellTag => false,
_ => true,
} && _allPairs.Any();
private bool RenderCount => _id switch
{
TagHandler.CustomUnpairedTag => false,
TagHandler.CustomOnlineTag => false,
TagHandler.CustomOfflineTag => false,
TagHandler.CustomVisibleTag => false,
TagHandler.CustomAllTag => false,
TagHandler.CustomOfflineSyncshellTag => false,
_ => true
};
protected override float DrawIcon()
{
var icon = _id switch
{
TagHandler.CustomUnpairedTag => FontAwesomeIcon.ArrowsLeftRight,
TagHandler.CustomOnlineTag => FontAwesomeIcon.Link,
TagHandler.CustomOfflineTag => FontAwesomeIcon.Unlink,
TagHandler.CustomOfflineSyncshellTag => FontAwesomeIcon.Unlink,
TagHandler.CustomVisibleTag => FontAwesomeIcon.Eye,
TagHandler.CustomAllTag => FontAwesomeIcon.User,
_ => FontAwesomeIcon.Folder
};
ImGui.AlignTextToFramePadding();
_uiSharedService.IconText(icon);
if (RenderCount)
{
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemSpacing.X / 2f }))
{
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("[" + OnlinePairs.ToString() + "]");
}
UiSharedService.AttachToolTip(OnlinePairs + " online" + Environment.NewLine + TotalPairs + " total");
}
ImGui.SameLine();
return ImGui.GetCursorPosX();
}
protected override void DrawMenu(float menuWidth)
{
ImGui.TextUnformatted("Group Menu");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Users, "Select Pairs", menuWidth, true))
{
_selectPairForTagUi.Open(_id);
}
UiSharedService.AttachToolTip("Select Individual Pairs for this Pair Group");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Pair Group", menuWidth, true) && UiSharedService.CtrlPressed())
{
_tagHandler.RemoveTag(_id);
}
UiSharedService.AttachToolTip("Hold CTRL to remove this Group permanently." + Environment.NewLine +
"Note: this will not unpair with users in this Group.");
}
protected override void DrawName(float width)
{
ImGui.AlignTextToFramePadding();
string name = _id switch
{
TagHandler.CustomUnpairedTag => "One-sided Individual Pairs",
TagHandler.CustomOnlineTag => "Online / Paused by you",
TagHandler.CustomOfflineTag => "Offline / Paused by other",
TagHandler.CustomOfflineSyncshellTag => "Offline Syncshell Users",
TagHandler.CustomVisibleTag => "Visible",
TagHandler.CustomAllTag => "Users",
_ => _id
};
ImGui.TextUnformatted(name);
}
protected override float DrawRightSide(float currentRightSideX)
{
if (!RenderPause) return currentRightSideX;
var allArePaused = _allPairs.All(pair => pair.UserPair!.OwnPermissions.IsPaused());
var pauseButton = allArePaused ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
var pauseButtonX = _uiSharedService.GetIconButtonSize(pauseButton).X;
var buttonPauseOffset = currentRightSideX - pauseButtonX;
ImGui.SameLine(buttonPauseOffset);
if (_uiSharedService.IconButton(pauseButton))
{
if (allArePaused)
{
ResumeAllPairs(_allPairs);
}
else
{
PauseRemainingPairs(_allPairs);
}
}
if (allArePaused)
{
UiSharedService.AttachToolTip($"Resume pairing with all pairs in {_id}");
}
else
{
UiSharedService.AttachToolTip($"Pause pairing with all pairs in {_id}");
}
return currentRightSideX;
}
private void PauseRemainingPairs(IEnumerable<Pair> availablePairs)
{
_ = _apiController.SetBulkPermissions(new(availablePairs
.ToDictionary(g => g.UserData.UID, g =>
{
var perm = g.UserPair.OwnPermissions;
perm.SetPaused(paused: true);
return perm;
}, StringComparer.Ordinal), new(StringComparer.Ordinal)))
.ConfigureAwait(false);
}
private void ResumeAllPairs(IEnumerable<Pair> availablePairs)
{
_ = _apiController.SetBulkPermissions(new(availablePairs
.ToDictionary(g => g.UserData.UID, g =>
{
var perm = g.UserPair.OwnPermissions;
perm.SetPaused(paused: false);
return perm;
}, StringComparer.Ordinal), new(StringComparer.Ordinal)))
.ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,79 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.UI.Handlers;
using System.Collections.Immutable;
using System.Numerics;
namespace LightlessSync.UI.Components;
public class DrawGroupedGroupFolder : IDrawFolder
{
private readonly IEnumerable<IDrawFolder> _groups;
private readonly TagHandler _tagHandler;
private readonly UiSharedService _uiSharedService;
private bool _wasHovered = false;
public IImmutableList<DrawUserPair> DrawPairs => throw new NotSupportedException();
public int OnlinePairs => _groups.SelectMany(g => g.DrawPairs).Where(g => g.Pair.IsOnline).DistinctBy(g => g.Pair.UserData.UID).Count();
public int TotalPairs => _groups.Sum(g => g.TotalPairs);
public DrawGroupedGroupFolder(IEnumerable<IDrawFolder> groups, TagHandler tagHandler, UiSharedService uiSharedService)
{
_groups = groups;
_tagHandler = tagHandler;
_uiSharedService = uiSharedService;
}
public void Draw()
{
if (!_groups.Any()) return;
string _id = "__folder_syncshells";
using var id = ImRaii.PushId(_id);
var color = ImRaii.PushColor(ImGuiCol.ChildBg, ImGui.GetColorU32(ImGuiCol.FrameBgHovered), _wasHovered);
using (ImRaii.Child("folder__" + _id, new System.Numerics.Vector2(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetCursorPosX(), ImGui.GetFrameHeight())))
{
ImGui.Dummy(new Vector2(0f, ImGui.GetFrameHeight()));
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(0f, 0f)))
ImGui.SameLine();
var icon = _tagHandler.IsTagOpen(_id) ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight;
ImGui.AlignTextToFramePadding();
_uiSharedService.IconText(icon);
if (ImGui.IsItemClicked())
{
_tagHandler.SetTagOpen(_id, !_tagHandler.IsTagOpen(_id));
}
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
_uiSharedService.IconText(FontAwesomeIcon.UsersRectangle);
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemSpacing.X / 2f }))
{
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("[" + OnlinePairs.ToString() + "]");
}
UiSharedService.AttachToolTip(OnlinePairs + " online in all of your joined syncshells" + Environment.NewLine +
TotalPairs + " pairs combined in all of your joined syncshells");
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("All Syncshells");
}
color.Dispose();
_wasHovered = ImGui.IsItemHovered();
ImGui.Separator();
if (_tagHandler.IsTagOpen(_id))
{
using var indent = ImRaii.PushIndent(20f);
foreach (var entry in _groups)
{
entry.Draw();
}
}
}
}

View File

@@ -0,0 +1,590 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.Group;
using LightlessSync.API.Dto.User;
using LightlessSync.MareConfiguration;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Handlers;
using LightlessSync.WebAPI;
namespace LightlessSync.UI.Components;
public class DrawUserPair
{
protected readonly ApiController _apiController;
protected readonly IdDisplayHandler _displayHandler;
protected readonly MareMediator _mediator;
protected readonly List<GroupFullInfoDto> _syncedGroups;
private readonly GroupFullInfoDto? _currentGroup;
protected Pair _pair;
private readonly string _id;
private readonly SelectTagForPairUi _selectTagForPairUi;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly UiSharedService _uiSharedService;
private readonly PlayerPerformanceConfigService _performanceConfigService;
private readonly CharaDataManager _charaDataManager;
private float _menuWidth = -1;
private bool _wasHovered = false;
public DrawUserPair(string id, Pair entry, List<GroupFullInfoDto> syncedGroups,
GroupFullInfoDto? currentGroup,
ApiController apiController, IdDisplayHandler uIDDisplayHandler,
MareMediator mareMediator, SelectTagForPairUi selectTagForPairUi,
ServerConfigurationManager serverConfigurationManager,
UiSharedService uiSharedService, PlayerPerformanceConfigService performanceConfigService,
CharaDataManager charaDataManager)
{
_id = id;
_pair = entry;
_syncedGroups = syncedGroups;
_currentGroup = currentGroup;
_apiController = apiController;
_displayHandler = uIDDisplayHandler;
_mediator = mareMediator;
_selectTagForPairUi = selectTagForPairUi;
_serverConfigurationManager = serverConfigurationManager;
_uiSharedService = uiSharedService;
_performanceConfigService = performanceConfigService;
_charaDataManager = charaDataManager;
}
public Pair Pair => _pair;
public UserFullPairDto UserPair => _pair.UserPair!;
public void DrawPairedClient()
{
using var id = ImRaii.PushId(GetType() + _id);
var color = ImRaii.PushColor(ImGuiCol.ChildBg, ImGui.GetColorU32(ImGuiCol.FrameBgHovered), _wasHovered);
using (ImRaii.Child(GetType() + _id, new System.Numerics.Vector2(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetCursorPosX(), ImGui.GetFrameHeight())))
{
DrawLeftSide();
ImGui.SameLine();
var posX = ImGui.GetCursorPosX();
var rightSide = DrawRightSide();
DrawName(posX, rightSide);
}
_wasHovered = ImGui.IsItemHovered();
color.Dispose();
}
private void DrawCommonClientMenu()
{
if (!_pair.IsPaused)
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.User, "Open Profile", _menuWidth, true))
{
_displayHandler.OpenProfile(_pair);
ImGui.CloseCurrentPopup();
}
UiSharedService.AttachToolTip("Opens the profile for this user in a new window");
}
if (_pair.IsVisible)
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Sync, "Reload last data", _menuWidth, true))
{
_pair.ApplyLastReceivedData(forced: true);
ImGui.CloseCurrentPopup();
}
UiSharedService.AttachToolTip("This reapplies the last received character data to this character");
}
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Cycle pause state", _menuWidth, true))
{
_ = _apiController.CyclePauseAsync(_pair.UserData);
ImGui.CloseCurrentPopup();
}
ImGui.Separator();
ImGui.TextUnformatted("Pair Permission Functions");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.WindowMaximize, "Open Permissions Window", _menuWidth, true))
{
_mediator.Publish(new OpenPermissionWindow(_pair));
ImGui.CloseCurrentPopup();
}
UiSharedService.AttachToolTip("Opens the Permissions Window which allows you to manage multiple permissions at once.");
var isSticky = _pair.UserPair!.OwnPermissions.IsSticky();
string stickyText = isSticky ? "Disable Preferred Permissions" : "Enable Preferred Permissions";
var stickyIcon = isSticky ? FontAwesomeIcon.ArrowCircleDown : FontAwesomeIcon.ArrowCircleUp;
if (_uiSharedService.IconTextButton(stickyIcon, stickyText, _menuWidth, true))
{
var permissions = _pair.UserPair.OwnPermissions;
permissions.SetSticky(!isSticky);
_ = _apiController.UserSetPairPermissions(new(_pair.UserData, permissions));
}
UiSharedService.AttachToolTip("Preferred permissions means that this pair will not" + Environment.NewLine + " be affected by any syncshell permission changes through you.");
string individualText = Environment.NewLine + Environment.NewLine + "Note: changing this permission will turn the permissions for this"
+ Environment.NewLine + "user to preferred permissions. You can change this behavior"
+ Environment.NewLine + "in the permission settings.";
bool individual = !_pair.IsDirectlyPaired && _apiController.DefaultPermissions!.IndividualIsSticky;
var isDisableSounds = _pair.UserPair!.OwnPermissions.IsDisableSounds();
string disableSoundsText = isDisableSounds ? "Enable sound sync" : "Disable sound sync";
var disableSoundsIcon = isDisableSounds ? FontAwesomeIcon.VolumeUp : FontAwesomeIcon.VolumeMute;
if (_uiSharedService.IconTextButton(disableSoundsIcon, disableSoundsText, _menuWidth, true))
{
var permissions = _pair.UserPair.OwnPermissions;
permissions.SetDisableSounds(!isDisableSounds);
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions));
}
UiSharedService.AttachToolTip("Changes sound sync permissions with this user." + (individual ? individualText : string.Empty));
var isDisableAnims = _pair.UserPair!.OwnPermissions.IsDisableAnimations();
string disableAnimsText = isDisableAnims ? "Enable animation sync" : "Disable animation sync";
var disableAnimsIcon = isDisableAnims ? FontAwesomeIcon.Running : FontAwesomeIcon.Stop;
if (_uiSharedService.IconTextButton(disableAnimsIcon, disableAnimsText, _menuWidth, true))
{
var permissions = _pair.UserPair.OwnPermissions;
permissions.SetDisableAnimations(!isDisableAnims);
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions));
}
UiSharedService.AttachToolTip("Changes animation sync permissions with this user." + (individual ? individualText : string.Empty));
var isDisableVFX = _pair.UserPair!.OwnPermissions.IsDisableVFX();
string disableVFXText = isDisableVFX ? "Enable VFX sync" : "Disable VFX sync";
var disableVFXIcon = isDisableVFX ? FontAwesomeIcon.Sun : FontAwesomeIcon.Circle;
if (_uiSharedService.IconTextButton(disableVFXIcon, disableVFXText, _menuWidth, true))
{
var permissions = _pair.UserPair.OwnPermissions;
permissions.SetDisableVFX(!isDisableVFX);
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions));
}
UiSharedService.AttachToolTip("Changes VFX sync permissions with this user." + (individual ? individualText : string.Empty));
}
private void DrawIndividualMenu()
{
ImGui.TextUnformatted("Individual Pair Functions");
var entryUID = _pair.UserData.AliasOrUID;
if (_pair.IndividualPairStatus != API.Data.Enum.IndividualPairStatus.None)
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Folder, "Pair Groups", _menuWidth, true))
{
_selectTagForPairUi.Open(_pair);
}
UiSharedService.AttachToolTip("Choose pair groups for " + entryUID);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Unpair Permanently", _menuWidth, true) && UiSharedService.CtrlPressed())
{
_ = _apiController.UserRemovePair(new(_pair.UserData));
}
UiSharedService.AttachToolTip("Hold CTRL and click to unpair permanently from " + entryUID);
}
else
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Pair individually", _menuWidth, true))
{
_ = _apiController.UserAddPair(new(_pair.UserData));
}
UiSharedService.AttachToolTip("Pair individually with " + entryUID);
}
}
private void DrawLeftSide()
{
string userPairText = string.Empty;
ImGui.AlignTextToFramePadding();
if (_pair.IsPaused)
{
using var _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow);
_uiSharedService.IconText(FontAwesomeIcon.PauseCircle);
userPairText = _pair.UserData.AliasOrUID + " is paused";
}
else if (!_pair.IsOnline)
{
using var _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
_uiSharedService.IconText(_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.OneSided
? FontAwesomeIcon.ArrowsLeftRight
: (_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.Bidirectional
? FontAwesomeIcon.User : FontAwesomeIcon.Users));
userPairText = _pair.UserData.AliasOrUID + " is offline";
}
else if (_pair.IsVisible)
{
_uiSharedService.IconText(FontAwesomeIcon.Eye, ImGuiColors.ParsedGreen);
userPairText = _pair.UserData.AliasOrUID + " is visible: " + _pair.PlayerName + Environment.NewLine + "Click to target this player";
if (ImGui.IsItemClicked())
{
_mediator.Publish(new TargetPairMessage(_pair));
}
}
else
{
using var _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen);
_uiSharedService.IconText(_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.Bidirectional
? FontAwesomeIcon.User : FontAwesomeIcon.Users);
userPairText = _pair.UserData.AliasOrUID + " is online";
}
if (_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.OneSided)
{
userPairText += UiSharedService.TooltipSeparator + "User has not added you back";
}
else if (_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.Bidirectional)
{
userPairText += UiSharedService.TooltipSeparator + "You are directly Paired";
}
if (_pair.LastAppliedDataBytes >= 0)
{
userPairText += UiSharedService.TooltipSeparator;
userPairText += ((!_pair.IsPaired) ? "(Last) " : string.Empty) + "Mods Info" + Environment.NewLine;
userPairText += "Files Size: " + UiSharedService.ByteToString(_pair.LastAppliedDataBytes, true);
if (_pair.LastAppliedApproximateVRAMBytes >= 0)
{
userPairText += Environment.NewLine + "Approx. VRAM Usage: " + UiSharedService.ByteToString(_pair.LastAppliedApproximateVRAMBytes, true);
}
if (_pair.LastAppliedDataTris >= 0)
{
userPairText += Environment.NewLine + "Approx. Triangle Count (excl. Vanilla): "
+ (_pair.LastAppliedDataTris > 1000 ? (_pair.LastAppliedDataTris / 1000d).ToString("0.0'k'") : _pair.LastAppliedDataTris);
}
}
if (_syncedGroups.Any())
{
userPairText += UiSharedService.TooltipSeparator + string.Join(Environment.NewLine,
_syncedGroups.Select(g =>
{
var groupNote = _serverConfigurationManager.GetNoteForGid(g.GID);
var groupString = string.IsNullOrEmpty(groupNote) ? g.GroupAliasOrGID : $"{groupNote} ({g.GroupAliasOrGID})";
return "Paired through " + groupString;
}));
}
UiSharedService.AttachToolTip(userPairText);
if (_performanceConfigService.Current.ShowPerformanceIndicator
&& !_performanceConfigService.Current.UIDsToIgnore
.Exists(uid => string.Equals(uid, UserPair.User.Alias, StringComparison.Ordinal) || string.Equals(uid, UserPair.User.UID, StringComparison.Ordinal))
&& ((_performanceConfigService.Current.VRAMSizeWarningThresholdMiB > 0 && _performanceConfigService.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024 < _pair.LastAppliedApproximateVRAMBytes)
|| (_performanceConfigService.Current.TrisWarningThresholdThousands > 0 && _performanceConfigService.Current.TrisWarningThresholdThousands * 1000 < _pair.LastAppliedDataTris))
&& (!_pair.UserPair.OwnPermissions.IsSticky()
|| _performanceConfigService.Current.WarnOnPreferredPermissionsExceedingThresholds))
{
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudYellow);
string userWarningText = "WARNING: This user exceeds one or more of your defined thresholds:" + UiSharedService.TooltipSeparator;
bool shownVram = false;
if (_performanceConfigService.Current.VRAMSizeWarningThresholdMiB > 0
&& _performanceConfigService.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024 < _pair.LastAppliedApproximateVRAMBytes)
{
shownVram = true;
userWarningText += $"Approx. VRAM Usage: Used: {UiSharedService.ByteToString(_pair.LastAppliedApproximateVRAMBytes)}, Threshold: {_performanceConfigService.Current.VRAMSizeWarningThresholdMiB} MiB";
}
if (_performanceConfigService.Current.TrisWarningThresholdThousands > 0
&& _performanceConfigService.Current.TrisWarningThresholdThousands * 1024 < _pair.LastAppliedDataTris)
{
if (shownVram) userWarningText += Environment.NewLine;
userWarningText += $"Approx. Triangle count: Used: {_pair.LastAppliedDataTris}, Threshold: {_performanceConfigService.Current.TrisWarningThresholdThousands * 1000}";
}
UiSharedService.AttachToolTip(userWarningText);
}
ImGui.SameLine();
}
private void DrawName(float leftSide, float rightSide)
{
_displayHandler.DrawPairText(_id, _pair, leftSide, () => rightSide - leftSide);
}
private void DrawPairedClientMenu()
{
DrawIndividualMenu();
if (_syncedGroups.Any()) ImGui.Separator();
foreach (var entry in _syncedGroups)
{
bool selfIsOwner = string.Equals(_apiController.UID, entry.Owner.UID, StringComparison.Ordinal);
bool selfIsModerator = entry.GroupUserInfo.IsModerator();
bool userIsModerator = entry.GroupPairUserInfos.TryGetValue(_pair.UserData.UID, out var modinfo) && modinfo.IsModerator();
bool userIsPinned = entry.GroupPairUserInfos.TryGetValue(_pair.UserData.UID, out var info) && info.IsPinned();
if (selfIsOwner || selfIsModerator)
{
var groupNote = _serverConfigurationManager.GetNoteForGid(entry.GID);
var groupString = string.IsNullOrEmpty(groupNote) ? entry.GroupAliasOrGID : $"{groupNote} ({entry.GroupAliasOrGID})";
if (ImGui.BeginMenu(groupString + " Moderation Functions"))
{
DrawSyncshellMenu(entry, selfIsOwner, selfIsModerator, userIsPinned, userIsModerator);
ImGui.EndMenu();
}
}
}
}
private float DrawRightSide()
{
var pauseIcon = _pair.UserPair!.OwnPermissions.IsPaused() ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
var pauseButtonSize = _uiSharedService.GetIconButtonSize(pauseIcon);
var barButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.EllipsisV);
var spacingX = ImGui.GetStyle().ItemSpacing.X;
var windowEndX = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth();
float currentRightSide = windowEndX - barButtonSize.X;
ImGui.SameLine(currentRightSide);
ImGui.AlignTextToFramePadding();
if (_uiSharedService.IconButton(FontAwesomeIcon.EllipsisV))
{
ImGui.OpenPopup("User Flyout Menu");
}
currentRightSide -= (pauseButtonSize.X + spacingX);
ImGui.SameLine(currentRightSide);
if (_uiSharedService.IconButton(pauseIcon))
{
var perm = _pair.UserPair!.OwnPermissions;
if (UiSharedService.CtrlPressed() && !perm.IsPaused())
{
perm.SetSticky(true);
}
perm.SetPaused(!perm.IsPaused());
_ = _apiController.UserSetPairPermissions(new(_pair.UserData, perm));
}
UiSharedService.AttachToolTip(!_pair.UserPair!.OwnPermissions.IsPaused()
? ("Pause pairing with " + _pair.UserData.AliasOrUID
+ (_pair.UserPair!.OwnPermissions.IsSticky()
? string.Empty
: UiSharedService.TooltipSeparator + "Hold CTRL to enable preferred permissions while pausing." + Environment.NewLine + "This will leave this pair paused even if unpausing syncshells including this pair."))
: "Resume pairing with " + _pair.UserData.AliasOrUID);
if (_pair.IsPaired)
{
var individualSoundsDisabled = (_pair.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableSounds() ?? false);
var individualAnimDisabled = (_pair.UserPair?.OwnPermissions.IsDisableAnimations() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableAnimations() ?? false);
var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false);
var individualIsSticky = _pair.UserPair!.OwnPermissions.IsSticky();
var individualIcon = individualIsSticky ? FontAwesomeIcon.ArrowCircleUp : FontAwesomeIcon.InfoCircle;
if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky)
{
currentRightSide -= (_uiSharedService.GetIconSize(individualIcon).X + spacingX);
ImGui.SameLine(currentRightSide);
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled))
_uiSharedService.IconText(individualIcon);
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
ImGui.TextUnformatted("Individual User permissions");
ImGui.Separator();
if (individualIsSticky)
{
_uiSharedService.IconText(individualIcon);
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Preferred permissions enabled");
if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled)
ImGui.Separator();
}
if (individualSoundsDisabled)
{
var userSoundsText = "Sound sync";
_uiSharedService.IconText(FontAwesomeIcon.VolumeOff);
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(userSoundsText);
ImGui.NewLine();
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("You");
_uiSharedService.BooleanToColoredIcon(!_pair.UserPair!.OwnPermissions.IsDisableSounds());
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("They");
_uiSharedService.BooleanToColoredIcon(!_pair.UserPair!.OtherPermissions.IsDisableSounds());
}
if (individualAnimDisabled)
{
var userAnimText = "Animation sync";
_uiSharedService.IconText(FontAwesomeIcon.Stop);
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(userAnimText);
ImGui.NewLine();
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("You");
_uiSharedService.BooleanToColoredIcon(!_pair.UserPair!.OwnPermissions.IsDisableAnimations());
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("They");
_uiSharedService.BooleanToColoredIcon(!_pair.UserPair!.OtherPermissions.IsDisableAnimations());
}
if (individualVFXDisabled)
{
var userVFXText = "VFX sync";
_uiSharedService.IconText(FontAwesomeIcon.Circle);
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(userVFXText);
ImGui.NewLine();
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("You");
_uiSharedService.BooleanToColoredIcon(!_pair.UserPair!.OwnPermissions.IsDisableVFX());
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("They");
_uiSharedService.BooleanToColoredIcon(!_pair.UserPair!.OtherPermissions.IsDisableVFX());
}
ImGui.EndTooltip();
}
}
}
if (_charaDataManager.SharedWithYouData.TryGetValue(_pair.UserData, out var sharedData))
{
currentRightSide -= (_uiSharedService.GetIconSize(FontAwesomeIcon.Running).X + (spacingX / 2f));
ImGui.SameLine(currentRightSide);
_uiSharedService.IconText(FontAwesomeIcon.Running);
UiSharedService.AttachToolTip($"This user has shared {sharedData.Count} Character Data Sets with you." + UiSharedService.TooltipSeparator
+ "Click to open the Character Data Hub and show the entries.");
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
_mediator.Publish(new OpenCharaDataHubWithFilterMessage(_pair.UserData));
}
}
if (_currentGroup != null)
{
var icon = FontAwesomeIcon.None;
var text = string.Empty;
if (string.Equals(_currentGroup.OwnerUID, _pair.UserData.UID, StringComparison.Ordinal))
{
icon = FontAwesomeIcon.Crown;
text = "User is owner of this syncshell";
}
else if (_currentGroup.GroupPairUserInfos.TryGetValue(_pair.UserData.UID, out var userinfo))
{
if (userinfo.IsModerator())
{
icon = FontAwesomeIcon.UserShield;
text = "User is moderator in this syncshell";
}
else if (userinfo.IsPinned())
{
icon = FontAwesomeIcon.Thumbtack;
text = "User is pinned in this syncshell";
}
}
if (!string.IsNullOrEmpty(text))
{
currentRightSide -= (_uiSharedService.GetIconSize(icon).X + spacingX);
ImGui.SameLine(currentRightSide);
_uiSharedService.IconText(icon);
UiSharedService.AttachToolTip(text);
}
}
if (ImGui.BeginPopup("User Flyout Menu"))
{
using (ImRaii.PushId($"buttons-{_pair.UserData.UID}"))
{
ImGui.TextUnformatted("Common Pair Functions");
DrawCommonClientMenu();
ImGui.Separator();
DrawPairedClientMenu();
if (_menuWidth <= 0)
{
_menuWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
}
}
ImGui.EndPopup();
}
return currentRightSide - spacingX;
}
private void DrawSyncshellMenu(GroupFullInfoDto group, bool selfIsOwner, bool selfIsModerator, bool userIsPinned, bool userIsModerator)
{
if (selfIsOwner || ((selfIsModerator) && (!userIsModerator)))
{
ImGui.TextUnformatted("Syncshell Moderator Functions");
var pinText = userIsPinned ? "Unpin user" : "Pin user";
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Thumbtack, pinText, _menuWidth, true))
{
ImGui.CloseCurrentPopup();
if (!group.GroupPairUserInfos.TryGetValue(_pair.UserData.UID, out var userinfo))
{
userinfo = API.Data.Enum.GroupPairUserInfo.IsPinned;
}
else
{
userinfo.SetPinned(!userinfo.IsPinned());
}
_ = _apiController.GroupSetUserInfo(new GroupPairUserInfoDto(group.Group, _pair.UserData, userinfo));
}
UiSharedService.AttachToolTip("Pin this user to the Syncshell. Pinned users will not be deleted in case of a manually initiated Syncshell clean");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Remove user", _menuWidth, true) && UiSharedService.CtrlPressed())
{
ImGui.CloseCurrentPopup();
_ = _apiController.GroupRemoveUser(new(group.Group, _pair.UserData));
}
UiSharedService.AttachToolTip("Hold CTRL and click to remove user " + (_pair.UserData.AliasOrUID) + " from Syncshell");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, "Ban User", _menuWidth, true))
{
_mediator.Publish(new OpenBanUserPopupMessage(_pair, group));
ImGui.CloseCurrentPopup();
}
UiSharedService.AttachToolTip("Ban user from this Syncshell");
ImGui.Separator();
}
if (selfIsOwner)
{
ImGui.TextUnformatted("Syncshell Owner Functions");
string modText = userIsModerator ? "Demod user" : "Mod user";
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserShield, modText, _menuWidth, true) && UiSharedService.CtrlPressed())
{
ImGui.CloseCurrentPopup();
if (!group.GroupPairUserInfos.TryGetValue(_pair.UserData.UID, out var userinfo))
{
userinfo = API.Data.Enum.GroupPairUserInfo.IsModerator;
}
else
{
userinfo.SetModerator(!userinfo.IsModerator());
}
_ = _apiController.GroupSetUserInfo(new GroupPairUserInfoDto(group.Group, _pair.UserData, userinfo));
}
UiSharedService.AttachToolTip("Hold CTRL to change the moderator status for " + (_pair.UserData.AliasOrUID) + Environment.NewLine +
"Moderators can kick, ban/unban, pin/unpin users and clear the Syncshell.");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Crown, "Transfer Ownership", _menuWidth, true) && UiSharedService.CtrlPressed() && UiSharedService.ShiftPressed())
{
ImGui.CloseCurrentPopup();
_ = _apiController.GroupChangeOwnership(new(group.Group, _pair.UserData));
}
UiSharedService.AttachToolTip("Hold CTRL and SHIFT and click to transfer ownership of this Syncshell to "
+ (_pair.UserData.AliasOrUID) + Environment.NewLine + "WARNING: This action is irreversible.");
}
}
}

View File

@@ -0,0 +1,12 @@

using System.Collections.Immutable;
namespace LightlessSync.UI.Components;
public interface IDrawFolder
{
int TotalPairs { get; }
int OnlinePairs { get; }
IImmutableList<DrawUserPair> DrawPairs { get; }
void Draw();
}

View File

@@ -0,0 +1,50 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using LightlessSync.API.Dto.Group;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator;
using LightlessSync.WebAPI;
using System.Numerics;
namespace LightlessSync.UI.Components.Popup;
public class BanUserPopupHandler : IPopupHandler
{
private readonly ApiController _apiController;
private readonly UiSharedService _uiSharedService;
private string _banReason = string.Empty;
private GroupFullInfoDto _group = null!;
private Pair _reportedPair = null!;
public BanUserPopupHandler(ApiController apiController, UiSharedService uiSharedService)
{
_apiController = apiController;
_uiSharedService = uiSharedService;
}
public Vector2 PopupSize => new(500, 250);
public bool ShowClose => true;
public void DrawContent()
{
UiSharedService.TextWrapped("User " + (_reportedPair.UserData.AliasOrUID) + " will be banned and removed from this Syncshell.");
ImGui.InputTextWithHint("##banreason", "Ban Reason", ref _banReason, 255);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, "Ban User"))
{
ImGui.CloseCurrentPopup();
var reason = _banReason;
_ = _apiController.GroupBanUser(new GroupPairDto(_group.Group, _reportedPair.UserData), reason);
_banReason = string.Empty;
}
UiSharedService.TextWrapped("The reason will be displayed in the banlist. The current server-side alias if present (Vanity ID) will automatically be attached to the reason.");
}
public void Open(OpenBanUserPopupMessage message)
{
_reportedPair = message.PairToBan;
_group = message.GroupFullInfoDto;
_banReason = string.Empty;
}
}

View File

@@ -0,0 +1,64 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using LightlessSync.Services.ServerConfiguration;
using System.Numerics;
namespace LightlessSync.UI.Components.Popup;
public class CensusPopupHandler : IPopupHandler
{
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly UiSharedService _uiSharedService;
public CensusPopupHandler(ServerConfigurationManager serverConfigurationManager, UiSharedService uiSharedService)
{
_serverConfigurationManager = serverConfigurationManager;
_uiSharedService = uiSharedService;
}
private Vector2 _size = new(600, 450);
public Vector2 PopupSize => _size;
public bool ShowClose => false;
public void DrawContent()
{
var start = 0f;
using (_uiSharedService.UidFont.Push())
{
start = ImGui.GetCursorPosY() - ImGui.CalcTextSize("Mare Census Data").Y;
UiSharedService.TextWrapped("Mare Census Participation");
}
ImGuiHelpers.ScaledDummy(5f);
UiSharedService.TextWrapped("If you are seeing this popup you are updating from a Mare version that did not collect census data. Please read the following carefully.");
ImGui.Separator();
UiSharedService.TextWrapped("Mare Census is a data collecting service that can be used for statistical purposes. " +
"All data collected through Mare Census is temporary and will be stored associated with your UID on the connected service as long as you are connected. " +
"The data cannot be used for long term tracking of individuals.");
UiSharedService.TextWrapped("If enabled, Mare Census will collect following data:" + Environment.NewLine
+ "- Currently connected World" + Environment.NewLine
+ "- Current Gender (reflecting Glamourer changes)" + Environment.NewLine
+ "- Current Race (reflecting Glamourer changes)" + Environment.NewLine
+ "- Current Clan (i.e. Seeker of the Sun, Keeper of the Moon, etc., reflecting Glamourer changes)");
UiSharedService.TextWrapped("To consent to collecting census data press the appropriate button below.");
UiSharedService.TextWrapped("This setting can be changed anytime in the Mare Settings.");
var width = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
var buttonSize = ImGuiHelpers.GetButtonSize("I consent to send my census data");
ImGuiHelpers.ScaledDummy(5f);
if (ImGui.Button("I consent to send my census data", new Vector2(width, buttonSize.Y * 2.5f)))
{
_serverConfigurationManager.SendCensusData = true;
_serverConfigurationManager.ShownCensusPopup = true;
ImGui.CloseCurrentPopup();
}
ImGuiHelpers.ScaledDummy(1f);
if (ImGui.Button("I do not consent to send my census data", new Vector2(width, buttonSize.Y)))
{
_serverConfigurationManager.SendCensusData = false;
_serverConfigurationManager.ShownCensusPopup = true;
ImGui.CloseCurrentPopup();
}
var height = ImGui.GetCursorPosY() - start;
_size = _size with { Y = height };
}
}

View File

@@ -0,0 +1,11 @@
using System.Numerics;
namespace LightlessSync.UI.Components.Popup;
public interface IPopupHandler
{
Vector2 PopupSize { get; }
bool ShowClose { get; }
void DrawContent();
}

View File

@@ -0,0 +1,80 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging;
using System.Numerics;
namespace LightlessSync.UI.Components.Popup;
public class PopupHandler : WindowMediatorSubscriberBase
{
protected bool _openPopup = false;
private readonly HashSet<IPopupHandler> _handlers;
private readonly UiSharedService _uiSharedService;
private IPopupHandler? _currentHandler = null;
public PopupHandler(ILogger<PopupHandler> logger, MareMediator mediator, IEnumerable<IPopupHandler> popupHandlers,
PerformanceCollectorService performanceCollectorService, UiSharedService uiSharedService)
: base(logger, mediator, "MarePopupHandler", performanceCollectorService)
{
Flags = ImGuiWindowFlags.NoBringToFrontOnFocus
| ImGuiWindowFlags.NoDecoration
| ImGuiWindowFlags.NoInputs
| ImGuiWindowFlags.NoSavedSettings
| ImGuiWindowFlags.NoBackground
| ImGuiWindowFlags.NoMove
| ImGuiWindowFlags.NoNav
| ImGuiWindowFlags.NoTitleBar
| ImGuiWindowFlags.NoFocusOnAppearing;
IsOpen = true;
_handlers = popupHandlers.ToHashSet();
Mediator.Subscribe<OpenBanUserPopupMessage>(this, (msg) =>
{
_openPopup = true;
_currentHandler = _handlers.OfType<BanUserPopupHandler>().Single();
((BanUserPopupHandler)_currentHandler).Open(msg);
IsOpen = true;
});
Mediator.Subscribe<OpenCensusPopupMessage>(this, (msg) =>
{
_openPopup = true;
_currentHandler = _handlers.OfType<CensusPopupHandler>().Single();
IsOpen = true;
});
_uiSharedService = uiSharedService;
DisableWindowSounds = true;
}
protected override void DrawInternal()
{
if (_currentHandler == null) return;
if (_openPopup)
{
ImGui.OpenPopup(WindowName);
_openPopup = false;
}
var viewportSize = ImGui.GetWindowViewport().Size;
ImGui.SetNextWindowSize(_currentHandler!.PopupSize * ImGuiHelpers.GlobalScale);
ImGui.SetNextWindowPos(viewportSize / 2, ImGuiCond.Always, new Vector2(0.5f));
using var popup = ImRaii.Popup(WindowName, ImGuiWindowFlags.Modal);
if (!popup) return;
_currentHandler.DrawContent();
if (_currentHandler.ShowClose)
{
ImGui.Separator();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Times, "Close"))
{
ImGui.CloseCurrentPopup();
}
}
}
}

View File

@@ -0,0 +1,93 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.UI.Handlers;
using System.Numerics;
namespace LightlessSync.UI.Components;
public class SelectPairForTagUi
{
private readonly TagHandler _tagHandler;
private readonly IdDisplayHandler _uidDisplayHandler;
private string _filter = string.Empty;
private bool _opened = false;
private HashSet<string> _peopleInGroup = new(StringComparer.Ordinal);
private bool _show = false;
private string _tag = string.Empty;
public SelectPairForTagUi(TagHandler tagHandler, IdDisplayHandler uidDisplayHandler)
{
_tagHandler = tagHandler;
_uidDisplayHandler = uidDisplayHandler;
}
public void Draw(List<Pair> pairs)
{
var workHeight = ImGui.GetMainViewport().WorkSize.Y / ImGuiHelpers.GlobalScale;
var minSize = new Vector2(300, workHeight < 400 ? workHeight : 400) * ImGuiHelpers.GlobalScale;
var maxSize = new Vector2(300, 1000) * ImGuiHelpers.GlobalScale;
var popupName = $"Choose Users for Group {_tag}";
if (!_show)
{
_opened = false;
}
if (_show && !_opened)
{
ImGui.SetNextWindowSize(minSize);
UiSharedService.CenterNextWindow(minSize.X, minSize.Y, ImGuiCond.Always);
ImGui.OpenPopup(popupName);
_opened = true;
}
ImGui.SetNextWindowSizeConstraints(minSize, maxSize);
if (ImGui.BeginPopupModal(popupName, ref _show, ImGuiWindowFlags.Popup | ImGuiWindowFlags.Modal))
{
ImGui.TextUnformatted($"Select users for group {_tag}");
ImGui.InputTextWithHint("##filter", "Filter", ref _filter, 255, ImGuiInputTextFlags.None);
foreach (var item in pairs
.Where(p => string.IsNullOrEmpty(_filter) || PairName(p).Contains(_filter, StringComparison.OrdinalIgnoreCase))
.OrderBy(p => PairName(p), StringComparer.OrdinalIgnoreCase)
.ToList())
{
var isInGroup = _peopleInGroup.Contains(item.UserData.UID);
if (ImGui.Checkbox(PairName(item), ref isInGroup))
{
if (isInGroup)
{
_tagHandler.AddTagToPairedUid(item.UserData.UID, _tag);
_peopleInGroup.Add(item.UserData.UID);
}
else
{
_tagHandler.RemoveTagFromPairedUid(item.UserData.UID, _tag);
_peopleInGroup.Remove(item.UserData.UID);
}
}
}
ImGui.EndPopup();
}
else
{
_filter = string.Empty;
_show = false;
}
}
public void Open(string tag)
{
_peopleInGroup = _tagHandler.GetOtherUidsForTag(tag);
_tag = tag;
_show = true;
}
private string PairName(Pair pair)
{
return _uidDisplayHandler.GetPlayerText(pair).text;
}
}

View File

@@ -0,0 +1,140 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Utility;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.UI.Handlers;
using System.Numerics;
namespace LightlessSync.UI.Components;
public class SelectTagForPairUi
{
private readonly TagHandler _tagHandler;
private readonly IdDisplayHandler _uidDisplayHandler;
private readonly UiSharedService _uiSharedService;
/// <summary>
/// The group UI is always open for a specific pair. This defines which pair the UI is open for.
/// </summary>
/// <returns></returns>
private Pair? _pair;
/// <summary>
/// Should the panel show, yes/no
/// </summary>
private bool _show;
/// <summary>
/// For the add category option, this stores the currently typed in tag name
/// </summary>
private string _tagNameToAdd = "";
public SelectTagForPairUi(TagHandler tagHandler, IdDisplayHandler uidDisplayHandler, UiSharedService uiSharedService)
{
_show = false;
_pair = null;
_tagHandler = tagHandler;
_uidDisplayHandler = uidDisplayHandler;
_uiSharedService = uiSharedService;
}
public void Draw()
{
if (_pair == null)
{
return;
}
var name = PairName(_pair);
var popupName = $"Choose Groups for {name}";
// Is the popup supposed to show but did not open yet? Open it
if (_show)
{
ImGui.OpenPopup(popupName);
_show = false;
}
if (ImGui.BeginPopup(popupName))
{
var tags = _tagHandler.GetAllTagsSorted();
var childHeight = tags.Count != 0 ? tags.Count * 25 : 1;
var childSize = new Vector2(0, childHeight > 100 ? 100 : childHeight) * ImGuiHelpers.GlobalScale;
ImGui.TextUnformatted($"Select the groups you want {name} to be in.");
if (ImGui.BeginChild(name + "##listGroups", childSize))
{
foreach (var tag in tags)
{
using (ImRaii.PushId($"groups-pair-{_pair.UserData.UID}-{tag}")) DrawGroupName(_pair, tag);
}
ImGui.EndChild();
}
ImGui.Separator();
ImGui.TextUnformatted($"Create a new group for {name}.");
if (_uiSharedService.IconButton(FontAwesomeIcon.Plus))
{
HandleAddTag();
}
ImGui.SameLine();
ImGui.InputTextWithHint("##category_name", "New Group", ref _tagNameToAdd, 40);
if (ImGui.IsKeyDown(ImGuiKey.Enter))
{
HandleAddTag();
}
ImGui.EndPopup();
}
}
public void Open(Pair pair)
{
_pair = pair;
// Using "_show" here to de-couple the opening of the popup
// The popup name is derived from the name the user currently sees, which is
// based on the showUidForEntry dictionary.
// We'd have to derive the name here to open it popup modal here, when the Open() is called
_show = true;
}
private void DrawGroupName(Pair pair, string name)
{
var hasTagBefore = _tagHandler.HasTag(pair.UserData.UID, name);
var hasTag = hasTagBefore;
if (ImGui.Checkbox(name, ref hasTag))
{
if (hasTag)
{
_tagHandler.AddTagToPairedUid(pair.UserData.UID, name);
}
else
{
_tagHandler.RemoveTagFromPairedUid(pair.UserData.UID, name);
}
}
}
private void HandleAddTag()
{
if (!_tagNameToAdd.IsNullOrWhitespace() && _tagNameToAdd is not (TagHandler.CustomOfflineTag or TagHandler.CustomOnlineTag or TagHandler.CustomVisibleTag))
{
_tagHandler.AddTag(_tagNameToAdd);
if (_pair != null)
{
_tagHandler.AddTagToPairedUid(_pair.UserData.UID, _tagNameToAdd);
}
_tagNameToAdd = string.Empty;
}
else
{
_tagNameToAdd = string.Empty;
}
}
private string PairName(Pair pair)
{
return _uidDisplayHandler.GetPlayerText(pair).text;
}
}

View File

@@ -0,0 +1,117 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.Group;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging;
using System.Numerics;
namespace LightlessSync.UI;
public class CreateSyncshellUI : WindowMediatorSubscriberBase
{
private readonly ApiController _apiController;
private readonly UiSharedService _uiSharedService;
private bool _errorGroupCreate;
private GroupJoinDto? _lastCreatedGroup;
public CreateSyncshellUI(ILogger<CreateSyncshellUI> logger, MareMediator mareMediator, ApiController apiController, UiSharedService uiSharedService,
PerformanceCollectorService performanceCollectorService)
: base(logger, mareMediator, "Create new Syncshell###LightlessSyncCreateSyncshell", performanceCollectorService)
{
_apiController = apiController;
_uiSharedService = uiSharedService;
SizeConstraints = new()
{
MinimumSize = new(550, 330),
MaximumSize = new(550, 330)
};
Flags = ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse;
Mediator.Subscribe<DisconnectedMessage>(this, (_) => IsOpen = false);
}
protected override void DrawInternal()
{
using (_uiSharedService.UidFont.Push())
ImGui.TextUnformatted("Create new Syncshell");
if (_lastCreatedGroup == null)
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Create Syncshell"))
{
try
{
_lastCreatedGroup = _apiController.GroupCreate().Result;
}
catch
{
_lastCreatedGroup = null;
_errorGroupCreate = true;
}
}
ImGui.SameLine();
}
ImGui.Separator();
if (_lastCreatedGroup == null)
{
UiSharedService.TextWrapped("Creating a new Syncshell will create it with your current preferred permissions for Syncshells as default suggested permissions." + Environment.NewLine +
"- You can own up to " + _apiController.ServerInfo.MaxGroupsCreatedByUser + " Syncshells on this server." + Environment.NewLine +
"- You can join up to " + _apiController.ServerInfo.MaxGroupsJoinedByUser + " Syncshells on this server (including your own)" + Environment.NewLine +
"- Syncshells on this server can have a maximum of " + _apiController.ServerInfo.MaxGroupUserCount + " users");
ImGuiHelpers.ScaledDummy(2f);
ImGui.TextUnformatted("Your current Syncshell preferred permissions are:");
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("- Animations");
_uiSharedService.BooleanToColoredIcon(!_apiController.DefaultPermissions!.DisableGroupAnimations);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("- Sounds");
_uiSharedService.BooleanToColoredIcon(!_apiController.DefaultPermissions!.DisableGroupSounds);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("- VFX");
_uiSharedService.BooleanToColoredIcon(!_apiController.DefaultPermissions!.DisableGroupVFX);
UiSharedService.TextWrapped("(Those preferred permissions can be changed anytime after Syncshell creation, your defaults can be changed anytime in the Mare Settings)");
}
else
{
_errorGroupCreate = false;
ImGui.TextUnformatted("Syncshell ID: " + _lastCreatedGroup.Group.GID);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Syncshell Password: " + _lastCreatedGroup.Password);
ImGui.SameLine();
if (_uiSharedService.IconButton(FontAwesomeIcon.Copy))
{
ImGui.SetClipboardText(_lastCreatedGroup.Password);
}
UiSharedService.TextWrapped("You can change the Syncshell password later at any time.");
ImGui.Separator();
UiSharedService.TextWrapped("These settings were set based on your preferred syncshell permissions:");
ImGuiHelpers.ScaledDummy(2f);
ImGui.AlignTextToFramePadding();
UiSharedService.TextWrapped("Suggest Animation sync:");
_uiSharedService.BooleanToColoredIcon(!_lastCreatedGroup.GroupUserPreferredPermissions.IsDisableAnimations());
ImGui.AlignTextToFramePadding();
UiSharedService.TextWrapped("Suggest Sounds sync:");
_uiSharedService.BooleanToColoredIcon(!_lastCreatedGroup.GroupUserPreferredPermissions.IsDisableSounds());
ImGui.AlignTextToFramePadding();
UiSharedService.TextWrapped("Suggest VFX sync:");
_uiSharedService.BooleanToColoredIcon(!_lastCreatedGroup.GroupUserPreferredPermissions.IsDisableVFX());
}
if (_errorGroupCreate)
{
UiSharedService.ColorTextWrapped("Something went wrong during creation of a new Syncshell", new Vector4(1, 0, 0, 1));
}
}
public override void OnOpen()
{
_lastCreatedGroup = null;
}
}

View File

@@ -0,0 +1,879 @@
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.FileCache;
using LightlessSync.Interop.Ipc;
using LightlessSync.MareConfiguration;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using Microsoft.Extensions.Logging;
using System.Numerics;
namespace LightlessSync.UI;
public class DataAnalysisUi : WindowMediatorSubscriberBase
{
private readonly CharacterAnalyzer _characterAnalyzer;
private readonly Progress<(string, int)> _conversionProgress = new();
private readonly IpcManager _ipcManager;
private readonly UiSharedService _uiSharedService;
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
private readonly TransientResourceManager _transientResourceManager;
private readonly TransientConfigService _transientConfigService;
private readonly Dictionary<string, string[]> _texturesToConvert = new(StringComparer.Ordinal);
private Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>? _cachedAnalysis;
private CancellationTokenSource _conversionCancellationTokenSource = new();
private string _conversionCurrentFileName = string.Empty;
private int _conversionCurrentFileProgress = 0;
private Task? _conversionTask;
private bool _enableBc7ConversionMode = false;
private bool _hasUpdate = false;
private bool _modalOpen = false;
private string _selectedFileTypeTab = string.Empty;
private string _selectedHash = string.Empty;
private ObjectKind _selectedObjectTab;
private bool _showModal = false;
private CancellationTokenSource _transientRecordCts = new();
public DataAnalysisUi(ILogger<DataAnalysisUi> logger, MareMediator mediator,
CharacterAnalyzer characterAnalyzer, IpcManager ipcManager,
PerformanceCollectorService performanceCollectorService, UiSharedService uiSharedService,
PlayerPerformanceConfigService playerPerformanceConfig, TransientResourceManager transientResourceManager,
TransientConfigService transientConfigService)
: base(logger, mediator, "Mare Character Data Analysis", performanceCollectorService)
{
_characterAnalyzer = characterAnalyzer;
_ipcManager = ipcManager;
_uiSharedService = uiSharedService;
_playerPerformanceConfig = playerPerformanceConfig;
_transientResourceManager = transientResourceManager;
_transientConfigService = transientConfigService;
Mediator.Subscribe<CharacterDataAnalyzedMessage>(this, (_) =>
{
_hasUpdate = true;
});
SizeConstraints = new()
{
MinimumSize = new()
{
X = 800,
Y = 600
},
MaximumSize = new()
{
X = 3840,
Y = 2160
}
};
_conversionProgress.ProgressChanged += ConversionProgress_ProgressChanged;
}
protected override void DrawInternal()
{
if (_conversionTask != null && !_conversionTask.IsCompleted)
{
_showModal = true;
if (ImGui.BeginPopupModal("BC7 Conversion in Progress"))
{
ImGui.TextUnformatted("BC7 Conversion in progress: " + _conversionCurrentFileProgress + "/" + _texturesToConvert.Count);
UiSharedService.TextWrapped("Current file: " + _conversionCurrentFileName);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel conversion"))
{
_conversionCancellationTokenSource.Cancel();
}
UiSharedService.SetScaledWindowSize(500);
ImGui.EndPopup();
}
else
{
_modalOpen = false;
}
}
else if (_conversionTask != null && _conversionTask.IsCompleted && _texturesToConvert.Count > 0)
{
_conversionTask = null;
_texturesToConvert.Clear();
_showModal = false;
_modalOpen = false;
_enableBc7ConversionMode = false;
}
if (_showModal && !_modalOpen)
{
ImGui.OpenPopup("BC7 Conversion in Progress");
_modalOpen = true;
}
if (_hasUpdate)
{
_cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone();
_hasUpdate = false;
}
using var tabBar = ImRaii.TabBar("analysisRecordingTabBar");
using (var tabItem = ImRaii.TabItem("Analysis"))
{
if (tabItem)
{
using var id = ImRaii.PushId("analysis");
DrawAnalysis();
}
}
using (var tabItem = ImRaii.TabItem("Transient Files"))
{
if (tabItem)
{
using var tabbar = ImRaii.TabBar("transientData");
using (var transientData = ImRaii.TabItem("Stored Transient File Data"))
{
using var id = ImRaii.PushId("data");
if (transientData)
{
DrawStoredData();
}
}
using (var transientRecord = ImRaii.TabItem("Record Transient Data"))
{
using var id = ImRaii.PushId("recording");
if (transientRecord)
{
DrawRecording();
}
}
}
}
}
private bool _showAlreadyAddedTransients = false;
private bool _acknowledgeReview = false;
private string _selectedStoredCharacter = string.Empty;
private string _selectedJobEntry = string.Empty;
private readonly List<string> _storedPathsToRemove = [];
private readonly Dictionary<string, string> _filePathResolve = [];
private string _filterGamePath = string.Empty;
private string _filterFilePath = string.Empty;
private void DrawStoredData()
{
UiSharedService.DrawTree("What is this? (Explanation / Help)", () =>
{
UiSharedService.TextWrapped("This tab allows you to see which transient files are attached to your character.");
UiSharedService.TextWrapped("Transient files are files that cannot be resolved to your character permanently. Mare gathers these files in the background while you execute animations, VFX, sound effects, etc.");
UiSharedService.TextWrapped("When sending your character data to others, Mare will combine the files listed in \"All Jobs\" and the corresponding currently used job.");
UiSharedService.TextWrapped("The purpose of this tab is primarily informational for you to see which files you are carrying with you. You can remove added game paths, however if you are using the animations etc. again, "
+ "Mare will automatically attach these after using them. If you disable associated mods in Penumbra, the associated entries here will also be deleted automatically.");
});
ImGuiHelpers.ScaledDummy(5);
var config = _transientConfigService.Current.TransientConfigs;
Vector2 availableContentRegion = Vector2.Zero;
using (ImRaii.Group())
{
ImGui.TextUnformatted("Character");
ImGui.Separator();
ImGuiHelpers.ScaledDummy(3);
availableContentRegion = ImGui.GetContentRegionAvail();
using (ImRaii.ListBox("##characters", new Vector2(200, availableContentRegion.Y)))
{
foreach (var entry in config)
{
var name = entry.Key.Split("_");
if (!_uiSharedService.WorldData.TryGetValue(ushort.Parse(name[1]), out var worldname))
{
continue;
}
if (ImGui.Selectable(name[0] + " (" + worldname + ")", string.Equals(_selectedStoredCharacter, entry.Key, StringComparison.Ordinal)))
{
_selectedStoredCharacter = entry.Key;
_selectedJobEntry = string.Empty;
_storedPathsToRemove.Clear();
_filePathResolve.Clear();
_filterFilePath = string.Empty;
_filterGamePath = string.Empty;
}
}
}
}
ImGui.SameLine();
bool selectedData = config.TryGetValue(_selectedStoredCharacter, out var transientStorage) && transientStorage != null;
using (ImRaii.Group())
{
ImGui.TextUnformatted("Job");
ImGui.Separator();
ImGuiHelpers.ScaledDummy(3);
using (ImRaii.ListBox("##data", new Vector2(150, availableContentRegion.Y)))
{
if (selectedData)
{
if (ImGui.Selectable("All Jobs", string.Equals(_selectedJobEntry, "alljobs", StringComparison.Ordinal)))
{
_selectedJobEntry = "alljobs";
}
foreach (var job in transientStorage!.JobSpecificCache)
{
if (!_uiSharedService.JobData.TryGetValue(job.Key, out var jobName)) continue;
if (ImGui.Selectable(jobName, string.Equals(_selectedJobEntry, job.Key.ToString(), StringComparison.Ordinal)))
{
_selectedJobEntry = job.Key.ToString();
_storedPathsToRemove.Clear();
_filePathResolve.Clear();
_filterFilePath = string.Empty;
_filterGamePath = string.Empty;
}
}
}
}
}
ImGui.SameLine();
using (ImRaii.Group())
{
var selectedList = string.Equals(_selectedJobEntry, "alljobs", StringComparison.Ordinal)
? config[_selectedStoredCharacter].GlobalPersistentCache
: (string.IsNullOrEmpty(_selectedJobEntry) ? [] : config[_selectedStoredCharacter].JobSpecificCache[uint.Parse(_selectedJobEntry)]);
ImGui.TextUnformatted($"Attached Files (Total Files: {selectedList.Count})");
ImGui.Separator();
ImGuiHelpers.ScaledDummy(3);
using (ImRaii.Disabled(string.IsNullOrEmpty(_selectedJobEntry)))
{
var restContent = availableContentRegion.X - ImGui.GetCursorPosX();
using var group = ImRaii.Group();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Resolve Game Paths to used File Paths"))
{
_ = Task.Run(async () =>
{
var paths = selectedList.ToArray();
var resolved = await _ipcManager.Penumbra.ResolvePathsAsync(paths, []).ConfigureAwait(false);
_filePathResolve.Clear();
for (int i = 0; i < resolved.forward.Length; i++)
{
_filePathResolve[paths[i]] = resolved.forward[i];
}
});
}
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(20, 1);
ImGui.SameLine();
using (ImRaii.Disabled(!_storedPathsToRemove.Any()))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Remove selected Game Paths"))
{
foreach (var item in _storedPathsToRemove)
{
selectedList.Remove(item);
}
_transientConfigService.Save();
_transientResourceManager.RebuildSemiTransientResources();
_filterFilePath = string.Empty;
_filterGamePath = string.Empty;
}
}
ImGui.SameLine();
using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear ALL Game Paths"))
{
selectedList.Clear();
_transientConfigService.Save();
_transientResourceManager.RebuildSemiTransientResources();
_filterFilePath = string.Empty;
_filterGamePath = string.Empty;
}
}
UiSharedService.AttachToolTip("Hold CTRL to delete all game paths from the displayed list"
+ UiSharedService.TooltipSeparator + "You usually do not need to do this. All animation and VFX data will be automatically handled through Mare.");
ImGuiHelpers.ScaledDummy(5);
ImGuiHelpers.ScaledDummy(30);
ImGui.SameLine();
ImGui.SetNextItemWidth((restContent - 30) / 2f);
ImGui.InputTextWithHint("##filterGamePath", "Filter by Game Path", ref _filterGamePath, 255);
ImGui.SameLine();
ImGui.SetNextItemWidth((restContent - 30) / 2f);
ImGui.InputTextWithHint("##filterFilePath", "Filter by File Path", ref _filterFilePath, 255);
using (var dataTable = ImRaii.Table("##table", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollY | ImGuiTableFlags.RowBg))
{
if (dataTable)
{
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 30);
ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthFixed, (restContent - 30) / 2f);
ImGui.TableSetupColumn("File Path", ImGuiTableColumnFlags.WidthFixed, (restContent - 30) / 2f);
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableHeadersRow();
int id = 0;
foreach (var entry in selectedList)
{
if (!string.IsNullOrWhiteSpace(_filterGamePath) && !entry.Contains(_filterGamePath, StringComparison.OrdinalIgnoreCase))
continue;
bool hasFileResolve = _filePathResolve.TryGetValue(entry, out var filePath);
if (hasFileResolve && !string.IsNullOrEmpty(_filterFilePath) && !filePath!.Contains(_filterFilePath, StringComparison.OrdinalIgnoreCase))
continue;
using var imguiid = ImRaii.PushId(id++);
ImGui.TableNextColumn();
bool isSelected = _storedPathsToRemove.Contains(entry, StringComparer.Ordinal);
if (ImGui.Checkbox("##", ref isSelected))
{
if (isSelected)
_storedPathsToRemove.Add(entry);
else
_storedPathsToRemove.Remove(entry);
}
ImGui.TableNextColumn();
ImGui.TextUnformatted(entry);
UiSharedService.AttachToolTip(entry + UiSharedService.TooltipSeparator + "Click to copy to clipboard");
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
ImGui.SetClipboardText(entry);
}
ImGui.TableNextColumn();
if (hasFileResolve)
{
ImGui.TextUnformatted(filePath ?? "Unk");
UiSharedService.AttachToolTip(filePath ?? "Unk" + UiSharedService.TooltipSeparator + "Click to copy to clipboard");
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
ImGui.SetClipboardText(filePath);
}
}
else
{
ImGui.TextUnformatted("-");
UiSharedService.AttachToolTip("Resolve Game Paths to used File Paths to display the associated file paths.");
}
}
}
}
}
}
}
private void DrawRecording()
{
UiSharedService.DrawTree("What is this? (Explanation / Help)", () =>
{
UiSharedService.TextWrapped("This tab allows you to attempt to fix mods that do not sync correctly, especially those with modded models and animations." + Environment.NewLine + Environment.NewLine
+ "To use this, start the recording, execute one or multiple emotes/animations you want to attempt to fix and check if new data appears in the table below." + Environment.NewLine
+ "If it doesn't, Mare is not able to catch the data or already has recorded the animation files (check 'Show previously added transient files' to see if not all is already present)." + Environment.NewLine + Environment.NewLine
+ "For most animations, vfx, etc. it is enough to just run them once unless they have random variations. Longer animations do not require to play out in their entirety to be captured.");
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("Important Note: If you need to fix an animation that should apply across multiple jobs, you need to repeat this process with at least one additional job, " +
"otherwise the animation will only be fixed for the currently active job. This goes primarily for emotes that are used across multiple jobs.",
ImGuiColors.DalamudYellow, 800);
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("WARNING: WHILE RECORDING TRANSIENT DATA, DO NOT CHANGE YOUR APPEARANCE, ENABLED MODS OR ANYTHING. JUST DO THE ANIMATION(S) OR WHATEVER YOU NEED DOING AND STOP THE RECORDING.",
ImGuiColors.DalamudRed, 800);
ImGuiHelpers.ScaledDummy(5);
});
using (ImRaii.Disabled(_transientResourceManager.IsTransientRecording))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Play, "Start Transient Recording"))
{
_transientRecordCts.Cancel();
_transientRecordCts.Dispose();
_transientRecordCts = new();
_transientResourceManager.StartRecording(_transientRecordCts.Token);
_acknowledgeReview = false;
}
}
ImGui.SameLine();
using (ImRaii.Disabled(!_transientResourceManager.IsTransientRecording))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Stop, "Stop Transient Recording"))
{
_transientRecordCts.Cancel();
}
}
if (_transientResourceManager.IsTransientRecording)
{
ImGui.SameLine();
UiSharedService.ColorText($"RECORDING - Time Remaining: {_transientResourceManager.RecordTimeRemaining.Value}", ImGuiColors.DalamudYellow);
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("DO NOT CHANGE YOUR APPEARANCE OR MODS WHILE RECORDING, YOU CAN ACCIDENTALLY MAKE SOME OF YOUR APPEARANCE RELATED MODS PERMANENT.", ImGuiColors.DalamudRed, 800);
}
ImGuiHelpers.ScaledDummy(5);
ImGui.Checkbox("Show previously added transient files in the recording", ref _showAlreadyAddedTransients);
_uiSharedService.DrawHelpText("Use this only if you want to see what was previously already caught by Mare");
ImGuiHelpers.ScaledDummy(5);
using (ImRaii.Disabled(_transientResourceManager.IsTransientRecording || _transientResourceManager.RecordedTransients.All(k => !k.AddTransient) || !_acknowledgeReview))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Recorded Transient Data"))
{
_transientResourceManager.SaveRecording();
_acknowledgeReview = false;
}
}
ImGui.SameLine();
ImGui.Checkbox("I acknowledge I have reviewed the recorded data", ref _acknowledgeReview);
if (_transientResourceManager.RecordedTransients.Any(k => !k.AlreadyTransient))
{
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("Please review the recorded mod files before saving and deselect files that got into the recording on accident.", ImGuiColors.DalamudYellow);
ImGuiHelpers.ScaledDummy(5);
}
ImGuiHelpers.ScaledDummy(5);
var width = ImGui.GetContentRegionAvail();
using var table = ImRaii.Table("Recorded Transients", 4, ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
if (table)
{
int id = 0;
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 30);
ImGui.TableSetupColumn("Owner", ImGuiTableColumnFlags.WidthFixed, 100);
ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthFixed, (width.X - 30 - 100) / 2f);
ImGui.TableSetupColumn("File Path", ImGuiTableColumnFlags.WidthFixed, (width.X - 30 - 100) / 2f);
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableHeadersRow();
var transients = _transientResourceManager.RecordedTransients.ToList();
transients.Reverse();
foreach (var value in transients)
{
if (value.AlreadyTransient && !_showAlreadyAddedTransients)
continue;
using var imguiid = ImRaii.PushId(id++);
if (value.AlreadyTransient)
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
}
ImGui.TableNextColumn();
bool addTransient = value.AddTransient;
if (ImGui.Checkbox("##add", ref addTransient))
{
value.AddTransient = addTransient;
}
ImGui.TableNextColumn();
ImGui.TextUnformatted(value.Owner.Name);
ImGui.TableNextColumn();
ImGui.TextUnformatted(value.GamePath);
UiSharedService.AttachToolTip(value.GamePath);
ImGui.TableNextColumn();
ImGui.TextUnformatted(value.FilePath);
UiSharedService.AttachToolTip(value.FilePath);
if (value.AlreadyTransient)
{
ImGui.PopStyleColor();
}
}
}
}
private void DrawAnalysis()
{
UiSharedService.DrawTree("What is this? (Explanation / Help)", () =>
{
UiSharedService.TextWrapped("This tab shows you all files and their sizes that are currently in use through your character and associated entities in Mare");
});
if (_cachedAnalysis!.Count == 0) return;
bool isAnalyzing = _characterAnalyzer.IsAnalysisRunning;
if (isAnalyzing)
{
UiSharedService.ColorTextWrapped($"Analyzing {_characterAnalyzer.CurrentFile}/{_characterAnalyzer.TotalFiles}",
ImGuiColors.DalamudYellow);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel analysis"))
{
_characterAnalyzer.CancelAnalyze();
}
}
else
{
if (_cachedAnalysis!.Any(c => c.Value.Any(f => !f.Value.IsComputed)))
{
UiSharedService.ColorTextWrapped("Some entries in the analysis have file size not determined yet, press the button below to analyze your current data",
ImGuiColors.DalamudYellow);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (missing entries)"))
{
_ = _characterAnalyzer.ComputeAnalysis(print: false);
}
}
else
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (recalculate all entries)"))
{
_ = _characterAnalyzer.ComputeAnalysis(print: false, recalculate: true);
}
}
}
ImGui.Separator();
ImGui.TextUnformatted("Total files:");
ImGui.SameLine();
ImGui.TextUnformatted(_cachedAnalysis!.Values.Sum(c => c.Values.Count).ToString());
ImGui.SameLine();
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString());
}
if (ImGui.IsItemHovered())
{
string text = "";
var groupedfiles = _cachedAnalysis.Values.SelectMany(f => f.Values).GroupBy(f => f.FileType, StringComparer.Ordinal);
text = string.Join(Environment.NewLine, groupedfiles.OrderBy(f => f.Key, StringComparer.Ordinal)
.Select(f => f.Key + ": " + f.Count() + " files, size: " + UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))
+ ", compressed: " + UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))));
ImGui.SetTooltip(text);
}
ImGui.TextUnformatted("Total size (actual):");
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.OriginalSize))));
ImGui.TextUnformatted("Total size (compressed for up/download only):");
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.CompressedSize))));
ImGui.TextUnformatted($"Total modded model triangles: {_cachedAnalysis.Sum(c => c.Value.Sum(f => f.Value.Triangles))}");
ImGui.Separator();
using var tabbar = ImRaii.TabBar("objectSelection");
foreach (var kvp in _cachedAnalysis)
{
using var id = ImRaii.PushId(kvp.Key.ToString());
string tabText = kvp.Key.ToString();
if (kvp.Value.Any(f => !f.Value.IsComputed)) tabText += " (!)";
using var tab = ImRaii.TabItem(tabText + "###" + kvp.Key.ToString());
if (tab.Success)
{
var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal)
.OrderBy(k => k.Key, StringComparer.Ordinal).ToList();
ImGui.TextUnformatted("Files for " + kvp.Key);
ImGui.SameLine();
ImGui.TextUnformatted(kvp.Value.Count.ToString());
ImGui.SameLine();
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString());
}
if (ImGui.IsItemHovered())
{
string text = "";
text = string.Join(Environment.NewLine, groupedfiles
.Select(f => f.Key + ": " + f.Count() + " files, size: " + UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))
+ ", compressed: " + UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))));
ImGui.SetTooltip(text);
}
ImGui.TextUnformatted($"{kvp.Key} size (actual):");
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize)));
ImGui.TextUnformatted($"{kvp.Key} size (compressed for up/download only):");
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize)));
ImGui.Separator();
var vramUsage = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal));
if (vramUsage != null)
{
var actualVramUsage = vramUsage.Sum(f => f.OriginalSize);
ImGui.TextUnformatted($"{kvp.Key} VRAM usage:");
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(actualVramUsage));
if (_playerPerformanceConfig.Current.WarnOnExceedingThresholds
|| _playerPerformanceConfig.Current.ShowPerformanceIndicator)
{
using var _ = ImRaii.PushIndent(10f);
var currentVramWarning = _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB;
ImGui.TextUnformatted($"Configured VRAM warning threshold: {currentVramWarning} MiB.");
if (currentVramWarning * 1024 * 1024 < actualVramUsage)
{
UiSharedService.ColorText($"You exceed your own threshold by " +
$"{UiSharedService.ByteToString(actualVramUsage - (currentVramWarning * 1024 * 1024))}.",
ImGuiColors.DalamudYellow);
}
}
}
var actualTriCount = kvp.Value.Sum(f => f.Value.Triangles);
ImGui.TextUnformatted($"{kvp.Key} modded model triangles: {actualTriCount}");
if (_playerPerformanceConfig.Current.WarnOnExceedingThresholds
|| _playerPerformanceConfig.Current.ShowPerformanceIndicator)
{
using var _ = ImRaii.PushIndent(10f);
var currentTriWarning = _playerPerformanceConfig.Current.TrisWarningThresholdThousands;
ImGui.TextUnformatted($"Configured triangle warning threshold: {currentTriWarning * 1000} triangles.");
if (currentTriWarning * 1000 < actualTriCount)
{
UiSharedService.ColorText($"You exceed your own threshold by " +
$"{actualTriCount - (currentTriWarning * 1000)} triangles.",
ImGuiColors.DalamudYellow);
}
}
ImGui.Separator();
if (_selectedObjectTab != kvp.Key)
{
_selectedHash = string.Empty;
_selectedObjectTab = kvp.Key;
_selectedFileTypeTab = string.Empty;
_enableBc7ConversionMode = false;
_texturesToConvert.Clear();
}
using var fileTabBar = ImRaii.TabBar("fileTabs");
foreach (IGrouping<string, CharacterAnalyzer.FileDataEntry>? fileGroup in groupedfiles)
{
string fileGroupText = fileGroup.Key + " [" + fileGroup.Count() + "]";
var requiresCompute = fileGroup.Any(k => !k.IsComputed);
using var tabcol = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.Color(ImGuiColors.DalamudYellow), requiresCompute);
if (requiresCompute)
{
fileGroupText += " (!)";
}
ImRaii.IEndObject fileTab;
using (var textcol = ImRaii.PushColor(ImGuiCol.Text, UiSharedService.Color(new(0, 0, 0, 1)),
requiresCompute && !string.Equals(_selectedFileTypeTab, fileGroup.Key, StringComparison.Ordinal)))
{
fileTab = ImRaii.TabItem(fileGroupText + "###" + fileGroup.Key);
}
if (!fileTab) { fileTab.Dispose(); continue; }
if (!string.Equals(fileGroup.Key, _selectedFileTypeTab, StringComparison.Ordinal))
{
_selectedFileTypeTab = fileGroup.Key;
_selectedHash = string.Empty;
_enableBc7ConversionMode = false;
_texturesToConvert.Clear();
}
ImGui.TextUnformatted($"{fileGroup.Key} files");
ImGui.SameLine();
ImGui.TextUnformatted(fileGroup.Count().ToString());
ImGui.TextUnformatted($"{fileGroup.Key} files size (actual):");
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(fileGroup.Sum(c => c.OriginalSize)));
ImGui.TextUnformatted($"{fileGroup.Key} files size (compressed for up/download only):");
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(fileGroup.Sum(c => c.CompressedSize)));
if (string.Equals(_selectedFileTypeTab, "tex", StringComparison.Ordinal))
{
ImGui.Checkbox("Enable BC7 Conversion Mode", ref _enableBc7ConversionMode);
if (_enableBc7ConversionMode)
{
UiSharedService.ColorText("WARNING BC7 CONVERSION:", ImGuiColors.DalamudYellow);
ImGui.SameLine();
UiSharedService.ColorText("Converting textures to BC7 is irreversible!", ImGuiColors.DalamudRed);
UiSharedService.ColorTextWrapped("- Converting textures to BC7 will reduce their size (compressed and uncompressed) drastically. It is recommended to be used for large (4k+) textures." +
Environment.NewLine + "- Some textures, especially ones utilizing colorsets, might not be suited for BC7 conversion and might produce visual artifacts." +
Environment.NewLine + "- Before converting textures, make sure to have the original files of the mod you are converting so you can reimport it in case of issues." +
Environment.NewLine + "- Conversion will convert all found texture duplicates (entries with more than 1 file path) automatically." +
Environment.NewLine + "- Converting textures to BC7 is a very expensive operation and, depending on the amount of textures to convert, will take a while to complete."
, ImGuiColors.DalamudYellow);
if (_texturesToConvert.Count > 0 && _uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start conversion of " + _texturesToConvert.Count + " texture(s)"))
{
_conversionCancellationTokenSource = _conversionCancellationTokenSource.CancelRecreate();
_conversionTask = _ipcManager.Penumbra.ConvertTextureFiles(_logger, _texturesToConvert, _conversionProgress, _conversionCancellationTokenSource.Token);
}
}
}
ImGui.Separator();
DrawTable(fileGroup);
fileTab.Dispose();
}
}
}
ImGui.Separator();
ImGui.TextUnformatted("Selected file:");
ImGui.SameLine();
UiSharedService.ColorText(_selectedHash, ImGuiColors.DalamudYellow);
if (_cachedAnalysis[_selectedObjectTab].TryGetValue(_selectedHash, out CharacterAnalyzer.FileDataEntry? item))
{
var filePaths = item.FilePaths;
ImGui.TextUnformatted("Local file path:");
ImGui.SameLine();
UiSharedService.TextWrapped(filePaths[0]);
if (filePaths.Count > 1)
{
ImGui.SameLine();
ImGui.TextUnformatted($"(and {filePaths.Count - 1} more)");
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.InfoCircle);
UiSharedService.AttachToolTip(string.Join(Environment.NewLine, filePaths.Skip(1)));
}
var gamepaths = item.GamePaths;
ImGui.TextUnformatted("Used by game path:");
ImGui.SameLine();
UiSharedService.TextWrapped(gamepaths[0]);
if (gamepaths.Count > 1)
{
ImGui.SameLine();
ImGui.TextUnformatted($"(and {gamepaths.Count - 1} more)");
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.InfoCircle);
UiSharedService.AttachToolTip(string.Join(Environment.NewLine, gamepaths.Skip(1)));
}
}
}
public override void OnOpen()
{
_hasUpdate = true;
_selectedHash = string.Empty;
_enableBc7ConversionMode = false;
_texturesToConvert.Clear();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_conversionProgress.ProgressChanged -= ConversionProgress_ProgressChanged;
}
private void ConversionProgress_ProgressChanged(object? sender, (string, int) e)
{
_conversionCurrentFileName = e.Item1;
_conversionCurrentFileProgress = e.Item2;
}
private void DrawTable(IGrouping<string, CharacterAnalyzer.FileDataEntry> fileGroup)
{
var tableColumns = string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal)
? (_enableBc7ConversionMode ? 7 : 6)
: (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal) ? 6 : 5);
using var table = ImRaii.Table("Analysis", tableColumns, ImGuiTableFlags.Sortable | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit,
new Vector2(0, 300));
if (!table.Success) return;
ImGui.TableSetupColumn("Hash");
ImGui.TableSetupColumn("Filepaths");
ImGui.TableSetupColumn("Gamepaths");
ImGui.TableSetupColumn("Original Size");
ImGui.TableSetupColumn("Compressed Size");
if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal))
{
ImGui.TableSetupColumn("Format");
if (_enableBc7ConversionMode) ImGui.TableSetupColumn("Convert to BC7");
}
if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal))
{
ImGui.TableSetupColumn("Triangles");
}
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableHeadersRow();
var sortSpecs = ImGui.TableGetSortSpecs();
if (sortSpecs.SpecsDirty)
{
var idx = sortSpecs.Specs.ColumnIndex;
if (idx == 0 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Key, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 0 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Key, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 1 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.FilePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 1 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.FilePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 2 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.GamePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 2 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.GamePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 3 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.OriginalSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 3 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.OriginalSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 4 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.CompressedSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 4 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.CompressedSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal) && idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.Triangles).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal) && idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.Triangles).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal) && idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.Format.Value, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal) && idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.Format.Value, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
sortSpecs.SpecsDirty = false;
}
foreach (var item in fileGroup)
{
using var text = ImRaii.PushColor(ImGuiCol.Text, new Vector4(0, 0, 0, 1), string.Equals(item.Hash, _selectedHash, StringComparison.Ordinal));
using var text2 = ImRaii.PushColor(ImGuiCol.Text, new Vector4(1, 1, 1, 1), !item.IsComputed);
ImGui.TableNextColumn();
if (!item.IsComputed)
{
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, UiSharedService.Color(ImGuiColors.DalamudRed));
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, UiSharedService.Color(ImGuiColors.DalamudRed));
}
if (string.Equals(_selectedHash, item.Hash, StringComparison.Ordinal))
{
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, UiSharedService.Color(ImGuiColors.DalamudYellow));
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, UiSharedService.Color(ImGuiColors.DalamudYellow));
}
ImGui.TextUnformatted(item.Hash);
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
ImGui.TableNextColumn();
ImGui.TextUnformatted(item.FilePaths.Count.ToString());
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
ImGui.TableNextColumn();
ImGui.TextUnformatted(item.GamePaths.Count.ToString());
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
ImGui.TableNextColumn();
ImGui.TextUnformatted(UiSharedService.ByteToString(item.OriginalSize));
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
ImGui.TableNextColumn();
ImGui.TextUnformatted(UiSharedService.ByteToString(item.CompressedSize));
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal))
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(item.Format.Value);
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
if (_enableBc7ConversionMode)
{
ImGui.TableNextColumn();
if (string.Equals(item.Format.Value, "BC7", StringComparison.Ordinal))
{
ImGui.TextUnformatted("");
continue;
}
var filePath = item.FilePaths[0];
bool toConvert = _texturesToConvert.ContainsKey(filePath);
if (ImGui.Checkbox("###convert" + item.Hash, ref toConvert))
{
if (toConvert && !_texturesToConvert.ContainsKey(filePath))
{
_texturesToConvert[filePath] = item.FilePaths.Skip(1).ToArray();
}
else if (!toConvert && _texturesToConvert.ContainsKey(filePath))
{
_texturesToConvert.Remove(filePath);
}
}
}
}
if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal))
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(item.Triangles.ToString());
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
}
}
}
}

View File

@@ -0,0 +1,248 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors;
using LightlessSync.MareConfiguration;
using LightlessSync.PlayerData.Handlers;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.WebAPI.Files;
using LightlessSync.WebAPI.Files.Models;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Numerics;
namespace LightlessSync.UI;
public class DownloadUi : WindowMediatorSubscriberBase
{
private readonly MareConfigService _configService;
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
private readonly DalamudUtilService _dalamudUtilService;
private readonly FileUploadManager _fileTransferManager;
private readonly UiSharedService _uiShared;
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, MareConfigService configService,
FileUploadManager fileTransferManager, MareMediator mediator, UiSharedService uiShared, PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "Mare Synchronos Downloads", performanceCollectorService)
{
_dalamudUtilService = dalamudUtilService;
_configService = configService;
_fileTransferManager = fileTransferManager;
_uiShared = uiShared;
SizeConstraints = new WindowSizeConstraints()
{
MaximumSize = new Vector2(500, 90),
MinimumSize = new Vector2(500, 90),
};
Flags |= ImGuiWindowFlags.NoMove;
Flags |= ImGuiWindowFlags.NoBackground;
Flags |= ImGuiWindowFlags.NoInputs;
Flags |= ImGuiWindowFlags.NoNavFocus;
Flags |= ImGuiWindowFlags.NoResize;
Flags |= ImGuiWindowFlags.NoScrollbar;
Flags |= ImGuiWindowFlags.NoTitleBar;
Flags |= ImGuiWindowFlags.NoDecoration;
Flags |= ImGuiWindowFlags.NoFocusOnAppearing;
DisableWindowSounds = true;
ForceMainWindow = true;
IsOpen = true;
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen = false);
Mediator.Subscribe<GposeEndMessage>(this, (_) => IsOpen = true);
Mediator.Subscribe<PlayerUploadingMessage>(this, (msg) =>
{
if (msg.IsUploading)
{
_uploadingPlayers[msg.Handler] = true;
}
else
{
_uploadingPlayers.TryRemove(msg.Handler, out _);
}
});
}
protected override void DrawInternal()
{
if (_configService.Current.ShowTransferWindow)
{
try
{
if (_fileTransferManager.CurrentUploads.Any())
{
var currentUploads = _fileTransferManager.CurrentUploads.ToList();
var totalUploads = currentUploads.Count;
var doneUploads = currentUploads.Count(c => c.IsTransferred);
var totalUploaded = currentUploads.Sum(c => c.Transferred);
var totalToUpload = currentUploads.Sum(c => c.Total);
UiSharedService.DrawOutlinedFont($"▲", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
ImGui.SameLine();
var xDistance = ImGui.GetCursorPosX();
UiSharedService.DrawOutlinedFont($"Compressing+Uploading {doneUploads}/{totalUploads}",
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
ImGui.NewLine();
ImGui.SameLine(xDistance);
UiSharedService.DrawOutlinedFont(
$"{UiSharedService.ByteToString(totalUploaded, addSuffix: false)}/{UiSharedService.ByteToString(totalToUpload)}",
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
if (_currentDownloads.Any()) ImGui.Separator();
}
}
catch
{
// ignore errors thrown from UI
}
try
{
foreach (var item in _currentDownloads.ToList())
{
var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot);
var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue);
var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading);
var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing);
var totalFiles = item.Value.Sum(c => c.Value.TotalFiles);
var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles);
var totalBytes = item.Value.Sum(c => c.Value.TotalBytes);
var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes);
UiSharedService.DrawOutlinedFont($"▼", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
ImGui.SameLine();
var xDistance = ImGui.GetCursorPosX();
UiSharedService.DrawOutlinedFont(
$"{item.Key.Name} [W:{dlSlot}/Q:{dlQueue}/P:{dlProg}/D:{dlDecomp}]",
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
ImGui.NewLine();
ImGui.SameLine(xDistance);
UiSharedService.DrawOutlinedFont(
$"{transferredFiles}/{totalFiles} ({UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)})",
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
}
}
catch
{
// ignore errors thrown from UI
}
}
if (_configService.Current.ShowTransferBars)
{
const int transparency = 100;
const int dlBarBorder = 3;
foreach (var transfer in _currentDownloads.ToList())
{
var screenPos = _dalamudUtilService.WorldToScreen(transfer.Key.GetGameObject());
if (screenPos == Vector2.Zero) continue;
var totalBytes = transfer.Value.Sum(c => c.Value.TotalBytes);
var transferredBytes = transfer.Value.Sum(c => c.Value.TransferredBytes);
var maxDlText = $"{UiSharedService.ByteToString(totalBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
var textSize = _configService.Current.TransferBarsShowText ? ImGui.CalcTextSize(maxDlText) : new Vector2(10, 10);
int dlBarHeight = _configService.Current.TransferBarsHeight > ((int)textSize.Y + 5) ? _configService.Current.TransferBarsHeight : (int)textSize.Y + 5;
int dlBarWidth = _configService.Current.TransferBarsWidth > ((int)textSize.X + 10) ? _configService.Current.TransferBarsWidth : (int)textSize.X + 10;
var dlBarStart = new Vector2(screenPos.X - dlBarWidth / 2f, screenPos.Y - dlBarHeight / 2f);
var dlBarEnd = new Vector2(screenPos.X + dlBarWidth / 2f, screenPos.Y + dlBarHeight / 2f);
var drawList = ImGui.GetBackgroundDrawList();
drawList.AddRectFilled(
dlBarStart with { X = dlBarStart.X - dlBarBorder - 1, Y = dlBarStart.Y - dlBarBorder - 1 },
dlBarEnd with { X = dlBarEnd.X + dlBarBorder + 1, Y = dlBarEnd.Y + dlBarBorder + 1 },
UiSharedService.Color(0, 0, 0, transparency), 1);
drawList.AddRectFilled(dlBarStart with { X = dlBarStart.X - dlBarBorder, Y = dlBarStart.Y - dlBarBorder },
dlBarEnd with { X = dlBarEnd.X + dlBarBorder, Y = dlBarEnd.Y + dlBarBorder },
UiSharedService.Color(220, 220, 220, transparency), 1);
drawList.AddRectFilled(dlBarStart, dlBarEnd,
UiSharedService.Color(0, 0, 0, transparency), 1);
var dlProgressPercent = transferredBytes / (double)totalBytes;
drawList.AddRectFilled(dlBarStart,
dlBarEnd with { X = dlBarStart.X + (float)(dlProgressPercent * dlBarWidth) },
UiSharedService.Color(50, 205, 50, transparency), 1);
if (_configService.Current.TransferBarsShowText)
{
var downloadText = $"{UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
UiSharedService.DrawOutlinedFont(drawList, downloadText,
screenPos with { X = screenPos.X - textSize.X / 2f - 1, Y = screenPos.Y - textSize.Y / 2f - 1 },
UiSharedService.Color(255, 255, 255, transparency),
UiSharedService.Color(0, 0, 0, transparency), 1);
}
}
if (_configService.Current.ShowUploading)
{
foreach (var player in _uploadingPlayers.Select(p => p.Key).ToList())
{
var screenPos = _dalamudUtilService.WorldToScreen(player.GetGameObject());
if (screenPos == Vector2.Zero) continue;
try
{
using var _ = _uiShared.UidFont.Push();
var uploadText = "Uploading";
var textSize = ImGui.CalcTextSize(uploadText);
var drawList = ImGui.GetBackgroundDrawList();
UiSharedService.DrawOutlinedFont(drawList, uploadText,
screenPos with { X = screenPos.X - textSize.X / 2f - 1, Y = screenPos.Y - textSize.Y / 2f - 1 },
UiSharedService.Color(255, 255, 0, transparency),
UiSharedService.Color(0, 0, 0, transparency), 2);
}
catch
{
// ignore errors thrown on UI
}
}
}
}
}
public override bool DrawConditions()
{
if (_uiShared.EditTrackerPosition) return true;
if (!_configService.Current.ShowTransferWindow && !_configService.Current.ShowTransferBars) return false;
if (!_currentDownloads.Any() && !_fileTransferManager.CurrentUploads.Any() && !_uploadingPlayers.Any()) return false;
if (!IsOpen) return false;
return true;
}
public override void PreDraw()
{
base.PreDraw();
if (_uiShared.EditTrackerPosition)
{
Flags &= ~ImGuiWindowFlags.NoMove;
Flags &= ~ImGuiWindowFlags.NoBackground;
Flags &= ~ImGuiWindowFlags.NoInputs;
Flags &= ~ImGuiWindowFlags.NoResize;
}
else
{
Flags |= ImGuiWindowFlags.NoMove;
Flags |= ImGuiWindowFlags.NoBackground;
Flags |= ImGuiWindowFlags.NoInputs;
Flags |= ImGuiWindowFlags.NoResize;
}
var maxHeight = ImGui.GetTextLineHeight() * (_configService.Current.ParallelDownloads + 3);
SizeConstraints = new()
{
MinimumSize = new Vector2(300, maxHeight),
MaximumSize = new Vector2(300, maxHeight),
};
}
}

View File

@@ -0,0 +1,71 @@
using LightlessSync.API.Dto.Group;
using LightlessSync.MareConfiguration;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Components;
using LightlessSync.UI.Handlers;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging;
using System.Collections.Immutable;
namespace LightlessSync.UI;
public class DrawEntityFactory
{
private readonly ILogger<DrawEntityFactory> _logger;
private readonly ApiController _apiController;
private readonly MareMediator _mediator;
private readonly SelectPairForTagUi _selectPairForTagUi;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly UiSharedService _uiSharedService;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly CharaDataManager _charaDataManager;
private readonly SelectTagForPairUi _selectTagForPairUi;
private readonly TagHandler _tagHandler;
private readonly IdDisplayHandler _uidDisplayHandler;
public DrawEntityFactory(ILogger<DrawEntityFactory> logger, ApiController apiController, IdDisplayHandler uidDisplayHandler,
SelectTagForPairUi selectTagForPairUi, MareMediator mediator,
TagHandler tagHandler, SelectPairForTagUi selectPairForTagUi,
ServerConfigurationManager serverConfigurationManager, UiSharedService uiSharedService,
PlayerPerformanceConfigService playerPerformanceConfigService, CharaDataManager charaDataManager)
{
_logger = logger;
_apiController = apiController;
_uidDisplayHandler = uidDisplayHandler;
_selectTagForPairUi = selectTagForPairUi;
_mediator = mediator;
_tagHandler = tagHandler;
_selectPairForTagUi = selectPairForTagUi;
_serverConfigurationManager = serverConfigurationManager;
_uiSharedService = uiSharedService;
_playerPerformanceConfigService = playerPerformanceConfigService;
_charaDataManager = charaDataManager;
}
public DrawFolderGroup CreateDrawGroupFolder(GroupFullInfoDto groupFullInfoDto,
Dictionary<Pair, List<GroupFullInfoDto>> filteredPairs,
IImmutableList<Pair> allPairs)
{
return new DrawFolderGroup(groupFullInfoDto.Group.GID, groupFullInfoDto, _apiController,
filteredPairs.Select(p => CreateDrawPair(groupFullInfoDto.Group.GID + p.Key.UserData.UID, p.Key, p.Value, groupFullInfoDto)).ToImmutableList(),
allPairs, _tagHandler, _uidDisplayHandler, _mediator, _uiSharedService);
}
public DrawFolderTag CreateDrawTagFolder(string tag,
Dictionary<Pair, List<GroupFullInfoDto>> filteredPairs,
IImmutableList<Pair> allPairs)
{
return new(tag, filteredPairs.Select(u => CreateDrawPair(tag, u.Key, u.Value, null)).ToImmutableList(),
allPairs, _tagHandler, _apiController, _selectPairForTagUi, _uiSharedService);
}
public DrawUserPair CreateDrawPair(string id, Pair user, List<GroupFullInfoDto> groups, GroupFullInfoDto? currentGroup)
{
return new DrawUserPair(id + user.UserData.UID, user, groups, currentGroup, _apiController, _uidDisplayHandler,
_mediator, _selectTagForPairUi, _serverConfigurationManager, _uiSharedService, _playerPerformanceConfigService,
_charaDataManager);
}
}

View File

@@ -0,0 +1,206 @@
using Dalamud.Game.Gui.Dtr;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Plugin.Services;
using LightlessSync.MareConfiguration;
using LightlessSync.MareConfiguration.Configurations;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Runtime.InteropServices;
namespace LightlessSync.UI;
public sealed class DtrEntry : IDisposable, IHostedService
{
private readonly ApiController _apiController;
private readonly CancellationTokenSource _cancellationTokenSource = new();
private readonly ConfigurationServiceBase<MareConfig> _configService;
private readonly IDtrBar _dtrBar;
private readonly Lazy<IDtrBarEntry> _entry;
private readonly ILogger<DtrEntry> _logger;
private readonly MareMediator _mareMediator;
private readonly PairManager _pairManager;
private Task? _runTask;
private string? _text;
private string? _tooltip;
private Colors _colors;
public DtrEntry(ILogger<DtrEntry> logger, IDtrBar dtrBar, ConfigurationServiceBase<MareConfig> configService, MareMediator mareMediator, PairManager pairManager, ApiController apiController)
{
_logger = logger;
_dtrBar = dtrBar;
_entry = new(CreateEntry);
_configService = configService;
_mareMediator = mareMediator;
_pairManager = pairManager;
_apiController = apiController;
}
public void Dispose()
{
if (_entry.IsValueCreated)
{
_logger.LogDebug("Disposing DtrEntry");
Clear();
_entry.Value.Remove();
}
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting DtrEntry");
_runTask = Task.Run(RunAsync, _cancellationTokenSource.Token);
_logger.LogInformation("Started DtrEntry");
return Task.CompletedTask;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_cancellationTokenSource.Cancel();
try
{
await _runTask!.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// ignore cancelled
}
finally
{
_cancellationTokenSource.Dispose();
}
}
private void Clear()
{
if (!_entry.IsValueCreated) return;
_logger.LogInformation("Clearing entry");
_text = null;
_tooltip = null;
_colors = default;
_entry.Value.Shown = false;
}
private IDtrBarEntry CreateEntry()
{
_logger.LogTrace("Creating new DtrBar entry");
var entry = _dtrBar.Get("Mare Synchronos");
entry.OnClick = _ => _mareMediator.Publish(new UiToggleMessage(typeof(CompactUi)));
return entry;
}
private async Task RunAsync()
{
while (!_cancellationTokenSource.IsCancellationRequested)
{
await Task.Delay(1000, _cancellationTokenSource.Token).ConfigureAwait(false);
Update();
}
}
private void Update()
{
if (!_configService.Current.EnableDtrEntry || !_configService.Current.HasValidSetup())
{
if (_entry.IsValueCreated && _entry.Value.Shown)
{
_logger.LogInformation("Disabling entry");
Clear();
}
return;
}
if (!_entry.Value.Shown)
{
_logger.LogInformation("Showing entry");
_entry.Value.Shown = true;
}
string text;
string tooltip;
Colors colors;
if (_apiController.IsConnected)
{
var pairCount = _pairManager.GetVisibleUserCount();
text = $"\uE044 {pairCount}";
if (pairCount > 0)
{
IEnumerable<string> visiblePairs;
if (_configService.Current.ShowUidInDtrTooltip)
{
visiblePairs = _pairManager.GetOnlineUserPairs()
.Where(x => x.IsVisible)
.Select(x => string.Format("{0} ({1})", _configService.Current.PreferNoteInDtrTooltip ? x.GetNote() ?? x.PlayerName : x.PlayerName, x.UserData.AliasOrUID));
}
else
{
visiblePairs = _pairManager.GetOnlineUserPairs()
.Where(x => x.IsVisible)
.Select(x => string.Format("{0}", _configService.Current.PreferNoteInDtrTooltip ? x.GetNote() ?? x.PlayerName : x.PlayerName));
}
tooltip = $"Mare Synchronos: Connected{Environment.NewLine}----------{Environment.NewLine}{string.Join(Environment.NewLine, visiblePairs)}";
colors = _configService.Current.DtrColorsPairsInRange;
}
else
{
tooltip = "Mare Synchronos: Connected";
colors = _configService.Current.DtrColorsDefault;
}
}
else
{
text = "\uE044 \uE04C";
tooltip = "Mare Synchronos: Not Connected";
colors = _configService.Current.DtrColorsNotConnected;
}
if (!_configService.Current.UseColorsInDtr)
colors = default;
if (!string.Equals(text, _text, StringComparison.Ordinal) || !string.Equals(tooltip, _tooltip, StringComparison.Ordinal) || colors != _colors)
{
_text = text;
_tooltip = tooltip;
_colors = colors;
_entry.Value.Text = BuildColoredSeString(text, colors);
_entry.Value.Tooltip = tooltip;
}
}
#region Colored SeString
private const byte _colorTypeForeground = 0x13;
private const byte _colorTypeGlow = 0x14;
private static SeString BuildColoredSeString(string text, Colors colors)
{
var ssb = new SeStringBuilder();
if (colors.Foreground != default)
ssb.Add(BuildColorStartPayload(_colorTypeForeground, colors.Foreground));
if (colors.Glow != default)
ssb.Add(BuildColorStartPayload(_colorTypeGlow, colors.Glow));
ssb.AddText(text);
if (colors.Glow != default)
ssb.Add(BuildColorEndPayload(_colorTypeGlow));
if (colors.Foreground != default)
ssb.Add(BuildColorEndPayload(_colorTypeForeground));
return ssb.Build();
}
private static RawPayload BuildColorStartPayload(byte colorType, uint color)
=> new(unchecked([0x02, colorType, 0x05, 0xF6, byte.Max((byte)color, 0x01), byte.Max((byte)(color >> 8), 0x01), byte.Max((byte)(color >> 16), 0x01), 0x03]));
private static RawPayload BuildColorEndPayload(byte colorType)
=> new([0x02, colorType, 0x02, 0xEC, 0x03]);
[StructLayout(LayoutKind.Sequential)]
public readonly record struct Colors(uint Foreground = default, uint Glow = default);
#endregion
}

View File

@@ -0,0 +1,233 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using LightlessSync.API.Data;
using LightlessSync.API.Dto.User;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace LightlessSync.UI;
public class EditProfileUi : WindowMediatorSubscriberBase
{
private readonly ApiController _apiController;
private readonly FileDialogManager _fileDialogManager;
private readonly MareProfileManager _mareProfileManager;
private readonly UiSharedService _uiSharedService;
private bool _adjustedForScollBarsLocalProfile = false;
private bool _adjustedForScollBarsOnlineProfile = false;
private string _descriptionText = string.Empty;
private IDalamudTextureWrap? _pfpTextureWrap;
private string _profileDescription = string.Empty;
private byte[] _profileImage = [];
private bool _showFileDialogError = false;
private bool _wasOpen;
public EditProfileUi(ILogger<EditProfileUi> logger, MareMediator mediator,
ApiController apiController, UiSharedService uiSharedService, FileDialogManager fileDialogManager,
MareProfileManager mareProfileManager, PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "Mare Synchronos Edit Profile###LightlessSyncEditProfileUI", performanceCollectorService)
{
IsOpen = false;
this.SizeConstraints = new()
{
MinimumSize = new(768, 512),
MaximumSize = new(768, 2000)
};
_apiController = apiController;
_uiSharedService = uiSharedService;
_fileDialogManager = fileDialogManager;
_mareProfileManager = mareProfileManager;
Mediator.Subscribe<GposeStartMessage>(this, (_) => { _wasOpen = IsOpen; IsOpen = false; });
Mediator.Subscribe<GposeEndMessage>(this, (_) => IsOpen = _wasOpen);
Mediator.Subscribe<DisconnectedMessage>(this, (_) => IsOpen = false);
Mediator.Subscribe<ClearProfileDataMessage>(this, (msg) =>
{
if (msg.UserData == null || string.Equals(msg.UserData.UID, _apiController.UID, StringComparison.Ordinal))
{
_pfpTextureWrap?.Dispose();
_pfpTextureWrap = null;
}
});
}
protected override void DrawInternal()
{
_uiSharedService.BigText("Current Profile (as saved on server)");
var profile = _mareProfileManager.GetMareProfile(new UserData(_apiController.UID));
if (profile.IsFlagged)
{
UiSharedService.ColorTextWrapped(profile.Description, ImGuiColors.DalamudRed);
return;
}
if (!_profileImage.SequenceEqual(profile.ImageData.Value))
{
_profileImage = profile.ImageData.Value;
_pfpTextureWrap?.Dispose();
_pfpTextureWrap = _uiSharedService.LoadImage(_profileImage);
}
if (!string.Equals(_profileDescription, profile.Description, StringComparison.OrdinalIgnoreCase))
{
_profileDescription = profile.Description;
_descriptionText = _profileDescription;
}
if (_pfpTextureWrap != null)
{
ImGui.Image(_pfpTextureWrap.Handle, ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height));
}
var spacing = ImGui.GetStyle().ItemSpacing.X;
ImGuiHelpers.ScaledRelativeSameLine(256, spacing);
using (_uiSharedService.GameFont.Push())
{
var descriptionTextSize = ImGui.CalcTextSize(profile.Description, wrapWidth: 256f);
var childFrame = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 256);
if (descriptionTextSize.Y > childFrame.Y)
{
_adjustedForScollBarsOnlineProfile = true;
}
else
{
_adjustedForScollBarsOnlineProfile = false;
}
childFrame = childFrame with
{
X = childFrame.X + (_adjustedForScollBarsOnlineProfile ? ImGui.GetStyle().ScrollbarSize : 0),
};
if (ImGui.BeginChildFrame(101, childFrame))
{
UiSharedService.TextWrapped(profile.Description);
}
ImGui.EndChildFrame();
}
var nsfw = profile.IsNSFW;
ImGui.BeginDisabled();
ImGui.Checkbox("Is NSFW", ref nsfw);
ImGui.EndDisabled();
ImGui.Separator();
_uiSharedService.BigText("Notes and Rules for Profiles");
ImGui.TextWrapped($"- All users that are paired and unpaused with you will be able to see your profile picture and description.{Environment.NewLine}" +
$"- Other users have the possibility to report your profile for breaking the rules.{Environment.NewLine}" +
$"- !!! AVOID: anything as profile image that can be considered highly illegal or obscene (bestiality, anything that could be considered a sexual act with a minor (that includes Lalafells), etc.){Environment.NewLine}" +
$"- !!! AVOID: slurs of any kind in the description that can be considered highly offensive{Environment.NewLine}" +
$"- In case of valid reports from other users this can lead to disabling your profile forever or terminating your Mare account indefinitely.{Environment.NewLine}" +
$"- Judgement of your profile validity from reports through staff is not up to debate and the decisions to disable your profile/account permanent.{Environment.NewLine}" +
$"- If your profile picture or profile description could be considered NSFW, enable the toggle below.");
ImGui.Separator();
_uiSharedService.BigText("Profile Settings");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture"))
{
_fileDialogManager.OpenFileDialog("Select new Profile picture", ".png", (success, file) =>
{
if (!success) return;
_ = Task.Run(async () =>
{
var fileContent = File.ReadAllBytes(file);
using MemoryStream ms = new(fileContent);
var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false);
if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase))
{
_showFileDialogError = true;
return;
}
using var image = Image.Load<Rgba32>(fileContent);
if (image.Width > 256 || image.Height > 256 || (fileContent.Length > 250 * 1024))
{
_showFileDialogError = true;
return;
}
_showFileDialogError = false;
await _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, Convert.ToBase64String(fileContent), Description: null))
.ConfigureAwait(false);
});
});
}
UiSharedService.AttachToolTip("Select and upload a new profile picture");
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture"))
{
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, "", Description: null));
}
UiSharedService.AttachToolTip("Clear your currently uploaded profile picture");
if (_showFileDialogError)
{
UiSharedService.ColorTextWrapped("The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size", ImGuiColors.DalamudRed);
}
var isNsfw = profile.IsNSFW;
if (ImGui.Checkbox("Profile is NSFW", ref isNsfw))
{
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, isNsfw, ProfilePictureBase64: null, Description: null));
}
_uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON");
var widthTextBox = 400;
var posX = ImGui.GetCursorPosX();
ImGui.TextUnformatted($"Description {_descriptionText.Length}/1500");
ImGui.SetCursorPosX(posX);
ImGuiHelpers.ScaledRelativeSameLine(widthTextBox, ImGui.GetStyle().ItemSpacing.X);
ImGui.TextUnformatted("Preview (approximate)");
using (_uiSharedService.GameFont.Push())
ImGui.InputTextMultiline("##description", ref _descriptionText, 1500, ImGuiHelpers.ScaledVector2(widthTextBox, 200));
ImGui.SameLine();
using (_uiSharedService.GameFont.Push())
{
var descriptionTextSizeLocal = ImGui.CalcTextSize(_descriptionText, wrapWidth: 256f);
var childFrameLocal = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 200);
if (descriptionTextSizeLocal.Y > childFrameLocal.Y)
{
_adjustedForScollBarsLocalProfile = true;
}
else
{
_adjustedForScollBarsLocalProfile = false;
}
childFrameLocal = childFrameLocal with
{
X = childFrameLocal.X + (_adjustedForScollBarsLocalProfile ? ImGui.GetStyle().ScrollbarSize : 0),
};
if (ImGui.BeginChildFrame(102, childFrameLocal))
{
UiSharedService.TextWrapped(_descriptionText);
}
ImGui.EndChildFrame();
}
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description"))
{
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, _descriptionText));
}
UiSharedService.AttachToolTip("Sets your profile description text");
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description"))
{
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, ""));
}
UiSharedService.AttachToolTip("Clears your profile description text");
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_pfpTextureWrap?.Dispose();
}
}

View File

@@ -0,0 +1,222 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.Services;
using LightlessSync.Services.Events;
using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.Globalization;
using System.Numerics;
namespace LightlessSync.UI;
internal class EventViewerUI : WindowMediatorSubscriberBase
{
private readonly EventAggregator _eventAggregator;
private readonly UiSharedService _uiSharedService;
private List<Event> _currentEvents = new();
private Lazy<List<Event>> _filteredEvents;
private string _filterFreeText = string.Empty;
private string _filterCharacter = string.Empty;
private string _filterUid = string.Empty;
private string _filterSource = string.Empty;
private string _filterEvent = string.Empty;
private List<Event> CurrentEvents
{
get
{
return _currentEvents;
}
set
{
_currentEvents = value;
_filteredEvents = RecreateFilter();
}
}
public EventViewerUI(ILogger<EventViewerUI> logger, MareMediator mediator,
EventAggregator eventAggregator, UiSharedService uiSharedService, PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "Event Viewer", performanceCollectorService)
{
_eventAggregator = eventAggregator;
_uiSharedService = uiSharedService;
SizeConstraints = new()
{
MinimumSize = new(600, 500),
MaximumSize = new(1000, 2000)
};
_filteredEvents = RecreateFilter();
}
private Lazy<List<Event>> RecreateFilter()
{
return new(() =>
CurrentEvents.Where(f =>
(string.IsNullOrEmpty(_filterFreeText)
|| (f.EventSource.Contains(_filterFreeText, StringComparison.OrdinalIgnoreCase)
|| f.Character.Contains(_filterFreeText, StringComparison.OrdinalIgnoreCase)
|| f.UID.Contains(_filterFreeText, StringComparison.OrdinalIgnoreCase)
|| f.Message.Contains(_filterFreeText, StringComparison.OrdinalIgnoreCase)
))
&&
(string.IsNullOrEmpty(_filterUid)
|| (f.UID.Contains(_filterUid, StringComparison.OrdinalIgnoreCase))
)
&&
(string.IsNullOrEmpty(_filterSource)
|| (f.EventSource.Contains(_filterSource, StringComparison.OrdinalIgnoreCase))
)
&&
(string.IsNullOrEmpty(_filterCharacter)
|| (f.Character.Contains(_filterCharacter, StringComparison.OrdinalIgnoreCase))
)
&&
(string.IsNullOrEmpty(_filterEvent)
|| (f.Message.Contains(_filterEvent, StringComparison.OrdinalIgnoreCase))
)
).ToList());
}
private void ClearFilters()
{
_filterFreeText = string.Empty;
_filterCharacter = string.Empty;
_filterUid = string.Empty;
_filterSource = string.Empty;
_filterEvent = string.Empty;
_filteredEvents = RecreateFilter();
}
public override void OnOpen()
{
CurrentEvents = _eventAggregator.EventList.Value.OrderByDescending(f => f.EventTime).ToList();
ClearFilters();
}
protected override void DrawInternal()
{
using (ImRaii.Disabled(!_eventAggregator.NewEventsAvailable))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowsToCircle, "Refresh events"))
{
CurrentEvents = _eventAggregator.EventList.Value.OrderByDescending(f => f.EventTime).ToList();
}
}
if (_eventAggregator.NewEventsAvailable)
{
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
UiSharedService.ColorTextWrapped("New events are available, press refresh to update", ImGuiColors.DalamudYellow);
}
var buttonSize = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.FolderOpen, "Open EventLog Folder");
var dist = ImGui.GetWindowContentRegionMax().X - buttonSize;
ImGui.SameLine(dist);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FolderOpen, "Open EventLog folder"))
{
ProcessStartInfo ps = new()
{
FileName = _eventAggregator.EventLogFolder,
UseShellExecute = true,
WindowStyle = ProcessWindowStyle.Normal
};
Process.Start(ps);
}
_uiSharedService.BigText("Last Events");
var foldOut = ImRaii.TreeNode("Filter");
if (foldOut)
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Ban, "Clear Filters"))
{
ClearFilters();
}
bool changedFilter = false;
ImGui.SetNextItemWidth(200);
changedFilter |= ImGui.InputText("Search all columns", ref _filterFreeText, 50);
ImGui.SetNextItemWidth(200);
changedFilter |= ImGui.InputText("Filter by Source", ref _filterSource, 50);
ImGui.SetNextItemWidth(200);
changedFilter |= ImGui.InputText("Filter by UID", ref _filterUid, 50);
ImGui.SetNextItemWidth(200);
changedFilter |= ImGui.InputText("Filter by Character", ref _filterCharacter, 50);
ImGui.SetNextItemWidth(200);
changedFilter |= ImGui.InputText("Filter by Event", ref _filterEvent, 50);
if (changedFilter) _filteredEvents = RecreateFilter();
}
foldOut.Dispose();
var cursorPos = ImGui.GetCursorPosY();
var max = ImGui.GetWindowContentRegionMax();
var min = ImGui.GetWindowContentRegionMin();
var width = max.X - min.X;
var height = max.Y - cursorPos;
using var table = ImRaii.Table("eventTable", 6, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollY | ImGuiTableFlags.RowBg,
new Vector2(width, height));
if (table)
{
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.NoSort);
ImGui.TableSetupColumn("Time");
ImGui.TableSetupColumn("Source");
ImGui.TableSetupColumn("UID");
ImGui.TableSetupColumn("Character");
ImGui.TableSetupColumn("Event");
ImGui.TableHeadersRow();
foreach (var ev in _filteredEvents.Value)
{
var icon = ev.EventSeverity switch
{
EventSeverity.Informational => FontAwesomeIcon.InfoCircle,
EventSeverity.Warning => FontAwesomeIcon.ExclamationTriangle,
EventSeverity.Error => FontAwesomeIcon.Cross,
_ => FontAwesomeIcon.QuestionCircle
};
var iconColor = ev.EventSeverity switch
{
EventSeverity.Informational => new Vector4(),
EventSeverity.Warning => ImGuiColors.DalamudYellow,
EventSeverity.Error => ImGuiColors.DalamudRed,
_ => new Vector4()
};
ImGui.TableNextColumn();
_uiSharedService.IconText(icon, iconColor == new Vector4() ? null : iconColor);
UiSharedService.AttachToolTip(ev.EventSeverity.ToString());
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(ev.EventTime.ToString("G", CultureInfo.CurrentCulture));
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(ev.EventSource);
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(string.IsNullOrEmpty(ev.UID) ? "--" : ev.UID);
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(string.IsNullOrEmpty(ev.Character) ? "--" : ev.Character);
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
var posX = ImGui.GetCursorPosX();
var maxTextLength = ImGui.GetWindowContentRegionMax().X - posX;
var textSize = ImGui.CalcTextSize(ev.Message).X;
var msg = ev.Message;
while (textSize > maxTextLength)
{
msg = msg[..^5] + "...";
textSize = ImGui.CalcTextSize(msg).X;
}
ImGui.TextUnformatted(msg);
if (!string.Equals(msg, ev.Message, StringComparison.Ordinal))
{
UiSharedService.AttachToolTip(ev.Message);
}
}
}
}
}

View File

@@ -0,0 +1,266 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Dto.Group;
using LightlessSync.MareConfiguration;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
namespace LightlessSync.UI.Handlers;
public class IdDisplayHandler
{
private readonly MareConfigService _mareConfigService;
private readonly MareMediator _mediator;
private readonly ServerConfigurationManager _serverManager;
private readonly Dictionary<string, bool> _showIdForEntry = new(StringComparer.Ordinal);
private string _editComment = string.Empty;
private string _editEntry = string.Empty;
private bool _editIsUid = false;
private string _lastMouseOverUid = string.Empty;
private bool _popupShown = false;
private DateTime? _popupTime;
public IdDisplayHandler(MareMediator mediator, ServerConfigurationManager serverManager, MareConfigService mareConfigService)
{
_mediator = mediator;
_serverManager = serverManager;
_mareConfigService = mareConfigService;
}
public void DrawGroupText(string id, GroupFullInfoDto group, float textPosX, Func<float> editBoxWidth)
{
ImGui.SameLine(textPosX);
(bool textIsUid, string playerText) = GetGroupText(group);
if (!string.Equals(_editEntry, group.GID, StringComparison.Ordinal))
{
ImGui.AlignTextToFramePadding();
using (ImRaii.PushFont(UiBuilder.MonoFont, textIsUid))
ImGui.TextUnformatted(playerText);
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
var prevState = textIsUid;
if (_showIdForEntry.ContainsKey(group.GID))
{
prevState = _showIdForEntry[group.GID];
}
_showIdForEntry[group.GID] = !prevState;
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
if (_editIsUid)
{
_serverManager.SetNoteForUid(_editEntry, _editComment, save: true);
}
else
{
_serverManager.SetNoteForGid(_editEntry, _editComment, save: true);
}
_editComment = _serverManager.GetNoteForGid(group.GID) ?? string.Empty;
_editEntry = group.GID;
_editIsUid = false;
}
}
else
{
ImGui.AlignTextToFramePadding();
ImGui.SetNextItemWidth(editBoxWidth.Invoke());
if (ImGui.InputTextWithHint("", "Name/Notes", ref _editComment, 255, ImGuiInputTextFlags.EnterReturnsTrue))
{
_serverManager.SetNoteForGid(group.GID, _editComment, save: true);
_editEntry = string.Empty;
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_editEntry = string.Empty;
}
UiSharedService.AttachToolTip("Hit ENTER to save\nRight click to cancel");
}
}
public void DrawPairText(string id, Pair pair, float textPosX, Func<float> editBoxWidth)
{
ImGui.SameLine(textPosX);
(bool textIsUid, string playerText) = GetPlayerText(pair);
if (!string.Equals(_editEntry, pair.UserData.UID, StringComparison.Ordinal))
{
ImGui.AlignTextToFramePadding();
using (ImRaii.PushFont(UiBuilder.MonoFont, textIsUid)) ImGui.TextUnformatted(playerText);
if (ImGui.IsItemHovered())
{
if (!string.Equals(_lastMouseOverUid, id))
{
_popupTime = DateTime.UtcNow.AddSeconds(_mareConfigService.Current.ProfileDelay);
}
_lastMouseOverUid = id;
if (_popupTime > DateTime.UtcNow || !_mareConfigService.Current.ProfilesShow)
{
ImGui.SetTooltip("Left click to switch between UID display and nick" + Environment.NewLine
+ "Right click to change nick for " + pair.UserData.AliasOrUID + Environment.NewLine
+ "Middle Mouse Button to open their profile in a separate window");
}
else if (_popupTime < DateTime.UtcNow && !_popupShown)
{
_popupShown = true;
_mediator.Publish(new ProfilePopoutToggle(pair));
}
}
else
{
if (string.Equals(_lastMouseOverUid, id))
{
_mediator.Publish(new ProfilePopoutToggle(Pair: null));
_lastMouseOverUid = string.Empty;
_popupShown = false;
}
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
var prevState = textIsUid;
if (_showIdForEntry.ContainsKey(pair.UserData.UID))
{
prevState = _showIdForEntry[pair.UserData.UID];
}
_showIdForEntry[pair.UserData.UID] = !prevState;
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
if (_editIsUid)
{
_serverManager.SetNoteForUid(_editEntry, _editComment, save: true);
}
else
{
_serverManager.SetNoteForGid(_editEntry, _editComment, save: true);
}
_editComment = pair.GetNote() ?? string.Empty;
_editEntry = pair.UserData.UID;
_editIsUid = true;
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Middle))
{
_mediator.Publish(new ProfileOpenStandaloneMessage(pair));
}
}
else
{
ImGui.AlignTextToFramePadding();
ImGui.SetNextItemWidth(editBoxWidth.Invoke());
if (ImGui.InputTextWithHint("##" + pair.UserData.UID, "Nick/Notes", ref _editComment, 255, ImGuiInputTextFlags.EnterReturnsTrue))
{
_serverManager.SetNoteForUid(pair.UserData.UID, _editComment);
_serverManager.SaveNotes();
_editEntry = string.Empty;
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_editEntry = string.Empty;
}
UiSharedService.AttachToolTip("Hit ENTER to save\nRight click to cancel");
}
}
public (bool isGid, string text) GetGroupText(GroupFullInfoDto group)
{
var textIsGid = true;
bool showUidInsteadOfName = ShowGidInsteadOfName(group);
string? groupText = _serverManager.GetNoteForGid(group.GID);
if (!showUidInsteadOfName && groupText != null)
{
if (string.IsNullOrEmpty(groupText))
{
groupText = group.GroupAliasOrGID;
}
else
{
textIsGid = false;
}
}
else
{
groupText = group.GroupAliasOrGID;
}
return (textIsGid, groupText!);
}
public (bool isUid, string text) GetPlayerText(Pair pair)
{
var textIsUid = true;
bool showUidInsteadOfName = ShowUidInsteadOfName(pair);
string? playerText = _serverManager.GetNoteForUid(pair.UserData.UID);
if (!showUidInsteadOfName && playerText != null)
{
if (string.IsNullOrEmpty(playerText))
{
playerText = pair.UserData.AliasOrUID;
}
else
{
textIsUid = false;
}
}
else
{
playerText = pair.UserData.AliasOrUID;
}
if (_mareConfigService.Current.ShowCharacterNameInsteadOfNotesForVisible && pair.IsVisible && !showUidInsteadOfName)
{
playerText = pair.PlayerName;
textIsUid = false;
if (_mareConfigService.Current.PreferNotesOverNamesForVisible)
{
var note = pair.GetNote();
if (note != null)
{
playerText = note;
}
}
}
return (textIsUid, playerText!);
}
internal void Clear()
{
_editEntry = string.Empty;
_editComment = string.Empty;
}
internal void OpenProfile(Pair entry)
{
_mediator.Publish(new ProfileOpenStandaloneMessage(entry));
}
private bool ShowGidInsteadOfName(GroupFullInfoDto group)
{
_showIdForEntry.TryGetValue(group.GID, out var showidInsteadOfName);
return showidInsteadOfName;
}
private bool ShowUidInsteadOfName(Pair pair)
{
_showIdForEntry.TryGetValue(pair.UserData.UID, out var showidInsteadOfName);
return showidInsteadOfName;
}
}

View File

@@ -0,0 +1,86 @@
using LightlessSync.Services.ServerConfiguration;
namespace LightlessSync.UI.Handlers;
public class TagHandler
{
public const string CustomAllTag = "Mare_All";
public const string CustomOfflineTag = "Mare_Offline";
public const string CustomOfflineSyncshellTag = "Mare_OfflineSyncshell";
public const string CustomOnlineTag = "Mare_Online";
public const string CustomUnpairedTag = "Mare_Unpaired";
public const string CustomVisibleTag = "Mare_Visible";
private readonly ServerConfigurationManager _serverConfigurationManager;
public TagHandler(ServerConfigurationManager serverConfigurationManager)
{
_serverConfigurationManager = serverConfigurationManager;
}
public void AddTag(string tag)
{
_serverConfigurationManager.AddTag(tag);
}
public void AddTagToPairedUid(string uid, string tagName)
{
_serverConfigurationManager.AddTagForUid(uid, tagName);
}
public List<string> GetAllTagsSorted()
{
return
[
.. _serverConfigurationManager.GetServerAvailablePairTags()
.OrderBy(s => s, StringComparer.OrdinalIgnoreCase)
,
];
}
public HashSet<string> GetOtherUidsForTag(string tag)
{
return _serverConfigurationManager.GetUidsForTag(tag);
}
public bool HasAnyTag(string uid)
{
return _serverConfigurationManager.HasTags(uid);
}
public bool HasTag(string uid, string tagName)
{
return _serverConfigurationManager.ContainsTag(uid, tagName);
}
/// <summary>
/// Is this tag opened in the paired clients UI?
/// </summary>
/// <param name="tag">the tag</param>
/// <returns>open true/false</returns>
public bool IsTagOpen(string tag)
{
return _serverConfigurationManager.ContainsOpenPairTag(tag);
}
public void RemoveTag(string tag)
{
_serverConfigurationManager.RemoveTag(tag);
}
public void RemoveTagFromPairedUid(string uid, string tagName)
{
_serverConfigurationManager.RemoveTagForUid(uid, tagName);
}
public void SetTagOpen(string tag, bool open)
{
if (open)
{
_serverConfigurationManager.AddOpenPairTag(tag);
}
else
{
_serverConfigurationManager.RemoveOpenPairTag(tag);
}
}
}

365
LightlessSync/UI/IntroUI.cs Normal file
View File

@@ -0,0 +1,365 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Utility;
using LightlessSync.FileCache;
using LightlessSync.Localization;
using LightlessSync.MareConfiguration;
using LightlessSync.MareConfiguration.Models;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using Microsoft.Extensions.Logging;
using System.Numerics;
using System.Text.RegularExpressions;
namespace LightlessSync.UI;
public partial class IntroUi : WindowMediatorSubscriberBase
{
private readonly MareConfigService _configService;
private readonly CacheMonitor _cacheMonitor;
private readonly Dictionary<string, string> _languages = new(StringComparer.Ordinal) { { "English", "en" }, { "Deutsch", "de" }, { "Français", "fr" } };
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly DalamudUtilService _dalamudUtilService;
private readonly UiSharedService _uiShared;
private int _currentLanguage;
private bool _readFirstPage;
private string _secretKey = string.Empty;
private string _timeoutLabel = string.Empty;
private Task? _timeoutTask;
private string[]? _tosParagraphs;
private bool _useLegacyLogin = false;
public IntroUi(ILogger<IntroUi> logger, UiSharedService uiShared, MareConfigService configService,
CacheMonitor fileCacheManager, ServerConfigurationManager serverConfigurationManager, MareMediator mareMediator,
PerformanceCollectorService performanceCollectorService, DalamudUtilService dalamudUtilService) : base(logger, mareMediator, "Mare Synchronos Setup", performanceCollectorService)
{
_uiShared = uiShared;
_configService = configService;
_cacheMonitor = fileCacheManager;
_serverConfigurationManager = serverConfigurationManager;
_dalamudUtilService = dalamudUtilService;
IsOpen = false;
ShowCloseButton = false;
RespectCloseHotkey = false;
SizeConstraints = new WindowSizeConstraints()
{
MinimumSize = new Vector2(600, 400),
MaximumSize = new Vector2(600, 2000),
};
GetToSLocalization();
Mediator.Subscribe<SwitchToMainUiMessage>(this, (_) => IsOpen = false);
Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) =>
{
_configService.Current.UseCompactor = !dalamudUtilService.IsWine;
IsOpen = true;
});
}
private int _prevIdx = -1;
protected override void DrawInternal()
{
if (_uiShared.IsInGpose) return;
if (!_configService.Current.AcceptedAgreement && !_readFirstPage)
{
_uiShared.BigText("Welcome to Mare Synchronos");
ImGui.Separator();
UiSharedService.TextWrapped("Mare Synchronos is a plugin that will replicate your full current character state including all Penumbra mods to other paired Mare Synchronos users. " +
"Note that you will have to have Penumbra as well as Glamourer installed to use this plugin.");
UiSharedService.TextWrapped("We will have to setup a few things first before you can start using this plugin. Click on next to continue.");
UiSharedService.ColorTextWrapped("Note: Any modifications you have applied through anything but Penumbra cannot be shared and your character state on other clients " +
"might look broken because of this or others players mods might not apply on your end altogether. " +
"If you want to use this plugin you will have to move your mods to Penumbra.", ImGuiColors.DalamudYellow);
if (!_uiShared.DrawOtherPluginState()) return;
ImGui.Separator();
if (ImGui.Button("Next##toAgreement"))
{
_readFirstPage = true;
#if !DEBUG
_timeoutTask = Task.Run(async () =>
{
for (int i = 60; i > 0; i--)
{
_timeoutLabel = $"{Strings.ToS.ButtonWillBeAvailableIn} {i}s";
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
}
});
#else
_timeoutTask = Task.CompletedTask;
#endif
}
}
else if (!_configService.Current.AcceptedAgreement && _readFirstPage)
{
Vector2 textSize;
using (_uiShared.UidFont.Push())
{
textSize = ImGui.CalcTextSize(Strings.ToS.LanguageLabel);
ImGui.TextUnformatted(Strings.ToS.AgreementLabel);
}
ImGui.SameLine();
var languageSize = ImGui.CalcTextSize(Strings.ToS.LanguageLabel);
ImGui.SetCursorPosX(ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - languageSize.X - 80);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + textSize.Y / 2 - languageSize.Y / 2);
ImGui.TextUnformatted(Strings.ToS.LanguageLabel);
ImGui.SameLine();
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + textSize.Y / 2 - (languageSize.Y + ImGui.GetStyle().FramePadding.Y) / 2);
ImGui.SetNextItemWidth(80);
if (ImGui.Combo("", ref _currentLanguage, _languages.Keys.ToArray(), _languages.Count))
{
GetToSLocalization(_currentLanguage);
}
ImGui.Separator();
ImGui.SetWindowFontScale(1.5f);
string readThis = Strings.ToS.ReadLabel;
textSize = ImGui.CalcTextSize(readThis);
ImGui.SetCursorPosX(ImGui.GetWindowSize().X / 2 - textSize.X / 2);
UiSharedService.ColorText(readThis, ImGuiColors.DalamudRed);
ImGui.SetWindowFontScale(1.0f);
ImGui.Separator();
UiSharedService.TextWrapped(_tosParagraphs![0]);
UiSharedService.TextWrapped(_tosParagraphs![1]);
UiSharedService.TextWrapped(_tosParagraphs![2]);
UiSharedService.TextWrapped(_tosParagraphs![3]);
UiSharedService.TextWrapped(_tosParagraphs![4]);
UiSharedService.TextWrapped(_tosParagraphs![5]);
ImGui.Separator();
if (_timeoutTask?.IsCompleted ?? true)
{
if (ImGui.Button(Strings.ToS.AgreeLabel + "##toSetup"))
{
_configService.Current.AcceptedAgreement = true;
_configService.Save();
}
}
else
{
UiSharedService.TextWrapped(_timeoutLabel);
}
}
else if (_configService.Current.AcceptedAgreement
&& (string.IsNullOrEmpty(_configService.Current.CacheFolder)
|| !_configService.Current.InitialScanComplete
|| !Directory.Exists(_configService.Current.CacheFolder)))
{
using (_uiShared.UidFont.Push())
ImGui.TextUnformatted("File Storage Setup");
ImGui.Separator();
if (!_uiShared.HasValidPenumbraModPath)
{
UiSharedService.ColorTextWrapped("You do not have a valid Penumbra path set. Open Penumbra and set up a valid path for the mod directory.", ImGuiColors.DalamudRed);
}
else
{
UiSharedService.TextWrapped("To not unnecessary download files already present on your computer, Mare Synchronos will have to scan your Penumbra mod directory. " +
"Additionally, a local storage folder must be set where Mare Synchronos will download other character files to. " +
"Once the storage folder is set and the scan complete, this page will automatically forward to registration at a service.");
UiSharedService.TextWrapped("Note: The initial scan, depending on the amount of mods you have, might take a while. Please wait until it is completed.");
UiSharedService.ColorTextWrapped("Warning: once past this step you should not delete the FileCache.csv of Mare Synchronos in the Plugin Configurations folder of Dalamud. " +
"Otherwise on the next launch a full re-scan of the file cache database will be initiated.", ImGuiColors.DalamudYellow);
UiSharedService.ColorTextWrapped("Warning: if the scan is hanging and does nothing for a long time, chances are high your Penumbra folder is not set up properly.", ImGuiColors.DalamudYellow);
_uiShared.DrawCacheDirectorySetting();
}
if (!_cacheMonitor.IsScanRunning && !string.IsNullOrEmpty(_configService.Current.CacheFolder) && _uiShared.HasValidPenumbraModPath && Directory.Exists(_configService.Current.CacheFolder))
{
if (ImGui.Button("Start Scan##startScan"))
{
_cacheMonitor.InvokeScan();
}
}
else
{
_uiShared.DrawFileScanState();
}
if (!_dalamudUtilService.IsWine)
{
var useFileCompactor = _configService.Current.UseCompactor;
if (ImGui.Checkbox("Use File Compactor", ref useFileCompactor))
{
_configService.Current.UseCompactor = useFileCompactor;
_configService.Save();
}
UiSharedService.ColorTextWrapped("The File Compactor can save a tremendeous amount of space on the hard disk for downloads through Mare. It will incur a minor CPU penalty on download but can speed up " +
"loading of other characters. It is recommended to keep it enabled. You can change this setting later anytime in the Mare settings.", ImGuiColors.DalamudYellow);
}
}
else if (!_uiShared.ApiController.ServerAlive)
{
using (_uiShared.UidFont.Push())
ImGui.TextUnformatted("Service Registration");
ImGui.Separator();
UiSharedService.TextWrapped("To be able to use Mare Synchronos you will have to register an account.");
UiSharedService.TextWrapped("For the official Mare Synchronos Servers the account creation will be handled on the official Mare Synchronos Discord. Due to security risks for the server, there is no way to handle this sensibly otherwise.");
UiSharedService.TextWrapped("If you want to register at the main server \"" + WebAPI.ApiController.MainServer + "\" join the Discord and follow the instructions as described in #mare-service.");
if (ImGui.Button("Join the Mare Synchronos Discord"))
{
Util.OpenLink("https://discord.gg/mpNdkrTRjW");
}
UiSharedService.TextWrapped("For all other non official services you will have to contact the appropriate service provider how to obtain a secret key.");
UiSharedService.DistanceSeparator();
UiSharedService.TextWrapped("Once you have registered you can connect to the service using the tools provided below.");
int serverIdx = 0;
var selectedServer = _serverConfigurationManager.GetServerByIndex(serverIdx);
using (var node = ImRaii.TreeNode("Advanced Options"))
{
if (node)
{
serverIdx = _uiShared.DrawServiceSelection(selectOnChange: true, showConnect: false);
if (serverIdx != _prevIdx)
{
_uiShared.ResetOAuthTasksState();
_prevIdx = serverIdx;
}
selectedServer = _serverConfigurationManager.GetServerByIndex(serverIdx);
_useLegacyLogin = !selectedServer.UseOAuth2;
if (ImGui.Checkbox("Use Legacy Login with Secret Key", ref _useLegacyLogin))
{
_serverConfigurationManager.GetServerByIndex(serverIdx).UseOAuth2 = !_useLegacyLogin;
_serverConfigurationManager.Save();
}
}
}
if (_useLegacyLogin)
{
var text = "Enter Secret Key";
var buttonText = "Save";
var buttonWidth = _secretKey.Length != 64 ? 0 : ImGuiHelpers.GetButtonSize(buttonText).X + ImGui.GetStyle().ItemSpacing.X;
var textSize = ImGui.CalcTextSize(text);
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("Strongly consider to use OAuth2 to authenticate, if the server supports it (the current main server does). " +
"The authentication flow is simpler and you do not require to store or maintain Secret Keys. " +
"You already implicitly register using Discord, so the OAuth2 method will be cleaner and more straight-forward to use.", ImGuiColors.DalamudYellow, 500);
ImGuiHelpers.ScaledDummy(5);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(text);
ImGui.SameLine();
ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonWidth - textSize.X);
ImGui.InputText("", ref _secretKey, 64);
if (_secretKey.Length > 0 && _secretKey.Length != 64)
{
UiSharedService.ColorTextWrapped("Your secret key must be exactly 64 characters long. Don't enter your Lodestone auth here.", ImGuiColors.DalamudRed);
}
else if (_secretKey.Length == 64 && !HexRegex().IsMatch(_secretKey))
{
UiSharedService.ColorTextWrapped("Your secret key can only contain ABCDEF and the numbers 0-9.", ImGuiColors.DalamudRed);
}
else if (_secretKey.Length == 64)
{
ImGui.SameLine();
if (ImGui.Button(buttonText))
{
if (_serverConfigurationManager.CurrentServer == null) _serverConfigurationManager.SelectServer(0);
if (!_serverConfigurationManager.CurrentServer!.SecretKeys.Any())
{
_serverConfigurationManager.CurrentServer!.SecretKeys.Add(_serverConfigurationManager.CurrentServer.SecretKeys.Select(k => k.Key).LastOrDefault() + 1, new SecretKey()
{
FriendlyName = $"Secret Key added on Setup ({DateTime.Now:yyyy-MM-dd})",
Key = _secretKey,
});
_serverConfigurationManager.AddCurrentCharacterToServer();
}
else
{
_serverConfigurationManager.CurrentServer!.SecretKeys[0] = new SecretKey()
{
FriendlyName = $"Secret Key added on Setup ({DateTime.Now:yyyy-MM-dd})",
Key = _secretKey,
};
}
_secretKey = string.Empty;
_ = Task.Run(() => _uiShared.ApiController.CreateConnectionsAsync());
}
}
}
else
{
if (string.IsNullOrEmpty(selectedServer.OAuthToken))
{
UiSharedService.TextWrapped("Press the button below to verify the server has OAuth2 capabilities. Afterwards, authenticate using Discord in the Browser window.");
_uiShared.DrawOAuth(selectedServer);
}
else
{
UiSharedService.ColorTextWrapped($"OAuth2 is connected. Linked to: Discord User {_serverConfigurationManager.GetDiscordUserFromToken(selectedServer)}", ImGuiColors.HealerGreen);
UiSharedService.TextWrapped("Now press the update UIDs button to get a list of all of your UIDs on the server.");
_uiShared.DrawUpdateOAuthUIDsButton(selectedServer);
var playerName = _dalamudUtilService.GetPlayerName();
var playerWorld = _dalamudUtilService.GetHomeWorldId();
UiSharedService.TextWrapped($"Once pressed, select the UID you want to use for your current character {_dalamudUtilService.GetPlayerName()}. If no UIDs are visible, make sure you are connected to the correct Discord account. " +
$"If that is not the case, use the unlink button below (hold CTRL to unlink).");
_uiShared.DrawUnlinkOAuthButton(selectedServer);
var auth = selectedServer.Authentications.Find(a => string.Equals(a.CharacterName, playerName, StringComparison.Ordinal) && a.WorldId == playerWorld);
if (auth == null)
{
auth = new Authentication()
{
CharacterName = playerName,
WorldId = playerWorld
};
selectedServer.Authentications.Add(auth);
_serverConfigurationManager.Save();
}
_uiShared.DrawUIDComboForAuthentication(0, auth, selectedServer.ServerUri);
using (ImRaii.Disabled(string.IsNullOrEmpty(auth.UID)))
{
if (_uiShared.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Link, "Connect to Service"))
{
_ = Task.Run(() => _uiShared.ApiController.CreateConnectionsAsync());
}
}
if (string.IsNullOrEmpty(auth.UID))
UiSharedService.AttachToolTip("Select a UID to be able to connect to the service");
}
}
}
else
{
Mediator.Publish(new SwitchToMainUiMessage());
IsOpen = false;
}
}
private void GetToSLocalization(int changeLanguageTo = -1)
{
if (changeLanguageTo != -1)
{
_uiShared.LoadLocalization(_languages.ElementAt(changeLanguageTo).Value);
}
_tosParagraphs = [Strings.ToS.Paragraph1, Strings.ToS.Paragraph2, Strings.ToS.Paragraph3, Strings.ToS.Paragraph4, Strings.ToS.Paragraph5, Strings.ToS.Paragraph6];
}
[GeneratedRegex("^([A-F0-9]{2})+")]
private static partial Regex HexRegex();
}

View File

@@ -0,0 +1,181 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
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.Mediator;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging;
namespace LightlessSync.UI;
internal class JoinSyncshellUI : WindowMediatorSubscriberBase
{
private readonly ApiController _apiController;
private readonly UiSharedService _uiSharedService;
private string _desiredSyncshellToJoin = string.Empty;
private GroupJoinInfoDto? _groupJoinInfo = null;
private DefaultPermissionsDto _ownPermissions = null!;
private string _previousPassword = string.Empty;
private string _syncshellPassword = string.Empty;
public JoinSyncshellUI(ILogger<JoinSyncshellUI> logger, MareMediator mediator,
UiSharedService uiSharedService, ApiController apiController, PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "Join existing Syncshell###LightlessSyncJoinSyncshell", performanceCollectorService)
{
_uiSharedService = uiSharedService;
_apiController = apiController;
SizeConstraints = new()
{
MinimumSize = new(700, 400),
MaximumSize = new(700, 400)
};
Mediator.Subscribe<DisconnectedMessage>(this, (_) => IsOpen = false);
Flags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize;
}
public override void OnOpen()
{
_desiredSyncshellToJoin = string.Empty;
_syncshellPassword = string.Empty;
_previousPassword = string.Empty;
_groupJoinInfo = null;
_ownPermissions = _apiController.DefaultPermissions.DeepClone()!;
}
protected override void DrawInternal()
{
using (_uiSharedService.UidFont.Push())
ImGui.TextUnformatted(_groupJoinInfo == null || !_groupJoinInfo.Success ? "Join Syncshell" : "Finalize join Syncshell " + _groupJoinInfo.GroupAliasOrGID);
ImGui.Separator();
if (_groupJoinInfo == null || !_groupJoinInfo.Success)
{
UiSharedService.TextWrapped("Here you can join existing Syncshells. " +
"Please keep in mind that you cannot join more than " + _apiController.ServerInfo.MaxGroupsJoinedByUser + " syncshells on this server." + Environment.NewLine +
"Joining a Syncshell will pair you implicitly with all existing users in the Syncshell." + Environment.NewLine +
"All permissions to all users in the Syncshell will be set to the preferred Syncshell permissions on joining, excluding prior set preferred permissions.");
ImGui.Separator();
ImGui.TextUnformatted("Note: Syncshell ID and Password are case sensitive. MSS- is part of Syncshell IDs, unless using Vanity IDs.");
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Syncshell ID");
ImGui.SameLine(200);
ImGui.InputTextWithHint("##syncshellId", "Full Syncshell ID", ref _desiredSyncshellToJoin, 20);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Syncshell Password");
ImGui.SameLine(200);
ImGui.InputTextWithHint("##syncshellpw", "Password", ref _syncshellPassword, 50, ImGuiInputTextFlags.Password);
using (ImRaii.Disabled(string.IsNullOrEmpty(_desiredSyncshellToJoin) || string.IsNullOrEmpty(_syncshellPassword)))
{
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Plus, "Join Syncshell"))
{
_groupJoinInfo = _apiController.GroupJoin(new GroupPasswordDto(new API.Data.GroupData(_desiredSyncshellToJoin), _syncshellPassword)).Result;
_previousPassword = _syncshellPassword;
_syncshellPassword = string.Empty;
}
}
if (_groupJoinInfo != null && !_groupJoinInfo.Success)
{
UiSharedService.ColorTextWrapped("Failed to join the Syncshell. This is due to one of following reasons:" + Environment.NewLine +
"- The Syncshell does not exist or the password is incorrect" + Environment.NewLine +
"- You are already in that Syncshell or are banned from that Syncshell" + Environment.NewLine +
"- The Syncshell is at capacity or has invites disabled" + Environment.NewLine, ImGuiColors.DalamudYellow);
}
}
else
{
ImGui.TextUnformatted("You are about to join the Syncshell " + _groupJoinInfo.GroupAliasOrGID + " by " + _groupJoinInfo.OwnerAliasOrUID);
ImGuiHelpers.ScaledDummy(2f);
ImGui.TextUnformatted("This Syncshell staff has set the following suggested Syncshell permissions:");
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("- Sounds ");
_uiSharedService.BooleanToColoredIcon(!_groupJoinInfo.GroupPermissions.IsPreferDisableSounds());
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("- Animations");
_uiSharedService.BooleanToColoredIcon(!_groupJoinInfo.GroupPermissions.IsPreferDisableAnimations());
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("- VFX");
_uiSharedService.BooleanToColoredIcon(!_groupJoinInfo.GroupPermissions.IsPreferDisableVFX());
if (_groupJoinInfo.GroupPermissions.IsPreferDisableSounds() != _ownPermissions.DisableGroupSounds
|| _groupJoinInfo.GroupPermissions.IsPreferDisableVFX() != _ownPermissions.DisableGroupVFX
|| _groupJoinInfo.GroupPermissions.IsPreferDisableAnimations() != _ownPermissions.DisableGroupAnimations)
{
ImGuiHelpers.ScaledDummy(2f);
UiSharedService.ColorText("Your current preferred default Syncshell permissions deviate from the suggested permissions:", ImGuiColors.DalamudYellow);
if (_groupJoinInfo.GroupPermissions.IsPreferDisableSounds() != _ownPermissions.DisableGroupSounds)
{
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("- Sounds");
_uiSharedService.BooleanToColoredIcon(!_ownPermissions.DisableGroupSounds);
ImGui.SameLine(200);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Suggested");
_uiSharedService.BooleanToColoredIcon(!_groupJoinInfo.GroupPermissions.IsPreferDisableSounds());
ImGui.SameLine();
using var id = ImRaii.PushId("suggestedSounds");
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.ArrowRight, "Apply suggested"))
{
_ownPermissions.DisableGroupSounds = _groupJoinInfo.GroupPermissions.IsPreferDisableSounds();
}
}
if (_groupJoinInfo.GroupPermissions.IsPreferDisableAnimations() != _ownPermissions.DisableGroupAnimations)
{
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("- Animations");
_uiSharedService.BooleanToColoredIcon(!_ownPermissions.DisableGroupAnimations);
ImGui.SameLine(200);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Suggested");
_uiSharedService.BooleanToColoredIcon(!_groupJoinInfo.GroupPermissions.IsPreferDisableAnimations());
ImGui.SameLine();
using var id = ImRaii.PushId("suggestedAnims");
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.ArrowRight, "Apply suggested"))
{
_ownPermissions.DisableGroupAnimations = _groupJoinInfo.GroupPermissions.IsPreferDisableAnimations();
}
}
if (_groupJoinInfo.GroupPermissions.IsPreferDisableVFX() != _ownPermissions.DisableGroupVFX)
{
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("- VFX");
_uiSharedService.BooleanToColoredIcon(!_ownPermissions.DisableGroupVFX);
ImGui.SameLine(200);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Suggested");
_uiSharedService.BooleanToColoredIcon(!_groupJoinInfo.GroupPermissions.IsPreferDisableVFX());
ImGui.SameLine();
using var id = ImRaii.PushId("suggestedVfx");
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.ArrowRight, "Apply suggested"))
{
_ownPermissions.DisableGroupVFX = _groupJoinInfo.GroupPermissions.IsPreferDisableVFX();
}
}
UiSharedService.TextWrapped("Note: you do not need to apply the suggested Syncshell permissions, they are solely suggestions by the staff of the Syncshell.");
}
else
{
UiSharedService.TextWrapped("Your default syncshell permissions on joining are in line with the suggested Syncshell permissions through the owner.");
}
ImGuiHelpers.ScaledDummy(2f);
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Plus, "Finalize and join " + _groupJoinInfo.GroupAliasOrGID))
{
GroupUserPreferredPermissions joinPermissions = GroupUserPreferredPermissions.NoneSet;
joinPermissions.SetDisableSounds(_ownPermissions.DisableGroupSounds);
joinPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations);
joinPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX);
_ = _apiController.GroupJoinFinalize(new GroupJoinDto(_groupJoinInfo.Group, _previousPassword, joinPermissions));
IsOpen = false;
}
}
}
}

View File

@@ -0,0 +1,188 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging;
namespace LightlessSync.UI;
public class PermissionWindowUI : WindowMediatorSubscriberBase
{
public Pair Pair { get; init; }
private readonly UiSharedService _uiSharedService;
private readonly ApiController _apiController;
private UserPermissions _ownPermissions;
public PermissionWindowUI(ILogger<PermissionWindowUI> logger, Pair pair, MareMediator mediator, UiSharedService uiSharedService,
ApiController apiController, PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "Permissions for " + pair.UserData.AliasOrUID + "###LightlessSyncPermissions" + pair.UserData.UID, performanceCollectorService)
{
Pair = pair;
_uiSharedService = uiSharedService;
_apiController = apiController;
_ownPermissions = pair.UserPair.OwnPermissions.DeepClone();
Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoResize;
SizeConstraints = new()
{
MinimumSize = new(450, 100),
MaximumSize = new(450, 500)
};
IsOpen = true;
}
protected override void DrawInternal()
{
var sticky = _ownPermissions.IsSticky();
var paused = _ownPermissions.IsPaused();
var disableSounds = _ownPermissions.IsDisableSounds();
var disableAnimations = _ownPermissions.IsDisableAnimations();
var disableVfx = _ownPermissions.IsDisableVFX();
var style = ImGui.GetStyle();
var indentSize = ImGui.GetFrameHeight() + style.ItemSpacing.X;
_uiSharedService.BigText("Permissions for " + Pair.UserData.AliasOrUID);
ImGuiHelpers.ScaledDummy(1f);
if (ImGui.Checkbox("Preferred Permissions", ref sticky))
{
_ownPermissions.SetSticky(sticky);
}
_uiSharedService.DrawHelpText("Preferred Permissions, when enabled, will exclude this user from any permission changes on any syncshells you share with this user.");
ImGuiHelpers.ScaledDummy(1f);
if (ImGui.Checkbox("Pause Sync", ref paused))
{
_ownPermissions.SetPaused(paused);
}
_uiSharedService.DrawHelpText("Pausing will completely cease any sync with this user." + UiSharedService.TooltipSeparator
+ "Note: this is bidirectional, either user pausing will cease sync completely.");
var otherPerms = Pair.UserPair.OtherPermissions;
var otherIsPaused = otherPerms.IsPaused();
var otherDisableSounds = otherPerms.IsDisableSounds();
var otherDisableAnimations = otherPerms.IsDisableAnimations();
var otherDisableVFX = otherPerms.IsDisableVFX();
using (ImRaii.PushIndent(indentSize, false))
{
_uiSharedService.BooleanToColoredIcon(!otherIsPaused, false);
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.Text(Pair.UserData.AliasOrUID + " has " + (!otherIsPaused ? "not " : string.Empty) + "paused you");
}
ImGuiHelpers.ScaledDummy(0.5f);
ImGui.Separator();
ImGuiHelpers.ScaledDummy(0.5f);
if (ImGui.Checkbox("Disable Sounds", ref disableSounds))
{
_ownPermissions.SetDisableSounds(disableSounds);
}
_uiSharedService.DrawHelpText("Disabling sounds will remove all sounds synced with this user on both sides." + UiSharedService.TooltipSeparator
+ "Note: this is bidirectional, either user disabling sound sync will stop sound sync on both sides.");
using (ImRaii.PushIndent(indentSize, false))
{
_uiSharedService.BooleanToColoredIcon(!otherDisableSounds, false);
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.Text(Pair.UserData.AliasOrUID + " has " + (!otherDisableSounds ? "not " : string.Empty) + "disabled sound sync with you");
}
if (ImGui.Checkbox("Disable Animations", ref disableAnimations))
{
_ownPermissions.SetDisableAnimations(disableAnimations);
}
_uiSharedService.DrawHelpText("Disabling sounds will remove all animations synced with this user on both sides." + UiSharedService.TooltipSeparator
+ "Note: this is bidirectional, either user disabling animation sync will stop animation sync on both sides.");
using (ImRaii.PushIndent(indentSize, false))
{
_uiSharedService.BooleanToColoredIcon(!otherDisableAnimations, false);
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.Text(Pair.UserData.AliasOrUID + " has " + (!otherDisableAnimations ? "not " : string.Empty) + "disabled animation sync with you");
}
if (ImGui.Checkbox("Disable VFX", ref disableVfx))
{
_ownPermissions.SetDisableVFX(disableVfx);
}
_uiSharedService.DrawHelpText("Disabling sounds will remove all VFX synced with this user on both sides." + UiSharedService.TooltipSeparator
+ "Note: this is bidirectional, either user disabling VFX sync will stop VFX sync on both sides.");
using (ImRaii.PushIndent(indentSize, false))
{
_uiSharedService.BooleanToColoredIcon(!otherDisableVFX, false);
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.Text(Pair.UserData.AliasOrUID + " has " + (!otherDisableVFX ? "not " : string.Empty) + "disabled VFX sync with you");
}
ImGuiHelpers.ScaledDummy(0.5f);
ImGui.Separator();
ImGuiHelpers.ScaledDummy(0.5f);
bool hasChanges = _ownPermissions != Pair.UserPair.OwnPermissions;
using (ImRaii.Disabled(!hasChanges))
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Save, "Save"))
{
_ = _apiController.SetBulkPermissions(new(
new(StringComparer.Ordinal)
{
{ Pair.UserData.UID, _ownPermissions }
},
new(StringComparer.Ordinal)
));
}
UiSharedService.AttachToolTip("Save and apply all changes");
var rightSideButtons = _uiSharedService.GetIconTextButtonSize(Dalamud.Interface.FontAwesomeIcon.Undo, "Revert") +
_uiSharedService.GetIconTextButtonSize(Dalamud.Interface.FontAwesomeIcon.ArrowsSpin, "Reset to Default");
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
ImGui.SameLine(availableWidth - rightSideButtons);
using (ImRaii.Disabled(!hasChanges))
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Undo, "Revert"))
{
_ownPermissions = Pair.UserPair.OwnPermissions.DeepClone();
}
UiSharedService.AttachToolTip("Revert all changes");
ImGui.SameLine();
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.ArrowsSpin, "Reset to Default"))
{
var defaultPermissions = _apiController.DefaultPermissions!;
_ownPermissions.SetSticky(Pair.IsDirectlyPaired || defaultPermissions.IndividualIsSticky);
_ownPermissions.SetPaused(false);
_ownPermissions.SetDisableVFX(Pair.IsDirectlyPaired ? defaultPermissions.DisableIndividualVFX : defaultPermissions.DisableGroupVFX);
_ownPermissions.SetDisableSounds(Pair.IsDirectlyPaired ? defaultPermissions.DisableIndividualSounds : defaultPermissions.DisableGroupSounds);
_ownPermissions.SetDisableAnimations(Pair.IsDirectlyPaired ? defaultPermissions.DisableIndividualAnimations : defaultPermissions.DisableGroupAnimations);
_ = _apiController.SetBulkPermissions(new(
new(StringComparer.Ordinal)
{
{ Pair.UserData.UID, _ownPermissions }
},
new(StringComparer.Ordinal)
));
}
UiSharedService.AttachToolTip("This will set all permissions to your defined default permissions in the Mare Settings");
var ySize = ImGui.GetCursorPosY() + style.FramePadding.Y * ImGuiHelpers.GlobalScale + style.FrameBorderSize;
ImGui.SetWindowSize(new(400, ySize));
}
public override void OnClose()
{
Mediator.Publish(new RemoveWindowMessage(this));
}
}

View File

@@ -0,0 +1,194 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using LightlessSync.API.Data.Extensions;
using LightlessSync.MareConfiguration;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using Microsoft.Extensions.Logging;
using System.Numerics;
namespace LightlessSync.UI;
public class PopoutProfileUi : WindowMediatorSubscriberBase
{
private readonly MareProfileManager _mareProfileManager;
private readonly PairManager _pairManager;
private readonly ServerConfigurationManager _serverManager;
private readonly UiSharedService _uiSharedService;
private Vector2 _lastMainPos = Vector2.Zero;
private Vector2 _lastMainSize = Vector2.Zero;
private byte[] _lastProfilePicture = [];
private byte[] _lastSupporterPicture = [];
private Pair? _pair;
private IDalamudTextureWrap? _supporterTextureWrap;
private IDalamudTextureWrap? _textureWrap;
public PopoutProfileUi(ILogger<PopoutProfileUi> logger, MareMediator mediator, UiSharedService uiBuilder,
ServerConfigurationManager serverManager, MareConfigService mareConfigService,
MareProfileManager mareProfileManager, PairManager pairManager, PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "###LightlessSyncPopoutProfileUI", performanceCollectorService)
{
_uiSharedService = uiBuilder;
_serverManager = serverManager;
_mareProfileManager = mareProfileManager;
_pairManager = pairManager;
Flags = ImGuiWindowFlags.NoDecoration;
Mediator.Subscribe<ProfilePopoutToggle>(this, (msg) =>
{
IsOpen = msg.Pair != null;
_pair = msg.Pair;
_lastProfilePicture = [];
_lastSupporterPicture = [];
_textureWrap?.Dispose();
_textureWrap = null;
_supporterTextureWrap?.Dispose();
_supporterTextureWrap = null;
});
Mediator.Subscribe<CompactUiChange>(this, (msg) =>
{
if (msg.Size != Vector2.Zero)
{
var border = ImGui.GetStyle().WindowBorderSize;
var padding = ImGui.GetStyle().WindowPadding;
Size = new(256 + (padding.X * 2) + border, msg.Size.Y / ImGuiHelpers.GlobalScale);
_lastMainSize = msg.Size;
}
var mainPos = msg.Position == Vector2.Zero ? _lastMainPos : msg.Position;
if (mareConfigService.Current.ProfilePopoutRight)
{
Position = new(mainPos.X + _lastMainSize.X * ImGuiHelpers.GlobalScale, mainPos.Y);
}
else
{
Position = new(mainPos.X - Size!.Value.X * ImGuiHelpers.GlobalScale, mainPos.Y);
}
if (msg.Position != Vector2.Zero)
{
_lastMainPos = msg.Position;
}
});
IsOpen = false;
}
protected override void DrawInternal()
{
if (_pair == null) return;
try
{
var spacing = ImGui.GetStyle().ItemSpacing;
var mareProfile = _mareProfileManager.GetMareProfile(_pair.UserData);
if (_textureWrap == null || !mareProfile.ImageData.Value.SequenceEqual(_lastProfilePicture))
{
_textureWrap?.Dispose();
_lastProfilePicture = mareProfile.ImageData.Value;
_textureWrap = _uiSharedService.LoadImage(_lastProfilePicture);
}
if (_supporterTextureWrap == null || !mareProfile.SupporterImageData.Value.SequenceEqual(_lastSupporterPicture))
{
_supporterTextureWrap?.Dispose();
_supporterTextureWrap = null;
if (!string.IsNullOrEmpty(mareProfile.Base64SupporterPicture))
{
_lastSupporterPicture = mareProfile.SupporterImageData.Value;
_supporterTextureWrap = _uiSharedService.LoadImage(_lastSupporterPicture);
}
}
var drawList = ImGui.GetWindowDrawList();
var rectMin = drawList.GetClipRectMin();
var rectMax = drawList.GetClipRectMax();
using (_uiSharedService.UidFont.Push())
UiSharedService.ColorText(_pair.UserData.AliasOrUID, ImGuiColors.HealerGreen);
ImGuiHelpers.ScaledDummy(spacing.Y, spacing.Y);
var textPos = ImGui.GetCursorPosY();
ImGui.Separator();
var imagePos = ImGui.GetCursorPos();
ImGuiHelpers.ScaledDummy(256, 256 * ImGuiHelpers.GlobalScale + spacing.Y);
var note = _serverManager.GetNoteForUid(_pair.UserData.UID);
if (!string.IsNullOrEmpty(note))
{
UiSharedService.ColorText(note, ImGuiColors.DalamudGrey);
}
string status = _pair.IsVisible ? "Visible" : (_pair.IsOnline ? "Online" : "Offline");
UiSharedService.ColorText(status, (_pair.IsVisible || _pair.IsOnline) ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed);
if (_pair.IsVisible)
{
ImGui.SameLine();
ImGui.TextUnformatted($"({_pair.PlayerName})");
}
if (_pair.UserPair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.Bidirectional)
{
ImGui.TextUnformatted("Directly paired");
if (_pair.UserPair.OwnPermissions.IsPaused())
{
ImGui.SameLine();
UiSharedService.ColorText("You: paused", ImGuiColors.DalamudYellow);
}
if (_pair.UserPair.OtherPermissions.IsPaused())
{
ImGui.SameLine();
UiSharedService.ColorText("They: paused", ImGuiColors.DalamudYellow);
}
}
if (_pair.UserPair.Groups.Any())
{
ImGui.TextUnformatted("Paired through Syncshells:");
foreach (var group in _pair.UserPair.Groups)
{
var groupNote = _serverManager.GetNoteForGid(group);
var groupName = _pairManager.GroupPairs.First(f => string.Equals(f.Key.GID, group, StringComparison.Ordinal)).Key.GroupAliasOrGID;
var groupString = string.IsNullOrEmpty(groupNote) ? groupName : $"{groupNote} ({groupName})";
ImGui.TextUnformatted("- " + groupString);
}
}
ImGui.Separator();
var font = _uiSharedService.GameFont.Push();
var remaining = ImGui.GetWindowContentRegionMax().Y - ImGui.GetCursorPosY();
var descText = mareProfile.Description;
var textSize = ImGui.CalcTextSize(descText, wrapWidth: 256f * ImGuiHelpers.GlobalScale);
bool trimmed = textSize.Y > remaining;
while (textSize.Y > remaining && descText.Contains(' '))
{
descText = descText[..descText.LastIndexOf(' ')].TrimEnd();
textSize = ImGui.CalcTextSize(descText + $"...{Environment.NewLine}[Open Full Profile for complete description]", wrapWidth: 256f * ImGuiHelpers.GlobalScale);
}
UiSharedService.TextWrapped(trimmed ? descText + $"...{Environment.NewLine}[Open Full Profile for complete description]" : mareProfile.Description);
font.Dispose();
var padding = ImGui.GetStyle().WindowPadding.X / 2;
bool tallerThanWide = _textureWrap.Height >= _textureWrap.Width;
var stretchFactor = tallerThanWide ? 256f * ImGuiHelpers.GlobalScale / _textureWrap.Height : 256f * ImGuiHelpers.GlobalScale / _textureWrap.Width;
var newWidth = _textureWrap.Width * stretchFactor;
var newHeight = _textureWrap.Height * stretchFactor;
var remainingWidth = (256f * ImGuiHelpers.GlobalScale - newWidth) / 2f;
var remainingHeight = (256f * ImGuiHelpers.GlobalScale - newHeight) / 2f;
drawList.AddImage(_textureWrap.Handle, new Vector2(rectMin.X + padding + remainingWidth, rectMin.Y + spacing.Y + imagePos.Y + remainingHeight),
new Vector2(rectMin.X + padding + remainingWidth + newWidth, rectMin.Y + spacing.Y + imagePos.Y + remainingHeight + newHeight));
if (_supporterTextureWrap != null)
{
const float iconSize = 38;
drawList.AddImage(_supporterTextureWrap.Handle,
new Vector2(rectMax.X - iconSize - spacing.X, rectMin.Y + (textPos / 2) - (iconSize / 2)),
new Vector2(rectMax.X - spacing.X, rectMin.Y + iconSize + (textPos / 2) - (iconSize / 2)));
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error during draw tooltip");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,181 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using LightlessSync.API.Data.Extensions;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using Microsoft.Extensions.Logging;
using System.Numerics;
namespace LightlessSync.UI;
public class StandaloneProfileUi : WindowMediatorSubscriberBase
{
private readonly MareProfileManager _mareProfileManager;
private readonly PairManager _pairManager;
private readonly ServerConfigurationManager _serverManager;
private readonly UiSharedService _uiSharedService;
private bool _adjustedForScrollBars = false;
private byte[] _lastProfilePicture = [];
private byte[] _lastSupporterPicture = [];
private IDalamudTextureWrap? _supporterTextureWrap;
private IDalamudTextureWrap? _textureWrap;
public StandaloneProfileUi(ILogger<StandaloneProfileUi> logger, MareMediator mediator, UiSharedService uiBuilder,
ServerConfigurationManager serverManager, MareProfileManager mareProfileManager, PairManager pairManager, Pair pair,
PerformanceCollectorService performanceCollector)
: base(logger, mediator, "Mare Profile of " + pair.UserData.AliasOrUID + "##LightlessSyncStandaloneProfileUI" + pair.UserData.AliasOrUID, performanceCollector)
{
_uiSharedService = uiBuilder;
_serverManager = serverManager;
_mareProfileManager = mareProfileManager;
Pair = pair;
_pairManager = pairManager;
Flags = ImGuiWindowFlags.NoResize | ImGuiWindowFlags.AlwaysAutoResize;
var spacing = ImGui.GetStyle().ItemSpacing;
Size = new(512 + spacing.X * 3 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 512);
IsOpen = true;
}
public Pair Pair { get; init; }
protected override void DrawInternal()
{
try
{
var spacing = ImGui.GetStyle().ItemSpacing;
var mareProfile = _mareProfileManager.GetMareProfile(Pair.UserData);
if (_textureWrap == null || !mareProfile.ImageData.Value.SequenceEqual(_lastProfilePicture))
{
_textureWrap?.Dispose();
_lastProfilePicture = mareProfile.ImageData.Value;
_textureWrap = _uiSharedService.LoadImage(_lastProfilePicture);
}
if (_supporterTextureWrap == null || !mareProfile.SupporterImageData.Value.SequenceEqual(_lastSupporterPicture))
{
_supporterTextureWrap?.Dispose();
_supporterTextureWrap = null;
if (!string.IsNullOrEmpty(mareProfile.Base64SupporterPicture))
{
_lastSupporterPicture = mareProfile.SupporterImageData.Value;
_supporterTextureWrap = _uiSharedService.LoadImage(_lastSupporterPicture);
}
}
var drawList = ImGui.GetWindowDrawList();
var rectMin = drawList.GetClipRectMin();
var rectMax = drawList.GetClipRectMax();
var headerSize = ImGui.GetCursorPosY() - ImGui.GetStyle().WindowPadding.Y;
using (_uiSharedService.UidFont.Push())
UiSharedService.ColorText(Pair.UserData.AliasOrUID, ImGuiColors.HealerGreen);
ImGuiHelpers.ScaledDummy(new Vector2(spacing.Y, spacing.Y));
var textPos = ImGui.GetCursorPosY() - headerSize;
ImGui.Separator();
var pos = ImGui.GetCursorPos() with { Y = ImGui.GetCursorPosY() - headerSize };
ImGuiHelpers.ScaledDummy(new Vector2(256, 256 + spacing.Y));
var postDummy = ImGui.GetCursorPosY();
ImGui.SameLine();
var descriptionTextSize = ImGui.CalcTextSize(mareProfile.Description, wrapWidth: 256f);
var descriptionChildHeight = rectMax.Y - pos.Y - rectMin.Y - spacing.Y * 2;
if (descriptionTextSize.Y > descriptionChildHeight && !_adjustedForScrollBars)
{
Size = Size!.Value with { X = Size.Value.X + ImGui.GetStyle().ScrollbarSize };
_adjustedForScrollBars = true;
}
else if (descriptionTextSize.Y < descriptionChildHeight && _adjustedForScrollBars)
{
Size = Size!.Value with { X = Size.Value.X - ImGui.GetStyle().ScrollbarSize };
_adjustedForScrollBars = false;
}
var childFrame = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, descriptionChildHeight);
childFrame = childFrame with
{
X = childFrame.X + (_adjustedForScrollBars ? ImGui.GetStyle().ScrollbarSize : 0),
Y = childFrame.Y / ImGuiHelpers.GlobalScale
};
if (ImGui.BeginChildFrame(1000, childFrame))
{
using var _ = _uiSharedService.GameFont.Push();
ImGui.TextWrapped(mareProfile.Description);
}
ImGui.EndChildFrame();
ImGui.SetCursorPosY(postDummy);
var note = _serverManager.GetNoteForUid(Pair.UserData.UID);
if (!string.IsNullOrEmpty(note))
{
UiSharedService.ColorText(note, ImGuiColors.DalamudGrey);
}
string status = Pair.IsVisible ? "Visible" : (Pair.IsOnline ? "Online" : "Offline");
UiSharedService.ColorText(status, (Pair.IsVisible || Pair.IsOnline) ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed);
if (Pair.IsVisible)
{
ImGui.SameLine();
ImGui.TextUnformatted($"({Pair.PlayerName})");
}
if (Pair.UserPair != null)
{
ImGui.TextUnformatted("Directly paired");
if (Pair.UserPair.OwnPermissions.IsPaused())
{
ImGui.SameLine();
UiSharedService.ColorText("You: paused", ImGuiColors.DalamudYellow);
}
if (Pair.UserPair.OtherPermissions.IsPaused())
{
ImGui.SameLine();
UiSharedService.ColorText("They: paused", ImGuiColors.DalamudYellow);
}
}
if (Pair.UserPair.Groups.Any())
{
ImGui.TextUnformatted("Paired through Syncshells:");
foreach (var group in Pair.UserPair.Groups)
{
var groupNote = _serverManager.GetNoteForGid(group);
var groupName = _pairManager.GroupPairs.First(f => string.Equals(f.Key.GID, group, StringComparison.Ordinal)).Key.GroupAliasOrGID;
var groupString = string.IsNullOrEmpty(groupNote) ? groupName : $"{groupNote} ({groupName})";
ImGui.TextUnformatted("- " + groupString);
}
}
var padding = ImGui.GetStyle().WindowPadding.X / 2;
bool tallerThanWide = _textureWrap.Height >= _textureWrap.Width;
var stretchFactor = tallerThanWide ? 256f * ImGuiHelpers.GlobalScale / _textureWrap.Height : 256f * ImGuiHelpers.GlobalScale / _textureWrap.Width;
var newWidth = _textureWrap.Width * stretchFactor;
var newHeight = _textureWrap.Height * stretchFactor;
var remainingWidth = (256f * ImGuiHelpers.GlobalScale - newWidth) / 2f;
var remainingHeight = (256f * ImGuiHelpers.GlobalScale - newHeight) / 2f;
drawList.AddImage(_textureWrap.Handle, new Vector2(rectMin.X + padding + remainingWidth, rectMin.Y + spacing.Y + pos.Y + remainingHeight),
new Vector2(rectMin.X + padding + remainingWidth + newWidth, rectMin.Y + spacing.Y + pos.Y + remainingHeight + newHeight));
if (_supporterTextureWrap != null)
{
const float iconSize = 38;
drawList.AddImage(_supporterTextureWrap.Handle,
new Vector2(rectMax.X - iconSize - spacing.X, rectMin.Y + (textPos / 2) - (iconSize / 2)),
new Vector2(rectMax.X - spacing.X, rectMin.Y + iconSize + (textPos / 2) - (iconSize / 2)));
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error during draw tooltip");
}
}
public override void OnClose()
{
Mediator.Publish(new RemoveWindowMessage(this));
}
}

View File

@@ -0,0 +1,456 @@
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.Data.Extensions;
using LightlessSync.API.Dto.Group;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging;
using System.Globalization;
namespace LightlessSync.UI.Components.Popup;
public class SyncshellAdminUI : WindowMediatorSubscriberBase
{
private readonly ApiController _apiController;
private readonly bool _isModerator = false;
private readonly bool _isOwner = false;
private readonly List<string> _oneTimeInvites = [];
private readonly PairManager _pairManager;
private readonly UiSharedService _uiSharedService;
private List<BannedGroupUserDto> _bannedUsers = [];
private int _multiInvites;
private string _newPassword;
private bool _pwChangeSuccess;
private Task<int>? _pruneTestTask;
private Task<int>? _pruneTask;
private int _pruneDays = 14;
public SyncshellAdminUI(ILogger<SyncshellAdminUI> logger, MareMediator mediator, ApiController apiController,
UiSharedService uiSharedService, PairManager pairManager, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService)
{
GroupFullInfo = groupFullInfo;
_apiController = apiController;
_uiSharedService = uiSharedService;
_pairManager = pairManager;
_isOwner = string.Equals(GroupFullInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal);
_isModerator = GroupFullInfo.GroupUserInfo.IsModerator();
_newPassword = string.Empty;
_multiInvites = 30;
_pwChangeSuccess = true;
IsOpen = true;
SizeConstraints = new WindowSizeConstraints()
{
MinimumSize = new(700, 500),
MaximumSize = new(700, 2000),
};
}
public GroupFullInfoDto GroupFullInfo { get; private set; }
protected override void DrawInternal()
{
if (!_isModerator && !_isOwner) return;
GroupFullInfo = _pairManager.Groups[GroupFullInfo.Group];
using var id = ImRaii.PushId("syncshell_admin_" + GroupFullInfo.GID);
using (_uiSharedService.UidFont.Push())
ImGui.TextUnformatted(GroupFullInfo.GroupAliasOrGID + " Administrative Panel");
ImGui.Separator();
var perm = GroupFullInfo.GroupPermissions;
using var tabbar = ImRaii.TabBar("syncshell_tab_" + GroupFullInfo.GID);
if (tabbar)
{
var inviteTab = ImRaii.TabItem("Invites");
if (inviteTab)
{
bool isInvitesDisabled = perm.IsDisableInvites();
if (_uiSharedService.IconTextButton(isInvitesDisabled ? FontAwesomeIcon.Unlock : FontAwesomeIcon.Lock,
isInvitesDisabled ? "Unlock Syncshell" : "Lock Syncshell"))
{
perm.SetDisableInvites(!isInvitesDisabled);
_ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm));
}
ImGuiHelpers.ScaledDummy(2f);
UiSharedService.TextWrapped("One-time invites work as single-use passwords. Use those if you do not want to distribute your Syncshell password.");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Envelope, "Single one-time invite"))
{
ImGui.SetClipboardText(_apiController.GroupCreateTempInvite(new(GroupFullInfo.Group), 1).Result.FirstOrDefault() ?? string.Empty);
}
UiSharedService.AttachToolTip("Creates a single-use password for joining the syncshell which is valid for 24h and copies it to the clipboard.");
ImGui.InputInt("##amountofinvites", ref _multiInvites);
ImGui.SameLine();
using (ImRaii.Disabled(_multiInvites <= 1 || _multiInvites > 100))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Envelope, "Generate " + _multiInvites + " one-time invites"))
{
_oneTimeInvites.AddRange(_apiController.GroupCreateTempInvite(new(GroupFullInfo.Group), _multiInvites).Result);
}
}
if (_oneTimeInvites.Any())
{
var invites = string.Join(Environment.NewLine, _oneTimeInvites);
ImGui.InputTextMultiline("Generated Multi Invites", ref invites, 5000, new(0, 0), ImGuiInputTextFlags.ReadOnly);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Copy, "Copy Invites to clipboard"))
{
ImGui.SetClipboardText(invites);
}
}
}
inviteTab.Dispose();
var mgmtTab = ImRaii.TabItem("User Management");
if (mgmtTab)
{
var userNode = ImRaii.TreeNode("User List & Administration");
if (userNode)
{
if (!_pairManager.GroupPairs.TryGetValue(GroupFullInfo, out var pairs))
{
UiSharedService.ColorTextWrapped("No users found in this Syncshell", ImGuiColors.DalamudYellow);
}
else
{
using var table = ImRaii.Table("userList#" + GroupFullInfo.Group.GID, 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.ScrollY);
if (table)
{
ImGui.TableSetupColumn("Alias/UID/Note", ImGuiTableColumnFlags.None, 3);
ImGui.TableSetupColumn("Online/Name", ImGuiTableColumnFlags.None, 2);
ImGui.TableSetupColumn("Flags", ImGuiTableColumnFlags.None, 1);
ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 2);
ImGui.TableHeadersRow();
var groupedPairs = new Dictionary<Pair, GroupPairUserInfo?>(pairs.Select(p => new KeyValuePair<Pair, GroupPairUserInfo?>(p,
GroupFullInfo.GroupPairUserInfos.TryGetValue(p.UserData.UID, out GroupPairUserInfo value) ? value : null)));
foreach (var pair in groupedPairs.OrderBy(p =>
{
if (p.Value == null) return 10;
if (p.Value.Value.IsModerator()) return 0;
if (p.Value.Value.IsPinned()) return 1;
return 10;
}).ThenBy(p => p.Key.GetNote() ?? p.Key.UserData.AliasOrUID, StringComparer.OrdinalIgnoreCase))
{
using var tableId = ImRaii.PushId("userTable_" + pair.Key.UserData.UID);
ImGui.TableNextColumn(); // alias/uid/note
var note = pair.Key.GetNote();
var text = note == null ? pair.Key.UserData.AliasOrUID : note + " (" + pair.Key.UserData.AliasOrUID + ")";
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(text);
ImGui.TableNextColumn(); // online/name
string onlineText = pair.Key.IsOnline ? "Online" : "Offline";
if (!string.IsNullOrEmpty(pair.Key.PlayerName))
{
onlineText += " (" + pair.Key.PlayerName + ")";
}
var boolcolor = UiSharedService.GetBoolColor(pair.Key.IsOnline);
ImGui.AlignTextToFramePadding();
UiSharedService.ColorText(onlineText, boolcolor);
ImGui.TableNextColumn(); // special flags
if (pair.Value != null && (pair.Value.Value.IsModerator() || pair.Value.Value.IsPinned()))
{
if (pair.Value.Value.IsModerator())
{
_uiSharedService.IconText(FontAwesomeIcon.UserShield);
UiSharedService.AttachToolTip("Moderator");
}
if (pair.Value.Value.IsPinned())
{
_uiSharedService.IconText(FontAwesomeIcon.Thumbtack);
UiSharedService.AttachToolTip("Pinned");
}
}
else
{
_uiSharedService.IconText(FontAwesomeIcon.None);
}
ImGui.TableNextColumn(); // actions
if (_isOwner)
{
if (_uiSharedService.IconButton(FontAwesomeIcon.UserShield))
{
GroupPairUserInfo userInfo = pair.Value ?? GroupPairUserInfo.None;
userInfo.SetModerator(!userInfo.IsModerator());
_ = _apiController.GroupSetUserInfo(new GroupPairUserInfoDto(GroupFullInfo.Group, pair.Key.UserData, userInfo));
}
UiSharedService.AttachToolTip(pair.Value != null && pair.Value.Value.IsModerator() ? "Demod user" : "Mod user");
ImGui.SameLine();
}
if (_isOwner || (pair.Value == null || (pair.Value != null && !pair.Value.Value.IsModerator())))
{
if (_uiSharedService.IconButton(FontAwesomeIcon.Thumbtack))
{
GroupPairUserInfo userInfo = pair.Value ?? GroupPairUserInfo.None;
userInfo.SetPinned(!userInfo.IsPinned());
_ = _apiController.GroupSetUserInfo(new GroupPairUserInfoDto(GroupFullInfo.Group, pair.Key.UserData, userInfo));
}
UiSharedService.AttachToolTip(pair.Value != null && pair.Value.Value.IsPinned() ? "Unpin user" : "Pin user");
ImGui.SameLine();
using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
{
if (_uiSharedService.IconButton(FontAwesomeIcon.Trash))
{
_ = _apiController.GroupRemoveUser(new GroupPairDto(GroupFullInfo.Group, pair.Key.UserData));
}
}
UiSharedService.AttachToolTip("Remove user from Syncshell"
+ UiSharedService.TooltipSeparator + "Hold CTRL to enable this button");
ImGui.SameLine();
using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
{
if (_uiSharedService.IconButton(FontAwesomeIcon.Ban))
{
Mediator.Publish(new OpenBanUserPopupMessage(pair.Key, GroupFullInfo));
}
}
UiSharedService.AttachToolTip("Ban user from Syncshell"
+ UiSharedService.TooltipSeparator + "Hold CTRL to enable this button");
}
}
}
}
}
userNode.Dispose();
var clearNode = ImRaii.TreeNode("Mass Cleanup");
if (clearNode)
{
using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Broom, "Clear Syncshell"))
{
_ = _apiController.GroupClear(new(GroupFullInfo.Group));
}
}
UiSharedService.AttachToolTip("This will remove all non-pinned, non-moderator users from the Syncshell."
+ UiSharedService.TooltipSeparator + "Hold CTRL to enable this button");
ImGuiHelpers.ScaledDummy(2f);
ImGui.Separator();
ImGuiHelpers.ScaledDummy(2f);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Unlink, "Check for Inactive Users"))
{
_pruneTestTask = _apiController.GroupPrune(new(GroupFullInfo.Group), _pruneDays, execute: false);
_pruneTask = null;
}
UiSharedService.AttachToolTip($"This will start the prune process for this Syncshell of inactive Mare users that have not logged in in the past {_pruneDays} days."
+ Environment.NewLine + "You will be able to review the amount of inactive users before executing the prune."
+ UiSharedService.TooltipSeparator + "Note: this check excludes pinned users and moderators of this Syncshell.");
ImGui.SameLine();
ImGui.SetNextItemWidth(150);
_uiSharedService.DrawCombo("Days of inactivity", [7, 14, 30, 90], (count) =>
{
return count + " days";
},
(selected) =>
{
_pruneDays = selected;
_pruneTestTask = null;
_pruneTask = null;
},
_pruneDays);
if (_pruneTestTask != null)
{
if (!_pruneTestTask.IsCompleted)
{
UiSharedService.ColorTextWrapped("Calculating inactive users...", ImGuiColors.DalamudYellow);
}
else
{
ImGui.AlignTextToFramePadding();
UiSharedService.TextWrapped($"Found {_pruneTestTask.Result} user(s) that have not logged into Mare in the past {_pruneDays} days.");
if (_pruneTestTask.Result > 0)
{
using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Broom, "Prune Inactive Users"))
{
_pruneTask = _apiController.GroupPrune(new(GroupFullInfo.Group), _pruneDays, execute: true);
_pruneTestTask = null;
}
}
UiSharedService.AttachToolTip($"Pruning will remove {_pruneTestTask?.Result ?? 0} inactive user(s)."
+ UiSharedService.TooltipSeparator + "Hold CTRL to enable this button");
}
}
}
if (_pruneTask != null)
{
if (!_pruneTask.IsCompleted)
{
UiSharedService.ColorTextWrapped("Pruning Syncshell...", ImGuiColors.DalamudYellow);
}
else
{
UiSharedService.TextWrapped($"Syncshell was pruned and {_pruneTask.Result} inactive user(s) have been removed.");
}
}
}
clearNode.Dispose();
var banNode = ImRaii.TreeNode("User Bans");
if (banNode)
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist from Server"))
{
_bannedUsers = _apiController.GroupGetBannedUsers(new GroupDto(GroupFullInfo.Group)).Result;
}
if (ImGui.BeginTable("bannedusertable" + GroupFullInfo.GID, 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.ScrollY))
{
ImGui.TableSetupColumn("UID", ImGuiTableColumnFlags.None, 1);
ImGui.TableSetupColumn("Alias", ImGuiTableColumnFlags.None, 1);
ImGui.TableSetupColumn("By", ImGuiTableColumnFlags.None, 1);
ImGui.TableSetupColumn("Date", ImGuiTableColumnFlags.None, 2);
ImGui.TableSetupColumn("Reason", ImGuiTableColumnFlags.None, 3);
ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 1);
ImGui.TableHeadersRow();
foreach (var bannedUser in _bannedUsers.ToList())
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(bannedUser.UID);
ImGui.TableNextColumn();
ImGui.TextUnformatted(bannedUser.UserAlias ?? string.Empty);
ImGui.TableNextColumn();
ImGui.TextUnformatted(bannedUser.BannedBy);
ImGui.TableNextColumn();
ImGui.TextUnformatted(bannedUser.BannedOn.ToLocalTime().ToString(CultureInfo.CurrentCulture));
ImGui.TableNextColumn();
UiSharedService.TextWrapped(bannedUser.Reason);
ImGui.TableNextColumn();
using var _ = ImRaii.PushId(bannedUser.UID);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Check, "Unban"))
{
_apiController.GroupUnbanUser(bannedUser);
_bannedUsers.RemoveAll(b => string.Equals(b.UID, bannedUser.UID, StringComparison.Ordinal));
}
}
ImGui.EndTable();
}
}
banNode.Dispose();
}
mgmtTab.Dispose();
var permissionTab = ImRaii.TabItem("Permissions");
if (permissionTab)
{
bool isDisableAnimations = perm.IsPreferDisableAnimations();
bool isDisableSounds = perm.IsPreferDisableSounds();
bool isDisableVfx = perm.IsPreferDisableVFX();
ImGui.AlignTextToFramePadding();
ImGui.Text("Suggest Sound Sync");
_uiSharedService.BooleanToColoredIcon(!isDisableSounds);
ImGui.SameLine(230);
if (_uiSharedService.IconTextButton(isDisableSounds ? FontAwesomeIcon.VolumeUp : FontAwesomeIcon.VolumeMute,
isDisableSounds ? "Suggest to enable sound sync" : "Suggest to disable sound sync"))
{
perm.SetPreferDisableSounds(!perm.IsPreferDisableSounds());
_ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm));
}
ImGui.AlignTextToFramePadding();
ImGui.Text("Suggest Animation Sync");
_uiSharedService.BooleanToColoredIcon(!isDisableAnimations);
ImGui.SameLine(230);
if (_uiSharedService.IconTextButton(isDisableAnimations ? FontAwesomeIcon.Running : FontAwesomeIcon.Stop,
isDisableAnimations ? "Suggest to enable animation sync" : "Suggest to disable animation sync"))
{
perm.SetPreferDisableAnimations(!perm.IsPreferDisableAnimations());
_ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm));
}
ImGui.AlignTextToFramePadding();
ImGui.Text("Suggest VFX Sync");
_uiSharedService.BooleanToColoredIcon(!isDisableVfx);
ImGui.SameLine(230);
if (_uiSharedService.IconTextButton(isDisableVfx ? FontAwesomeIcon.Sun : FontAwesomeIcon.Circle,
isDisableVfx ? "Suggest to enable vfx sync" : "Suggest to disable vfx sync"))
{
perm.SetPreferDisableVFX(!perm.IsPreferDisableVFX());
_ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm));
}
UiSharedService.TextWrapped("Note: those suggested permissions will be shown to users on joining the Syncshell.");
}
permissionTab.Dispose();
if (_isOwner)
{
var ownerTab = ImRaii.TabItem("Owner Settings");
if (ownerTab)
{
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("New Password");
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
var buttonSize = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.Passport, "Change Password");
var textSize = ImGui.CalcTextSize("New Password").X;
var spacing = ImGui.GetStyle().ItemSpacing.X;
ImGui.SameLine();
ImGui.SetNextItemWidth(availableWidth - buttonSize - textSize - spacing * 2);
ImGui.InputTextWithHint("##changepw", "Min 10 characters", ref _newPassword, 50);
ImGui.SameLine();
using (ImRaii.Disabled(_newPassword.Length < 10))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Passport, "Change Password"))
{
_pwChangeSuccess = _apiController.GroupChangePassword(new GroupPasswordDto(GroupFullInfo.Group, _newPassword)).Result;
_newPassword = string.Empty;
}
}
UiSharedService.AttachToolTip("Password requires to be at least 10 characters long. This action is irreversible.");
if (!_pwChangeSuccess)
{
UiSharedService.ColorTextWrapped("Failed to change the password. Password requires to be at least 10 characters long.", ImGuiColors.DalamudYellow);
}
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Syncshell") && UiSharedService.CtrlPressed() && UiSharedService.ShiftPressed())
{
IsOpen = false;
_ = _apiController.GroupDelete(new(GroupFullInfo.Group));
}
UiSharedService.AttachToolTip("Hold CTRL and Shift and click to delete this Syncshell." + Environment.NewLine + "WARNING: this action is irreversible.");
}
ownerTab.Dispose();
}
}
}
public override void OnClose()
{
Mediator.Publish(new RemoveWindowMessage(this));
}
}

View File

@@ -0,0 +1,594 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Utility;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator;
using LightlessSync.WebAPI;
using System.Numerics;
namespace LightlessSync.UI;
public class TopTabMenu
{
private readonly ApiController _apiController;
private readonly MareMediator _mareMediator;
private readonly PairManager _pairManager;
private readonly UiSharedService _uiSharedService;
private string _filter = string.Empty;
private int _globalControlCountdown = 0;
private string _pairToAdd = string.Empty;
private SelectedTab _selectedTab = SelectedTab.None;
public TopTabMenu(MareMediator mareMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService)
{
_mareMediator = mareMediator;
_apiController = apiController;
_pairManager = pairManager;
_uiSharedService = uiSharedService;
}
private enum SelectedTab
{
None,
Individual,
Syncshell,
Filter,
UserConfig
}
public string Filter
{
get => _filter;
private set
{
if (!string.Equals(_filter, value, StringComparison.OrdinalIgnoreCase))
{
_mareMediator.Publish(new RefreshUiMessage());
}
_filter = value;
}
}
private SelectedTab TabSelection
{
get => _selectedTab; set
{
if (_selectedTab == SelectedTab.Filter && value != SelectedTab.Filter)
{
Filter = string.Empty;
}
_selectedTab = value;
}
}
public void Draw()
{
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
var spacing = ImGui.GetStyle().ItemSpacing;
var buttonX = (availableWidth - (spacing.X * 3)) / 4f;
var buttonY = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Pause).Y;
var buttonSize = new Vector2(buttonX, buttonY);
var drawList = ImGui.GetWindowDrawList();
var underlineColor = 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;
}
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;
}
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");
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.Filter.ToIconString(), buttonSize))
{
TabSelection = TabSelection == SelectedTab.Filter ? SelectedTab.None : SelectedTab.Filter;
}
ImGui.SameLine();
var xAfter = ImGui.GetCursorScreenPos();
if (TabSelection == SelectedTab.Filter)
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("Filter");
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;
}
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.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.Filter)
{
DrawFilter(availableWidth, spacing.X);
}
else if (TabSelection == SelectedTab.UserConfig)
{
DrawUserConfig(availableWidth, spacing.X);
}
if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f);
ImGui.Separator();
}
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 = _pairManager.DirectPairs.Exists(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 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;
}
ImGui.SameLine();
using var disabled = ImRaii.Disabled(string.IsNullOrEmpty(Filter));
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Ban, "Clear"))
{
Filter = string.Empty;
}
}
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 = _pairManager.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(_pairManager.GroupPairs.Select(k => k.Key).Distinct()
.Count(g => string.Equals(g.OwnerUID, _apiController.UID, StringComparison.Ordinal)) >= _apiController.ServerInfo.MaxGroupsCreatedByUser))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Create new Syncshell", buttonX))
{
_mareMediator.Publish(new UiToggleMessage(typeof(CreateSyncshellUI)));
}
ImGui.SameLine();
}
using (ImRaii.Disabled(_pairManager.GroupPairs.Select(k => k.Key).Distinct().Count() >= _apiController.ServerInfo.MaxGroupsJoinedByUser))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Users, "Join existing Syncshell", buttonX))
{
_mareMediator.Publish(new UiToggleMessage(typeof(JoinSyncshellUI)));
}
}
}
private void DrawUserConfig(float availableWidth, float spacingX)
{
var buttonX = (availableWidth - spacingX) / 2f;
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserCircle, "Edit Mare Profile", buttonX))
{
_mareMediator.Publish(new UiToggleMessage(typeof(EditProfileUi)));
}
UiSharedService.AttachToolTip("Edit your Mare Profile");
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, "Chara Data Analysis", buttonX))
{
_mareMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
}
UiSharedService.AttachToolTip("View and analyze your generated character data");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Running, "Character Data Hub", availableWidth))
{
_mareMediator.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 = _pairManager.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 = _pairManager.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 = _pairManager.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 = _pairManager.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();
}
}
}

File diff suppressed because it is too large Load Diff