-
-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
- Loading branch information
1 parent
b1e8234
commit 18c4632
Showing
1 changed file
with
248 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,248 @@ | ||
from typing import Any, List, Optional | ||
|
||
import discord | ||
from redbot.core import commands | ||
from redbot.vendored.discord.ext import menus | ||
|
||
|
||
class StopButton(discord.ui.Button): | ||
def __init__( | ||
self, | ||
style: discord.ButtonStyle, | ||
row: Optional[int], | ||
): | ||
super().__init__(style=style, row=row) | ||
self.style = style | ||
self.emoji = "\N{HEAVY MULTIPLICATION X}\N{VARIATION SELECTOR-16}" | ||
|
||
async def callback(self, interaction: discord.Interaction): | ||
self.view.stop() | ||
if interaction.message.flags.ephemeral: | ||
await interaction.response.edit_message(view=None) | ||
return | ||
await interaction.message.delete() | ||
|
||
|
||
class ForwardButton(discord.ui.Button): | ||
def __init__( | ||
self, | ||
style: discord.ButtonStyle, | ||
row: Optional[int], | ||
): | ||
super().__init__(style=style, row=row) | ||
self.style = style | ||
self.emoji = "\N{BLACK RIGHT-POINTING TRIANGLE}\N{VARIATION SELECTOR-16}" | ||
|
||
async def callback(self, interaction: discord.Interaction): | ||
await self.view.show_checked_page(self.view.current_page + 1, interaction) | ||
|
||
|
||
class BackButton(discord.ui.Button): | ||
def __init__( | ||
self, | ||
style: discord.ButtonStyle, | ||
row: Optional[int], | ||
): | ||
super().__init__(style=style, row=row) | ||
self.style = style | ||
self.emoji = "\N{BLACK LEFT-POINTING TRIANGLE}\N{VARIATION SELECTOR-16}" | ||
|
||
async def callback(self, interaction: discord.Interaction): | ||
await self.view.show_checked_page(self.view.current_page - 1, interaction) | ||
|
||
|
||
class LastItemButton(discord.ui.Button): | ||
def __init__( | ||
self, | ||
style: discord.ButtonStyle, | ||
row: Optional[int], | ||
): | ||
super().__init__(style=style, row=row) | ||
self.style = style | ||
self.emoji = ( | ||
"\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}" | ||
) | ||
|
||
async def callback(self, interaction: discord.Interaction): | ||
await self.view.show_page(self.view._source.get_max_pages() - 1, interaction) | ||
|
||
|
||
class FirstItemButton(discord.ui.Button): | ||
def __init__( | ||
self, | ||
style: discord.ButtonStyle, | ||
row: Optional[int], | ||
): | ||
super().__init__(style=style, row=row) | ||
self.style = style | ||
self.emoji = ( | ||
"\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}" | ||
) | ||
|
||
async def callback(self, interaction: discord.Interaction): | ||
await self.view.show_page(0, interaction) | ||
|
||
|
||
class _SelectMenu(discord.ui.Select): | ||
def __init__(self, options: List[discord.SelectOption]): | ||
super().__init__(placeholder="Select a Page", min_values=1, max_values=1, options=options) | ||
|
||
async def callback(self, interaction: discord.Interaction): | ||
index = int(self.values[0]) | ||
await self.view.show_page(index, interaction) | ||
|
||
|
||
class BaseView(discord.ui.View): | ||
def __init__( | ||
self, | ||
source: menus.PageSource, | ||
clear_reactions_after: bool = True, | ||
delete_message_after: bool = False, | ||
timeout: int = 180, | ||
message: discord.Message = None, | ||
page_start: int = 0, | ||
**kwargs: Any, | ||
) -> None: | ||
super().__init__( | ||
timeout=timeout, | ||
) | ||
self._source = source | ||
self.page_start = page_start | ||
self.current_page = page_start | ||
self.message = message | ||
self.ctx = kwargs.get("ctx", None) | ||
self.forward_button = ForwardButton(discord.ButtonStyle.grey, 0) | ||
self.back_button = BackButton(discord.ButtonStyle.grey, 0) | ||
self.first_item = FirstItemButton(discord.ButtonStyle.grey, 0) | ||
self.last_item = LastItemButton(discord.ButtonStyle.grey, 0) | ||
self.stop_button = StopButton(discord.ButtonStyle.red, 0) | ||
self.add_item(self.stop_button) | ||
self.add_item(self.first_item) | ||
self.add_item(self.back_button) | ||
self.add_item(self.forward_button) | ||
self.add_item(self.last_item) | ||
self.select_menu = self._get_select_menu() | ||
self.add_item(self.select_menu) | ||
|
||
@property | ||
def source(self): | ||
return self._source | ||
|
||
async def on_timeout(self): | ||
await self.message.edit(view=None) | ||
|
||
async def start(self, ctx: commands.Context): | ||
await self.send_initial_message(ctx) | ||
|
||
def select_options(self): | ||
return getattr(self.source, "select_options", []) | ||
|
||
async def change_source(self, new_source: menus.PageSource): | ||
self.current_page = 0 | ||
self._source = new_source | ||
if not self.source.is_paginating(): | ||
self.disable_navigation() | ||
page = await self._source.get_page(self.page_start) | ||
return await self._get_kwargs_from_page(page) | ||
|
||
def _get_select_menu(self): | ||
# handles modifying the select menu if more than 25 pages are provided | ||
# this will show the previous 12 and next 13 pages in the select menu | ||
# based on the currently displayed page. Once you reach close to the max | ||
# pages it will display the last 25 pages. | ||
if len(self.select_options()) > 25: | ||
minus_diff = None | ||
plus_diff = 25 | ||
if 12 < self.current_page < len(self.select_options()) - 25: | ||
minus_diff = self.current_page - 12 | ||
plus_diff = self.current_page + 13 | ||
elif self.current_page >= len(self.select_options()) - 25: | ||
minus_diff = len(self.select_options()) - 25 | ||
plus_diff = None | ||
options = self.select_options()[minus_diff:plus_diff] | ||
else: | ||
options = self.select_options()[:25] | ||
return _SelectMenu(options) | ||
|
||
def disable_navigation(self): | ||
self.first_item.disabled = True | ||
self.back_button.disabled = True | ||
self.forward_button.disabled = True | ||
self.last_item.disabled = True | ||
|
||
def enable_navigation(self): | ||
self.first_item.disabled = False | ||
self.back_button.disabled = False | ||
self.forward_button.disabled = False | ||
self.last_item.disabled = False | ||
|
||
async def send_initial_message(self, ctx: commands.Context): | ||
"""|coro| | ||
The default implementation of :meth:`Menu.send_initial_message` | ||
for the interactive pagination session. | ||
This implementation shows the first page of the source. | ||
""" | ||
self.ctx = ctx | ||
if not self.source.is_paginating(): | ||
self.disable_navigation() | ||
page = await self._source.get_page(self.page_start) | ||
kwargs = await self._get_kwargs_from_page(page) | ||
self.message = await ctx.send(**kwargs, view=self) | ||
return self.message | ||
|
||
async def _get_kwargs_from_page(self, page): | ||
value = await discord.utils.maybe_coroutine(self._source.format_page, self, page) | ||
if isinstance(value, dict): | ||
return value | ||
elif isinstance(value, str): | ||
return {"content": value, "embed": None} | ||
elif isinstance(value, discord.Embed): | ||
return {"embed": value, "content": None} | ||
|
||
async def get_page(self, page_number: int): | ||
if not self.source.is_paginating(): | ||
self.disable_navigation() | ||
else: | ||
self.enable_navigation() | ||
page = await self._source.get_page(page_number) | ||
self.current_page = page_number | ||
return await self._get_kwargs_from_page(page) | ||
|
||
async def show_page(self, page_number: int, interaction: discord.Interaction): | ||
if not self.source.is_paginating(): | ||
self.disable_navigation() | ||
else: | ||
self.enable_navigation() | ||
if len(self.source.select_options) > 25 and self.source.is_paginating(): | ||
self.remove_item(self.select_menu) | ||
self.select_menu = self._get_select_menu() | ||
self.add_item(self.select_menu) | ||
page = await self._source.get_page(page_number) | ||
self.current_page = page_number | ||
kwargs = await self._get_kwargs_from_page(page) | ||
await interaction.response.edit_message(**kwargs, view=self) | ||
|
||
async def show_checked_page(self, page_number: int, interaction: discord.Interaction) -> None: | ||
max_pages = self._source.get_max_pages() | ||
try: | ||
if max_pages is None: | ||
# If it doesn't give maximum pages, it cannot be checked | ||
await self.show_page(page_number, interaction) | ||
elif page_number >= max_pages: | ||
await self.show_page(0, interaction) | ||
elif page_number < 0: | ||
await self.show_page(max_pages - 1, interaction) | ||
elif max_pages > page_number >= 0: | ||
await self.show_page(page_number, interaction) | ||
except IndexError: | ||
# An error happened that can be handled, so ignore it. | ||
pass | ||
|
||
async def interaction_check(self, interaction: discord.Interaction): | ||
"""Just extends the default reaction_check to use owner_ids""" | ||
if interaction.user.id not in (*self.ctx.bot.owner_ids, self.ctx.author.id): | ||
await interaction.response.send_message( | ||
content="You are not authorized to interact with this.", ephemeral=True | ||
) | ||
return False | ||
return True |