[Draft] Update 0.8 (#46)
* move stuff out into file transfer manager * obnoxious unsupported version text, adjustments to filetransfermanager * add back file upload transfer progress * restructure code * cleanup some more stuff I guess * downloadids by playername * individual anim/sound bs * fix migration stuff, finalize impl of individual sound/anim pause * fixes with logging stuff * move download manager to transient * rework dl ui first iteration * some refactoring and cleanup * more code cleanup * refactoring * switch to hostbuilder * some more rework I guess * more refactoring * clean up mediator calls and disposal * fun code cleanup * push error message when log level is set to anything but information in non-debug builds * remove notificationservice * move message to after login * add download bars to gameworld * fixes download progress bar * set gpose ui min and max size * remove unnecessary usings * adjustments to reconnection logic * add options to set visible/offline groups visibility * add impl of uploading display, transfer list in settings ui * attempt to fix issues with server selection * add back download status to compact ui * make dl bar fixed size based * some fixes for upload/download handling * adjust text from Syncing back to Uploading --------- Co-authored-by: rootdarkarchon <root.darkarchon@outlook.com> Co-authored-by: Stanley Dimant <stanley.dimant@varian.com>
This commit is contained in:
@@ -2,215 +2,236 @@
|
||||
using Dalamud.Interface.Components;
|
||||
using ImGuiNET;
|
||||
using MareSynchronos.API.Data.Extensions;
|
||||
using MareSynchronos.Models;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.UI.Handlers;
|
||||
using MareSynchronos.WebAPI;
|
||||
|
||||
namespace MareSynchronos.UI.Components
|
||||
namespace MareSynchronos.UI.Components;
|
||||
|
||||
public class PairGroupsUi
|
||||
{
|
||||
public class PairGroupsUi
|
||||
private readonly ApiController _apiController;
|
||||
private readonly Action<Pair> _clientRenderFn;
|
||||
private readonly MareConfigService _mareConfig;
|
||||
private readonly SelectPairForGroupUi _selectGroupForPairUi;
|
||||
private readonly TagHandler _tagHandler;
|
||||
|
||||
public PairGroupsUi(MareConfigService mareConfig, TagHandler tagHandler, Action<Pair> clientRenderFn, ApiController apiController, SelectPairForGroupUi selectGroupForPairUi)
|
||||
{
|
||||
private readonly Action<Pair> _clientRenderFn;
|
||||
private readonly TagHandler _tagHandler;
|
||||
private readonly ApiController _apiController;
|
||||
private readonly SelectPairForGroupUi _selectGroupForPairUi;
|
||||
_clientRenderFn = clientRenderFn;
|
||||
_mareConfig = mareConfig;
|
||||
_tagHandler = tagHandler;
|
||||
_apiController = apiController;
|
||||
_selectGroupForPairUi = selectGroupForPairUi;
|
||||
}
|
||||
|
||||
public PairGroupsUi(TagHandler tagHandler, Action<Pair> clientRenderFn, ApiController apiController, SelectPairForGroupUi selectGroupForPairUi)
|
||||
public void Draw(List<Pair> visibleUsers, List<Pair> onlineUsers, List<Pair> offlineUsers)
|
||||
{
|
||||
// Only render those tags that actually have pairs in them, otherwise
|
||||
// we can end up with a bunch of useless pair groups
|
||||
var tagsWithPairsInThem = _tagHandler.GetAllTagsSorted();
|
||||
var allUsers = visibleUsers.Concat(onlineUsers).Concat(offlineUsers).ToList();
|
||||
if (_mareConfig.Current.ShowVisibleUsersSeparately)
|
||||
{
|
||||
_clientRenderFn = clientRenderFn;
|
||||
_tagHandler = tagHandler;
|
||||
_apiController = apiController;
|
||||
_selectGroupForPairUi = selectGroupForPairUi;
|
||||
UiSharedService.DrawWithID("$group-VisibleCustomTag", () => DrawCategory(TagHandler.CustomVisibleTag, visibleUsers, allUsers));
|
||||
}
|
||||
|
||||
public void Draw(List<Pair> visibleUsers, List<Pair> onlineUsers, List<Pair> offlineUsers)
|
||||
foreach (var tag in tagsWithPairsInThem)
|
||||
{
|
||||
// Only render those tags that actually have pairs in them, otherwise
|
||||
// we can end up with a bunch of useless pair groups
|
||||
var tagsWithPairsInThem = _tagHandler.GetAllTagsSorted();
|
||||
var allUsers = visibleUsers.Concat(onlineUsers).Concat(offlineUsers).ToList();
|
||||
UiShared.DrawWithID("$group-VisibleCustomTag", () => DrawCategory(TagHandler.CustomVisibleTag, visibleUsers, allUsers));
|
||||
foreach (var tag in tagsWithPairsInThem)
|
||||
if (_mareConfig.Current.ShowOfflineUsersSeparately)
|
||||
{
|
||||
UiShared.DrawWithID($"group-{tag}", () => DrawCategory(tag, onlineUsers, allUsers, visibleUsers));
|
||||
}
|
||||
UiShared.DrawWithID($"group-OnlineCustomTag", () => DrawCategory(TagHandler.CustomOnlineTag, onlineUsers.Where(u => !_tagHandler.HasAnyTag(u.UserPair!)).ToList(), allUsers));
|
||||
UiShared.DrawWithID($"group-OfflineCustomTag", () => DrawCategory(TagHandler.CustomOfflineTag, offlineUsers, allUsers));
|
||||
}
|
||||
|
||||
private void DrawCategory(string tag, List<Pair> onlineUsers, List<Pair> allUsers, List<Pair>? visibleUsers = null)
|
||||
{
|
||||
List<Pair> usersInThisTag;
|
||||
HashSet<string>? otherUidsTaggedWithTag = null;
|
||||
bool isSpecialTag = false;
|
||||
int visibleInThisTag = 0;
|
||||
if (tag is TagHandler.CustomOfflineTag or TagHandler.CustomOnlineTag or TagHandler.CustomVisibleTag)
|
||||
{
|
||||
usersInThisTag = onlineUsers;
|
||||
isSpecialTag = true;
|
||||
UiSharedService.DrawWithID($"group-{tag}", () => DrawCategory(tag, onlineUsers, allUsers, visibleUsers));
|
||||
}
|
||||
else
|
||||
{
|
||||
otherUidsTaggedWithTag = _tagHandler.GetOtherUidsForTag(tag);
|
||||
usersInThisTag = onlineUsers
|
||||
.Where(pair => otherUidsTaggedWithTag.Contains(pair.UserData.UID))
|
||||
.ToList();
|
||||
visibleInThisTag = visibleUsers?.Count(p => otherUidsTaggedWithTag.Contains(p.UserData.UID)) ?? 0;
|
||||
}
|
||||
|
||||
if (isSpecialTag && !usersInThisTag.Any()) return;
|
||||
|
||||
DrawName(tag, isSpecialTag, visibleInThisTag, usersInThisTag.Count, otherUidsTaggedWithTag?.Count);
|
||||
if (!isSpecialTag)
|
||||
UiShared.DrawWithID($"group-{tag}-buttons", () => DrawButtons(tag, allUsers.Where(p => otherUidsTaggedWithTag!.Contains(p.UserData.UID)).ToList()));
|
||||
|
||||
if (!_tagHandler.IsTagOpen(tag)) return;
|
||||
|
||||
ImGui.Indent(20);
|
||||
DrawPairs(tag, usersInThisTag);
|
||||
ImGui.Unindent(20);
|
||||
}
|
||||
|
||||
private void DrawName(string tag, bool isSpecialTag, int visible, int online, int? total)
|
||||
{
|
||||
string displayedName = tag switch
|
||||
{
|
||||
TagHandler.CustomOfflineTag => "Offline/Unpaired",
|
||||
TagHandler.CustomOnlineTag => "Online/Paused",
|
||||
TagHandler.CustomVisibleTag => "Visible",
|
||||
_ => tag
|
||||
};
|
||||
|
||||
string resultFolderName = !isSpecialTag ? $"{displayedName} ({visible}/{online}/{total} Pairs)" : $"{displayedName} ({online} Pairs)";
|
||||
|
||||
// FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight
|
||||
var icon = _tagHandler.IsTagOpen(tag) ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight;
|
||||
UiShared.FontText(icon.ToIconString(), UiBuilder.IconFont);
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
||||
{
|
||||
ToggleTagOpen(tag);
|
||||
}
|
||||
ImGui.SameLine();
|
||||
UiShared.FontText(resultFolderName, UiBuilder.DefaultFont);
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
||||
{
|
||||
ToggleTagOpen(tag);
|
||||
}
|
||||
|
||||
if (!isSpecialTag && ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
ImGui.TextUnformatted($"Group {tag}");
|
||||
ImGui.Separator();
|
||||
ImGui.TextUnformatted($"{visible} Pairs visible");
|
||||
ImGui.TextUnformatted($"{online} Pairs online/paused");
|
||||
ImGui.TextUnformatted($"{total} Pairs total");
|
||||
ImGui.EndTooltip();
|
||||
UiSharedService.DrawWithID($"group-{tag}", () => DrawCategory(tag, onlineUsers.Concat(offlineUsers).ToList(), allUsers, visibleUsers));
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawButtons(string tag, List<Pair> availablePairsInThisTag)
|
||||
if (_mareConfig.Current.ShowOfflineUsersSeparately)
|
||||
{
|
||||
var allArePaused = availablePairsInThisTag.All(pair => pair.UserPair!.OwnPermissions.IsPaused());
|
||||
var pauseButton = allArePaused ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
|
||||
var flyoutMenuX = UiShared.GetIconButtonSize(FontAwesomeIcon.Bars).X;
|
||||
var pauseButtonX = UiShared.GetIconButtonSize(pauseButton).X;
|
||||
var windowX = ImGui.GetWindowContentRegionMin().X;
|
||||
var windowWidth = UiShared.GetWindowContentRegionWidth();
|
||||
var spacingX = ImGui.GetStyle().ItemSpacing.X;
|
||||
UiSharedService.DrawWithID($"group-OnlineCustomTag", () => DrawCategory(TagHandler.CustomOnlineTag,
|
||||
onlineUsers.Where(u => !_tagHandler.HasAnyTag(u.UserPair!)).ToList(), allUsers));
|
||||
UiSharedService.DrawWithID($"group-OfflineCustomTag", () => DrawCategory(TagHandler.CustomOfflineTag,
|
||||
offlineUsers.Where(u => u.UserPair!.OtherPermissions.IsPaired()).ToList(), allUsers));
|
||||
}
|
||||
else
|
||||
{
|
||||
UiSharedService.DrawWithID($"group-OnlineCustomTag", () => DrawCategory(TagHandler.CustomOnlineTag,
|
||||
onlineUsers.Concat(offlineUsers).Where(u => u.UserPair!.OtherPermissions.IsPaired() && !_tagHandler.HasAnyTag(u.UserPair!)).ToList(), allUsers));
|
||||
}
|
||||
UiSharedService.DrawWithID($"group-UnpairedCustomTag", () => DrawCategory(TagHandler.CustomUnpairedTag,
|
||||
offlineUsers.Where(u => !u.UserPair!.OtherPermissions.IsPaired()).ToList(), allUsers));
|
||||
}
|
||||
|
||||
var buttonPauseOffset = windowX + windowWidth - flyoutMenuX - spacingX - pauseButtonX;
|
||||
ImGui.SameLine(buttonPauseOffset);
|
||||
if (ImGuiComponents.IconButton(pauseButton))
|
||||
{
|
||||
// If all of the currently visible pairs (after applying filters to the pairs)
|
||||
// are paused we display a resume button to resume all currently visible (after filters)
|
||||
// pairs. Otherwise, we just pause all the remaining pairs.
|
||||
if (allArePaused)
|
||||
{
|
||||
// If all are paused => resume all
|
||||
ResumeAllPairs(availablePairsInThisTag);
|
||||
}
|
||||
else
|
||||
{
|
||||
// otherwise pause all remaining
|
||||
PauseRemainingPairs(availablePairsInThisTag);
|
||||
}
|
||||
}
|
||||
private void DrawButtons(string tag, List<Pair> availablePairsInThisTag)
|
||||
{
|
||||
var allArePaused = availablePairsInThisTag.All(pair => pair.UserPair!.OwnPermissions.IsPaused());
|
||||
var pauseButton = allArePaused ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
|
||||
var flyoutMenuX = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X;
|
||||
var pauseButtonX = UiSharedService.GetIconButtonSize(pauseButton).X;
|
||||
var windowX = ImGui.GetWindowContentRegionMin().X;
|
||||
var windowWidth = UiSharedService.GetWindowContentRegionWidth();
|
||||
var spacingX = ImGui.GetStyle().ItemSpacing.X;
|
||||
|
||||
var buttonPauseOffset = windowX + windowWidth - flyoutMenuX - spacingX - pauseButtonX;
|
||||
ImGui.SameLine(buttonPauseOffset);
|
||||
if (ImGuiComponents.IconButton(pauseButton))
|
||||
{
|
||||
// If all of the currently visible pairs (after applying filters to the pairs)
|
||||
// are paused we display a resume button to resume all currently visible (after filters)
|
||||
// pairs. Otherwise, we just pause all the remaining pairs.
|
||||
if (allArePaused)
|
||||
{
|
||||
UiShared.AttachToolTip($"Resume pairing with all pairs in {tag}");
|
||||
// If all are paused => resume all
|
||||
ResumeAllPairs(availablePairsInThisTag);
|
||||
}
|
||||
else
|
||||
{
|
||||
UiShared.AttachToolTip($"Pause pairing with all pairs in {tag}");
|
||||
}
|
||||
|
||||
var buttonDeleteOffset = windowX + windowWidth - flyoutMenuX;
|
||||
ImGui.SameLine(buttonDeleteOffset);
|
||||
if (ImGuiComponents.IconButton(FontAwesomeIcon.Bars))
|
||||
{
|
||||
ImGui.OpenPopup("Group Flyout Menu");
|
||||
|
||||
}
|
||||
|
||||
if (ImGui.BeginPopup("Group Flyout Menu"))
|
||||
{
|
||||
UiShared.DrawWithID($"buttons-{tag}", () => DrawGroupMenu(tag));
|
||||
ImGui.EndPopup();
|
||||
// otherwise pause all remaining
|
||||
PauseRemainingPairs(availablePairsInThisTag);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawGroupMenu(string tag)
|
||||
if (allArePaused)
|
||||
{
|
||||
if (UiShared.IconTextButton(FontAwesomeIcon.Users, "Add people to " + tag))
|
||||
{
|
||||
_selectGroupForPairUi.Open(tag);
|
||||
}
|
||||
UiShared.AttachToolTip($"Add more users to Group {tag}");
|
||||
|
||||
if (UiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete " + tag))
|
||||
{
|
||||
if (UiShared.CtrlPressed())
|
||||
{
|
||||
_tagHandler.RemoveTag(tag);
|
||||
}
|
||||
}
|
||||
UiShared.AttachToolTip($"Delete Group {tag} (Will not delete the pairs)" + Environment.NewLine + "Hold CTRL to delete");
|
||||
UiSharedService.AttachToolTip($"Resume pairing with all pairs in {tag}");
|
||||
}
|
||||
else
|
||||
{
|
||||
UiSharedService.AttachToolTip($"Pause pairing with all pairs in {tag}");
|
||||
}
|
||||
|
||||
private void DrawPairs(string tag, List<Pair> availablePairsInThisCategory)
|
||||
var buttonDeleteOffset = windowX + windowWidth - flyoutMenuX;
|
||||
ImGui.SameLine(buttonDeleteOffset);
|
||||
if (ImGuiComponents.IconButton(FontAwesomeIcon.Bars))
|
||||
{
|
||||
// These are all the OtherUIDs that are tagged with this tag
|
||||
availablePairsInThisCategory
|
||||
.ForEach(pair => UiShared.DrawWithID($"tag-{tag}-pair-${pair.UserData.UID}", () => _clientRenderFn(pair)));
|
||||
ImGui.Separator();
|
||||
ImGui.OpenPopup("Group Flyout Menu");
|
||||
}
|
||||
|
||||
private void ToggleTagOpen(string tag)
|
||||
if (ImGui.BeginPopup("Group Flyout Menu"))
|
||||
{
|
||||
bool open = !_tagHandler.IsTagOpen(tag);
|
||||
_tagHandler.SetTagOpen(tag, open);
|
||||
}
|
||||
|
||||
private void PauseRemainingPairs(List<Pair> availablePairs)
|
||||
{
|
||||
foreach (var pairToPause in availablePairs.Where(pair => !pair.UserPair!.OwnPermissions.IsPaused()))
|
||||
{
|
||||
var perm = pairToPause.UserPair!.OwnPermissions;
|
||||
perm.SetPaused(paused: true);
|
||||
_ = _apiController.UserSetPairPermissions(new(pairToPause.UserData, perm));
|
||||
}
|
||||
}
|
||||
|
||||
private void ResumeAllPairs(List<Pair> availablePairs)
|
||||
{
|
||||
foreach (var pairToPause in availablePairs)
|
||||
{
|
||||
var perm = pairToPause.UserPair!.OwnPermissions;
|
||||
perm.SetPaused(paused: false);
|
||||
_ = _apiController.UserSetPairPermissions(new(pairToPause.UserData, perm));
|
||||
}
|
||||
UiSharedService.DrawWithID($"buttons-{tag}", () => DrawGroupMenu(tag));
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawCategory(string tag, List<Pair> onlineUsers, List<Pair> allUsers, List<Pair>? visibleUsers = null)
|
||||
{
|
||||
List<Pair> usersInThisTag;
|
||||
HashSet<string>? otherUidsTaggedWithTag = null;
|
||||
bool isSpecialTag = false;
|
||||
int visibleInThisTag = 0;
|
||||
if (tag is TagHandler.CustomOfflineTag or TagHandler.CustomOnlineTag or TagHandler.CustomVisibleTag or TagHandler.CustomUnpairedTag)
|
||||
{
|
||||
usersInThisTag = onlineUsers;
|
||||
isSpecialTag = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
otherUidsTaggedWithTag = _tagHandler.GetOtherUidsForTag(tag);
|
||||
usersInThisTag = onlineUsers
|
||||
.Where(pair => otherUidsTaggedWithTag.Contains(pair.UserData.UID))
|
||||
.ToList();
|
||||
visibleInThisTag = visibleUsers?.Count(p => otherUidsTaggedWithTag.Contains(p.UserData.UID)) ?? 0;
|
||||
}
|
||||
|
||||
if (isSpecialTag && !usersInThisTag.Any()) return;
|
||||
|
||||
DrawName(tag, isSpecialTag, visibleInThisTag, usersInThisTag.Count, otherUidsTaggedWithTag?.Count);
|
||||
if (!isSpecialTag)
|
||||
UiSharedService.DrawWithID($"group-{tag}-buttons", () => DrawButtons(tag, allUsers.Where(p => otherUidsTaggedWithTag!.Contains(p.UserData.UID)).ToList()));
|
||||
|
||||
if (!_tagHandler.IsTagOpen(tag)) return;
|
||||
|
||||
ImGui.Indent(20);
|
||||
DrawPairs(tag, usersInThisTag);
|
||||
ImGui.Unindent(20);
|
||||
}
|
||||
|
||||
private void DrawGroupMenu(string tag)
|
||||
{
|
||||
if (UiSharedService.IconTextButton(FontAwesomeIcon.Users, "Add people to " + tag))
|
||||
{
|
||||
_selectGroupForPairUi.Open(tag);
|
||||
}
|
||||
UiSharedService.AttachToolTip($"Add more users to Group {tag}");
|
||||
|
||||
if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete " + tag) && UiSharedService.CtrlPressed())
|
||||
{
|
||||
_tagHandler.RemoveTag(tag);
|
||||
}
|
||||
UiSharedService.AttachToolTip($"Delete Group {tag} (Will not delete the pairs)" + Environment.NewLine + "Hold CTRL to delete");
|
||||
}
|
||||
|
||||
private void DrawName(string tag, bool isSpecialTag, int visible, int online, int? total)
|
||||
{
|
||||
string displayedName = tag switch
|
||||
{
|
||||
TagHandler.CustomUnpairedTag => "Unpaired",
|
||||
TagHandler.CustomOfflineTag => "Offline",
|
||||
TagHandler.CustomOnlineTag => _mareConfig.Current.ShowOfflineUsersSeparately ? "Online/Paused" : "Contacts",
|
||||
TagHandler.CustomVisibleTag => "Visible",
|
||||
_ => tag
|
||||
};
|
||||
|
||||
string resultFolderName = !isSpecialTag ? $"{displayedName} ({visible}/{online}/{total} Pairs)" : $"{displayedName} ({online} Pairs)";
|
||||
|
||||
// FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight
|
||||
var icon = _tagHandler.IsTagOpen(tag) ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight;
|
||||
UiSharedService.FontText(icon.ToIconString(), UiBuilder.IconFont);
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
||||
{
|
||||
ToggleTagOpen(tag);
|
||||
}
|
||||
ImGui.SameLine();
|
||||
UiSharedService.FontText(resultFolderName, UiBuilder.DefaultFont);
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
||||
{
|
||||
ToggleTagOpen(tag);
|
||||
}
|
||||
|
||||
if (!isSpecialTag && ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
ImGui.TextUnformatted($"Group {tag}");
|
||||
ImGui.Separator();
|
||||
ImGui.TextUnformatted($"{visible} Pairs visible");
|
||||
ImGui.TextUnformatted($"{online} Pairs online/paused");
|
||||
ImGui.TextUnformatted($"{total} Pairs total");
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPairs(string tag, List<Pair> availablePairsInThisCategory)
|
||||
{
|
||||
// These are all the OtherUIDs that are tagged with this tag
|
||||
availablePairsInThisCategory
|
||||
.ForEach(pair => UiSharedService.DrawWithID($"tag-{tag}-pair-${pair.UserData.UID}", () => _clientRenderFn(pair)));
|
||||
ImGui.Separator();
|
||||
}
|
||||
|
||||
private void PauseRemainingPairs(List<Pair> availablePairs)
|
||||
{
|
||||
foreach (var pairToPause in availablePairs.Where(pair => !pair.UserPair!.OwnPermissions.IsPaused()))
|
||||
{
|
||||
var perm = pairToPause.UserPair!.OwnPermissions;
|
||||
perm.SetPaused(paused: true);
|
||||
_ = _apiController.UserSetPairPermissions(new(pairToPause.UserData, perm));
|
||||
}
|
||||
}
|
||||
|
||||
private void ResumeAllPairs(List<Pair> availablePairs)
|
||||
{
|
||||
foreach (var pairToPause in availablePairs)
|
||||
{
|
||||
var perm = pairToPause.UserPair!.OwnPermissions;
|
||||
perm.SetPaused(paused: false);
|
||||
_ = _apiController.UserSetPairPermissions(new(pairToPause.UserData, perm));
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleTagOpen(string tag)
|
||||
{
|
||||
bool open = !_tagHandler.IsTagOpen(tag);
|
||||
_tagHandler.SetTagOpen(tag, open);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user