Metadata-Version: 2.4
Name: discord-pager
Version: 0.1.0
Summary: A lightweight modern embed + Components V2 layout paginator for discord.py 2.6.0+ (2.7.1+ for cv2).
License: MIT
Project-URL: Homepage, https://github.com/zprix/discord-pager
Project-URL: Repository, https://github.com/zprix/discord-pager
Project-URL: Bug Tracker, https://github.com/zprix/discord-pager/issues
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Communications :: Chat
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: discord.py>=2.6.0
Dynamic: license-file

# discord-pager

[![GitHub license](https://img.shields.io/github/license/zprix/discord-pager.svg)](https://github.com/zprix/discord-pager)
[![Python 3.8+](https://img.shields.io/badge/python-3.8%2B-blue.svg)](https://www.python.org/)
[![Discord.py 2.6.0+](https://img.shields.io/badge/discord.py-2.6.0%2B-blue.svg)](https://github.com/Rapptz/discord.py)
[![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/zprix/discord-pager/pulls)

A lightweight modern embed + Components V2 paginator for discord.py 2.6.0+ (2.7.1+ for cv2)

**Yes!!** You heard that right, Pager also supports the new Components V2 system (`discord.ui.layout`)!


## Table of Contents
- [Requirements](#requirements)
- [Install](#install)
- [Basic Usage](#basic-usage)
- [Custom Views](#merging-in-a-custom-view)
- [Per-Page Views](#per-page-custom-views)
- [Components V2](#components-v2)
- [API Reference](#api)
- [Common Issues](#common-issues)
- [Contributing](#contributing)
- [License](#license)


![Showcase](https://raw.githubusercontent.com/zprix/discord-pager/refs/heads/main/assets/showcase.gif)

![Showcase 2](https://raw.githubusercontent.com/zprix/discord-pager/refs/heads/main/assets/showcase3.png)

![Showcase 3](https://raw.githubusercontent.com/zprix/discord-pager/refs/heads/main/assets/showcase4.png)


## Why discord-pager?
- **We do it all for you!** All the burden of manual page navigation is removed when using Pager
- **CV2 Ready!** Supports Discord's new Components V2 system unlike older paginators before this
- **Flexible!** You can add custom buttons, per-page views, and extra components

## Requirements
- Python 3.8+
- discord.py 2.6.0+ (2.7.1+ for Components V2 features)

## Install

```bash
pip install discord-pager
```

or just copy the `discord_pager/` folder into your bot's project if u want.

## Basic usage

```python
import discord
from discord.ext import commands
from discord_pager import Pager

bot = commands.Bot(command_prefix="pager!!!", intents=discord.Intents.default())


@bot.command()
async def pages(ctx: commands.Context):
    embeds = [
        discord.Embed(title="Page 1", description="This is the first page."),
        discord.Embed(title="Page 2", description="This is the second page."),
        discord.Embed(title="Page 3", description="This is the third page."),
    ]

    paginator = Pager()
    await paginator.start(ctx, embeds)
```

## Merging in a custom view

Custom views can use rows 0-3 (4 rows max). Pager's own controls take
whatever row comes right after the highest one you used, so they always
end up on the last row instead of colliding with your layout.

![Showcase](https://raw.githubusercontent.com/zprix/discord-pager/refs/heads/main/assets/showcase2.png)

```python
import discord
from discord.ext import commands
from discord_pager import Pager, PagerViewError

bot = commands.Bot(command_prefix="pager!!!", intents=discord.Intents.default())


@bot.command()
async def shop(ctx: commands.Context):
    embeds = [
        discord.Embed(title="Sword", description="A sharp blade. 100 gold."),
        discord.Embed(title="Shield", description="Sturdy protection. 80 gold."),
    ]

    custom_view = discord.ui.View(timeout=None)

    buy_button = discord.ui.Button(
        label="Buy", style=discord.ButtonStyle.green, row=0
    )

    async def buy_callback(interaction: discord.Interaction):
        await interaction.response.send_message("Purchased!", ephemeral=True)

    buy_button.callback = buy_callback
    custom_view.add_item(buy_button)

    wishlist_button = discord.ui.Button(
        label="Add to wishlist", style=discord.ButtonStyle.blurple, row=1
    )
    custom_view.add_item(wishlist_button)

    paginator = Pager()

    try:
        await paginator.start(ctx, embeds, view=custom_view)
    except PagerViewError as e:
        await ctx.send(f"Couldn't build the paginator: {e}")
```

## Per-page custom views

If you want the custom controls to *change* alongside the embed (eg. a
different "Buy" button and price per page), pass a `list[discord.ui.View]`
the same length as `pages` (IMPORTANT!!!) instead of a single view. Pager
swaps in the matching view's items every time the page changes:

```python
import discord
from discord.ext import commands
from discord_pager import Pager

bot = commands.Bot(command_prefix="pager!!!", intents=discord.Intents.default())


@bot.command()
async def shop(ctx: commands.Context):
    items = [
        {"name": "Sword", "price": 100},
        {"name": "Shield", "price": 80},
        {"name": "Potion", "price": 15},
    ]

    embeds = [
        discord.Embed(title=item["name"], description=f"{item['price']} gold")
        for item in items
    ]

    views = []
    for item in items:
        v = discord.ui.View(timeout=None)
        buy_button = discord.ui.Button(
            label=f"Buy for {item['price']} gold",
            style=discord.ButtonStyle.green,
            row=0,
        )

        async def buy_callback(interaction: discord.Interaction, item=item):
            await interaction.response.send_message(
                f"Bought {item['name']}!", ephemeral=True
            )

        buy_button.callback = buy_callback
        v.add_item(buy_button)
        views.append(v)

    paginator = Pager()
    await paginator.start(ctx, embeds, view=views)
```

Each view in the list is validated the same way as a single static view
(rows 0-3 only), and `start()` raises `PagerViewError` if the list's length
doesn't match the number of pages. The controls row is based on the highest
row used across *all* the views in the list, so it stays put as you page
through instead of jumping around.

## Components V2

![Components v2](https://raw.githubusercontent.com/zprix/discord-pager/refs/heads/main/assets/showcase5.png)

Discord's newer Components V2 system replaces `embed` and even `content` entirely
with layout components (`Container`, `TextDisplay`, `Section`, etc), so it
needs its own paginator: `PagerCV2`. It works the exact same way as `Pager`,
just with `discord.ui.Container` (or any other layout item really) standing in
for embeds, because a CV2 message can't send embeds at all.

You can also (if you are paging containers) pass `controls_in_container=True` into `PagerCV2(...)`
in order to put the pagination controls inside the container

```python
import discord
from discord.ext import commands
from discord_pager import PagerCV2

bot = commands.Bot(command_prefix="pager!!!", intents=discord.Intents.default())


class ContainerPage(discord.ui.Container):
    def __init__(self, name: str, price: int):
        super().__init__()
        self.add_item(discord.ui.TextDisplay(f"{name}"))


@bot.command()
async def pagescv2(ctx: commands.Context):
    pages = [
        ContainerPage("This is the first page."),
        ContainerPage("This is the second page."),
        ContainerPage("This is the third page."),
    ]

    paginator = PagerCV2()  # if u prefer the controls in the container, use controls_in_container=True
    await paginator.start(ctx, pages)
```

### Merging in extra components

Just like `Pager`'s `view` argument, pass `extra` to keep additional
top-level components (eg. a `Container` with its own buttons) on screen
alongside the paged item:

```python
from discord_pager import PagerCV2, PagerLayoutError

extra_container = discord.ui.Container()
extra_row = discord.ui.ActionRow()


@extra_row.button(label="Add to wishlist", style=discord.ButtonStyle.blurple)
async def wishlist(interaction: discord.Interaction, button: discord.ui.Button):
    await interaction.response.send_message("Added!", ephemeral=True)


extra_container.add_item(extra_row)

paginator = PagerCV2()

try:
    await paginator.start(ctx, pages, extra=extra_container)
except PagerLayoutError as e:
    await ctx.send(f"Couldn't build the paginator: {e}")
```

`extra` can also be a `list[list[discord.ui.Item]]` the same length as
`pages`, one list per page, the same way default `Pager`'s `view` accepts a
`list[discord.ui.View]` for per-page controls. Raises `PagerLayoutError`
if that list's length doesn't match the number of pages. Unlike the
classic `Pager`, there's no row 0-3 restriction to worry about here, since
CV2 top-level components aren't laid out on the same 5-row grid buttons
use.

## API

### `Pager(...)`

| Parameter             | Type                     | Default                       | Description                                                 |
| --------------------- | ------------------------ | ----------------------------- | ----------------------------------------------------------- |
| `timeout`             | `int`                    | `60`                          | Seconds before the view times out.                          |
| `previous_button`     | `discord.ui.Button`      | ◀ grey button                 | Overrides the default previous button.                      |
| `next_button`         | `discord.ui.Button`      | ▶ grey button                 | Overrides the default next button.                          |
| `page_counter_style`  | `discord.ButtonStyle`    | `discord.ButtonStyle.grey`    | Style of the disabled page-counter button.                  |
| `initial_page`        | `int`                    | `0`                           | Page index to start on.                                     |
| `ephemeral`           | `bool`                   | `False`                       | Whether the paginator message is ephemeral.                 |
| `restrict_to_author`  | `bool`                   | `False`                       | If `True`, only the original user can page through.         |

### `await Pager.start(source, pages, view=None)`

- `source`: `discord.Interaction` or `commands.ext.commands.Context`.
- `pages`: `list[discord.Embed]`.
- `view`: optional `discord.ui.View`, or a `list[discord.ui.View]` the same
  length as `pages` for per-page controls. Every item must set `row` to
  0-3; Pager's controls take the row right after the highest one used.
  Raises `PagerViewError` if a row is invalid, or if a list of views
  doesn't match the number of pages.


### `PagerCV2(...)`

Same parameters as `Pager` almost

| Parameter               | Type                     | Default                       | Description                                                          |
| ----------------------- | ------------------------ | ----------------------------- | -------------------------------------------------------------------- |
| `timeout`               | `int`                    | `60`                          | Seconds before the view times out.                                   |
| `previous_button`       | `discord.ui.Button`      | ◀ grey button                 | Overrides the default previous button.                               |
| `next_button`           | `discord.ui.Button`      | ▶ grey button                 | Overrides the default next button.                                   |
| `page_counter_style`    | `discord.ButtonStyle`    | `discord.ButtonStyle.grey`    | Style of the disabled page-counter button.                           |
| `initial_page`          | `int`                    | `0`                           | Page index to start on.                                              |
| `ephemeral`             | `bool`                   | `False`                       | Whether the paginator message is ephemeral.                          |
| `restrict_to_author`    | `bool`                   | `False`                       | If `True`, only the original user can page through.                  |
| `controls_in_container` | `bool`                   | `False`                       | If `True`, controls will be inside the passed containers (if any).   |

### `await PagerCV2.start(source, pages, extra=None)`

- `source`: `discord.Interaction` or `commands.ext.commands.Context`.
- `pages`: `list` of layout items, eg. `list[discord.ui.Container]`.
- `extra`: optional extra components, either a single item /
  list of items shown on every page, or a `list[list[discord.ui.Item]]`
  the same length as `pages` for per-page extras. Raises
  `PagerLayoutError` if a list of per-page extras doesn't match the number
  of pages.

## Common Issues

### `PagerViewError: Invalid row`
Custom views can only use rows 0-3 (meaning max 4 rows). Pager automatically uses the next available row for its controls.

### `PagerViewError: View list length doesn't match pages`
When passing per-page views, the list must have the same length as your pages.

### `PagerLayoutError: Extra list length doesn't match pages`
Same as above, but for CV2 per-page extras.

### My CV2 message isn't showing
Make sure you're using `PagerCV2` (not `Pager`!!!!!) and passing layout items like `discord.ui.Container`, not embeds! You cannot
have embeds in CV2 messages

## Contributing

Contributions are welcome! Feel free to open issues and PRs as you like.

Guidelines:
- Make sure your code is formatted by `yapf`

That's all!

## License

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
