Files

221 lines
8.5 KiB
Python

from __future__ import annotations
import asyncio
from enum import Flag, auto
from typing import List, Optional, Tuple
import discord
from bot import MusicBot
from .navbar_range import NavBarRange
from .paginators import BasePaginator, CantScrollError
DISCORD_MAX_SELECTOR_OPTIONS = 25
class ClearMode(Flag):
Timeout = auto()
ManualExit = auto()
AnyExit = Timeout | ManualExit
class ScrollerButton(discord.ui.Button):
def __init__(self, callback, label, style=discord.ButtonStyle.gray, **kwargs):
super().__init__(label=label, style=style, **kwargs)
self.callback = callback
class ScrollerNav(discord.ui.Select):
def __init__(self, callback, **kwargs):
super().__init__(**kwargs)
self.callback = callback
class Scroller:
def __init__(self, ctx, paginator, timeout=20.0, use_tick_for_stop_emoji: bool = False,
show_cancel_for_single_page: bool = False):
if not isinstance(paginator, BasePaginator):
raise TypeError('Paginator needs to be a subclass of BasePaginator.')
self.paginator = paginator
self.ctx = ctx
# No embeds to scroll through
if not self.paginator.pages:
raise Exception("Paginator contained no pages to display") # TODO: proper error
self.bot: MusicBot = ctx.bot
self.channel = ctx.channel
self.message: Optional[discord.Message] = None
# Initialize the view we'll be using for scrolling
self.view = discord.ui.View(timeout=timeout)
self.view.on_timeout = self.on_timeout
self.is_scrolling_paginator = len(self.paginator.pages) > 1
self.use_nav_bar = len(self.paginator.pages) > 3
self.scrolling_done = asyncio.Event()
stop_emoji = '✔️' if use_tick_for_stop_emoji else '❌'
self.control_buttons: List[ScrollerButton] = []
self.forward_button = ScrollerButton(self.next_page, '\N{BLACK RIGHT-POINTING TRIANGLE}', row=3)
self.back_button = ScrollerButton(self.previous_page, '\N{BLACK LEFT-POINTING TRIANGLE}', row=3)
first_page_button = ScrollerButton(self.first_page,
'\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', row=3)
last_page_button = ScrollerButton(self.last_page,
'\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', row=3)
self.stop_button = ScrollerButton(self.stop_scrolling, stop_emoji, row=3)
# We start on the first page, can't go back
self.back_button.disabled = True
# Determine which buttons should be visible depending on the number of pages
if (self.is_scrolling_paginator):
if (len(self.paginator.pages) > 2):
self.control_buttons = [first_page_button, self.back_button, self.forward_button,
last_page_button, self.stop_button]
else:
self.control_buttons = [self.back_button, self.forward_button, self.stop_button]
elif show_cancel_for_single_page:
self.control_buttons = [self.stop_button]
bot_user = ctx.guild if ctx.guild.me is not None else ctx.bot.user
self.permissions = self.channel.permissions_for(bot_user)
if not self.permissions.embed_links:
raise CantScrollError('Bot does not have embed links permission.')
if not self.permissions.send_messages:
raise CantScrollError('Bot cannot send messages.')
async def start_scrolling(self, clear_mode: ClearMode,
message: Optional[discord.Message] = None,
start_page: int = 0) -> Tuple[discord.Message, bool]:
self.clear_mode = clear_mode
self.page_number = min(start_page, len(self.paginator.pages))
self.build_view()
self.update_view()
if message:
self.message = message
await self.message.edit(embed=self.current_page_embed, view=self.view)
else:
self.message = await self.channel.send(embed=self.current_page_embed, view=self.view)
await self.scrolling_done.wait()
return self.message, self.timed_out
def update_view_on_interaction(self, _: discord.Interaction):
self.update_view()
def update_view(self):
if self.use_nav_bar:
self._update_navbar_items()
if self.is_scrolling_paginator:
self.back_button.disabled = self.page_number == 0
self.forward_button.disabled = self.page_number == len(self.paginator.pages)-1
def build_view(self):
for button in self.control_buttons:
self.view.add_item(item=button)
if self.use_nav_bar:
self.navigator = None
self._update_navbar_items()
def _update_navbar_items(self):
placeholder = f"Page: {self.page_number + 1}/{len(self.paginator.pages)}"
did_exist = False
# If we have more than 25 items, the navigator needs to be re-created.
num_selectable_items = len(self.paginator.pages)
if num_selectable_items > DISCORD_MAX_SELECTOR_OPTIONS and self.navigator is not None:
self.view.remove_item(self.navigator)
self.navigator = None
did_exist = True
if self.navigator is None:
if not did_exist:
placeholder = "Navigate to page"
self.navigator = ScrollerNav(self.navigate, placeholder=placeholder, row=4)
self.view.add_item(item=self.navigator)
navigatable_items = NavBarRange(num_items=num_selectable_items,
current_item=self.page_number,
max_iter_length=DISCORD_MAX_SELECTOR_OPTIONS,
add_ends=True)
for page in navigatable_items:
self.navigator.add_option(label=str(page+1), value=str(page))
else:
self.navigator.placeholder = placeholder
async def stop(self, was_timeout: bool, clear_scroller_view: bool = True):
self.is_scrolling_paginator = False
self.view.stop()
if clear_scroller_view:
self.view.clear_items()
self.timed_out = was_timeout
if (not was_timeout and self.clear_mode & ClearMode.ManualExit or
was_timeout and self.clear_mode & ClearMode.Timeout):
if self.message:
await self.message.delete()
self.message = None
# If this fails the message is not accessable either way, which
# means it is probably deleted
try:
if self.ctx.message:
await self.ctx.message.delete()
except discord.HTTPException:
pass
else:
if clear_scroller_view:
await self.update_message()
# Notify the start_scrolling function that we're done
self.scrolling_done.set()
async def _scroll(self, page, interaction: discord.Interaction):
if interaction.user.id != self.ctx.author.id:
return await interaction.response.defer()
if page < 0 or page >= len(self.paginator.pages):
return
self.page_number = page
if interaction.message:
# Update the view before we edit the message
self.update_view_on_interaction(interaction)
await self.update_message()
await interaction.response.defer()
async def update_message(self):
if self.message:
await self.message.edit(embed=self.current_page_embed, view=self.view)
async def first_page(self, interaction: discord.Interaction):
await self._scroll(0, interaction)
async def last_page(self, interaction: discord.Interaction):
await self._scroll(len(self.paginator.pages) - 1, interaction)
async def next_page(self, interaction: discord.Interaction):
await self._scroll(self.page_number + 1, interaction)
async def previous_page(self, interaction: discord.Interaction):
await self._scroll(self.page_number - 1, interaction)
async def navigate(self, interaction: discord.Interaction):
await self._scroll(int(self.navigator.values[0]), interaction)
async def stop_scrolling(self, interaction: discord.Interaction):
await interaction.response.defer()
await self.stop(was_timeout=False)
async def on_timeout(self):
await self.stop(was_timeout=True)
@property
def current_page_embed(self):
return self.paginator.pages[self.page_number]