@@ -0,0 +1,393 @@
# Pagination Walkthrough in discord.py
In this tutorial I will go through the entire length of pagination in discord.py
## Table Content
- [ Getting Started] ( #start )
- [ Menu] ( #menu )
- [ Pagination] ( #pag )
- [ MenuPages and ListPageSource] ( #menupages_list )
- [ MenuPages] ( #menupages )
- [ ListPageSource] ( #list )
- [ Combine MenuPages & ListPageSource] ( #cmenupages_list )
- [ Custom MenuPages] ( #cmenupages )
- [ View] ( #view )
- [ Brief explanation of View] ( #brief_view )
- [ Pagination with View] ( #pag_view )
- [ The end] ( #the_end )
Pagination is an extremely common thing to do in discord.py that
I decided to create this gist. One of the most common uses of
pagination is when you want to show contents that are more than
the limit of what discord allows you.
In this tutorial, you would need to install ` discord-ext-menus `
library that was written by Danny. The creator of discord.py. This
library mainly uses reactions as interfaces. Now don't worry, we
will go into discord button once we fully understand this library.
Before diving any further, make sure to install [ discord-ext-menus] ( https://github.com/Rapptz/discord-ext-menus ) .
# <a name =" start " ></a > Getting Started
While the library contains more than just pagination. We will mainly
go into pagination because that's the focus.
The library contains a few classes, mainly these ones.
1 . ` menus.Menu `
2 . ` menus.MenuPages `
3 . ` menus.ListPageSource `
4 . ` menus.GroupByPageSource `
5 . ` menus.AsyncIteratorPageSource `
However, we will only look into 3 of them, which is ` menus.Menu `
, ` menus.MenuPages ` and ` menus.ListPageSource ` . The rest are derived from
` menus.MenuPages ` which you can learn on your own after this tutorial.
## Menu <a name =" menu " ></a >
This class is responsible for handling the reactions given and
the behaviour of what a reaction would do.
Let's use the example given by the library.
``` python
from discord.ext import menus
class MyMenu (menus .Menu ):
async def send_initial_message (self , ctx , channel ):
return await channel.send(f ' Hello { ctx.author} ' )
@menus.button (' \N{THUMBS UP SIGN} ' )
async def on_thumbs_up (self , payload ):
await self .message.edit(content = f ' Thanks { self .ctx.author} ! ' )
@menus.button (' \N{THUMBS DOWN SIGN} ' )
async def on_thumbs_down (self , payload ):
await self .message.edit(content = f " That's not nice { self .ctx.author} ... " )
@menus.button (' \N{BLACK SQUARE FOR STOP}\ufe0f ' )
async def on_stop (self , payload ):
self .stop()
@bot.command ()
async def your_command (ctx ):
menu = MyMenu()
await menu.start(ctx)
```
#### Explanation
1 . ` send_initial_message ` method is called when you call ` menu.start ` . You're required
to return a message object for ` MyMenu ` to handle them.
2 . ` menus.button ` refers to your reaction. They are automatically reacted by your bot
as soon as ` menu.start ` is called. The first argument would be the emoji that you would want to
listen to and added to the Message.
3 . The callbacks of each decorator of ` menus.button ` are called when you reacted to the
corresponding reaction in the decorator.
4 . ` self.stop ` method stops the ` MyMenu ` class from listening to the reactions. This fully stops
the instance from operating.
To simply, this sums up on what ` menus.Menu ` class does.
Of course, it's a lot more complicated than this but this is the simplification i can give you.
![ menu_works] ( https://cdn.discordapp.com/attachments/777501555687292928/919237576597078096/Blank_diagram.png )
#### Output
![ menu_output] ( https://cdn.discordapp.com/attachments/777501555687292928/919238127862824960/ezgif.com-gif-maker.gif )
#### Discussion
As you can see, you can do plenty of things with this class. There's lots of application
that can be used from this class alone.
For example, you can do a confirmation button using pure reactions. Have a greater
control in controlling reactions without any headache. You can also use this for controls
in a game where it is purely based on reactions. While yes, discord button exist. This were
made before the existence of discord buttons.
It's great to have a fundamental understanding of how this will help us on
creating a pagination based on reactions AND discord buttons.
# Pagination <a name =" pag " ></a >
## MenuPages and ListPageSource <a name =" menupages_list " ></a >
_ [ Reaction based pagination] _
### MenuPages <a name =" menupages " ></a >
This class is responsible for controlling the reactions for pagination. It is responsible for
handling the reactions properly on what to do whether to show the first page, next page and so on.
This class inherits ` menus.Menu ` . Meaning, all functionality coming from ` menus.Menu ` is also available
in this class. This class fully has all the methods to control a pagination.
However, this class alone is entirely useless. To make it useful, we will need
` ListPageSource ` .
### ListPageSource <a name =" list " ></a >
This class is responsible for formatting each page in our paginator. It is also responsible for
handling each of our data. Given an iterable into the class would allow it to fully handle the
data into each pages. We will see this in action in [ Combine MenuPages & ListPageSource] ( #cmenupages_list )
### Combine MenuPages & ListPageSource <a name =" cmenupages_list " ></a >
With the help of ` MenuPages ` as our reaction manager and ` ListPageSource ` as our data and format
manager. We can fully make an operational paginator with them.
#### Example
``` python
from discord.ext import menus
class MySource (menus .ListPageSource ):
async def format_page (self , menu , entries ):
return f " This is number { entries} . "
@bot.command ()
async def your_command (ctx ):
data = [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ]
formatter = MySource(data, per_page = 1 )
menu = menus.MenuPages(formatter)
await menu.start(ctx)
```
#### Explanation
1 . ` MySource ` acts as a formatter, given ` data ` , elements will be separated given by ` per_page ` kwargs.
In this case, each element is separated as a single page.
2 . ` MenuPages ` accepts any class that subclasses ` menus.PageSource ` . This includes ` ListPageSource ` (our ` MySource ` ).
3 . ` MenuPages ` adds all the necessary reactions for navigation.
4 . everytime ` MenuPages ` receives a reaction, it processes them on which way to go. After that, it calls
` format_page ` after it processed on which page to show.
5 . When ` format_page ` is called. ` menu ` would be ` MenuPages ` instance and ` entries ` will be
the values that were separated given by ` per_page ` from ` data ` .
6 . Anything that is returned in ` format_page ` are displayed onto your Message.
The value that can be returned are ` Embed ` /` dict ` /` str ` .
#### Output
![ menupages] ( https://cdn.discordapp.com/attachments/652696440396840963/919248946260488222/ezgif.com-gif-maker_1.gif )
### Custom MenuPages <a name =" cmenupages " ></a >
If you want to customize the reaction. You would need to override ` MenuPages ` class. As we know,
` MenuPages ` is the class that handles the reaction. We can fully change this to fit our needs.
#### Example
``` python
import discord
from discord.ext import menus
from discord.ext.menus import button, First, Last
class MyMenuPages (menus .MenuPages , inherit_buttons = False ):
@button (' <:before_fast_check:754948796139569224>' , position = First(0 ))
async def go_to_first_page (self , payload ):
await self .show_page(0 )
@button (' <:before_check:754948796487565332>' , position = First(1 ))
async def go_to_previous_page (self , payload ):
await self .show_checked_page(self .current_page - 1 )
@button (' <:next_check:754948796361736213>' , position = Last(1 )
async def go_to_next_page(self , payload):
await self .show_checked_page(self .current_page + 1 )
@ button(' <:next_fast_check:754948796391227442>' , position = Last(2 ))
async def go_to_last_page(self , payload):
max_pages = self ._source.get_max_pages()
last_page = max (max_pages - 1 , 0 )
await self .show_page(last_page)
@ button(' <:stop_check:754948796365930517>' , position = Last(0 ))
async def stop_pages(self , payload):
self .stop()
class MySource(menus.ListPageSource):
async def format_page(self , menu, entries):
embed = discord.Embed(
description = f " This is number { entries} . " ,
color = discord.Colour.random()
)
embed.set_footer(text = f " Requested by { menu.ctx.author} " )
return embed
@ bot.command()
async def your_command(ctx):
data = [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ]
formatter = MySource(data, per_page = 1 )
menu = MyMenuPages(formatter)
await menu.start(ctx)
```
# ### Explanation
1 . Setting `inherit_buttons` kwargs to `False ` removes buttons that came from `MenuPages` .
2 . in each `button` decorator, the first argument would be your emoji. Followed by `position` kwargs.
`position` kwargs accepts `Position` , `First` , `Last` class .
3 . `First` refers to your position of button. This class inherits `Position` .
`First` acts as an anchor, where it will always be added before `Last` class gets added.
In this case, the position of buttons would be;
- `First(0 )` , `First(1 )` , `Last(0 )` , `Last(1 )` , `Last(2 )`
4 . `show_page` method sets the `current_page` to your page. In this case, it is used in
`go_to_first_page` where we only wanna show the first page, and `go_to_last_page` where we
only want to show the last page.
5 . `show_checked_page` method uses `show_page` , but checks if it is within the range of your page.
This ensure that there isn' t an out of bound exception.
6 . `format_page` this time returns an embed, with a randomized color to make it pretty.
7 . We would use `MyMenuPages` instead of `MenuPages` because we want to try the custom emoji.
8 . The rest of the explanation are available at [Menu](# menu)
# ### Output

# ### Discussion
As you can see, it is relatively easy to create your own pagination with the default MenuPages. The
complexity increases as you want to start doing customization, however this can be easy once you get the
hang of it.
It is recommended for you to explore other classes for other use cases. However, I' m not gonna be focusing
them here.
Now that we have the fundamentals of menus. We can also construct them using View on our own ways.
# # View <a name="view"></a>
_[Button based pagination]_
This tutorial is only compatible with discord.py 2.0 . Any lower such as discord.py 1.7 will not have this class because
it was not built with the newer feature of discord such as buttons and dropdowns.
You would need to install discord.py 2.0 via git instead of pypi. 2.0 were never released thus not available in pypi.
# ## Brief explanation of View <a name="brief_view"></a>
This class is responsible for handling classes that inherits from [discord.ui.Item](https:// discordpy.readthedocs.io/ en/ master/ api.html# discord.ui.Item).
This includes [discord.ui.Button](https:// discordpy.readthedocs.io/ en/ master/ api.html# discord.ui.Button) which we will
be using in this tutorial as navigation for the pagination.
# ### Classes relating to View
Here are a list of classes that is related to View.
1 . `discord.ui.View`
2 . `discord.ui.Item`
3 . `discord.ui.Button`
4 . `discord.ui.Select`
However, we will only talk about 2 here which is `discord.ui.View` and `discord.ui.Button` . Those are the only thing we
need for the pagination.
To start, `discord.ui.View` works similarly like `menus.Menu` . To demonstrate, here' s an example.
```python
import discord
from discord import ui
class MyView(ui.View):
@ ui.button(label = " Hello" , emoji = " \U0001f590 " , style = discord.ButtonStyle.blurple)
async def on_click_hello(self , button, interaction):
await interaction.response.send_message(" Hi" )
@ bot.command()
async def your_command(ctx):
view = MyView()
await ctx.send(" Click button" , view = view)
```
# ### Explanation
1 . `ui.button` is a decorator that will create a Button. The parameter are almost the same as [discord.ui.Button](https:// discordpy.readthedocs.io/ en/ master/ api.html# discord.ui.Button).
2 . The said decorator will call the callback when clicked which in this case refers to `on_click_hello` method. It will
pass `button` which is `discord.ui.Button` and `interaction` which is `discord.Interaction` as the parameter.
3 . `interaction.response` returns [`InteractionResponse` ](https:// discordpy.readthedocs.io/ en/ master/ api.html# discord.InteractionResponse)
instance, which you can use to respond to the user. We will use
`send_message` for now to send a message.
4 . `MyView` is instantiated
5 . `abc.Messageable.send` contains `view` kwargs which you can pass your `View` instance into. In our case, `view` is
the instance of `MyView` .
# ### Output

# ## Pagination with View <a name="pag_view"></a>
With this brief knowledge, we can now make pagination with View. As we know, `MenuPages` acts as the navigation of
the pagination while `ListPageSource` acts as the formatter which format the data and the page. While `menus.Menu` handles
reactions and `menus.MenuPages` contains everything about pagination handling that we really needed. We only need
the handling part.
Based on this knowledge alone, we can combine `MenuPages` with `discord.ui.View` to make a fully functioning paginator
with `discord.ui.Button` as the navigation. This code will be similar to [Custom MenuPages](# cmenupages) code.
# ### Example
```python
import discord
from discord import ui
from discord.ext import menus
class MyMenuPages(ui.View, menus.MenuPages):
def __init__ (self , source):
super ().__init__ (timeout = 60 )
self ._source = source
self .current_page = 0
self .ctx = None
self .message = None
async def start(self , ctx, * , channel = None , wait = False ):
# We wont be using wait/channel, you can implement them yourself. This is to match the MenuPages signature.
await self ._source._prepare_once()
self .ctx = ctx
self .message = await self .send_initial_message(ctx, ctx.channel)
async def _get_kwargs_from_page(self , page):
""" This method calls ListPageSource.format_page class"""
value = await super ()._get_kwargs_from_page(page)
if ' view' not in value:
value.update({' view' : self })
return value
async def interaction_check(self , interaction):
""" Only allow the author that invoke the command to be able to use the interaction"""
return interaction.user == self .ctx.author
# This is extremely similar to Custom MenuPages(I will not explain these)
@ ui.button(emoji = ' <:before_fast_check:754948796139569224>' , style = discord.ButtonStyle.blurple)
async def first_page(self , button, interaction):
await self .show_page(0 )
@ ui.button(emoji = ' <:before_check:754948796487565332>' , style = discord.ButtonStyle.blurple)
async def before_page(self , button, interaction):
await self .show_checked_page(self .current_page - 1 )
@ ui.button(emoji = ' <:stop_check:754948796365930517>' , style = discord.ButtonStyle.blurple)
async def stop_page(self , button, interaction):
self .stop()
@ ui.button(emoji = ' <:next_check:754948796361736213>' , style = discord.ButtonStyle.blurple)
async def next_page(self , button, interaction):
await self .show_checked_page(self .current_page + 1 )
@ ui.button(emoji = ' <:next_fast_check:754948796391227442>' , style = discord.ButtonStyle.blurple)
async def last_page(self , button, interaction):
await self .show_page(self ._source.get_max_pages() - 1 )
@ bot.command()
async def your_command(ctx):
data = [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ]
formatter = MySource(data, per_page = 1 ) # MySource came from Custom MenuPages subtopic. [Please refer to that]
menu = MyMenuPages(formatter)
await menu.start(ctx)
```
_Jesus christ that' s a long code_
# ### Explanation
1 . We inherit both `ui.View` and `menus.MenuPages` . Where we will borrow methods from `menus.MenuPages` while using
`ui.View` as our main navigation for the pagination.
2 . In `__init__ ` , the `super ().__init__ ` will refer to `ui.View` instead. To understand why this is , learn Method
Resolution Order(MRO ). `source` argument must be a `PageSource` instance. The rest of the attribute are required to be
assign because `MenuPages` will use them.
3 . In `start` method, it will follow the `MenuPages` method signature. `self .send_initial_message` is a method from
`MenuPages` , it acts as sending a message to the user and returns a Message object . We will store them in `self .message`
4 . `self ._source._prepare_once` is a method to declare that the `PageSource` object has started.
5 . `_get_kwargs_from_page` method is also from `MenuPages` , it is responsible for calling `format_page` and returns a
dictionary which will directly be used in `discord.Message.edit` kwargs. We take advantage of this and put `view` as the
to put our `View` object into the message for navigation.
6 . `interaction_check` is a method of `ui.View` . It gets called when a button is clicked. Return `True ` to allow for
callback to be called. We will use `interaction.user` where it' s the person who clicked the button to check if they are
our author. If it is , it' s True else return False. Optionally, you would send an ephemaral message to the user if it is
not the author.
7 . The rest of the explanation must refer to [Custom MenuPages](# cmenupages) subtopic. It is exactly the same explanation.
# ### Output

# ### Discussion
While yes it may seem long . But keep in mind, you would only do `MyMenuPages` once, after that, you can create infinite
amount of `ListPageSource` that fit your need for all of the pagination you will ever need. Feel free to derive this code
into a much more advance handler. I' ve only talked briefly on how to use `ui.View`. There' s plenty more uses you can do
with it and I would recommend exploring more on `ui.View` . Danny has created plenty of View examples here: [Click Here](https:// github.com/ Rapptz/ discord.py/ tree/ master/ examples/ views).
# The end <a name="the_end"></a>
Congrats, you' ve reached the end of this tutorial. I don' t recommend skipping any of the topic here because you will be
left confused on what you' ve just read and learnt nothing. Each subtopic is linked to each other hence why. Anyways, any
question regarding `ui.View` and `discord- ext- menus` can be asked in discord.py. I don' t check this gist that much. So your
question wont be answered that quickly.
Tbh i wrote this because i need to remind myself about this in about a year. that' s all, thanks.