refactor: pull layouts into a dictionary
This commit is contained in:
parent
c793cfc613
commit
7226458785
|
@ -20,14 +20,23 @@ namespace HudSwap {
|
|||
|
||||
public Guid defaultLayout = Guid.Empty;
|
||||
|
||||
[Obsolete("Individual layout fields are deprecated; use StatusLayouts instead")]
|
||||
public Guid combatLayout = Guid.Empty;
|
||||
[Obsolete("Individual layout fields are deprecated; use StatusLayouts instead")]
|
||||
public Guid weaponDrawnLayout = Guid.Empty;
|
||||
[Obsolete("Individual layout fields are deprecated; use StatusLayouts instead")]
|
||||
public Guid instanceLayout = Guid.Empty;
|
||||
[Obsolete("Individual layout fields are deprecated; use StatusLayouts instead")]
|
||||
public Guid craftingLayout = Guid.Empty;
|
||||
[Obsolete("Individual layout fields are deprecated; use StatusLayouts instead")]
|
||||
public Guid gatheringLayout = Guid.Empty;
|
||||
[Obsolete("Individual layout fields are deprecated; use StatusLayouts instead")]
|
||||
public Guid fishingLayout = Guid.Empty;
|
||||
[Obsolete("Individual layout fields are deprecated; use StatusLayouts instead")]
|
||||
public Guid roleplayingLayout = Guid.Empty;
|
||||
|
||||
public Dictionary<Status, Guid> StatusLayouts { get; set; } = new Dictionary<Status, Guid>();
|
||||
|
||||
public Dictionary<string, Guid> JobLayouts { get; set; } = new Dictionary<string, Guid>();
|
||||
public bool HighPriorityJobs { get; set; } = false;
|
||||
public bool JobsCombatOnly { get; set; } = false;
|
||||
|
@ -36,10 +45,51 @@ namespace HudSwap {
|
|||
|
||||
public void Initialize(DalamudPluginInterface pluginInterface) {
|
||||
this.pi = pluginInterface;
|
||||
this.Migrate();
|
||||
this.Save();
|
||||
}
|
||||
|
||||
public void Save() {
|
||||
this.pi.SavePluginConfig(this);
|
||||
}
|
||||
|
||||
private void Migrate() {
|
||||
#pragma warning disable 618
|
||||
if (this.combatLayout != Guid.Empty) {
|
||||
this.StatusLayouts[Status.InCombat] = this.combatLayout;
|
||||
this.combatLayout = Guid.Empty;
|
||||
}
|
||||
|
||||
if (this.weaponDrawnLayout != Guid.Empty) {
|
||||
this.StatusLayouts[Status.WeaponDrawn] = this.weaponDrawnLayout;
|
||||
this.weaponDrawnLayout = Guid.Empty;
|
||||
}
|
||||
|
||||
if (this.instanceLayout != Guid.Empty) {
|
||||
this.StatusLayouts[Status.InInstance] = this.instanceLayout;
|
||||
this.instanceLayout = Guid.Empty;
|
||||
}
|
||||
|
||||
if (this.craftingLayout != Guid.Empty) {
|
||||
this.StatusLayouts[Status.Crafting] = this.craftingLayout;
|
||||
this.craftingLayout = Guid.Empty;
|
||||
}
|
||||
|
||||
if (this.gatheringLayout != Guid.Empty) {
|
||||
this.StatusLayouts[Status.Gathering] = this.gatheringLayout;
|
||||
this.gatheringLayout = Guid.Empty;
|
||||
}
|
||||
|
||||
if (this.fishingLayout != Guid.Empty) {
|
||||
this.StatusLayouts[Status.Fishing] = this.fishingLayout;
|
||||
this.fishingLayout = Guid.Empty;
|
||||
}
|
||||
|
||||
if (this.roleplayingLayout != Guid.Empty) {
|
||||
this.StatusLayouts[Status.Roleplaying] = this.roleplayingLayout;
|
||||
this.roleplayingLayout = Guid.Empty;
|
||||
}
|
||||
#pragma warning restore 618
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,9 +64,10 @@
|
|||
<Compile Include="Plugin.cs" />
|
||||
<Compile Include="PluginUI.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Statuses.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="HudSwap.json" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
</Project>
|
|
@ -1,5 +1,4 @@
|
|||
using Dalamud.Game.ClientState;
|
||||
using Dalamud.Game.ClientState.Actors.Types;
|
||||
using Dalamud.Game.ClientState.Actors.Types;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
@ -7,7 +6,6 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// TODO: Zone swaps?
|
||||
|
||||
|
@ -192,17 +190,23 @@ namespace HudSwap {
|
|||
if (ImGui.BeginChild("##layout-selections", new Vector2(0, 125))) {
|
||||
ImGui.Columns(2);
|
||||
|
||||
List<float> textSizes = new List<float>();
|
||||
float maxSize = 0f;
|
||||
|
||||
this.LayoutBox("In combat", ref this.plugin.config.combatLayout, player, textSizes);
|
||||
this.LayoutBox("Weapon drawn", ref this.plugin.config.weaponDrawnLayout, player, textSizes);
|
||||
this.LayoutBox("In instance", ref this.plugin.config.instanceLayout, player, textSizes);
|
||||
this.LayoutBox("Crafting", ref this.plugin.config.craftingLayout, player, textSizes);
|
||||
this.LayoutBox("Gathering", ref this.plugin.config.gatheringLayout, player, textSizes);
|
||||
this.LayoutBox("Fishing", ref this.plugin.config.fishingLayout, player, textSizes);
|
||||
this.LayoutBox("Roleplaying", ref this.plugin.config.roleplayingLayout, player, textSizes);
|
||||
foreach (Status status in Statuses.ORDER.Reverse()) {
|
||||
maxSize = Math.Max(maxSize, ImGui.CalcTextSize(status.Name()).X);
|
||||
|
||||
ImGui.SetColumnWidth(0, textSizes.Max() + ImGui.GetStyle().ItemSpacing.X * 2);
|
||||
this.plugin.config.StatusLayouts.TryGetValue(status, out Guid layout);
|
||||
|
||||
if (this.LayoutBox(status.Name(), layout, out Guid newLayout)) {
|
||||
this.plugin.config.StatusLayouts[status] = newLayout;
|
||||
this.plugin.config.Save();
|
||||
if (this.plugin.config.SwapsEnabled) {
|
||||
this.statuses.SetHudLayout(player, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SetColumnWidth(0, maxSize + ImGui.GetStyle().ItemSpacing.X * 2);
|
||||
|
||||
ImGui.Columns(1);
|
||||
ImGui.EndChild();
|
||||
|
@ -217,21 +221,19 @@ namespace HudSwap {
|
|||
if (ImGui.BeginChild("##job-layout-selections", new Vector2(0, 125))) {
|
||||
ImGui.Columns(2);
|
||||
|
||||
List<float> textSizes = new List<float>();
|
||||
float maxSize = 0f;
|
||||
|
||||
var acceptableJobs = this.pi.Data.GetExcelSheet<ClassJob>()
|
||||
.Where(job => job.NameEnglish != "Adventurer")
|
||||
.Where(job => this.jobFilter.Length == 0 || (job.NameEnglish.ToLower().Contains(this.jobFilter) || job.Abbreviation.ToLower().Contains(this.jobFilter)));
|
||||
|
||||
foreach (ClassJob job in acceptableJobs) {
|
||||
maxSize = Math.Max(maxSize, ImGui.CalcTextSize(job.NameEnglish).X);
|
||||
|
||||
this.plugin.config.JobLayouts.TryGetValue(job.Abbreviation, out Guid layout);
|
||||
|
||||
Guid oldLayout = layout;
|
||||
|
||||
this.LayoutBox(job.NameEnglish, ref layout, player, textSizes);
|
||||
|
||||
if (oldLayout != layout) {
|
||||
this.plugin.config.JobLayouts[job.Abbreviation] = layout;
|
||||
if (this.LayoutBox(job.NameEnglish, layout, out Guid newLayout)) {
|
||||
this.plugin.config.JobLayouts[job.Abbreviation] = newLayout;
|
||||
this.plugin.config.Save();
|
||||
if (this.plugin.config.SwapsEnabled) {
|
||||
this.statuses.SetHudLayout(player, true);
|
||||
|
@ -239,7 +241,7 @@ namespace HudSwap {
|
|||
}
|
||||
}
|
||||
|
||||
ImGui.SetColumnWidth(0, textSizes.DefaultIfEmpty(0).Max() + ImGui.GetStyle().ItemSpacing.X * 2);
|
||||
ImGui.SetColumnWidth(0, maxSize + ImGui.GetStyle().ItemSpacing.X * 2);
|
||||
|
||||
ImGui.Columns(1);
|
||||
ImGui.EndChild();
|
||||
|
@ -314,31 +316,27 @@ namespace HudSwap {
|
|||
}
|
||||
}
|
||||
|
||||
private void LayoutBox(string name, ref Guid layout, PlayerCharacter player, List<float> textSizes) {
|
||||
textSizes.Add(ImGui.CalcTextSize(name).X);
|
||||
private bool LayoutBox(string name, Guid currentLayout, out Guid newLayout) {
|
||||
newLayout = Guid.Empty;
|
||||
bool updated = false;
|
||||
ImGui.Text(name);
|
||||
ImGui.NextColumn();
|
||||
if (ImGui.BeginCombo($"##{name}-layout", this.LayoutNameOrDefault(layout))) {
|
||||
if (ImGui.BeginCombo($"##{name}-layout", this.LayoutNameOrDefault(currentLayout))) {
|
||||
if (ImGui.Selectable("Not set")) {
|
||||
layout = Guid.Empty;
|
||||
this.plugin.config.Save();
|
||||
if (this.plugin.config.SwapsEnabled) {
|
||||
this.statuses.SetHudLayout(player, true);
|
||||
}
|
||||
updated = true;
|
||||
}
|
||||
ImGui.Separator();
|
||||
foreach (KeyValuePair<Guid, Tuple<string, byte[]>> entry in this.plugin.config.Layouts) {
|
||||
if (ImGui.Selectable(entry.Value.Item1)) {
|
||||
layout = entry.Key;
|
||||
this.plugin.config.Save();
|
||||
if (this.plugin.config.SwapsEnabled) {
|
||||
this.statuses.SetHudLayout(player, true);
|
||||
}
|
||||
updated = true;
|
||||
newLayout = entry.Key;
|
||||
}
|
||||
}
|
||||
ImGui.EndCombo();
|
||||
}
|
||||
ImGui.NextColumn();
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
public void ImportSlot(HudSlot slot, string name, bool save = true) {
|
||||
|
@ -348,148 +346,4 @@ namespace HudSwap {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Statuses {
|
||||
private readonly HudSwapPlugin plugin;
|
||||
private readonly DalamudPluginInterface pi;
|
||||
|
||||
private readonly bool[] condition = new bool[ORDER.Length];
|
||||
private ClassJob job;
|
||||
|
||||
// Order: lowest to highest priority
|
||||
// For conditions that require custom logic, use ConditionFlag.None
|
||||
private static readonly ConditionFlag[] ORDER = {
|
||||
ConditionFlag.RolePlaying,
|
||||
ConditionFlag.Fishing,
|
||||
ConditionFlag.Gathering,
|
||||
ConditionFlag.Crafting,
|
||||
ConditionFlag.BoundByDuty,
|
||||
ConditionFlag.None, // weapon drawn
|
||||
ConditionFlag.InCombat,
|
||||
};
|
||||
|
||||
private delegate bool CustomCondition(HudSwapPlugin plugin, DalamudPluginInterface pi, PlayerCharacter player);
|
||||
|
||||
// Add handlers in the order that ConditionFlag.None flags appear in ORDER.
|
||||
private static readonly CustomCondition[] CUSTOM = {
|
||||
// weapon drawn
|
||||
(plugin, pi, player) => (GetStatus(pi, player) & 4) > 0,
|
||||
};
|
||||
|
||||
protected static byte GetStatus(DalamudPluginInterface pi, Actor actor) {
|
||||
IntPtr statusPtr = pi.TargetModuleScanner.ResolveRelativeAddress(actor.Address, 0x1901);
|
||||
return Marshal.ReadByte(statusPtr);
|
||||
}
|
||||
|
||||
public Statuses(HudSwapPlugin plugin, DalamudPluginInterface pi) {
|
||||
this.plugin = plugin;
|
||||
this.pi = pi;
|
||||
if (ORDER.Length != this.GetLayouts().Length) {
|
||||
throw new ApplicationException("Statuses.ORDER is not the same length as the array returned by Statuses.GetLayouts()");
|
||||
}
|
||||
if (ORDER.Where(flag => flag == ConditionFlag.None).Count() != CUSTOM.Length) {
|
||||
throw new ApplicationException("Statuses.CUSTOM does not have an amount of handlers equalling the amount of ConditionFlag.None in Statuses.ORDER");
|
||||
}
|
||||
}
|
||||
|
||||
private Guid[] GetLayouts() {
|
||||
// These layouts must be in the same order as the flags in ORDER are defined
|
||||
Guid[] layouts = {
|
||||
this.plugin.config.roleplayingLayout,
|
||||
this.plugin.config.fishingLayout,
|
||||
this.plugin.config.gatheringLayout,
|
||||
this.plugin.config.craftingLayout,
|
||||
this.plugin.config.instanceLayout,
|
||||
this.plugin.config.weaponDrawnLayout,
|
||||
this.plugin.config.combatLayout,
|
||||
};
|
||||
return layouts;
|
||||
}
|
||||
|
||||
public bool Update(PlayerCharacter player) {
|
||||
if (player == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int customs = 0;
|
||||
bool[] old = (bool[])this.condition.Clone();
|
||||
Condition condition = this.pi.ClientState.Condition;
|
||||
|
||||
bool anyChanged = false;
|
||||
|
||||
ClassJob currentJob = this.pi.Data.GetExcelSheet<ClassJob>().GetRow(player.ClassJob.Id);
|
||||
if (this.job != null && this.job != currentJob) {
|
||||
anyChanged = true;
|
||||
}
|
||||
this.job = currentJob;
|
||||
|
||||
for (int i = 0; i < ORDER.Length; i++) {
|
||||
ConditionFlag flag = ORDER[i];
|
||||
if (flag == ConditionFlag.None) {
|
||||
this.condition[i] = CUSTOM[customs].Invoke(this.plugin, this.pi, player);
|
||||
customs += 1;
|
||||
} else {
|
||||
this.condition[i] = condition[flag];
|
||||
}
|
||||
anyChanged |= old[i] != this.condition[i];
|
||||
}
|
||||
|
||||
return anyChanged;
|
||||
}
|
||||
|
||||
public Guid CalculateCurrentHud() {
|
||||
PlayerCharacter player = this.pi.ClientState.LocalPlayer;
|
||||
if (player == null) {
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
// get the job layout if there is one and check if jobs are high priority
|
||||
if (this.plugin.config.JobLayouts.TryGetValue(this.job.Abbreviation, out Guid jobLayout) && this.plugin.config.HighPriorityJobs) {
|
||||
return jobLayout;
|
||||
}
|
||||
|
||||
Guid layout = Guid.Empty;
|
||||
Guid[] layouts = this.GetLayouts();
|
||||
|
||||
// check all status conditions and set layout as appropriate
|
||||
for (int i = 0; i < ORDER.Length; i++) {
|
||||
Guid flagLayout = layouts[i];
|
||||
|
||||
if (this.condition[i] && flagLayout != Guid.Empty) {
|
||||
layout = flagLayout;
|
||||
}
|
||||
}
|
||||
|
||||
// if a job layout is set for the current job
|
||||
if (jobLayout != Guid.Empty) {
|
||||
// if jobs are combat only and the player is either in combat or has their weapon drawn, use the job layout
|
||||
if (this.plugin.config.JobsCombatOnly && (this.condition[5] || this.condition[6])) {
|
||||
layout = jobLayout;
|
||||
}
|
||||
|
||||
// if the layout was going to be default, use job layout unless jobs are not combat only
|
||||
if (!this.plugin.config.JobsCombatOnly && layout == Guid.Empty) {
|
||||
layout = jobLayout;
|
||||
}
|
||||
}
|
||||
|
||||
return layout == Guid.Empty ? this.plugin.config.defaultLayout : layout;
|
||||
}
|
||||
|
||||
public void SetHudLayout(PlayerCharacter player, bool update = false) {
|
||||
if (update && player != null) {
|
||||
this.Update(player);
|
||||
}
|
||||
|
||||
Guid layout = this.CalculateCurrentHud();
|
||||
if (layout == Guid.Empty) {
|
||||
return; // FIXME: do something better
|
||||
}
|
||||
if (!this.plugin.config.Layouts.TryGetValue(layout, out Tuple<string, byte[]> entry)) {
|
||||
return; // FIXME: do something better
|
||||
}
|
||||
this.plugin.hud.WriteLayout(this.plugin.config.StagingSlot, entry.Item2);
|
||||
this.plugin.hud.SelectSlot(this.plugin.config.StagingSlot, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
using Dalamud.Game.ClientState;
|
||||
using Dalamud.Game.ClientState.Actors.Types;
|
||||
using Dalamud.Plugin;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// TODO: Zone swaps?
|
||||
|
||||
namespace HudSwap {
|
||||
public class Statuses {
|
||||
public static Status[] ORDER = {
|
||||
Status.Roleplaying,
|
||||
Status.Fishing,
|
||||
Status.Gathering,
|
||||
Status.Crafting,
|
||||
Status.InInstance,
|
||||
Status.WeaponDrawn,
|
||||
Status.InCombat,
|
||||
};
|
||||
|
||||
private readonly HudSwapPlugin plugin;
|
||||
private readonly DalamudPluginInterface pi;
|
||||
|
||||
private readonly bool[] condition = new bool[ORDER.Length];
|
||||
private ClassJob job;
|
||||
|
||||
internal static byte GetStatus(DalamudPluginInterface pi, Actor actor) {
|
||||
IntPtr statusPtr = pi.TargetModuleScanner.ResolveRelativeAddress(actor.Address, 0x1901);
|
||||
return Marshal.ReadByte(statusPtr);
|
||||
}
|
||||
|
||||
public Statuses(HudSwapPlugin plugin, DalamudPluginInterface pi) {
|
||||
this.plugin = plugin;
|
||||
this.pi = pi;
|
||||
}
|
||||
|
||||
public bool Update(PlayerCharacter player) {
|
||||
if (player == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool[] old = (bool[])this.condition.Clone();
|
||||
|
||||
bool anyChanged = false;
|
||||
|
||||
ClassJob currentJob = this.pi.Data.GetExcelSheet<ClassJob>().GetRow(player.ClassJob.Id);
|
||||
if (this.job != null && this.job != currentJob) {
|
||||
anyChanged = true;
|
||||
}
|
||||
this.job = currentJob;
|
||||
|
||||
for (int i = 0; i < ORDER.Length; i++) {
|
||||
Status status = ORDER[i];
|
||||
this.condition[i] = status.Active(player, this.pi);
|
||||
anyChanged |= old[i] != this.condition[i];
|
||||
}
|
||||
|
||||
return anyChanged;
|
||||
}
|
||||
|
||||
public Guid CalculateCurrentHud() {
|
||||
PlayerCharacter player = this.pi.ClientState.LocalPlayer;
|
||||
if (player == null) {
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
// get the job layout if there is one and check if jobs are high priority
|
||||
if (this.plugin.config.JobLayouts.TryGetValue(this.job.Abbreviation, out Guid jobLayout) && this.plugin.config.HighPriorityJobs) {
|
||||
return jobLayout;
|
||||
}
|
||||
|
||||
Guid layout = Guid.Empty;
|
||||
|
||||
// check all status conditions and set layout as appropriate
|
||||
for (int i = 0; i < ORDER.Length; i++) {
|
||||
if (!this.condition[i]) {
|
||||
continue;
|
||||
}
|
||||
Status status = ORDER[i];
|
||||
if (this.plugin.config.StatusLayouts.TryGetValue(status, out Guid statusLayout)) {
|
||||
layout = statusLayout;
|
||||
}
|
||||
}
|
||||
|
||||
// if a job layout is set for the current job
|
||||
if (jobLayout != Guid.Empty) {
|
||||
// if jobs are combat only and the player is either in combat or has their weapon drawn, use the job layout
|
||||
if (this.plugin.config.JobsCombatOnly && (this.condition[5] || this.condition[6])) {
|
||||
layout = jobLayout;
|
||||
}
|
||||
|
||||
// if the layout was going to be default, use job layout unless jobs are not combat only
|
||||
if (!this.plugin.config.JobsCombatOnly && layout == Guid.Empty) {
|
||||
layout = jobLayout;
|
||||
}
|
||||
}
|
||||
|
||||
return layout == Guid.Empty ? this.plugin.config.defaultLayout : layout;
|
||||
}
|
||||
|
||||
public void SetHudLayout(PlayerCharacter player, bool update = false) {
|
||||
if (update && player != null) {
|
||||
this.Update(player);
|
||||
}
|
||||
|
||||
Guid layout = this.CalculateCurrentHud();
|
||||
if (layout == Guid.Empty) {
|
||||
return; // FIXME: do something better
|
||||
}
|
||||
if (!this.plugin.config.Layouts.TryGetValue(layout, out Tuple<string, byte[]> entry)) {
|
||||
return; // FIXME: do something better
|
||||
}
|
||||
this.plugin.hud.WriteLayout(this.plugin.config.StagingSlot, entry.Item2);
|
||||
this.plugin.hud.SelectSlot(this.plugin.config.StagingSlot, true);
|
||||
}
|
||||
}
|
||||
|
||||
public enum Status {
|
||||
InCombat = ConditionFlag.InCombat,
|
||||
WeaponDrawn = ConditionFlag.None,
|
||||
InInstance = ConditionFlag.BoundByDuty,
|
||||
Crafting = ConditionFlag.Crafting,
|
||||
Gathering = ConditionFlag.Gathering,
|
||||
Fishing = ConditionFlag.Fishing,
|
||||
Roleplaying = ConditionFlag.RolePlaying,
|
||||
}
|
||||
|
||||
public static class StatusExtensions {
|
||||
public static string Name(this Status status) {
|
||||
switch (status) {
|
||||
case Status.InCombat:
|
||||
return "In combat";
|
||||
case Status.WeaponDrawn:
|
||||
return "Weapon drawn";
|
||||
case Status.InInstance:
|
||||
return "In instance";
|
||||
case Status.Crafting:
|
||||
return "Crafting";
|
||||
case Status.Gathering:
|
||||
return "Gathering";
|
||||
case Status.Fishing:
|
||||
return "Fishing";
|
||||
case Status.Roleplaying:
|
||||
return "Roleplaying";
|
||||
}
|
||||
|
||||
throw new ApplicationException($"No name was set up for {status}");
|
||||
}
|
||||
|
||||
public static bool Active(this Status status, PlayerCharacter player, DalamudPluginInterface pi) {
|
||||
ConditionFlag flag = (ConditionFlag)status;
|
||||
if (flag != ConditionFlag.None) {
|
||||
return pi.ClientState.Condition[flag];
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case Status.WeaponDrawn:
|
||||
return (Statuses.GetStatus(pi, player) & 4) > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue