feat(shop): Add paging for long shops.

This commit is contained in:
2023-09-12 20:33:24 +03:00
parent 7dfc720c16
commit fc56647bdd
4 changed files with 94 additions and 60 deletions

View File

@@ -114,7 +114,8 @@ class Shopping(LionCog):
return
@cmds.hybrid_group(
name=_p('group:shop', 'shop')
name=_p('cmd:shop', 'shop'),
description=_p('cmd:shop|desc', "Purchase coloures, roles, and other goodies with LionCoins.")
)
async def shop_group(self, ctx: LionContext):
return
@@ -188,8 +189,8 @@ class StoreManager(ui.LeoUI):
Ask the current shop widget to redraw.
"""
self.page_num %= len(self.stores)
await self.stores[self.page_num].refresh()
await self.stores[self.page_num].redraw()
store = self.stores[self.page_num]
await store.refresh()
def make_buttons(self):
"""

View File

@@ -5,7 +5,7 @@ import discord
from discord.ui.button import Button
from meta import LionBot, LionCog
from utils import ui
from utils.ui import MessageUI
from babel.translator import LazyStr
from ..data import ShopData
@@ -165,7 +165,7 @@ class Shop:
return self._store_cls_(self)
class Store(ui.LeoUI):
class Store(MessageUI):
"""
ABC for the UI used to interact with a given shop.
@@ -174,7 +174,7 @@ class Store(ui.LeoUI):
(Note that each Shop instance is specific to a single customer.)
"""
def __init__(self, shop: Shop, interaction: discord.Interaction, **kwargs):
super().__init__(**kwargs)
super().__init__(callerid=interaction.user.id, **kwargs)
# The shop this Store is an interface for
# Client, shop, and customer data is taken from here
@@ -189,36 +189,10 @@ class Store(ui.LeoUI):
self.embed: Optional[discord.Embed] = None
# Current interaction to use
self.interaction: discord.Interaction = interaction
self._original = interaction
# ----- UI API -----
def set_store_row(self, row):
self.store_row = row
for item in row:
self.add_item(item)
async def refresh(self):
"""
Refresh all UI elements.
"""
raise NotImplementedError
async def redraw(self):
"""
Redraw the store UI.
"""
if self.interaction.is_expired():
# This is actually possible,
# If the user keeps using the UI,
# but never closes it until the origin interaction expires
raise ValueError("This interaction has expired!")
if self.embed is None:
await self.refresh()
await self.interaction.edit_original_response(embed=self.embed, view=self)
async def make_embed(self):
"""
Embed page for this shop.
"""
raise NotImplementedError

View File

