""" Defines widgets for displaying a campaign's rewards. """ from typing import Optional, TYPE_CHECKING import asyncio import datetime as dt import discord from discord.ui import Modal from discord.ui.select import select, Select, SelectOption from discord.ui.button import button, Button, ButtonStyle from discord.ui.text_input import TextInput, TextStyle from meta import LionBot from meta.errors import UserInputError from meta.config import conf from utils.lib import tabulate, utc_now, MessageArgs, parse_duration from utils.ui import MessageUI from utils.ui.micros import FastModal from ...campaign import RewardCampaign from ...data import EarnedReward from .. import logger class RewardEditor(FastModal): # Title is the reward we are editing # Block of text with dates and user info blurb = discord.ui.TextDisplay(content='placeholder') # Fulfilled is a checkbox, and supports a fulfilled note flufbox = discord.ui.Label( text="Fulfilled", description="Whether this reward has been completed", component=discord.ui.Checkbox( default=False ) ) # Reference and further notes need to be editable, # and need to set the default text correctly reference = discord.ui.Label( text="Reference", description="Reference URL or other information", component=discord.ui.TextInput( style=discord.TextStyle.long, ), ) notes = discord.ui.Label( text="Notes", description="Further notes for this user/reward", component=discord.ui.TextInput( style=discord.TextStyle.long, ), ) @classmethod def from_reward(cls, reward: EarnedReward): title = f"Reward #{reward.earned_id} for {reward.twitch_user_name or reward.twitch_user_id}" self = cls(title=title) # blurb table = { 'Earned At': discord.utils.format_dt(reward.earned_at, 'F'), 'Earned From': reward.earned_from, } prop_table = '\n'.join(tabulate(*table.items())) self.blurb.content = prop_table # Fulfilled self.flufbox.component.default = (reward.fulfilled_at is not None) # Notes self.reference.component.default = reward.reference or '' self.notes.component.default = reward.modnote or '' return self class RewardList(MessageUI): blocklen = 10 def __init__(self, bot: LionBot, campaign: RewardCampaign, **kwargs): super().__init__(**kwargs) self.bot = bot self.campaign = campaign # UI state self.pagen = 0 self.pages = [] self.reward_blocks = [[]] self._rewards: list[EarnedReward] = [] # self._reward_viewer: Optional[RewardViewer] = None @property def page(self): self.pagen %= self.page_count return self.reward_blocks[self.pagen] @property def page_count(self): return len(self.reward_blocks) # ----- UI API ----- # ----- UI Components ----- @select( cls=Select, placeholder="Select reward to edit", min_values=0, max_values=1, ) async def reward_menu(self, selection: discord.Interaction, selected): """ Select reward to view and edit. Maybe just edit for a start? """ if selected.values: # Hopefully this is a list of reminderids values = selected.values # Create detail widget, add to children # Spawn editor modal for this reward, callback to refresh detail widget # Also want to refresh this widget # Show detail widget as followup # TODO: Dedicated editor maybe, could e.g. embed refs. Maybe. value = int(selected.values[0]) reward = next(r for r in self._rewards if r.earned_id == value) modal = RewardEditor.from_reward(reward) currently_fluffed = (reward.fulfilled_at is not None) @modal.submit_callback() async def on_editor_submit(interaction: discord.Interaction): update_args = {} if modal.flufbox.component.value and not currently_fluffed: # Reward has been fluffed update_args['fulfilled_at'] = utc_now() elif currently_fluffed and not modal.flufbox.component.value: # Reward has been unfluffed update_args['fulfilled_at'] = None new_ref_value = modal.reference.value or None if new_ref_value != reward.reference: update_args['reference'] = new_ref_value new_notes_value = modal.notes.value or None if new_notes_value != reward.modnote: update_args['modnote'] = new_notes_value if update_args: await interaction.response.defer(thinking=True, ephemeral=True) await self.campaign.update_reward(reward.earned_id, **update_args) await self.refresh(thinking=interaction) else: await interaction.response.defer(thinking=False) await selection.response.send_modal(modal) await self.refresh() else: await selection.response.defer() async def reward_menu_refresh(self): menu = self.reward_menu rewards = self.page if rewards: menu.options = [ self._format_reward_option(r) for r in rewards ] menu.disabled = False else: menu.options = [ SelectOption(label='DUMMY') ] menu.disabled = True # Meta buttons @button(emoji=conf.emojis.getemoji("forward")) async def next_page_button(self, press: discord.Interaction, pressed): await press.response.defer() self.pagen += 1 await self.refresh() @button(emoji=conf.emojis.getemoji("backward")) async def prev_page_button(self, press: discord.Interaction, pressed): await press.response.defer() self.pagen -= 1 await self.refresh() @button(emoji=conf.emojis.cancel, style=ButtonStyle.red) async def quit_button(self, press: discord.Interaction, pressed: Button): """Close the UI and all children.""" await press.response.defer(thinking=False) await self.quit() @button(emoji=conf.emojis.refresh) async def refresh_button(self, press: discord.Interaction, pressed: Button): await press.response.defer() await self.refresh() # ----- UI Flow ----- def _format_reward_section(self, reward: EarnedReward) -> tuple[str, str]: """ Format the earned reward as an embed section. """ # Title is Reward #n earned by twitch_username # Reward, Earned at, Earned by, Fulfilled at (not fulfilled/date), Ref, Added Notes name = f"Reward #{reward.earned_id} earned by {reward.twitch_user_name or reward.twitch_user_id}" if reward.fulfilled_at is not None: fat = discord.utils.format_dt(reward.fulfilled_at, 'F') if reward.fulfilled_note is not None: fluf = f"{fat} ({reward.fulfilled_note})" else: fluf = fat else: fluf = "*Not Fulfilled*" table = { 'Reward': "Plus Campaign Sketch", 'Earned At': discord.utils.format_dt(reward.earned_at, 'F'), 'Earned From': reward.earned_from, 'Fulfilled At': fluf, 'Reference': reward.reference or "*No reference set*", 'Further notes': reward.modnote or "*No notes*", } prop_table = '\n'.join(tabulate(*table.items())) return (name, prop_table) def _format_reward_option(self, reward: EarnedReward) -> SelectOption: """ Format the earned reward as a selectable option """ # Reward #n earned by twitch_username # Value is the reward id name = f"Reward #{reward.earned_id} earned by {reward.twitch_user_name or reward.twitch_user_id}" value = reward.earned_id return SelectOption( label=name, value=value ) async def refresh_layout(self): to_refresh = ( self.reward_menu_refresh(), ) await asyncio.gather(*to_refresh) if self.page_count > 1: self.prev_page_button.disabled = True self.next_page_button.disabled = True else: self.prev_page_button.disabled = False self.next_page_button.disabled = False self.set_layout( (self.prev_page_button, self.refresh_button, self.next_page_button, self.quit_button), (self.reward_menu,), ) async def make_message(self) -> MessageArgs: embed = discord.Embed(title=f"{self.campaign.row.campaign_name} Reward List") # embed.set_footer with last update return MessageArgs(embed=embed) async def reload(self): rewards = self._rewards = await self.campaign.get_rewards() # TODO: Consider filter by unfulfilled or reward type self.reward_blocks = [ rewards[i:i+self.blocklen] for i in range(0, len(rewards), self.blocklen) ] or [[]]