mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2026-02-19 22:17:44 +01:00
Add more mod association and modded utility.
This commit is contained in:
parent
ab2a3f5bd9
commit
d56c2db547
9 changed files with 277 additions and 90 deletions
|
|
@ -17,7 +17,6 @@ using OtterGui;
|
|||
using OtterGui.Classes;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Text;
|
||||
using OtterGuiInternal.Structs;
|
||||
using Penumbra.GameData.Enums;
|
||||
using static Glamourer.Gui.Tabs.HeaderDrawer;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace Glamourer.Gui.Tabs.DesignTab;
|
|||
|
||||
public class ModAssociationsTab(PenumbraService penumbra, DesignFileSystemSelector selector, DesignManager manager, Configuration config)
|
||||
{
|
||||
private readonly ModCombo _modCombo = new(penumbra, Glamourer.Log);
|
||||
private readonly ModCombo _modCombo = new(penumbra, Glamourer.Log, selector);
|
||||
private (Mod, ModSettings)[]? _copy;
|
||||
|
||||
public void Draw()
|
||||
|
|
|
|||
|
|
@ -4,19 +4,18 @@ using ImGuiNET;
|
|||
using OtterGui.Classes;
|
||||
using OtterGui.Log;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Text;
|
||||
using OtterGui.Widgets;
|
||||
|
||||
namespace Glamourer.Gui.Tabs.DesignTab;
|
||||
|
||||
public sealed class ModCombo : FilterComboCache<(Mod Mod, ModSettings Settings)>
|
||||
public sealed class ModCombo : FilterComboCache<(Mod Mod, ModSettings Settings, int Count)>
|
||||
{
|
||||
public ModCombo(PenumbraService penumbra, Logger log)
|
||||
: base(penumbra.GetMods, MouseWheelType.None, log)
|
||||
{
|
||||
SearchByParts = false;
|
||||
}
|
||||
public ModCombo(PenumbraService penumbra, Logger log, DesignFileSystemSelector selector)
|
||||
: base(() => penumbra.GetMods(selector.Selected?.FilteredItemNames.ToArray() ?? []), MouseWheelType.None, log)
|
||||
=> SearchByParts = false;
|
||||
|
||||
protected override string ToString((Mod Mod, ModSettings Settings) obj)
|
||||
protected override string ToString((Mod Mod, ModSettings Settings, int Count) obj)
|
||||
=> obj.Mod.Name;
|
||||
|
||||
protected override bool IsVisible(int globalIndex, LowerString filter)
|
||||
|
|
@ -24,36 +23,45 @@ public sealed class ModCombo : FilterComboCache<(Mod Mod, ModSettings Settings)>
|
|||
|
||||
protected override bool DrawSelectable(int globalIdx, bool selected)
|
||||
{
|
||||
using var id = ImRaii.PushId(globalIdx);
|
||||
var (mod, settings) = Items[globalIdx];
|
||||
using var id = ImUtf8.PushId(globalIdx);
|
||||
var (mod, settings, count) = Items[globalIdx];
|
||||
bool ret;
|
||||
using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !settings.Enabled))
|
||||
var color = settings.Enabled
|
||||
? count > 0
|
||||
? ColorId.ContainsItemsEnabled.Value()
|
||||
: ImGui.GetColorU32(ImGuiCol.Text)
|
||||
: count > 0
|
||||
? ColorId.ContainsItemsDisabled.Value()
|
||||
: ImGui.GetColorU32(ImGuiCol.TextDisabled);
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, color))
|
||||
{
|
||||
ret = ImGui.Selectable(mod.Name, selected);
|
||||
ret = ImUtf8.Selectable(mod.Name, selected);
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
using var style = ImRaii.PushStyle(ImGuiStyleVar.PopupBorderSize, 2 * ImGuiHelpers.GlobalScale);
|
||||
using var tt = ImRaii.Tooltip();
|
||||
using var tt = ImUtf8.Tooltip();
|
||||
var namesDifferent = mod.Name != mod.DirectoryName;
|
||||
ImGui.Dummy(new Vector2(300 * ImGuiHelpers.GlobalScale, 0));
|
||||
using (var group = ImRaii.Group())
|
||||
using (ImUtf8.Group())
|
||||
{
|
||||
if (namesDifferent)
|
||||
ImGui.TextUnformatted("Directory Name");
|
||||
ImGui.TextUnformatted("Enabled");
|
||||
ImGui.TextUnformatted("Priority");
|
||||
ImUtf8.Text("Directory Name"u8);
|
||||
ImUtf8.Text("Enabled"u8);
|
||||
ImUtf8.Text("Priority"u8);
|
||||
ImUtf8.Text("Affected Design Items"u8);
|
||||
DrawSettingsLeft(settings);
|
||||
}
|
||||
|
||||
ImGui.SameLine(Math.Max(ImGui.GetItemRectSize().X + 3 * ImGui.GetStyle().ItemSpacing.X, 150 * ImGuiHelpers.GlobalScale));
|
||||
using (var group = ImRaii.Group())
|
||||
using (ImUtf8.Group())
|
||||
{
|
||||
if (namesDifferent)
|
||||
ImGui.TextUnformatted(mod.DirectoryName);
|
||||
ImGui.TextUnformatted(settings.Enabled.ToString());
|
||||
ImGui.TextUnformatted(settings.Priority.ToString());
|
||||
ImUtf8.Text(mod.DirectoryName);
|
||||
ImUtf8.Text($"{settings.Enabled}");
|
||||
ImUtf8.Text($"{settings.Priority}");
|
||||
ImUtf8.Text($"{count}");
|
||||
DrawSettingsRight(settings);
|
||||
}
|
||||
}
|
||||
|
|
@ -65,7 +73,7 @@ public sealed class ModCombo : FilterComboCache<(Mod Mod, ModSettings Settings)>
|
|||
{
|
||||
foreach (var setting in settings.Settings)
|
||||
{
|
||||
ImGui.TextUnformatted(setting.Key);
|
||||
ImUtf8.Text(setting.Key);
|
||||
for (var i = 1; i < setting.Value.Count; ++i)
|
||||
ImGui.NewLine();
|
||||
}
|
||||
|
|
@ -76,10 +84,10 @@ public sealed class ModCombo : FilterComboCache<(Mod Mod, ModSettings Settings)>
|
|||
foreach (var setting in settings.Settings)
|
||||
{
|
||||
if (setting.Value.Count == 0)
|
||||
ImGui.TextUnformatted("<None Enabled>");
|
||||
ImUtf8.Text("<None Enabled>"u8);
|
||||
else
|
||||
foreach (var option in setting.Value)
|
||||
ImGui.TextUnformatted(option);
|
||||
ImUtf8.Text(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,15 @@ using ImGuiNET;
|
|||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Text;
|
||||
using static Glamourer.Gui.Tabs.HeaderDrawer;
|
||||
|
||||
namespace Glamourer.Gui.Tabs.DesignTab;
|
||||
|
||||
public class MultiDesignPanel(DesignFileSystemSelector selector, DesignManager editor, DesignColors colors)
|
||||
public class MultiDesignPanel(DesignFileSystemSelector selector, DesignManager editor, DesignColors colors, Configuration config)
|
||||
{
|
||||
private readonly Button[] _leftButtons = [];
|
||||
private readonly Button[] _rightButtons = [new IncognitoButton(config.Ephemeral)];
|
||||
|
||||
private readonly DesignColorCombo _colorCombo = new(colors, true);
|
||||
|
||||
public void Draw()
|
||||
|
|
@ -17,8 +21,12 @@ public class MultiDesignPanel(DesignFileSystemSelector selector, DesignManager e
|
|||
if (selector.SelectedPaths.Count == 0)
|
||||
return;
|
||||
|
||||
var width = ImGuiHelpers.ScaledVector2(145, 0);
|
||||
ImGui.NewLine();
|
||||
HeaderDrawer.Draw(string.Empty, 0, ImGui.GetColorU32(ImGuiCol.FrameBg), _leftButtons, _rightButtons);
|
||||
using var child = ImUtf8.Child("##MultiPanel"u8, default, true);
|
||||
if (!child)
|
||||
return;
|
||||
|
||||
var width = ImGuiHelpers.ScaledVector2(145, 0);
|
||||
var treeNodePos = ImGui.GetCursorPos();
|
||||
_numDesigns = DrawDesignList();
|
||||
DrawCounts(treeNodePos);
|
||||
|
|
@ -135,7 +143,7 @@ public class MultiDesignPanel(DesignFileSystemSelector selector, DesignManager e
|
|||
{
|
||||
ImUtf8.TextFrameAligned("Multi Tagger:"u8);
|
||||
ImGui.SameLine();
|
||||
var offset = ImGui.GetItemRectSize().X;
|
||||
var offset = ImGui.GetItemRectSize().X + ImGui.GetStyle().WindowPadding.X;
|
||||
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 2 * (width.X + ImGui.GetStyle().ItemSpacing.X));
|
||||
ImUtf8.InputText("##tag"u8, ref _tag, "Tag Name..."u8);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Glamourer.Designs;
|
||||
using Glamourer.GameData;
|
||||
using Glamourer.Interop;
|
||||
using Glamourer.Interop.Penumbra;
|
||||
using Glamourer.Services;
|
||||
using Glamourer.Unlocks;
|
||||
using ImGuiNET;
|
||||
|
|
@ -23,7 +23,8 @@ public class UnlockOverview(
|
|||
TextureService textures,
|
||||
CodeService codes,
|
||||
JobService jobs,
|
||||
FavoriteManager favorites)
|
||||
FavoriteManager favorites,
|
||||
PenumbraService penumbra)
|
||||
{
|
||||
private static readonly Vector4 UnavailableTint = new(0.3f, 0.3f, 0.3f, 1.0f);
|
||||
|
||||
|
|
@ -32,6 +33,9 @@ public class UnlockOverview(
|
|||
private Gender _selected3 = Gender.Unknown;
|
||||
private BonusItemFlag _selected4 = BonusItemFlag.Unknown;
|
||||
|
||||
private uint _favoriteColor;
|
||||
private uint _moddedColor;
|
||||
|
||||
private void DrawSelector()
|
||||
{
|
||||
using var child = ImRaii.Child("Selector", new Vector2(200 * ImGuiHelpers.GlobalScale, -1), true);
|
||||
|
|
@ -90,6 +94,9 @@ public class UnlockOverview(
|
|||
if (!child)
|
||||
return;
|
||||
|
||||
_moddedColor = ColorId.ModdedItemMarker.Value();
|
||||
_favoriteColor = ColorId.FavoriteStarOn.Value();
|
||||
|
||||
if (_selected1 is not FullEquipType.Unknown)
|
||||
DrawItems();
|
||||
else if (_selected2 is not SubRace.Unknown && _selected3 is not Gender.Unknown)
|
||||
|
|
@ -120,7 +127,7 @@ public class UnlockOverview(
|
|||
unlocked || codes.Enabled(CodeService.CodeFlag.Shirts) ? Vector4.One : UnavailableTint);
|
||||
|
||||
if (favorites.Contains(_selected3, _selected2, customize.Index, customize.Value))
|
||||
ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), ColorId.FavoriteStarOn.Value(),
|
||||
ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), _favoriteColor,
|
||||
12 * ImGuiHelpers.GlobalScale, ImDrawFlags.RoundCornersAll, 6 * ImGuiHelpers.GlobalScale);
|
||||
|
||||
if (hasIcon && ImGui.IsItemHovered())
|
||||
|
|
@ -192,9 +199,11 @@ public class UnlockOverview(
|
|||
ImGui.Image(icon, iconSize, Vector2.Zero, Vector2.One,
|
||||
unlocked || codes.Enabled(CodeService.CodeFlag.Shirts) ? Vector4.One : UnavailableTint);
|
||||
if (favorites.Contains(item))
|
||||
ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), ColorId.FavoriteStarOn.Value(),
|
||||
ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), _favoriteColor,
|
||||
2 * ImGuiHelpers.GlobalScale, ImDrawFlags.RoundCornersAll, 4 * ImGuiHelpers.GlobalScale);
|
||||
|
||||
var mods = DrawModdedMarker(item, iconSize);
|
||||
|
||||
// TODO handle clicking
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
|
|
@ -206,9 +215,10 @@ public class UnlockOverview(
|
|||
ImUtf8.Text($"{item.Id.Id}");
|
||||
ImUtf8.Text($"{item.PrimaryId.Id}-{item.Variant.Id}");
|
||||
// TODO
|
||||
ImUtf8.Text("Always Unlocked"); // : $"Unlocked on {time:g}" : "Not Unlocked.");
|
||||
ImUtf8.Text("Always Unlocked"u8); // : $"Unlocked on {time:g}" : "Not Unlocked.");
|
||||
// TODO
|
||||
//tooltip.CreateTooltip(item, string.Empty, false);
|
||||
DrawModTooltip(mods);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -263,6 +273,8 @@ public class UnlockOverview(
|
|||
ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), ColorId.FavoriteStarOn.Value(),
|
||||
2 * ImGuiHelpers.GlobalScale, ImDrawFlags.RoundCornersAll, 4 * ImGuiHelpers.GlobalScale);
|
||||
|
||||
var mods = DrawModdedMarker(item, iconSize);
|
||||
|
||||
if (ImGui.IsItemClicked())
|
||||
Glamourer.Messager.Chat.Print(new SeStringBuilder().AddItemLink(item.ItemId.Id, false).BuiltString);
|
||||
|
||||
|
|
@ -306,6 +318,7 @@ public class UnlockOverview(
|
|||
ImGui.TextUnformatted("Tradable");
|
||||
if (item.Flags.HasFlag(ItemFlags.IsCrestWorthy))
|
||||
ImGui.TextUnformatted("Can apply Crest");
|
||||
DrawModTooltip(mods);
|
||||
tooltip.CreateTooltip(item, string.Empty, false);
|
||||
}
|
||||
}
|
||||
|
|
@ -316,4 +329,36 @@ public class UnlockOverview(
|
|||
|
||||
private static int IconsPerRow(float iconWidth, float iconSpacing)
|
||||
=> (int)(ImGui.GetContentRegionAvail().X / (iconWidth + iconSpacing));
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
|
||||
private (string ModDirectory, string ModName)[] DrawModdedMarker(in EquipItem item, Vector2 iconSize)
|
||||
{
|
||||
var mods = penumbra.CheckCurrentChangedItem(item.Name);
|
||||
if (mods.Length == 0)
|
||||
return mods;
|
||||
|
||||
var center = ImGui.GetItemRectMin() + new Vector2(iconSize.X * 0.85f, iconSize.Y * 0.15f);
|
||||
ImGui.GetWindowDrawList().AddCircleFilled(center, iconSize.X * 0.1f, _moddedColor);
|
||||
ImGui.GetWindowDrawList().AddCircle(center, iconSize.X * 0.1f, 0xFF000000);
|
||||
return mods;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
|
||||
private void DrawModTooltip((string ModDirectory, string ModName)[] mods)
|
||||
{
|
||||
switch (mods.Length)
|
||||
{
|
||||
case 0: return;
|
||||
case 1:
|
||||
ImUtf8.Text("Modded by: "u8, _moddedColor);
|
||||
ImGui.SameLine(0, 0);
|
||||
ImUtf8.Text(mods[0].ModName);
|
||||
return;
|
||||
default:
|
||||
ImUtf8.Text("Modded by:"u8, _moddedColor);
|
||||
foreach (var (_, mod) in mods)
|
||||
ImUtf8.BulletText(mod);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Dalamud.Interface;
|
|||
using Dalamud.Interface.Utility;
|
||||
using Glamourer.Events;
|
||||
using Glamourer.Interop;
|
||||
using Glamourer.Interop.Penumbra;
|
||||
using Glamourer.Services;
|
||||
using Glamourer.Unlocks;
|
||||
using ImGuiNET;
|
||||
|
|
@ -17,12 +18,16 @@ namespace Glamourer.Gui.Tabs.UnlocksTab;
|
|||
|
||||
public class UnlockTable : Table<EquipItem>, IDisposable
|
||||
{
|
||||
private readonly ObjectUnlocked _event;
|
||||
private readonly ObjectUnlocked _event;
|
||||
private readonly PenumbraService _penumbra;
|
||||
|
||||
private Guid _lastCurrentCollection = Guid.Empty;
|
||||
|
||||
public UnlockTable(ItemManager items, TextureService textures, ItemUnlockManager itemUnlocks,
|
||||
PenumbraChangedItemTooltip tooltip, ObjectUnlocked @event, JobService jobs, FavoriteManager favorites)
|
||||
PenumbraChangedItemTooltip tooltip, ObjectUnlocked @event, JobService jobs, FavoriteManager favorites, PenumbraService penumbra)
|
||||
: base("ItemUnlockTable", new ItemList(items),
|
||||
new FavoriteColumn(favorites, @event) { Label = "F" },
|
||||
new ModdedColumn(penumbra) { Label = "M" },
|
||||
new NameColumn(textures, tooltip) { Label = "Item Name..." },
|
||||
new SlotColumn { Label = "Equip Slot" },
|
||||
new TypeColumn { Label = "Item Type..." },
|
||||
|
|
@ -36,14 +41,40 @@ public class UnlockTable : Table<EquipItem>, IDisposable
|
|||
new TradableColumn { Label = "Trade" }
|
||||
)
|
||||
{
|
||||
_event = @event;
|
||||
Sortable = true;
|
||||
Flags |= ImGuiTableFlags.Hideable | ImGuiTableFlags.Reorderable | ImGuiTableFlags.Resizable;
|
||||
_event = @event;
|
||||
_penumbra = penumbra;
|
||||
Sortable = true;
|
||||
Flags |= ImGuiTableFlags.Hideable | ImGuiTableFlags.Reorderable | ImGuiTableFlags.Resizable;
|
||||
_event.Subscribe(OnObjectUnlock, ObjectUnlocked.Priority.UnlockTable);
|
||||
_penumbra.ModSettingChanged += OnModSettingsChanged;
|
||||
|
||||
}
|
||||
|
||||
private void OnModSettingsChanged(Penumbra.Api.Enums.ModSettingChange type, Guid collection, string mod, bool inherited)
|
||||
{
|
||||
if (collection != _lastCurrentCollection)
|
||||
return;
|
||||
|
||||
FilterDirty = true;
|
||||
SortDirty = true;
|
||||
}
|
||||
|
||||
protected override void PreDraw()
|
||||
{
|
||||
var lastCurrentCollection = _penumbra.CurrentCollection.Id;
|
||||
if (_lastCurrentCollection != lastCurrentCollection)
|
||||
{
|
||||
_lastCurrentCollection = lastCurrentCollection;
|
||||
FilterDirty = true;
|
||||
SortDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
=> _event.Unsubscribe(OnObjectUnlock);
|
||||
{
|
||||
_event.Unsubscribe(OnObjectUnlock);
|
||||
_penumbra.ModSettingChanged -= OnModSettingsChanged;
|
||||
}
|
||||
|
||||
private sealed class FavoriteColumn : YesNoColumn<EquipItem>
|
||||
{
|
||||
|
|
@ -77,6 +108,66 @@ public class UnlockTable : Table<EquipItem>, IDisposable
|
|||
=> _favorites.Contains(rhs).CompareTo(_favorites.Contains(lhs));
|
||||
}
|
||||
|
||||
private sealed class ModdedColumn : YesNoColumn<EquipItem>
|
||||
{
|
||||
public override float Width
|
||||
=> ImGui.GetFrameHeightWithSpacing();
|
||||
|
||||
private readonly PenumbraService _penumbra;
|
||||
private readonly Dictionary<CustomItemId, int> _compareCache = [];
|
||||
|
||||
public ModdedColumn(PenumbraService penumbra)
|
||||
{
|
||||
_penumbra = penumbra;
|
||||
Flags |= ImGuiTableColumnFlags.NoResize;
|
||||
}
|
||||
|
||||
public override void PostSort()
|
||||
{
|
||||
_compareCache.Clear();
|
||||
}
|
||||
|
||||
public override void DrawColumn(EquipItem item, int idx)
|
||||
{
|
||||
var value = _penumbra.CheckCurrentChangedItem(item.Name);
|
||||
if (value.Length == 0)
|
||||
return;
|
||||
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
{
|
||||
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ModdedItemMarker.Value());
|
||||
ImGuiUtil.Center(FontAwesomeIcon.Circle.ToIconString());
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
using var tt = ImUtf8.Tooltip();
|
||||
foreach (var (_, mod) in value)
|
||||
ImUtf8.BulletText(mod);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool FilterFunc(EquipItem item)
|
||||
=> FilterValue.HasFlag(_penumbra.CheckCurrentChangedItem(item.Name).Length > 0 ? YesNoFlag.Yes : YesNoFlag.No);
|
||||
|
||||
public override int Compare(EquipItem lhs, EquipItem rhs)
|
||||
{
|
||||
if (!_compareCache.TryGetValue(lhs.Id, out var lhsCount))
|
||||
{
|
||||
lhsCount = _penumbra.CheckCurrentChangedItem(lhs.Name).Length;
|
||||
_compareCache[lhs.Id] = lhsCount;
|
||||
}
|
||||
|
||||
if (!_compareCache.TryGetValue(rhs.Id, out var rhsCount))
|
||||
{
|
||||
rhsCount = _penumbra.CheckCurrentChangedItem(rhs.Name).Length;
|
||||
_compareCache[rhs.Id] = rhsCount;
|
||||
}
|
||||
|
||||
return lhsCount.CompareTo(rhsCount);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NameColumn : ColumnString<EquipItem>
|
||||
{
|
||||
private readonly TextureService _textures;
|
||||
|
|
@ -317,7 +408,6 @@ public class UnlockTable : Table<EquipItem>, IDisposable
|
|||
{ }
|
||||
}
|
||||
|
||||
|
||||
private sealed class JobColumn : ColumnFlags<JobFlag, EquipItem>
|
||||
{
|
||||
public override float Width
|
||||
|
|
@ -415,7 +505,6 @@ public class UnlockTable : Table<EquipItem>, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private sealed class DyableColumn : ColumnFlags<DyableColumn.Dyable, EquipItem>
|
||||
{
|
||||
[Flags]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue