A lot of work on flower shop

This commit is contained in:
Kwoth 2017-04-06 21:00:46 +02:00
parent 6efd78ca21
commit 936ba264c9
11 changed files with 2207 additions and 88 deletions

View File

@ -0,0 +1,128 @@
using NadekoBot.Services.Database.Models;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace NadekoBot.DataStructures
{
public class IndexedCollection<T> : IList<T> where T : IIndexed
{
public List<T> Source { get; }
private readonly object _locker = new object();
public IndexedCollection(IEnumerable<T> source)
{
lock (_locker)
{
Source = source.OrderBy(x => x.Index).ToList();
for (var i = 0; i < Source.Count; i++)
{
if (Source[i].Index != i)
Source[i].Index = i;
}
}
}
public static implicit operator List<T>(IndexedCollection<T> x) =>
x.Source;
public IEnumerator<T> GetEnumerator() =>
Source.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() =>
Source.GetEnumerator();
public void Add(T item)
{
lock (_locker)
{
item.Index = Source.Count;
Source.Add(item);
}
}
public virtual void Clear()
{
lock (_locker)
{
Source.Clear();
}
}
public bool Contains(T item)
{
lock (_locker)
{
return Source.Contains(item);
}
}
public void CopyTo(T[] array, int arrayIndex)
{
lock (_locker)
{
Source.CopyTo(array, arrayIndex);
}
}
public virtual bool Remove(T item)
{
bool removed;
lock (_locker)
{
if (removed = Source.Remove(item))
{
for (int i = 0; i < Source.Count; i++)
{
// hm, no idea how ef works, so I don't want to set if it's not changed,
// maybe it will try to update db?
// But most likely it just compares old to new values, meh.
if (Source[i].Index != i)
Source[i].Index = i;
}
}
}
return removed;
}
public int Count => Source.Count;
public bool IsReadOnly => false;
public int IndexOf(T item) => item.Index;
public virtual void Insert(int index, T item)
{
lock (_locker)
{
Source.Insert(index, item);
for (int i = index; i < Source.Count; i++)
{
Source[i].Index = i;
}
}
}
public virtual void RemoveAt(int index)
{
lock (_locker)
{
Source.RemoveAt(index);
for (int i = index; i < Source.Count; i++)
{
Source[i].Index = i;
}
}
}
public virtual T this[int index] {
get { return Source[index]; }
set {
lock (_locker)
{
value.Index = index;
Source[index] = value;
}
}
}
}
}

View File

