diff --git a/src/modules/shop/cog.py b/src/modules/shop/cog.py index 644d9297..4892c2f2 100644 --- a/src/modules/shop/cog.py +++ b/src/modules/shop/cog.py @@ -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): """ diff --git a/src/modules/shop/shops/base.py b/src/modules/shop/shops/base.py index 99e53836..0222a3b2 100644 --- a/src/modules/shop/shops/base.py +++ b/src/modules/shop/shops/base.py @@ -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 diff --git a/src/modules/shop/shops/colours.py b/src/modules/shop/shops/colours.py index 4aae8db9..a2add0ed 100644 --- a/src/modules/shop/shops/colours.py +++ b/src/modules/shop/shops/colours.py @@ -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) diff --git a/src/utils/ui/leo.py b/src/utils/ui/leo.py index 9c3a4487..07979a2f 100644 --- a/src/utils/ui/leo.py +++ b/src/utils/ui/leo.py @@ -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):