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 return
@cmds.hybrid_group( @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): async def shop_group(self, ctx: LionContext):
return return
@@ -188,8 +189,8 @@ class StoreManager(ui.LeoUI):
Ask the current shop widget to redraw. Ask the current shop widget to redraw.
""" """
self.page_num %= len(self.stores) self.page_num %= len(self.stores)
await self.stores[self.page_num].refresh() store = self.stores[self.page_num]
await self.stores[self.page_num].redraw() await store.refresh()
def make_buttons(self): def make_buttons(self):
""" """

View File

@@ -5,7 +5,7 @@ import discord
from discord.ui.button import Button from discord.ui.button import Button
from meta import LionBot, LionCog from meta import LionBot, LionCog
from utils import ui from utils.ui import MessageUI
from babel.translator import LazyStr from babel.translator import LazyStr
from ..data import ShopData from ..data import ShopData
@@ -165,7 +165,7 @@ class Shop:
return self._store_cls_(self) return self._store_cls_(self)
class Store(ui.LeoUI): class Store(MessageUI):
""" """
ABC for the UI used to interact with a given shop. 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.) (Note that each Shop instance is specific to a single customer.)
""" """
def __init__(self, shop: Shop, interaction: discord.Interaction, **kwargs): 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 # The shop this Store is an interface for
# Client, shop, and customer data is taken from here # Client, shop, and customer data is taken from here
@@ -189,36 +189,10 @@ class Store(ui.LeoUI):
self.embed: Optional[discord.Embed] = None self.embed: Optional[discord.Embed] = None
# Current interaction to use # Current interaction to use
self.interaction: discord.Interaction = interaction self._original = interaction
# ----- UI API -----
def set_store_row(self, row): def set_store_row(self, row):
self.store_row = row self.store_row = row
for item in row: for item in row:
self.add_item(item) 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.select import select, Select, SelectOption
from discord.ui.button import button, Button from discord.ui.button import button, Button
from meta import conf
from meta import LionCog, LionContext, LionBot from meta import LionCog, LionContext, LionBot
from meta.errors import SafeCancellation from meta.errors import SafeCancellation
from meta.logger import log_wrap from meta.logger import log_wrap
from utils import ui from utils.lib import error_embed, MessageArgs
from utils.lib import error_embed
from constants import MAX_COINS from constants import MAX_COINS
from wards import equippable_role from wards import equippable_role
@@ -293,6 +293,7 @@ class ColourShop(Shop):
) )
except discord.HTTPException: except discord.HTTPException:
# Possibly Forbidden, or the role doesn't actually exist anymore (cache failure) # Possibly Forbidden, or the role doesn't actually exist anymore (cache failure)
# TODO: Event log
pass pass
await self.data.MemberInventory.table.delete_where(inventoryid=owned.data.inventoryid) 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_, item_type=self._shop_cls_._item_type_,
deleted=False deleted=False
) )
if len(current) >= 25: # Disabled because we can support more than 25 colours
if False and len(current) >= 25:
raise SafeCancellation( raise SafeCancellation(
t(_p( t(_p(
'cmd:editshop_colours_create|error:max_colours', 'cmd:editshop_colours_create|error:max_colours',
@@ -710,7 +712,7 @@ class ColourShopping(ShopCog):
item_type=self._shop_cls_._item_type_, item_type=self._shop_cls_._item_type_,
deleted=False deleted=False
) )
if len(current) >= 25: if False and len(current) >= 25:
raise SafeCancellation( raise SafeCancellation(
t(_p( t(_p(
'cmd:editshop_colours_add|error:max_colours', 'cmd:editshop_colours_add|error:max_colours',
@@ -1020,7 +1022,7 @@ class ColourShopping(ShopCog):
item = items[0] item = items[0]
# Delete the item, respecting the delete setting. # 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: if delete_role:
role = ctx.guild.get_role(item.roleid) role = ctx.guild.get_role(item.roleid)
@@ -1097,6 +1099,24 @@ class ColourStore(Store):
""" """
shop: ColourShop 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") @select(placeholder="SELECT_PLACEHOLDER")
async def select_colour(self, interaction: discord.Interaction, selection: Select): async def select_colour(self, interaction: discord.Interaction, selection: Select):
t = self.shop.bot.translator.t t = self.shop.bot.translator.t
@@ -1147,7 +1167,7 @@ class ColourStore(Store):
selector = self.select_colour selector = self.select_colour
# Get the list of ColourRoleItems that may be purchased # 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() owned = self.shop.owned()
option_map: dict[int, SelectOption] = {} option_map: dict[int, SelectOption] = {}
@@ -1172,37 +1192,54 @@ class ColourStore(Store):
selector.disabled = False selector.disabled = False
selector.options = list(option_map.values()) selector.options = list(option_map.values())
async def refresh(self): @button(emoji=conf.emojis.forward)
""" async def next_page_button(self, press: discord.Interaction, pressed: Button):
Refresh the UI elements 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() await self.select_colour_refresh()
if not self.select_colour.options: if self.page_count > 1:
self._layout = [self.store_row] buttons = (self.prev_page_button, *self.store_row, self.next_page_button)
else: 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() async def make_message(self) -> MessageArgs:
def make_embed(self):
"""
Embed for this shop.
"""
t = self.shop.bot.translator.t t = self.shop.bot.translator.t
if self.shop.items:
owned = self.shop.owned() owned = self.shop.owned()
if self.shop.items:
page_items = self.this_page
page_start = self.pagen * self.page_len + 1
lines = [] 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: if owned is not None and item.itemid == owned.itemid:
line = t(_p( line = t(_p(
'ui:colourstore|embed|line:owned_item', 'ui:colourstore|embed|line:owned_item',
"`[{j:02}]` | `{price} LC` | {mention} (You own this!)" "`[{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: else:
line = t(_p( line = t(_p(
'ui:colourstore|embed|line:item', 'ui:colourstore|embed|line:item',
"`[{j:02}]` | `{price} LC` | {mention}" "`[{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) lines.append(line)
description = '\n'.join(lines) description = '\n'.join(lines)
else: else:
@@ -1214,4 +1251,23 @@ class ColourStore(Store):
title=t(_p('ui:colourstore|embed|title', "Colour Role Shop")), title=t(_p('ui:colourstore|embed|title', "Colour Role Shop")),
description=description 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: try:
await self._redraw(args) await self._redraw(args)
except discord.HTTPException: except discord.HTTPException as e:
# Unknown communication erorr, nothing we can reliably do. Exit quietly. # 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() await self.close()
async def cleanup(self): async def cleanup(self):