Files
pluscampaign-plugin/plugin/discord/ui/rewards.py
T

296 lines
9.8 KiB
Python

"""
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,
required=False,
),
)
notes = discord.ui.Label(
text="Notes",
description="Further notes for this user/reward",
component=discord.ui.TextInput(
style=discord.TextStyle.long,
required=False,
),
)
@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.component.value or None
if new_ref_value != reward.reference:
update_args["reference"] = new_ref_value
new_notes_value = modal.notes.component.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)
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=str(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(text="Last Update")
embed.timestamp = utc_now()
campaign = self.campaign
all_rewards = await campaign.get_rewards()
rewards_earned = len(all_rewards)
reward_cap = campaign.row.target_rewards
if reward_cap is not None:
description = f"{rewards_earned} earned out of {reward_cap} available"
else:
description = f"{rewards_earned} earned so far"
embed.description = description
for reward in self.page:
name, value = self._format_reward_section(reward)
embed.add_field(name=name, value=value, inline=False)
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 [[]]