forked from liza/Deliveroo
Character specific settings
This commit is contained in:
parent
342564d6bf
commit
7c9f7e01bc
35
Deliveroo/CharacterConfiguration.cs
Normal file
35
Deliveroo/CharacterConfiguration.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
using System.IO;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Deliveroo;
|
||||||
|
|
||||||
|
internal sealed class CharacterConfiguration
|
||||||
|
{
|
||||||
|
public ulong LocalContentId { get; set; }
|
||||||
|
public string? CachedPlayerName { get; set; }
|
||||||
|
public string? CachedWorldName { get; set; }
|
||||||
|
|
||||||
|
public bool DisableForCharacter { get; set; } = false;
|
||||||
|
public bool UseHideArmouryChestItemsFilter { get; set; } = false;
|
||||||
|
|
||||||
|
public static string ResolveFilePath(DalamudPluginInterface pluginInterface, ulong localContentId)
|
||||||
|
=> Path.Join(pluginInterface.GetPluginConfigDirectory(), $"char.{localContentId:X}.json");
|
||||||
|
|
||||||
|
public static CharacterConfiguration? Load(DalamudPluginInterface pluginInterface, ulong localContentId)
|
||||||
|
{
|
||||||
|
string path = ResolveFilePath(pluginInterface, localContentId);
|
||||||
|
if (!File.Exists(path))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return JsonConvert.DeserializeObject<CharacterConfiguration>(File.ReadAllText(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save(DalamudPluginInterface pluginInterface)
|
||||||
|
{
|
||||||
|
File.WriteAllText(ResolveFilePath(pluginInterface, LocalContentId), JsonConvert.SerializeObject(this, Formatting.Indented));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Delete(DalamudPluginInterface pluginInterface) =>
|
||||||
|
File.Delete(ResolveFilePath(pluginInterface, LocalContentId));
|
||||||
|
}
|
@ -11,7 +11,6 @@ internal sealed class Configuration : IPluginConfiguration
|
|||||||
public List<PurchasePriority> ItemsToPurchase { get; set; } = new();
|
public List<PurchasePriority> ItemsToPurchase { get; set; } = new();
|
||||||
|
|
||||||
public int ReservedSealCount { get; set; } = 0;
|
public int ReservedSealCount { get; set; } = 0;
|
||||||
public ItemFilterType ItemFilter { get; set; } = ItemFilterType.HideGearSetItems;
|
|
||||||
public bool IgnoreCertainLimitations { get; set; } = false;
|
public bool IgnoreCertainLimitations { get; set; } = false;
|
||||||
|
|
||||||
internal sealed class PurchasePriority
|
internal sealed class PurchasePriority
|
||||||
@ -20,10 +19,4 @@ internal sealed class Configuration : IPluginConfiguration
|
|||||||
public int Limit { get; set; }
|
public int Limit { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ItemFilterType
|
|
||||||
{
|
|
||||||
ShowAllItems = 0,
|
|
||||||
HideGearSetItems = 1,
|
|
||||||
HideArmouryChestItems = 2,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -86,10 +86,11 @@ partial class DeliverooPlugin
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addonGc->SelectedFilter == 0 || addonGc->SelectedFilter != (int)_configuration.ItemFilter)
|
ItemFilterType configuredFilter = ResolveSelectedSupplyFilter();
|
||||||
|
if (addonGc->SelectedFilter == 0 || addonGc->SelectedFilter != (int)configuredFilter)
|
||||||
{
|
{
|
||||||
_turnInWindow.Error =
|
_turnInWindow.Error =
|
||||||
$"Wrong filter selected (expected {_configuration.ItemFilter}, but is {(Configuration.ItemFilterType)addonGc->SelectedFilter})";
|
$"Wrong filter selected (expected {configuredFilter}, but is {(ItemFilterType)addonGc->SelectedFilter})";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,7 +170,7 @@ partial class DeliverooPlugin
|
|||||||
var updateFilter = stackalloc AtkValue[]
|
var updateFilter = stackalloc AtkValue[]
|
||||||
{
|
{
|
||||||
new() { Type = ValueType.Int, Int = 5 },
|
new() { Type = ValueType.Int, Int = 5 },
|
||||||
new() { Type = ValueType.Int, Int = (int)_configuration.ItemFilter },
|
new() { Type = ValueType.Int, Int = (int)ResolveSelectedSupplyFilter() },
|
||||||
new() { Type = 0, Int = 0 }
|
new() { Type = 0, Int = 0 }
|
||||||
};
|
};
|
||||||
addonSupplyList->AtkUnitBase.FireCallback(3, updateFilter);
|
addonSupplyList->AtkUnitBase.FireCallback(3, updateFilter);
|
||||||
@ -217,4 +218,12 @@ partial class DeliverooPlugin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ItemFilterType ResolveSelectedSupplyFilter()
|
||||||
|
{
|
||||||
|
if (CharacterConfiguration is { UseHideArmouryChestItemsFilter: true })
|
||||||
|
return ItemFilterType.HideArmouryChestItems;
|
||||||
|
|
||||||
|
return ItemFilterType.HideGearSetItems;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Dalamud.Data;
|
using Dalamud.Data;
|
||||||
using Dalamud.Game;
|
using Dalamud.Game;
|
||||||
@ -18,6 +19,7 @@ using Deliveroo.Windows;
|
|||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
using Lumina.Excel.GeneratedSheets;
|
using Lumina.Excel.GeneratedSheets;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using Condition = Dalamud.Game.ClientState.Conditions.Condition;
|
using Condition = Dalamud.Game.ClientState.Conditions.Condition;
|
||||||
|
|
||||||
namespace Deliveroo;
|
namespace Deliveroo;
|
||||||
@ -72,7 +74,7 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
|
|||||||
_yesAlreadyIpc = new YesAlreadyIpc(dalamudReflector);
|
_yesAlreadyIpc = new YesAlreadyIpc(dalamudReflector);
|
||||||
_configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration();
|
_configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration();
|
||||||
_gcRewardsCache = new GcRewardsCache(dataManager);
|
_gcRewardsCache = new GcRewardsCache(dataManager);
|
||||||
_configWindow = new ConfigWindow(_pluginInterface, this, _configuration, _gcRewardsCache);
|
_configWindow = new ConfigWindow(_pluginInterface, this, _configuration, _gcRewardsCache, _clientState);
|
||||||
_windowSystem.AddWindow(_configWindow);
|
_windowSystem.AddWindow(_configWindow);
|
||||||
_turnInWindow = new TurnInWindow(this, _pluginInterface, _configuration, _gcRewardsCache, _configWindow);
|
_turnInWindow = new TurnInWindow(this, _pluginInterface, _configuration, _gcRewardsCache, _configWindow);
|
||||||
_windowSystem.AddWindow(_turnInWindow);
|
_windowSystem.AddWindow(_turnInWindow);
|
||||||
@ -82,14 +84,22 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
|
|||||||
_framework.Update += FrameworkUpdate;
|
_framework.Update += FrameworkUpdate;
|
||||||
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
|
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
|
||||||
_pluginInterface.UiBuilder.OpenConfigUi += _configWindow.Toggle;
|
_pluginInterface.UiBuilder.OpenConfigUi += _configWindow.Toggle;
|
||||||
|
_clientState.Login += Login;
|
||||||
|
_clientState.Logout += Logout;
|
||||||
_commandManager.AddHandler("/deliveroo", new CommandInfo(ProcessCommand)
|
_commandManager.AddHandler("/deliveroo", new CommandInfo(ProcessCommand)
|
||||||
{
|
{
|
||||||
HelpMessage = "Open the configuration"
|
HelpMessage = "Open the configuration"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (_clientState.IsLoggedIn)
|
||||||
|
Login(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Name => "Deliveroo";
|
public string Name => "Deliveroo";
|
||||||
|
|
||||||
|
internal CharacterConfiguration? CharacterConfiguration { get; set; }
|
||||||
|
|
||||||
|
|
||||||
internal Stage CurrentStage
|
internal Stage CurrentStage
|
||||||
{
|
{
|
||||||
get => _currentStageInternal;
|
get => _currentStageInternal;
|
||||||
@ -103,13 +113,53 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void Login(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CharacterConfiguration = CharacterConfiguration.Load(_pluginInterface, _clientState.LocalContentId);
|
||||||
|
if (CharacterConfiguration != null)
|
||||||
|
{
|
||||||
|
if (CharacterConfiguration.CachedPlayerName != _clientState.LocalPlayer!.Name.ToString() ||
|
||||||
|
CharacterConfiguration.CachedWorldName !=
|
||||||
|
_clientState.LocalPlayer.HomeWorld.GameData!.Name.ToString())
|
||||||
|
{
|
||||||
|
CharacterConfiguration.CachedPlayerName = _clientState.LocalPlayer!.Name.ToString();
|
||||||
|
CharacterConfiguration.CachedWorldName =
|
||||||
|
_clientState.LocalPlayer.HomeWorld.GameData!.Name.ToString();
|
||||||
|
|
||||||
|
CharacterConfiguration.Save(_pluginInterface);
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginLog.Information($"Loaded character-specific information for {_clientState.LocalContentId}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PluginLog.Verbose(
|
||||||
|
$"No character-specific information for {_clientState.LocalContentId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PluginLog.Error(ex, "Unable to load character configuration");
|
||||||
|
CharacterConfiguration = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Logout(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
CharacterConfiguration = null;
|
||||||
|
}
|
||||||
|
|
||||||
private unsafe void FrameworkUpdate(Framework f)
|
private unsafe void FrameworkUpdate(Framework f)
|
||||||
{
|
{
|
||||||
_turnInWindow.Error = string.Empty;
|
_turnInWindow.Error = string.Empty;
|
||||||
if (!_clientState.IsLoggedIn || _clientState.TerritoryType is not 128 and not 130 and not 132 ||
|
if (!_clientState.IsLoggedIn ||
|
||||||
|
_clientState.TerritoryType is not 128 and not 130 and not 132 ||
|
||||||
_condition[ConditionFlag.OccupiedInCutSceneEvent] ||
|
_condition[ConditionFlag.OccupiedInCutSceneEvent] ||
|
||||||
GetDistanceToNpc(GetQuartermasterId(), out GameObject? quartermaster) >= 7f ||
|
GetDistanceToNpc(GetQuartermasterId(), out GameObject? quartermaster) >= 7f ||
|
||||||
GetDistanceToNpc(GetPersonnelOfficerId(), out GameObject? personnelOfficer) >= 7f ||
|
GetDistanceToNpc(GetPersonnelOfficerId(), out GameObject? personnelOfficer) >= 7f ||
|
||||||
|
CharacterConfiguration is { DisableForCharacter: true } ||
|
||||||
_configWindow.IsOpen)
|
_configWindow.IsOpen)
|
||||||
{
|
{
|
||||||
_turnInWindow.IsOpen = false;
|
_turnInWindow.IsOpen = false;
|
||||||
@ -241,6 +291,8 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_commandManager.RemoveHandler("/deliveroo");
|
_commandManager.RemoveHandler("/deliveroo");
|
||||||
|
_clientState.Logout -= Logout;
|
||||||
|
_clientState.Login -= Login;
|
||||||
_pluginInterface.UiBuilder.OpenConfigUi -= _configWindow.Toggle;
|
_pluginInterface.UiBuilder.OpenConfigUi -= _configWindow.Toggle;
|
||||||
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
|
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
|
||||||
_framework.Update -= FrameworkUpdate;
|
_framework.Update -= FrameworkUpdate;
|
||||||
|
8
Deliveroo/GameData/ItemFilterType.cs
Normal file
8
Deliveroo/GameData/ItemFilterType.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace Deliveroo.GameData;
|
||||||
|
|
||||||
|
public enum ItemFilterType
|
||||||
|
{
|
||||||
|
ShowAllItems = 0,
|
||||||
|
HideGearSetItems = 1,
|
||||||
|
HideArmouryChestItems = 2,
|
||||||
|
}
|
@ -2,8 +2,11 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using Dalamud.Game.ClientState;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Components;
|
||||||
using Dalamud.Interface.Windowing;
|
using Dalamud.Interface.Windowing;
|
||||||
|
using Dalamud.Logging;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using Deliveroo.GameData;
|
using Deliveroo.GameData;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||||
@ -13,24 +16,24 @@ namespace Deliveroo.Windows;
|
|||||||
|
|
||||||
internal sealed class ConfigWindow : Window
|
internal sealed class ConfigWindow : Window
|
||||||
{
|
{
|
||||||
private static string[] _itemFilterValues = { "Hide Gear Set Items", "Hide Armoury Chest Items" };
|
|
||||||
|
|
||||||
private readonly DalamudPluginInterface _pluginInterface;
|
private readonly DalamudPluginInterface _pluginInterface;
|
||||||
private readonly DeliverooPlugin _plugin;
|
private readonly DeliverooPlugin _plugin;
|
||||||
private readonly Configuration _configuration;
|
private readonly Configuration _configuration;
|
||||||
private readonly GcRewardsCache _gcRewardsCache;
|
private readonly GcRewardsCache _gcRewardsCache;
|
||||||
|
private readonly ClientState _clientState;
|
||||||
|
|
||||||
private readonly Dictionary<uint, GcRewardItem> _itemLookup;
|
private readonly Dictionary<uint, GcRewardItem> _itemLookup;
|
||||||
private uint _dragDropSource = 0;
|
private uint _dragDropSource = 0;
|
||||||
|
|
||||||
public ConfigWindow(DalamudPluginInterface pluginInterface, DeliverooPlugin plugin, Configuration configuration,
|
public ConfigWindow(DalamudPluginInterface pluginInterface, DeliverooPlugin plugin, Configuration configuration,
|
||||||
GcRewardsCache gcRewardsCache)
|
GcRewardsCache gcRewardsCache, ClientState clientState)
|
||||||
: base("Deliveroo - Configuration###DeliverooConfig")
|
: base("Deliveroo - Configuration###DeliverooConfig")
|
||||||
{
|
{
|
||||||
_pluginInterface = pluginInterface;
|
_pluginInterface = pluginInterface;
|
||||||
_plugin = plugin;
|
_plugin = plugin;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_gcRewardsCache = gcRewardsCache;
|
_gcRewardsCache = gcRewardsCache;
|
||||||
|
_clientState = clientState;
|
||||||
|
|
||||||
_itemLookup = _gcRewardsCache.Rewards.Values
|
_itemLookup = _gcRewardsCache.Rewards.Values
|
||||||
.SelectMany(x => x)
|
.SelectMany(x => x)
|
||||||
@ -58,6 +61,7 @@ internal sealed class ConfigWindow : Window
|
|||||||
if (ImGui.BeginTabBar("DeliverooConfigTabs"))
|
if (ImGui.BeginTabBar("DeliverooConfigTabs"))
|
||||||
{
|
{
|
||||||
DrawBuyList();
|
DrawBuyList();
|
||||||
|
DrawCharacterSpecificSettings();
|
||||||
DrawAdditionalSettings();
|
DrawAdditionalSettings();
|
||||||
|
|
||||||
ImGui.EndTabBar();
|
ImGui.EndTabBar();
|
||||||
@ -154,7 +158,86 @@ internal sealed class ConfigWindow : Window
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
int currentItem = 0;
|
int currentItem = 0;
|
||||||
ImGui.Combo("Add Item", ref currentItem, new string[] { "(Not part of a GC)" }, 1);
|
ImGui.Combo("Add Item", ref currentItem, new[] { "(Not part of a GC)" }, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndTabItem();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawCharacterSpecificSettings()
|
||||||
|
{
|
||||||
|
if (ImGui.BeginTabItem("Character Settings"))
|
||||||
|
{
|
||||||
|
if (_clientState is { IsLoggedIn: true, LocalContentId: > 0 })
|
||||||
|
{
|
||||||
|
string currentCharacterName = _clientState.LocalPlayer!.Name.ToString();
|
||||||
|
string currentWorldName = _clientState.LocalPlayer.HomeWorld.GameData!.Name.ToString();
|
||||||
|
ImGui.Text($"Current Character: {currentCharacterName} @ {currentWorldName}");
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
var charConfiguration = _plugin.CharacterConfiguration;
|
||||||
|
if (charConfiguration != null)
|
||||||
|
{
|
||||||
|
|
||||||
|
bool disableForCharacter = charConfiguration.DisableForCharacter;
|
||||||
|
if (ImGui.Checkbox("Disable plugin for this character", ref disableForCharacter))
|
||||||
|
{
|
||||||
|
charConfiguration.DisableForCharacter = disableForCharacter;
|
||||||
|
charConfiguration.Save(_pluginInterface);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.BeginDisabled(charConfiguration.DisableForCharacter);
|
||||||
|
bool useHideArmouryChestItemsFilter = charConfiguration.UseHideArmouryChestItemsFilter;
|
||||||
|
if (ImGui.Checkbox("Use 'Hide Armoury Chest Items' filter", ref useHideArmouryChestItemsFilter))
|
||||||
|
{
|
||||||
|
charConfiguration.UseHideArmouryChestItemsFilter = useHideArmouryChestItemsFilter;
|
||||||
|
charConfiguration.Save(_pluginInterface);
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("The default filter for all characters is 'Hide Gear Set Items', but you may want to override this to hide all Armoury Chest items (regardless of whether they're part of a gear set) e.g. for your main character.");
|
||||||
|
|
||||||
|
ImGui.EndDisabled();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
ImGui.BeginDisabled(!ImGui.GetIO().KeyCtrl);
|
||||||
|
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.PersonCircleMinus,
|
||||||
|
"Remove character-specific settings"))
|
||||||
|
{
|
||||||
|
charConfiguration.Delete(_pluginInterface);
|
||||||
|
_plugin.CharacterConfiguration = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndDisabled();
|
||||||
|
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled) && !ImGui.GetIO().KeyCtrl)
|
||||||
|
ImGui.SetTooltip(
|
||||||
|
$"Hold CTRL to remove the configuration for {currentCharacterName} (non-reversible).");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// no settings
|
||||||
|
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.PersonCirclePlus,
|
||||||
|
"Enable character-specific settings"))
|
||||||
|
{
|
||||||
|
_plugin.CharacterConfiguration = new()
|
||||||
|
{
|
||||||
|
LocalContentId = _clientState.LocalContentId,
|
||||||
|
CachedPlayerName = currentCharacterName,
|
||||||
|
CachedWorldName = currentWorldName,
|
||||||
|
};
|
||||||
|
_plugin.CharacterConfiguration.Save(_pluginInterface);
|
||||||
|
PluginLog.Information(
|
||||||
|
$"Created character-specific configuration for {_clientState.LocalContentId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.Text("You are not currently logged in.");
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.EndTabItem();
|
ImGui.EndTabItem();
|
||||||
@ -173,14 +256,6 @@ internal sealed class ConfigWindow : Window
|
|||||||
Save();
|
Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.Separator();
|
|
||||||
int selectedItemFilter = Math.Max(0, (int)_configuration.ItemFilter - 1);
|
|
||||||
if (ImGui.Combo("Item Filter", ref selectedItemFilter, _itemFilterValues, _itemFilterValues.Length))
|
|
||||||
{
|
|
||||||
_configuration.ItemFilter = (Configuration.ItemFilterType)(selectedItemFilter + 1);
|
|
||||||
Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.EndTabItem();
|
ImGui.EndTabItem();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user