311 lines
11 KiB
Python
311 lines
11 KiB
Python
import asyncio
|
|
import contextlib
|
|
from typing import Any, Dict, Iterable, List, Optional
|
|
|
|
import discord
|
|
import tabulate
|
|
from redbot.core import commands
|
|
from redbot.core.i18n import Translator
|
|
from redbot.core.utils.predicates import MessagePredicate
|
|
from redbot.vendored.discord.ext import menus
|
|
|
|
from .functions import poke_embed
|
|
|
|
_ = Translator("Pokecord", __file__)
|
|
|
|
|
|
class PokeListMenu(menus.MenuPages, inherit_buttons=False):
|
|
def __init__(
|
|
self,
|
|
source: menus.PageSource,
|
|
cog: Optional[commands.Cog] = None,
|
|
ctx=None,
|
|
user=None,
|
|
clear_reactions_after: bool = True,
|
|
delete_message_after: bool = False,
|
|
add_reactions: bool = True,
|
|
using_custom_emoji: bool = False,
|
|
using_embeds: bool = False,
|
|
keyword_to_reaction_mapping: Dict[str, str] = None,
|
|
timeout: int = 180,
|
|
message: discord.Message = None,
|
|
**kwargs: Any,
|
|
) -> None:
|
|
self.cog = cog
|
|
self.ctx = ctx
|
|
self.user = user
|
|
self._search_lock = asyncio.Lock()
|
|
self._search_task: asyncio.Task = None
|
|
super().__init__(
|
|
source,
|
|
clear_reactions_after=clear_reactions_after,
|
|
delete_message_after=delete_message_after,
|
|
check_embeds=using_embeds,
|
|
timeout=timeout,
|
|
message=message,
|
|
**kwargs,
|
|
)
|
|
|
|
async def finalize(self, timed_out):
|
|
if not self._running:
|
|
return
|
|
|
|
await self.stop(do_super=False)
|
|
|
|
async def stop(self, do_super: bool = True):
|
|
if self._search_task is not None:
|
|
self._search_task.cancel()
|
|
if do_super:
|
|
super().stop()
|
|
|
|
async def _number_page_task(self):
|
|
async def cleanup(messages: List[discord.Message]):
|
|
with contextlib.suppress(discord.HTTPException):
|
|
for msg in messages:
|
|
await msg.delete()
|
|
|
|
async with self._search_lock:
|
|
prompt = await self.ctx.send(_("Please select the Pokémon ID number to jump to."))
|
|
try:
|
|
pred = MessagePredicate.valid_int(self.ctx)
|
|
msg = await self.bot.wait_for("message_without_command", check=pred, timeout=10.0)
|
|
jump_page = int(msg.content)
|
|
if jump_page > self._source.get_max_pages():
|
|
await self.ctx.send(
|
|
_("Invalid Pokémon ID, jumping to the end."), delete_after=5
|
|
)
|
|
jump_page = self._source.get_max_pages()
|
|
await self.show_checked_page(jump_page - 1)
|
|
await cleanup([prompt, msg])
|
|
except (ValueError, asyncio.TimeoutError, asyncio.CancelledError):
|
|
await cleanup([prompt])
|
|
|
|
self._search_task = None
|
|
|
|
def reaction_check(self, payload):
|
|
"""The function that is used to check whether the payload should be processed.
|
|
This is passed to :meth:`discord.ext.commands.Bot.wait_for <Bot.wait_for>`.
|
|
|
|
There should be no reason to override this function for most users.
|
|
|
|
Parameters
|
|
------------
|
|
payload: :class:`discord.RawReactionActionEvent`
|
|
The payload to check.
|
|
|
|
Returns
|
|
---------
|
|
:class:`bool`
|
|
Whether the payload should be processed.
|
|
"""
|
|
if payload.message_id != self.message.id:
|
|
return False
|
|
if payload.user_id not in (*self.bot.owner_ids, self._author_id):
|
|
return False
|
|
|
|
return payload.emoji in self.buttons
|
|
|
|
def _cant_select(self):
|
|
return self.ctx.author != self.user
|
|
|
|
@menus.button("\N{BLACK LEFT-POINTING TRIANGLE}", position=menus.First(0))
|
|
async def prev(self, payload: discord.RawReactionActionEvent):
|
|
if self.current_page == 0:
|
|
await self.show_page(self._source.get_max_pages() - 1)
|
|
else:
|
|
await self.show_checked_page(self.current_page - 1)
|
|
|
|
@menus.button("\N{CROSS MARK}", position=menus.First(1))
|
|
async def stop_pages_default(self, payload: discord.RawReactionActionEvent) -> None:
|
|
with contextlib.suppress(discord.NotFound):
|
|
await self.message.delete()
|
|
|
|
await self.stop()
|
|
|
|
@menus.button("\N{BLACK RIGHT-POINTING TRIANGLE}", position=menus.First(2))
|
|
async def next(self, payload: discord.RawReactionActionEvent):
|
|
if self.current_page == self._source.get_max_pages() - 1:
|
|
await self.show_page(0)
|
|
else:
|
|
await self.show_checked_page(self.current_page + 1)
|
|
|
|
@menus.button("\N{LEFT-POINTING MAGNIFYING GLASS}", position=menus.First(4))
|
|
async def number_page(self, payload: discord.RawReactionActionEvent):
|
|
if self._search_lock.locked() and self._search_task is not None:
|
|
return
|
|
|
|
self._search_task = asyncio.get_running_loop().create_task(self._number_page_task())
|
|
|
|
@menus.button("\N{WHITE HEAVY CHECK MARK}", position=menus.First(3), skip_if=_cant_select)
|
|
async def select(self, payload: discord.RawReactionActionEvent):
|
|
command = self.ctx.bot.get_command("select")
|
|
await self.ctx.invoke(command, _id=self.current_page + 1)
|
|
|
|
|
|
class PokeList(menus.ListPageSource):
|
|
def __init__(self, entries: Iterable[str]):
|
|
super().__init__(entries, per_page=1)
|
|
|
|
async def format_page(self, menu: PokeListMenu, pokemon: Dict) -> str:
|
|
embed = await poke_embed(menu.cog, menu.ctx, pokemon, menu=self)
|
|
return embed
|
|
|
|
|
|
class GenericMenu(menus.MenuPages, inherit_buttons=False):
|
|
def __init__(
|
|
self,
|
|
source: menus.PageSource,
|
|
cog: Optional[commands.Cog] = None,
|
|
len_poke: Optional[int] = 0,
|
|
clear_reactions_after: bool = True,
|
|
delete_message_after: bool = False,
|
|
add_reactions: bool = True,
|
|
using_custom_emoji: bool = False,
|
|
using_embeds: bool = False,
|
|
keyword_to_reaction_mapping: Dict[str, str] = None,
|
|
timeout: int = 180,
|
|
message: discord.Message = None,
|
|
**kwargs: Any,
|
|
) -> None:
|
|
self.cog = cog
|
|
self.len_poke = len_poke
|
|
super().__init__(
|
|
source,
|
|
clear_reactions_after=clear_reactions_after,
|
|
delete_message_after=delete_message_after,
|
|
check_embeds=using_embeds,
|
|
timeout=timeout,
|
|
message=message,
|
|
**kwargs,
|
|
)
|
|
|
|
def reaction_check(self, payload):
|
|
"""The function that is used to check whether the payload should be processed.
|
|
This is passed to :meth:`discord.ext.commands.Bot.wait_for <Bot.wait_for>`.
|
|
There should be no reason to override this function for most users.
|
|
Parameters
|
|
------------
|
|
payload: :class:`discord.RawReactionActionEvent`
|
|
The payload to check.
|
|
Returns
|
|
---------
|
|
:class:`bool`
|
|
Whether the payload should be processed.
|
|
"""
|
|
if payload.message_id != self.message.id:
|
|
return False
|
|
if payload.user_id not in (*self.bot.owner_ids, self._author_id):
|
|
return False
|
|
|
|
return payload.emoji in self.buttons
|
|
|
|
def _skip_single_arrows(self):
|
|
max_pages = self._source.get_max_pages()
|
|
if max_pages is None:
|
|
return True
|
|
return max_pages == 1
|
|
|
|
def _skip_double_triangle_buttons(self):
|
|
max_pages = self._source.get_max_pages()
|
|
if max_pages is None:
|
|
return True
|
|
return max_pages <= 2
|
|
|
|
# left
|
|
@menus.button(
|
|
"\N{BLACK LEFT-POINTING TRIANGLE}",
|
|
position=menus.First(1),
|
|
skip_if=_skip_single_arrows,
|
|
)
|
|
async def prev(self, payload: discord.RawReactionActionEvent):
|
|
if self.current_page == 0:
|
|
await self.show_page(self._source.get_max_pages() - 1)
|
|
else:
|
|
await self.show_checked_page(self.current_page - 1)
|
|
|
|
@menus.button("\N{CROSS MARK}", position=menus.First(2))
|
|
async def stop_pages_default(self, payload: discord.RawReactionActionEvent) -> None:
|
|
self.stop()
|
|
with contextlib.suppress(discord.NotFound):
|
|
await self.message.delete()
|
|
|
|
@menus.button(
|
|
"\N{BLACK RIGHT-POINTING TRIANGLE}",
|
|
position=menus.First(2),
|
|
skip_if=_skip_single_arrows,
|
|
)
|
|
async def next(self, payload: discord.RawReactionActionEvent):
|
|
if self.current_page == self._source.get_max_pages() - 1:
|
|
await self.show_page(0)
|
|
else:
|
|
await self.show_checked_page(self.current_page + 1)
|
|
|
|
@menus.button(
|
|
"\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\ufe0f",
|
|
position=menus.First(0),
|
|
skip_if=_skip_double_triangle_buttons,
|
|
)
|
|
async def go_to_first_page(self, payload):
|
|
"""go to the first page"""
|
|
await self.show_page(0)
|
|
|
|
@menus.button(
|
|
"\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\ufe0f",
|
|
position=menus.Last(1),
|
|
skip_if=_skip_double_triangle_buttons,
|
|
)
|
|
async def go_to_last_page(self, payload):
|
|
"""go to the last page"""
|
|
# The call here is safe because it's guarded by skip_if
|
|
await self.show_page(self._source.get_max_pages() - 1)
|
|
|
|
|
|
class SearchFormat(menus.ListPageSource):
|
|
def __init__(self, entries: Iterable[str]):
|
|
super().__init__(entries, per_page=1)
|
|
|
|
async def format_page(self, menu: GenericMenu, string: str) -> str:
|
|
embed = discord.Embed(
|
|
title="Pokemon Search",
|
|
color=await menu.ctx.embed_color(),
|
|
description=string,
|
|
)
|
|
embed.set_footer(
|
|
text=_("Page {page}/{amount}").format(
|
|
page=menu.current_page + 1, amount=menu._source.get_max_pages()
|
|
)
|
|
)
|
|
return embed
|
|
|
|
|
|
class PokedexFormat(menus.ListPageSource):
|
|
def __init__(self, entries: Iterable[str]):
|
|
super().__init__(entries, per_page=1)
|
|
|
|
async def format_page(self, menu: GenericMenu, item: List) -> str:
|
|
embed = discord.Embed(title=_("Pokédex"), color=await menu.ctx.embed_colour())
|
|
embed.set_footer(
|
|
text=_("Showing {page}-{lenpages} of {amount}.").format(
|
|
page=item[0][0], lenpages=item[-1][0], amount=menu.len_poke
|
|
)
|
|
)
|
|
for pokemon in item:
|
|
if pokemon[1]["amount"] > 0:
|
|
msg = _("{amount} caught! \N{WHITE HEAVY CHECK MARK}").format(
|
|
amount=pokemon[1]["amount"]
|
|
)
|
|
else:
|
|
msg = _("Not caught yet! \N{CROSS MARK}")
|
|
embed.add_field(
|
|
name="{pokemonname} {pokemonid}".format(
|
|
pokemonname=menu.cog.get_name(pokemon[1]["name"], menu.ctx.author),
|
|
pokemonid=pokemon[1]["id"],
|
|
),
|
|
value=msg,
|
|
)
|
|
if menu.current_page == 0:
|
|
embed.description = _("You've caught {total} out of {amount} pokémon.").format(
|
|
total=len(await menu.cog.config.user(menu.ctx.author).pokeids()),
|
|
amount=menu.len_poke,
|
|
)
|
|
return embed
|