@ -6,132 +6,67 @@ using NadekoBot.Services.Database.Models;
namespace NadekoBot.DataStructures
{
public class PermissionsCollection<T> : IList<T> where T : IIndexed
public class PermissionsCollection<T> : IndexedCollection<T> where T : IIndexed
{
public List<T> Source { get; }
private readonly object _locker = new object();
public PermissionsCollection(IEnumerable<T> source)
private readonly object _localLocker = new object();
public PermissionsCollection(IEnumerable<T> source) : base(source)
{
lock (_locker)
{
Source = source.OrderBy(x => x.Index).ToList();
for (var i = 0; i < Source.Count; i++)
{
if(Source[i].Index != i)
Source[i].Index = i;
}
}
}
public static implicit operator List<T>(PermissionsCollection<T> x) =>
x.Source;
public IEnumerator<T> GetEnumerator() =>
Source.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() =>
Source.GetEnumerator();
public void Add(T item)
public override void Clear()
{
lock (_locker)
{
item.Index = Source.Count;
Source.Add(item);
}
}
public void Clear()
{
lock (_locker)
lock (_localLocker)
{
var first = Source[0];
Source.Clear();
base.Clear();
Source[0] = first;
}
}
public bool Contains(T item)
{
lock (_locker)
{
return Source.Contains(item);
}
}
public void CopyTo(T[] array, int arrayIndex)
{
lock (_locker)
{
Source.CopyTo(array, arrayIndex);
}
}
public bool Remove(T item)
public override bool Remove(T item)
{
bool removed;
lock (_locker)
lock (_localLocker)
{
if(Source.IndexOf(item) == 0)
throw new ArgumentException("You can't remove first permsission (allow all)");
if (removed = Source.Remove(item))
{
for (int i = 0; i < Source.Count; i++)
{
// hm, no idea how ef works, so I don't want to set if it's not changed,
// maybe it will try to update db?
// But most likely it just compares old to new values, meh.
if (Source[i].Index != i)
Source[i].Index = i;
}
}
removed = base.Remove(item);
}
return removed;
}
public int Count => Source.Count;
public bool IsReadOnly => false;
public int IndexOf(T item) => item.Index;
public void Insert(int index, T item)
public override void Insert(int index, T item)
{
lock (_locker)
lock (_localLocker)
{
if(index == 0) // can't insert on first place. Last item is always allow all.
throw new IndexOutOfRangeException(nameof(index));
Source.Insert(index, item);
for (int i = index; i < Source.Count; i++)
{
Source[i].Index = i;
}
base.Insert(index, item);
}
}
public void RemoveAt(int index)
public override void RemoveAt(int index)
{
lock (_locker)
lock (_localLocker)
{
if(index == 0) // you can't remove first permission (allow all)
throw new IndexOutOfRangeException(nameof(index));
Source.RemoveAt(index);
for (int i = index; i < Source.Count; i++)
{
Source[i].Index = i;
}
base.RemoveAt(index);
}
}
public T this[int index] {
public override T this[int index] {
get { return Source[index]; }
set {
lock (_locker)
lock (_localLocker)
{
if(index == 0) // can't set first element. It's always allow all
throw new IndexOutOfRangeException(nameof(index));
value.Index = index;
Source[index] = value;
base[index] = value;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
namespace NadekoBot.Migrations
{
public partial class flowershop : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ShopEntry",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
AuthorId = table.Column<ulong>(nullable: false),
DateAdded = table.Column<DateTime>(nullable: true),
GuildConfigId = table.Column<int>(nullable: true),
Index = table.Column<int>(nullable: false),
Name = table.Column<string>(nullable: true),
Price = table.Column<int>(nullable: false),
RoleId = table.Column<ulong>(nullable: false),
RoleName = table.Column<string>(nullable: true),
Type = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ShopEntry", x => x.Id);
table.ForeignKey(
name: "FK_ShopEntry_GuildConfigs_GuildConfigId",
column: x => x.GuildConfigId,
principalTable: "GuildConfigs",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "ShopEntryItem",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
DateAdded = table.Column<DateTime>(nullable: true),
ShopEntryId = table.Column<int>(nullable: true),
Text = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ShopEntryItem", x => x.Id);
table.ForeignKey(
name: "FK_ShopEntryItem_ShopEntry_ShopEntryId",
column: x => x.ShopEntryId,
principalTable: "ShopEntry",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_ShopEntry_GuildConfigId",
table: "ShopEntry",
column: "GuildConfigId");
migrationBuilder.CreateIndex(
name: "IX_ShopEntryItem_ShopEntryId",
table: "ShopEntryItem",
column: "ShopEntryId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ShopEntryItem");
migrationBuilder.DropTable(
name: "ShopEntry");
}
}
}

View File

@ -971,6 +971,54 @@ namespace NadekoBot.Migrations
b.ToTable("SelfAssignableRoles");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<ulong>("AuthorId");
b.Property<DateTime?>("DateAdded");
b.Property<int?>("GuildConfigId");
b.Property<int>("Index");
b.Property<string>("Name");
b.Property<int>("Price");
b.Property<ulong>("RoleId");
b.Property<string>("RoleName");
b.Property<int>("Type");
b.HasKey("Id");
b.HasIndex("GuildConfigId");
b.ToTable("ShopEntry");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime?>("DateAdded");
b.Property<int?>("ShopEntryId");
b.Property<string>("Text");
b.HasKey("Id");
b.HasIndex("ShopEntryId");
b.ToTable("ShopEntryItem");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b =>
{
b.Property<int>("Id")
@ -1377,6 +1425,20 @@ namespace NadekoBot.Migrations
.HasForeignKey("BotConfigId");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig")
.WithMany("ShopEntries")
.HasForeignKey("GuildConfigId");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.ShopEntry")
.WithMany("Items")
.HasForeignKey("ShopEntryId");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig")

View File

@ -0,0 +1,170 @@
using Discord;
using Discord.Commands;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Attributes;
using NadekoBot.DataStructures;
using NadekoBot.Extensions;
using NadekoBot.Services;
using NadekoBot.Services.Database;
using NadekoBot.Services.Database.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling
{
[Group]
public class FlowerShop : NadekoSubmodule
{
public enum Role {
Role
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Shop(int page = 1)
{
if (page <= 0)
return;
page -= 1;
List<ShopEntry> entries;
using (var uow = DbHandler.UnitOfWork())
{
entries = new IndexedCollection<ShopEntry>(uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.ShopEntries)).ShopEntries);
}
await Context.Channel.SendPaginatedConfirmAsync(page + 1, (curPage) =>
{
var theseEntries = entries.Skip((curPage - 1) * 9).Take(9);
if (!theseEntries.Any())
return new EmbedBuilder().WithErrorColor()
.WithDescription(GetText("shop_none"));
var embed = new EmbedBuilder().WithOkColor()
.WithTitle(GetText("shop", NadekoBot.BotConfig.CurrencySign));
for (int i = 0; i < entries.Count; i++)
{
var entry = entries[i];
embed.AddField(efb => efb.WithName($"#{i + 1} - {entry.Price}{NadekoBot.BotConfig.CurrencySign}").WithValue(EntryToString(entry)).WithIsInline(true));
}
return embed;
}, entries.Count / 9, true);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Buy(int entryNumber)
{
var channel = (ITextChannel)Context.Channel;
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[RequireUserPermission(GuildPermission.Administrator)]
public async Task ShopAdd(ShopEntryType type, int price, string name)
{
var entry = new ShopEntry()
{
Name = name,
Price = price,
Type = type,
AuthorId = Context.User.Id,
};
using (var uow = DbHandler.UnitOfWork())
{
var entries = new IndexedCollection<ShopEntry>(uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.ShopEntries)).ShopEntries);
entries.Add(entry);
uow.GuildConfigs.For(Context.Guild.Id, set => set).ShopEntries = entries;
uow.Complete();
}
await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithTitle(GetText("shop_item_add"))
.AddField(efb => efb.WithName(GetText("name")).WithValue(name).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("price")).WithValue(price.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("type")).WithValue(type.ToString()).WithIsInline(true)));
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[RequireUserPermission(GuildPermission.Administrator)]
public async Task ShopAdd(Role _, int price, [Remainder] IRole role)
{
var entry = new ShopEntry()
{
Name = "-",
Price = price,
Type = ShopEntryType.Role,
AuthorId = Context.User.Id,
RoleId = role.Id,
RoleName = role.Name
};
using (var uow = DbHandler.UnitOfWork())
{
var entries = new IndexedCollection<ShopEntry>(uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.ShopEntries)).ShopEntries);
entries.Add(entry);
uow.GuildConfigs.For(Context.Guild.Id, set => set).ShopEntries = entries;
uow.Complete();
}
await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithTitle(GetText("shop_item_add"))
.AddField(efb => efb.WithName(GetText("name")).WithValue(GetText("shop_role", Format.Bold(entry.RoleName))).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("price")).WithValue(price.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("type")).WithValue("Role").WithIsInline(true)));
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task ShopRemove(int index)
{
if (index < 0)
return;
ShopEntry removed;
using (var uow = DbHandler.UnitOfWork())
{
var config = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.ShopEntries));
var entries = new IndexedCollection<ShopEntry>(config.ShopEntries);
removed = entries.ElementAtOrDefault(index);
if (removed != null)
{
entries.Remove(removed);
config.ShopEntries = entries;
uow.Complete();
}
}
if(removed == null)
await ReplyErrorLocalized("shop_rem_fail").ConfigureAwait(false);
else
await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithTitle(GetText("shop_item_add"))
.AddField(efb => efb.WithName(GetText("name")).WithValue(GetText("shop_role", Format.Bold(removed.RoleName))).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("price")).WithValue(removed.Price.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("type")).WithValue(removed.Type.ToString()).WithIsInline(true)));
}
public string EntryToString(ShopEntry entry)
{
if (entry.Type == ShopEntryType.Role)
{
return Format.Bold(entry.Name) + "\n" + GetText("shop_role", Format.Bold(entry.RoleName));
}
else if (entry.Type == ShopEntryType.List)
{
}
else if (entry.Type == ShopEntryType.Infinite_List)
{
}
return "";
}
}
}
}

