linastk/utils/paginator.py
2024-01-28 19:51:18 +06:00

204 lines
7.4 KiB
Python

from __future__ import annotations
from typing import (
Dict,
Generic,
List,
Optional,
TypeVar,
Any,
TYPE_CHECKING,
Sequence,
Union,
)
import discord
from discord.abc import Messageable
from discord.ext import commands
if TYPE_CHECKING:
from typing_extensions import Self
Interaction = discord.Interaction[Any]
Context = commands.Context[Any]
Page = Union[
str,
Sequence[str],
discord.Embed,
Sequence[discord.Embed],
discord.File,
Sequence[discord.File],
discord.Attachment,
Sequence[discord.Attachment],
dict[str, Any],
]
PageT_co = TypeVar("PageT_co", bound=Page, covariant=True)
class ButtonPaginator(Generic[PageT_co], discord.ui.View):
message: Optional[Union[discord.Message, discord.WebhookMessage]] = None
def __init__(
self,
pages: Sequence[PageT_co],
*,
author_id: Optional[int] = None,
timeout: Optional[float] = 180.0,
delete_message_after: bool = False,
per_page: int = 1,
) -> None:
super().__init__(timeout=timeout)
self.author_id: Optional[int] = author_id
self.delete_message_after: bool = delete_message_after
self.current_page: int = 0
self.per_page: int = per_page
self.pages: Any = pages
total_pages, left_over = divmod(len(self.pages), self.per_page)
if left_over:
total_pages += 1
self.max_pages: int = total_pages
self._page_kwargs: Dict[str, Any] = {"content": None, "embeds": [], "files": [], "view": self}
def stop(self) -> None:
self.message = None
super().stop()
async def interaction_check(self, interaction: Interaction) -> bool:
if not self.author_id:
return True
if self.author_id != interaction.user.id:
await interaction.response.send_message("You cannot interact with this menu.", ephemeral=True)
return False
return True
def get_page(self, page_number: int) -> Union[PageT_co, Sequence[PageT_co]]:
if page_number < 0 or page_number >= self.max_pages:
self.current_page = 0
return self.pages[self.current_page]
if self.per_page == 1:
return self.pages[page_number]
else:
base = page_number * self.per_page
return self.pages[base : base + self.per_page]
def format_page(self, page: Union[PageT_co, Sequence[PageT_co]]) -> Union[PageT_co, Sequence[PageT_co]]:
return page
async def get_page_kwargs(
self, page: Union[PageT_co, Sequence[PageT_co]], skip_formatting: bool = False
) -> Dict[str, Any]:
formatted_page: Union[PageT_co, Sequence[PageT_co]]
if not skip_formatting:
self._page_kwargs = {"content": None, "embeds": [], "files": [], "view": self}
formatted_page = await discord.utils.maybe_coroutine(self.format_page, page)
else:
formatted_page = page
if isinstance(formatted_page, str):
# idk about this
content = self._page_kwargs["content"]
if content is None:
self._page_kwargs["content"] = formatted_page
else:
self._page_kwargs["content"] = f"{content}\n{formatted_page}"
elif isinstance(formatted_page, discord.Embed):
self._page_kwargs["embeds"].append(formatted_page)
elif isinstance(formatted_page, (discord.File, discord.Attachment)):
if isinstance(formatted_page, discord.Attachment):
formatted_page = await formatted_page.to_file() # type: ignore
self._page_kwargs["files"].append(formatted_page)
elif isinstance(formatted_page, (tuple, list)):
for item in formatted_page:
await self.get_page_kwargs(item, skip_formatting=True) # type: ignore
elif isinstance(formatted_page, dict):
return formatted_page
else:
raise TypeError("Page content must be one of str, discord.Embed, list[discord.Embed], or dict")
return self._page_kwargs
def update_buttons(self) -> None:
self.first_page.disabled = self.max_pages < 2 or self.current_page <= 0
self.previous_page.disabled = self.max_pages < 2 or self.current_page <= 0
self.next_page.disabled = self.max_pages < 2 or self.current_page >= self.max_pages - 1
self.last_page.disabled = self.max_pages < 2 or self.current_page >= self.max_pages - 1
async def update_page(self, interaction: Interaction) -> None:
if self.message is None:
self.message = interaction.message
self.update_buttons()
kwargs = await self.get_page_kwargs(self.get_page(self.current_page))
self.reset_files(kwargs)
kwargs["attachments"] = kwargs.pop("files", [])
await interaction.response.edit_message(**kwargs)
@discord.ui.button(emoji="\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}", style=discord.ButtonStyle.blurple)
async def first_page(self, interaction: Interaction, _: discord.ui.Button[Self]):
self.current_page = 0
await self.update_page(interaction)
@discord.ui.button(label="Previous", style=discord.ButtonStyle.blurple, emoji="⬅️")
async def previous_page(self, interaction: Interaction, _: discord.ui.Button[Self]) -> None:
self.current_page -= 1
await self.update_page(interaction)
@discord.ui.button(label="Next", style=discord.ButtonStyle.blurple, emoji="➡️")
async def next_page(self, interaction: Interaction, _: discord.ui.Button[Self]) -> None:
self.current_page += 1
await self.update_page(interaction)
@discord.ui.button(emoji="\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}", style=discord.ButtonStyle.blurple)
async def last_page(self, interaction: Interaction, _: discord.ui.Button[Self]):
self.current_page = self.max_pages - 1
await self.update_page(interaction)
@discord.ui.button(label="Stop", style=discord.ButtonStyle.red, emoji="⏹️")
async def stop_paginator(self, interaction: Interaction, _: discord.ui.Button[Self]) -> None:
if self.delete_message_after:
if self.message is not None:
await self.message.delete()
else:
await self.message.delete()
self.stop()
def reset_files(self, page_kwargs: dict[str, Any]) -> None:
files: List[discord.File] = page_kwargs.get("files", [])
if not files:
return
for file in files:
file.reset()
async def start(
self, obj: Union[Interaction, Messageable], **send_kwargs: Any
) -> Optional[Union[discord.Message, discord.WebhookMessage]]:
self.update_buttons()
kwargs = await self.get_page_kwargs(self.get_page(self.current_page))
if self.max_pages < 2:
self.stop()
del kwargs["view"]
self.reset_files(kwargs)
if isinstance(obj, discord.Interaction):
if obj.response.is_done():
self.message = await obj.followup.send(**kwargs, **send_kwargs)
else:
await obj.response.send_message(**kwargs, **send_kwargs)
self.message = await obj.original_response()
elif isinstance(obj, Messageable):
self.message = await obj.send(**kwargs, **send_kwargs)
else:
raise TypeError(f"Expected Interaction or Messageable, got {obj.__class__.__name__}")
return self.message # type: ignore