Skip to content

Instantly share code, notes, and snippets.

@InterStella0
Last active December 26, 2024 15:10
Show Gist options
  • Save InterStella0/454cc51e05e60e63b81ea2e8490ef140 to your computer and use it in GitHub Desktop.
Save InterStella0/454cc51e05e60e63b81ea2e8490ef140 to your computer and use it in GitHub Desktop.

Revisions

  1. InterStella0 revised this gist May 18, 2022. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion Pagination_walkthrough.md
    Original file line number Diff line number Diff line change
    @@ -175,7 +175,7 @@ class MyMenuPages(menus.MenuPages, inherit_buttons=False):
    async def go_to_previous_page(self, payload):
    await self.show_checked_page(self.current_page - 1)

    @button('<:next_check:754948796361736213>', position=Last(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)

  2. InterStella0 revised this gist Dec 12, 2021. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions Pagination_walkthrough.md
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,7 @@
    # Pagination Walkthrough in discord.py
    In this tutorial I will go through the entire length of pagination in discord.py

    I kindly ask you to go through the entire thing, as it is **not recommended** to skip the subtopics because they are interconnected in some way.
    ## Table Content
    - [Getting Started](#start)
    - [Menu](#menu)
  3. InterStella0 revised this gist Dec 12, 2021. 1 changed file with 3 additions and 0 deletions.
    3 changes: 3 additions & 0 deletions Pagination_walkthrough.md
    Original file line number Diff line number Diff line change
    @@ -223,6 +223,9 @@ This ensure that there isn't an out of bound exception.
    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)

    As a reference, here are the emojis I'm using currently.
    ![emojis](https://cdn.discordapp.com/attachments/652696440396840963/919296978481975446/unknown.png)

    #### Output
    ![custom_menupages](https://cdn.discordapp.com/attachments/777501555687292928/919262731675250698/ezgif.com-gif-maker_2.gif)

  4. InterStella0 created this gist Dec 11, 2021.
    393 changes: 393 additions & 0 deletions Pagination_walkthrough.md
    Original file line number Diff line number Diff line change
    @@ -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
    ![custom_menupages](https://cdn.discordapp.com/attachments/777501555687292928/919262731675250698/ezgif.com-gif-maker_2.gif)

    #### 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
    ![view_button](https://cdn.discordapp.com/attachments/777501555687292928/919268448708730930/ezgif.com-gif-maker_3.gif)

    ### 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
    ![view_pagination](https://cdn.discordapp.com/attachments/777501555687292928/919281737446608916/ezgif.com-gif-maker_4.gif)

    #### 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.