@@ -8,11 +8,11 @@ from discord import app_commands as appcmds
from discord.ui.select import select, Select, SelectOption
from discord.ui.button import button, Button
from meta import conf
from meta import LionCog, LionContext, LionBot
from meta.errors import SafeCancellation
from meta.logger import log_wrap
from utils import ui
from utils.lib import error_embed
from utils.lib import error_embed, MessageArgs
from constants import MAX_COINS
from wards import equippable_role
@@ -293,6 +293,7 @@ class ColourShop(Shop):
)
except discord.HTTPException:
# Possibly Forbidden, or the role doesn't actually exist anymore (cache failure)
# TODO: Event log
pass
await self.data.MemberInventory.table.delete_where(inventoryid=owned.data.inventoryid)
@@ -415,7 +416,8 @@ class ColourShopping(ShopCog):
item_type=self._shop_cls_._item_type_,
deleted=False
)
if len(current) >= 25:
# Disabled because we can support more than 25 colours
if False and len(current) >= 25:
raise SafeCancellation(
t(_p(
'cmd:editshop_colours_create|error:max_colours',
@@ -710,7 +712,7 @@ class ColourShopping(ShopCog):
item_type=self._shop_cls_._item_type_,
deleted=False
)
if len(current) >= 25:
if False and len(current) >= 25:
raise SafeCancellation(
t(_p(
'cmd:editshop_colours_add|error:max_colours',
@@ -1020,7 +1022,7 @@ class ColourShopping(ShopCog):
item = items[0]
# Delete the item, respecting the delete setting.
await self.data.ShopItem.table.update_where(itemid=item.itemid, deleted=True)
await self.data.ShopItem.table.update_where(itemid=item.itemid).set(deleted=True)
if delete_role:
role = ctx.guild.get_role(item.roleid)
@@ -1097,6 +1099,24 @@ class ColourStore(Store):
"""
shop: ColourShop
page_len = 25
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pagen = 0
self.blocks = [[]]
@property
def page_count(self):
return len(self.blocks)
@property
def this_page(self):
self.pagen %= self.page_count
return self.blocks[self.pagen]
# ----- UI Components -----
@select(placeholder="SELECT_PLACEHOLDER")
async def select_colour(self, interaction: discord.Interaction, selection: Select):
t = self.shop.bot.translator.t
@@ -1147,7 +1167,7 @@ class ColourStore(Store):
selector = self.select_colour
# Get the list of ColourRoleItems that may be purchased
purchasable = self.shop.purchasable()
purchasable = [item for item in self.shop.purchasable() if item in self.this_page]
owned = self.shop.owned()
option_map: dict[int, SelectOption] = {}
@@ -1172,37 +1192,54 @@ class ColourStore(Store):
selector.disabled = False
selector.options = list(option_map.values())
async def refresh(self):
"""
Refresh the UI elements
"""
@button(emoji=conf.emojis.forward)
async def next_page_button(self, press: discord.Interaction, pressed: Button):
await press.response.defer()
self.pagen += 1
await self.refresh()
@button(emoji=conf.emojis.backward)
async def prev_page_button(self, press: discord.Interaction, pressed: Button):
await press.response.defer()
self.pagen -= 1
await self.refresh()
# ----- UI Flow -----
async def reload(self):
items = self.shop.items
self.blocks = [
items[i:i+self.page_len] for i in range(0, len(items), self.page_len)
] or [[]]
async def refresh_layout(self):
await self.select_colour_refresh()
if not self.select_colour.options:
self._layout = [self.store_row]
if self.page_count > 1:
buttons = (self.prev_page_button, *self.store_row, self.next_page_button)
else:
self._layout = [(self.select_colour,), self.store_row]
buttons = self.store_row
if not self.select_colour.options:
self._layout = [buttons]
else:
self._layout = [(self.select_colour,), buttons]
self.embed = self.make_embed()
def make_embed(self):
"""
Embed for this shop.
"""
async def make_message(self) -> MessageArgs:
t = self.shop.bot.translator.t
owned = self.shop.owned()
if self.shop.items:
owned = self.shop.owned()
page_items = self.this_page
page_start = self.pagen * self.page_len + 1
lines = []
for i, item in enumerate(self.shop.items):
for i, item in enumerate(page_items):
if owned is not None and item.itemid == owned.itemid:
line = t(_p(
'ui:colourstore|embed|line:owned_item',
"`[{j:02}]` | `{price} LC` | {mention} (You own this!)"
)).format(j=i+1, price=item.price, mention=item.mention)
)).format(j=i+page_start, price=item.price, mention=item.mention)
else:
line = t(_p(
'ui:colourstore|embed|line:item',
"`[{j:02}]` | `{price} LC` | {mention}"
)).format(j=i+1, price=item.price, mention=item.mention)
)).format(j=i+page_start, price=item.price, mention=item.mention)
lines.append(line)
description = '\n'.join(lines)
else:
@@ -1214,4 +1251,23 @@ class ColourStore(Store):
title=t(_p('ui:colourstore|embed|title', "Colour Role Shop")),
description=description
)
return embed
if self.page_count > 1:
footer = t(_p(
'ui:colourstore|embed|footer:paged',
"Page {current}/{total}"
)).format(current=self.pagen + 1, total=self.page_count)
embed.set_footer(text=footer)
if owned:
embed.add_field(
name=t(_p(
'ui:colourstore|embed|field:warning|name',
"Note!"
)),
value=t(_p(
'ui:colourstore|embed|field:warning|value',
"Purchasing a new colour role will *replace* your currently colour "
"{current} without refund!"
)).format(current=owned.mention)
)
return MessageArgs(embed=embed)

View File

@@ -419,8 +419,11 @@ class MessageUI(LeoUI):
try:
await self._redraw(args)
except discord.HTTPException:
# Unknown communication erorr, nothing we can reliably do. Exit quietly.
except discord.HTTPException as e:
# Unknown communication error, nothing we can reliably do. Exit quietly.
logger.warning(
f"Unexpected UI redraw failure occurred in {self}: {repr(e)}",
)
await self.close()
async def cleanup(self):