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:
J. Brown
2024-03-31 05:09:15 +01:00
committed by GitHub
parent ce71cde429
commit de62ec204b
5 changed files with 91 additions and 23 deletions

View File

@@ -1,22 +1,27 @@
using Content.Shared.Store;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using System.Linq;
using System.Threading;
using Serilog;
using Timer = Robust.Shared.Timing.Timer;
using Robust.Shared.Prototypes;
namespace Content.Client.Store.Ui;
[UsedImplicitly]
public sealed class StoreBoundUserInterface : BoundUserInterface
{
private IPrototypeManager _prototypeManager = default!;
[ViewVariables]
private StoreMenu? _menu;
[ViewVariables]
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)
{
}
@@ -49,6 +54,12 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
SendMessage(new StoreRequestUpdateInterfaceMessage());
};
_menu.SearchTextUpdated += (_, search) =>
{
_search = search.Trim().ToLowerInvariant();
UpdateListingsWithSearchFilter();
};
_menu.OnRefundAttempt += (_) =>
{
SendMessage(new StoreRequestRefundMessage());
@@ -64,10 +75,10 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
switch (state)
{
case StoreUpdateState msg:
_menu.UpdateBalance(msg.Balance);
_menu.PopulateStoreCategoryButtons(msg.Listings);
_listings = msg.Listings;
_menu.UpdateListing(msg.Listings.ToList());
_menu.UpdateBalance(msg.Balance);
UpdateListingsWithSearchFilter();
_menu.SetFooterVisibility(msg.ShowFooter);
_menu.UpdateRefund(msg.AllowRefund);
break;
@@ -89,4 +100,19 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
_menu?.Close();
_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());
}
}

View File

@@ -28,7 +28,8 @@
HorizontalAlignment="Right"
Text="Refund" />
</BoxContainer>
<PanelContainer VerticalExpand="True">
<LineEdit Name="SearchBar" Margin="4" PlaceHolder="Search" HorizontalExpand="True"/>
<PanelContainer VerticalExpand="True">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#000000FF" />
</PanelContainer.PanelOverride>

View File

@@ -1,5 +1,4 @@
using System.Linq;
using System.Threading;
using Content.Client.Actions;
using Content.Client.GameTicking.Managers;
using Content.Client.Message;
@@ -27,6 +26,7 @@ public sealed partial class StoreMenu : DefaultWindow
private StoreWithdrawWindow? _withdrawWindow;
public event EventHandler<string>? SearchTextUpdated;
public event Action<BaseButton.ButtonEventArgs, ListingData>? OnListingButtonPressed;
public event Action<BaseButton.ButtonEventArgs, string>? OnCategoryButtonPressed;
public event Action<BaseButton.ButtonEventArgs, string, int>? OnWithdrawAttempt;
@@ -46,6 +46,7 @@ public sealed partial class StoreMenu : DefaultWindow
WithdrawButton.OnButtonDown += OnWithdrawButtonDown;
RefreshButton.OnButtonDown += OnRefreshButtonDown;
RefundButton.OnButtonDown += OnRefundButtonDown;
SearchBar.OnTextChanged += _ => SearchTextUpdated?.Invoke(this, SearchBar.Text);
if (Window != null)
Window.Title = name;
@@ -59,7 +60,7 @@ public sealed partial class StoreMenu : DefaultWindow
(type.Key, type.Value), type => _prototypeManager.Index<CurrencyPrototype>(type.Key));
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),
("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());
// should probably chunk these out instead. to-do if this clogs the internet tubes.
// maybe read clients prototypes instead?
ClearListings();
@@ -129,8 +129,8 @@ public sealed partial class StoreMenu : DefaultWindow
if (!listing.Categories.Contains(CurrentCategory))
return;
var listingName = Loc.GetString(listing.Name);
var listingDesc = Loc.GetString(listing.Description);
var listingName = ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listing, _prototypeManager);
var listingDesc = ListingLocalisationHelpers.GetLocalisedDescriptionOrEntityDescription(listing, _prototypeManager);
var listingPrice = listing.Cost;
var canBuy = CanBuyListing(Balance, listingPrice);
@@ -144,12 +144,6 @@ public sealed partial class StoreMenu : DefaultWindow
{
if (texture == null)
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)
{
@@ -243,13 +237,16 @@ public sealed partial class StoreMenu : DefaultWindow
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)
CurrentCategory = allCategories.First().ID;
if (allCategories.Count <= 1)
return;
CategoryListContainer.Children.Clear();
if (allCategories.Count < 1)
return;
foreach (var proto in allCategories)
{

View File

@@ -15,6 +15,7 @@ using Content.Shared.UserInterface;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Store.Systems;
@@ -29,6 +30,7 @@ public sealed partial class StoreSystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly StackSystem _stack = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private void InitializeUi()
{
@@ -259,7 +261,7 @@ public sealed partial class StoreSystem
//log dat shit.
_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
_audio.PlayEntity(component.BuySuccessSound, msg.Session, uid); //cha-ching!

View 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;
}
}