feat(shop): Add paging for long shops.
This commit is contained in:
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user