mirror of
https://github.com/UiO-Gaming/ShiteMusicBot.git
synced 2026-11-05 22:04:32 +01:00
221 lines
8.5 KiB
Python
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]
|