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