diff --git a/plugin/discord/ui/__init__.py b/plugin/discord/ui/__init__.py new file mode 100644 index 0000000..cf60a8c --- /dev/null +++ b/plugin/discord/ui/__init__.py @@ -0,0 +1 @@ +from .rewards import RewardList, RewardEditor diff --git a/plugin/discord/ui/campaign.py b/plugin/discord/ui/campaign.py new file mode 100644 index 0000000..e69de29 diff --git a/plugin/discord/ui/rewards.py b/plugin/discord/ui/rewards.py new file mode 100644 index 0000000..ed7a2da --- /dev/null +++ b/plugin/discord/ui/rewards.py @@ -0,0 +1,254 @@ +""" +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, AButton, AsComponents, ConfigEditor +from utils.ui.micros import FastModal +from utils.ui.pagers import BasePager, Pager + +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) + 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': "*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 [[]]