Uplink store interface searchable with a searchbar. (#24287)
* Can now search the uplink store interface with a searchbar. * Search text updates no longer send server messages. Persists listings locally. * Formatting fixes and tidying. * Added helper method to get localised name and description (or otherwise, entity name and description) of store listing items. * Update Content.Client/Store/Ui/StoreMenu.xaml * Review change; moved localisation helper functions to their own class. * Prevent thread-unsafe behaviour as-per review. * Remove dummy boxcontainer --------- Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
This commit is contained in:
@@ -1,22 +1,27 @@
|
|||||||
using Content.Shared.Store;
|
using Content.Shared.Store;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Client.GameObjects;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using Robust.Shared.Prototypes;
|
||||||
using Serilog;
|
|
||||||
using Timer = Robust.Shared.Timing.Timer;
|
|
||||||
|
|
||||||
namespace Content.Client.Store.Ui;
|
namespace Content.Client.Store.Ui;
|
||||||
|
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
public sealed class StoreBoundUserInterface : BoundUserInterface
|
public sealed class StoreBoundUserInterface : BoundUserInterface
|
||||||
{
|
{
|
||||||
|
private IPrototypeManager _prototypeManager = default!;
|
||||||
|
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
private StoreMenu? _menu;
|
private StoreMenu? _menu;
|
||||||
|
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
private string _windowName = Loc.GetString("store-ui-default-title");
|
private string _windowName = Loc.GetString("store-ui-default-title");
|
||||||
|
|
||||||
|
[ViewVariables]
|
||||||
|
private string _search = "";
|
||||||
|
|
||||||
|
[ViewVariables]
|
||||||
|
private HashSet<ListingData> _listings = new();
|
||||||
|
|
||||||
public StoreBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
public StoreBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -49,6 +54,12 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
|
|||||||
SendMessage(new StoreRequestUpdateInterfaceMessage());
|
SendMessage(new StoreRequestUpdateInterfaceMessage());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_menu.SearchTextUpdated += (_, search) =>
|
||||||
|
{
|
||||||
|
_search = search.Trim().ToLowerInvariant();
|
||||||
|
UpdateListingsWithSearchFilter();
|
||||||
|
};
|
||||||
|
|
||||||
_menu.OnRefundAttempt += (_) =>
|
_menu.OnRefundAttempt += (_) =>
|
||||||
{
|
{
|
||||||
SendMessage(new StoreRequestRefundMessage());
|
SendMessage(new StoreRequestRefundMessage());
|
||||||
@@ -64,10 +75,10 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
|
|||||||
switch (state)
|
switch (state)
|
||||||
{
|
{
|
||||||
case StoreUpdateState msg:
|
case StoreUpdateState msg:
|
||||||
_menu.UpdateBalance(msg.Balance);
|
_listings = msg.Listings;
|
||||||
_menu.PopulateStoreCategoryButtons(msg.Listings);
|
|
||||||
|
|
||||||
_menu.UpdateListing(msg.Listings.ToList());
|
_menu.UpdateBalance(msg.Balance);
|
||||||
|
UpdateListingsWithSearchFilter();
|
||||||
_menu.SetFooterVisibility(msg.ShowFooter);
|
_menu.SetFooterVisibility(msg.ShowFooter);
|
||||||
_menu.UpdateRefund(msg.AllowRefund);
|
_menu.UpdateRefund(msg.AllowRefund);
|
||||||
break;
|
break;
|
||||||
@@ -89,4 +100,19 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
|
|||||||
_menu?.Close();
|
_menu?.Close();
|
||||||
_menu?.Dispose();
|
_menu?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateListingsWithSearchFilter()
|
||||||
|
{
|
||||||
|
if (_menu == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var filteredListings = new HashSet<ListingData>(_listings);
|
||||||
|
if (!string.IsNullOrEmpty(_search))
|
||||||
|
{
|
||||||
|
filteredListings.RemoveWhere(listingData => !ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listingData, _prototypeManager).Trim().ToLowerInvariant().Contains(_search) &&
|
||||||
|
!ListingLocalisationHelpers.GetLocalisedDescriptionOrEntityDescription(listingData, _prototypeManager).Trim().ToLowerInvariant().Contains(_search));
|
||||||
|
}
|
||||||
|
_menu.PopulateStoreCategoryButtons(filteredListings);
|
||||||
|
_menu.UpdateListing(filteredListings.ToList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
Text="Refund" />
|
Text="Refund" />
|
||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
<PanelContainer VerticalExpand="True">
|
<LineEdit Name="SearchBar" Margin="4" PlaceHolder="Search" HorizontalExpand="True"/>
|
||||||
|
<PanelContainer VerticalExpand="True">
|
||||||
<PanelContainer.PanelOverride>
|
<PanelContainer.PanelOverride>
|
||||||
<gfx:StyleBoxFlat BackgroundColor="#000000FF" />
|
<gfx:StyleBoxFlat BackgroundColor="#000000FF" />
|
||||||
</PanelContainer.PanelOverride>
|
</PanelContainer.PanelOverride>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
|
||||||
using Content.Client.Actions;
|
using Content.Client.Actions;
|
||||||
using Content.Client.GameTicking.Managers;
|
using Content.Client.GameTicking.Managers;
|
||||||
using Content.Client.Message;
|
using Content.Client.Message;
|
||||||
@@ -27,6 +26,7 @@ public sealed partial class StoreMenu : DefaultWindow
|
|||||||
|
|
||||||
private StoreWithdrawWindow? _withdrawWindow;
|
private StoreWithdrawWindow? _withdrawWindow;
|
||||||
|
|
||||||
|
public event EventHandler<string>? SearchTextUpdated;
|
||||||
public event Action<BaseButton.ButtonEventArgs, ListingData>? OnListingButtonPressed;
|
public event Action<BaseButton.ButtonEventArgs, ListingData>? OnListingButtonPressed;
|
||||||
public event Action<BaseButton.ButtonEventArgs, string>? OnCategoryButtonPressed;
|
public event Action<BaseButton.ButtonEventArgs, string>? OnCategoryButtonPressed;
|
||||||
public event Action<BaseButton.ButtonEventArgs, string, int>? OnWithdrawAttempt;
|
public event Action<BaseButton.ButtonEventArgs, string, int>? OnWithdrawAttempt;
|
||||||
@@ -46,6 +46,7 @@ public sealed partial class StoreMenu : DefaultWindow
|
|||||||
WithdrawButton.OnButtonDown += OnWithdrawButtonDown;
|
WithdrawButton.OnButtonDown += OnWithdrawButtonDown;
|
||||||
RefreshButton.OnButtonDown += OnRefreshButtonDown;
|
RefreshButton.OnButtonDown += OnRefreshButtonDown;
|
||||||
RefundButton.OnButtonDown += OnRefundButtonDown;
|
RefundButton.OnButtonDown += OnRefundButtonDown;
|
||||||
|
SearchBar.OnTextChanged += _ => SearchTextUpdated?.Invoke(this, SearchBar.Text);
|
||||||
|
|
||||||
if (Window != null)
|
if (Window != null)
|
||||||
Window.Title = name;
|
Window.Title = name;
|
||||||
@@ -59,7 +60,7 @@ public sealed partial class StoreMenu : DefaultWindow
|
|||||||
(type.Key, type.Value), type => _prototypeManager.Index<CurrencyPrototype>(type.Key));
|
(type.Key, type.Value), type => _prototypeManager.Index<CurrencyPrototype>(type.Key));
|
||||||
|
|
||||||
var balanceStr = string.Empty;
|
var balanceStr = string.Empty;
|
||||||
foreach (var ((type, amount),proto) in currency)
|
foreach (var ((_, amount), proto) in currency)
|
||||||
{
|
{
|
||||||
balanceStr += Loc.GetString("store-ui-balance-display", ("amount", amount),
|
balanceStr += Loc.GetString("store-ui-balance-display", ("amount", amount),
|
||||||
("currency", Loc.GetString(proto.DisplayName, ("amount", 1))));
|
("currency", Loc.GetString(proto.DisplayName, ("amount", 1))));
|
||||||
@@ -81,7 +82,6 @@ public sealed partial class StoreMenu : DefaultWindow
|
|||||||
{
|
{
|
||||||
var sorted = listings.OrderBy(l => l.Priority).ThenBy(l => l.Cost.Values.Sum());
|
var sorted = listings.OrderBy(l => l.Priority).ThenBy(l => l.Cost.Values.Sum());
|
||||||
|
|
||||||
|
|
||||||
// should probably chunk these out instead. to-do if this clogs the internet tubes.
|
// should probably chunk these out instead. to-do if this clogs the internet tubes.
|
||||||
// maybe read clients prototypes instead?
|
// maybe read clients prototypes instead?
|
||||||
ClearListings();
|
ClearListings();
|
||||||
@@ -129,8 +129,8 @@ public sealed partial class StoreMenu : DefaultWindow
|
|||||||
if (!listing.Categories.Contains(CurrentCategory))
|
if (!listing.Categories.Contains(CurrentCategory))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var listingName = Loc.GetString(listing.Name);
|
var listingName = ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listing, _prototypeManager);
|
||||||
var listingDesc = Loc.GetString(listing.Description);
|
var listingDesc = ListingLocalisationHelpers.GetLocalisedDescriptionOrEntityDescription(listing, _prototypeManager);
|
||||||
var listingPrice = listing.Cost;
|
var listingPrice = listing.Cost;
|
||||||
var canBuy = CanBuyListing(Balance, listingPrice);
|
var canBuy = CanBuyListing(Balance, listingPrice);
|
||||||
|
|
||||||
@@ -144,12 +144,6 @@ public sealed partial class StoreMenu : DefaultWindow
|
|||||||
{
|
{
|
||||||
if (texture == null)
|
if (texture == null)
|
||||||
texture = spriteSys.GetPrototypeIcon(listing.ProductEntity).Default;
|
texture = spriteSys.GetPrototypeIcon(listing.ProductEntity).Default;
|
||||||
|
|
||||||
var proto = _prototypeManager.Index<EntityPrototype>(listing.ProductEntity);
|
|
||||||
if (listingName == string.Empty)
|
|
||||||
listingName = proto.Name;
|
|
||||||
if (listingDesc == string.Empty)
|
|
||||||
listingDesc = proto.Description;
|
|
||||||
}
|
}
|
||||||
else if (listing.ProductAction != null)
|
else if (listing.ProductAction != null)
|
||||||
{
|
{
|
||||||
@@ -243,13 +237,16 @@ public sealed partial class StoreMenu : DefaultWindow
|
|||||||
|
|
||||||
allCategories = allCategories.OrderBy(c => c.Priority).ToList();
|
allCategories = allCategories.OrderBy(c => c.Priority).ToList();
|
||||||
|
|
||||||
|
// This will reset the Current Category selection if nothing matches the search.
|
||||||
|
if (allCategories.All(category => category.ID != CurrentCategory))
|
||||||
|
CurrentCategory = string.Empty;
|
||||||
|
|
||||||
if (CurrentCategory == string.Empty && allCategories.Count > 0)
|
if (CurrentCategory == string.Empty && allCategories.Count > 0)
|
||||||
CurrentCategory = allCategories.First().ID;
|
CurrentCategory = allCategories.First().ID;
|
||||||
|
|
||||||
if (allCategories.Count <= 1)
|
|
||||||
return;
|
|
||||||
|
|
||||||
CategoryListContainer.Children.Clear();
|
CategoryListContainer.Children.Clear();
|
||||||
|
if (allCategories.Count < 1)
|
||||||
|
return;
|
||||||
|
|
||||||
foreach (var proto in allCategories)
|
foreach (var proto in allCategories)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ using Content.Shared.UserInterface;
|
|||||||
using Robust.Server.GameObjects;
|
using Robust.Server.GameObjects;
|
||||||
using Robust.Shared.Audio.Systems;
|
using Robust.Shared.Audio.Systems;
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
namespace Content.Server.Store.Systems;
|
namespace Content.Server.Store.Systems;
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ public sealed partial class StoreSystem
|
|||||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||||
[Dependency] private readonly StackSystem _stack = default!;
|
[Dependency] private readonly StackSystem _stack = default!;
|
||||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||||
|
|
||||||
private void InitializeUi()
|
private void InitializeUi()
|
||||||
{
|
{
|
||||||
@@ -259,7 +261,7 @@ public sealed partial class StoreSystem
|
|||||||
|
|
||||||
//log dat shit.
|
//log dat shit.
|
||||||
_admin.Add(LogType.StorePurchase, LogImpact.Low,
|
_admin.Add(LogType.StorePurchase, LogImpact.Low,
|
||||||
$"{ToPrettyString(buyer):player} purchased listing \"{Loc.GetString(listing.Name)}\" from {ToPrettyString(uid)}");
|
$"{ToPrettyString(buyer):player} purchased listing \"{ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listing, _prototypeManager)}\" from {ToPrettyString(uid)}");
|
||||||
|
|
||||||
listing.PurchaseAmount++; //track how many times something has been purchased
|
listing.PurchaseAmount++; //track how many times something has been purchased
|
||||||
_audio.PlayEntity(component.BuySuccessSound, msg.Session, uid); //cha-ching!
|
_audio.PlayEntity(component.BuySuccessSound, msg.Session, uid); //cha-ching!
|
||||||
|
|||||||
42
Content.Shared/Store/ListingLocalisationHelpers.cs
Normal file
42
Content.Shared/Store/ListingLocalisationHelpers.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
|
namespace Content.Shared.Store;
|
||||||
|
|
||||||
|
public static class ListingLocalisationHelpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ListingData's Name field can be either a localisation string or the actual entity's name.
|
||||||
|
/// This function gets a localised name from the localisation string if it exists, and if not, it gets the entity's name.
|
||||||
|
/// If neither a localised string exists, or an associated entity name, it will return the value of the "Name" field.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetLocalisedNameOrEntityName(ListingData listingData, IPrototypeManager prototypeManager)
|
||||||
|
{
|
||||||
|
bool wasLocalised = Loc.TryGetString(listingData.Name, out string? listingName);
|
||||||
|
|
||||||
|
if (!wasLocalised && listingData.ProductEntity != null)
|
||||||
|
{
|
||||||
|
var proto = prototypeManager.Index<EntityPrototype>(listingData.ProductEntity);
|
||||||
|
listingName = proto.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return listingName ?? listingData.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ListingData's Description field can be either a localisation string or the actual entity's description.
|
||||||
|
/// This function gets a localised description from the localisation string if it exists, and if not, it gets the entity's description.
|
||||||
|
/// If neither a localised string exists, or an associated entity description, it will return the value of the "Description" field.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetLocalisedDescriptionOrEntityDescription(ListingData listingData, IPrototypeManager prototypeManager)
|
||||||
|
{
|
||||||
|
bool wasLocalised = Loc.TryGetString(listingData.Description, out string? listingDesc);
|
||||||
|
|
||||||
|
if (!wasLocalised && listingData.ProductEntity != null)
|
||||||
|
{
|
||||||
|
var proto = prototypeManager.Index<EntityPrototype>(listingData.ProductEntity);
|
||||||
|
listingDesc = proto.Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
return listingDesc ?? listingData.Description;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user