View File

@ -1716,7 +1716,7 @@ namespace NadekoBot.Resources {
}
/// <summary>
/// Looks up a localized string similar to Claim patreon rewards. If you&apos;re subscribed to bot owner&apos;s patreon you can user this command to claim your rewards - assuming bot owner did setup has their patreon key..
/// Looks up a localized string similar to Claim patreon rewards. If you&apos;re subscribed to bot owner&apos;s patreon you can use this command to claim your rewards - assuming bot owner did setup has their patreon key..
/// </summary>
public static string claimpatreonrewards_desc {
get {
@ -2103,7 +2103,7 @@ namespace NadekoBot.Resources {
}
/// <summary>
/// Looks up a localized string similar to `{0}crad 44`.
/// Looks up a localized string similar to `{0}crdm 44`.
/// </summary>
public static string crdm_usage {
get {
@ -4173,7 +4173,7 @@ namespace NadekoBot.Resources {
}
/// <summary>
/// Looks up a localized string similar to `{0}liqu` or `{0}liqu 3`.
/// Looks up a localized string similar to Lists all quotes on the server ordered alphabetically. 15 Per page..
/// </summary>
public static string listquotes_desc {
get {
@ -4182,7 +4182,7 @@ namespace NadekoBot.Resources {
}
/// <summary>
/// Looks up a localized string similar to Lists all quotes on the server ordered alphabetically. 15 Per page..
/// Looks up a localized string similar to `{0}liqu` or `{0}liqu 3`.
/// </summary>
public static string listquotes_usage {
get {
@ -5297,6 +5297,33 @@ namespace NadekoBot.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to parewrel.
/// </summary>
public static string patreonrewardsreload_cmd {
get {
return ResourceManager.GetString("patreonrewardsreload_cmd", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Forces the update of the list of patrons who are eligible for the reward..
/// </summary>
public static string patreonrewardsreload_desc {
get {
return ResourceManager.GetString("patreonrewardsreload_desc", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to `{0}parewrel`.
/// </summary>
public static string patreonrewardsreload_usage {
get {
return ResourceManager.GetString("patreonrewardsreload_usage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to pause p.
/// </summary>
@ -7484,6 +7511,78 @@ namespace NadekoBot.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to shop.
/// </summary>
public static string shop_cmd {
get {
return ResourceManager.GetString("shop_cmd", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Lists this server&apos;s administrators&apos; shop. Paginated..
/// </summary>
public static string shop_desc {
get {
return ResourceManager.GetString("shop_desc", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to `{0}shop` or `{0}shop 2`.
/// </summary>
public static string shop_usage {
get {
return ResourceManager.GetString("shop_usage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to shopadd.
/// </summary>
public static string shopadd_cmd {
get {
return ResourceManager.GetString("shopadd_cmd", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Adds an item to the shop by specifying type price and name..
/// </summary>
public static string shopadd_desc {
get {
return ResourceManager.GetString("shopadd_desc", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to `{0}shopadd role 1000 Rich`.
/// </summary>
public static string shopadd_usage {
get {
return ResourceManager.GetString("shopadd_usage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to shoprem shoprm.
/// </summary>
public static string shopremove_cmd {
get {
return ResourceManager.GetString("shopremove_cmd", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Removes an item from the shop by its color..
/// </summary>
public static string shopremove_desc {
get {
return ResourceManager.GetString("shopremove_desc", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to shorten.
/// </summary>

View File

@ -2755,6 +2755,15 @@ namespace NadekoBot.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Name.
/// </summary>
public static string gambling_name {
get {
return ResourceManager.GetString("gambling_name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No more cards in the deck..
/// </summary>
@ -2854,6 +2863,42 @@ namespace NadekoBot.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Shop.
/// </summary>
public static string gambling_shop {
get {
return ResourceManager.GetString("gambling_shop", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Shop item added.
/// </summary>
public static string gambling_shop_item_add {
get {
return ResourceManager.GetString("gambling_shop_item_add", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No shop items found on this page..
/// </summary>
public static string gambling_shop_none {
get {
return ResourceManager.GetString("gambling_shop_none", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You will get {0} role..
/// </summary>
public static string gambling_shop_role {
get {
return ResourceManager.GetString("gambling_shop_role", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bet.
/// </summary>
@ -2972,6 +3017,15 @@ namespace NadekoBot.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Type.
/// </summary>
public static string gambling_type {
get {
return ResourceManager.GetString("gambling_type", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to your affinity is already set to that waifu or you&apos;re trying to remove your affinity while not having one..
/// </summary>
@ -6115,6 +6169,15 @@ namespace NadekoBot.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Next update in {0}.
/// </summary>
public static string utility_clpa_next_update {
get {
return ResourceManager.GetString("utility_clpa_next_update", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You&apos;ve received {0} Thanks for supporting the project!.
/// </summary>

View File

@ -2406,4 +2406,26 @@ Owner ID: {2}</value>
<value>Time in {0} is {1} - {2}</value>
<comment>Time in London, UK is 15:30 - Time Zone Name</comment>
</data>
<data name="gambling_name" xml:space="preserve">
<value>Name</value>
</data>
<data name="gambling_shop" xml:space="preserve">
<value>Shop</value>
</data>
<data name="gambling_shop_item_add" xml:space="preserve">
<value>Shop item added</value>
</data>
<data name="gambling_shop_none" xml:space="preserve">
<value>No shop items found on this page.</value>
</data>
<data name="gambling_shop_role" xml:space="preserve">
<value>You will get {0} role.</value>
</data>
<data name="gambling_type" xml:space="preserve">
<value>Type</value>
</data>
<data name="utility_clpa_next_update" xml:space="preserve">
<value>Next update in {0}</value>
<comment>Next update in 05:30</comment>
</data>
</root>

View File

@ -76,6 +76,8 @@ namespace NadekoBot.Services.Database.Models
public HashSet<SlowmodeIgnoredUser> SlowmodeIgnoredUsers { get; set; }
public HashSet<SlowmodeIgnoredRole> SlowmodeIgnoredRoles { get; set; }
public List<ShopEntry> ShopEntries { get; set; }
//public List<ProtectionIgnoredChannel> ProtectionIgnoredChannels { get; set; } = new List<ProtectionIgnoredChannel>();
}

View File

@ -0,0 +1,41 @@
using System.Collections.Generic;
namespace NadekoBot.Services.Database.Models
{
public enum ShopEntryType
{
Role,
List,
Infinite_List,
}
public class ShopEntry : DbEntity, IIndexed
{
public int Index { get; set; }
public int Price { get; set; }
public string Name { get; set; }
public ulong AuthorId { get; set; }
public ShopEntryType Type { get; set; }
public string RoleName { get; set; }
public ulong RoleId { get; set; }
public List<ShopEntryItem> Items { get; set; }
}
public class ShopEntryItem : DbEntity
{
public string Text { get; set; }
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
{
return false;
}
return ((ShopEntryItem)obj).Text == Text;
}
public override int GetHashCode() =>
Text.GetHashCode();
}
}