dpy_paginator/README.md
disqualifier 20ea4f9291 fix: single page keeps custom buttons instead of dropping the view (v0.1.2)
a single-page result (max_pages < 2) suppressed the navigation row by dropping the
whole view, which also discarded the consumer's custom per-page buttons. now: if the
page carries custom buttons, keep the view and rebuild with update_buttons(nav=False)
— nav items suppressed, custom buttons kept, and stop() NOT called so their callbacks
still fire. a page with no custom buttons keeps the original drop-the-view behavior.

verified by execution against real discord.py: single page + custom button -> start()
-> callback FIRES on click (view kept, stop() not called); negative control on the old
code drops the button entirely; the no-button single-page case is unregressed.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 17:26:04 -04:00

143 lines
4.7 KiB
Markdown

# dpy_paginator
Button-navigated paginator for discord.py. A `discord.ui.View` that paginates mixed
content (strings, embeds, files, attachments, or dicts of mixed content with custom
buttons) behind previous / jump / next navigation, with an optional cache button.
## Install
`requirements.txt`:
```
dpy_paginator @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_paginator.git@v0.1.2
```
Direct:
```bash
pip install "dpy_paginator @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_paginator.git@v0.1.2"
```
Requires `discord.py` (pulled transitively).
## Basic usage
Plain pages — just navigation:
```python
from dpy_paginator import ButtonPaginator
pages = [discord.Embed(title=f"Page {i}") for i in range(5)]
await ButtonPaginator(pages, author_id=ctx.author.id).start(ctx)
```
`start()` accepts an `Interaction` or any `Messageable` (a `Context`, channel, etc.).
## Emojis
Navigation uses plain Unicode by default — no setup, no emoji upload required. Pass
`emojis=` to override with custom application/guild emojis the bot can use:
```python
ButtonPaginator(pages, emojis={
"previous": "<:icon_back:123...>",
"next": "<:icon_next:123...>",
"cache": "<:icon_cache:123...>",
})
```
Unset keys fall back to the Unicode defaults.
## Page types
A page may be a `str`, `discord.Embed`, `discord.File`/`Attachment`, a sequence of
those, or a `dict`. A dict page can carry `content`, `embed`/`embeds`,
`file`/`files`, and a `buttons` list of custom button configs.
## Custom per-page buttons
Each page can declare its own buttons. They render alongside the navigation row and
are rebuilt from the page dict on every render — so mutating a page's button config
and re-rendering updates the button live.
```python
async def callback_reorder(interaction, button, data, paginator):
await interaction.response.send_message(f"Reordering {data['email']}", ephemeral=True)
pages = []
for session in sessions:
pages.append({
"content": session["_id"],
"embed": build_embed(session),
"buttons": [
{
"label": "Reorder",
"style": discord.ButtonStyle.green,
"emoji": some_emoji, # optional, your own emoji
"disabled": False,
"data": {"email": session["_id"]},
"callback": callback_reorder,
}
],
})
paginator = ButtonPaginator(
pages, cache=None, timeout=900, delete_message_after=True,
mentions_allowed=discord.AllowedMentions.none(), ephemeral=True,
page_text="Session {} of {}",
)
await paginator.start(interaction)
```
Button config keys: `label`, `style`, `emoji`, `disabled`, `data` (arbitrary, passed
to the callback), `callback`. The callback signature is
`async def cb(interaction, button, data, paginator)`. With no callback, the button
echoes its `data`.
## Cache button (mention priming)
Discord clients render `<@id>` as a raw id until the user object is cached locally.
The cache button fixes that: pass `cache=[...]` with one entry per page, where each
entry is a string of the user mentions on that page (`"<@111> <@222> <@333>"`).
When clicked, the button posts the page's mentions in a throwaway ephemeral message
(rendering the `<@id>` tags primes the viewer's client — `allowed_mentions` is
`none()` so no actual ping fires), waits `cache_sleep` seconds (default 1.0) for the
client to resolve them, then re-renders the current page — so the mentions now
display as names without the user having to flip pages manually.
```python
pages, cache = [], []
for group in groups:
pages.append(build_embed(group))
cache.append(" ".join(f"<@{uid}>" for uid in group["user_ids"]))
await ButtonPaginator(pages, cache=cache, cache_sleep=1.0).start(ctx)
```
Omit `cache` or pass `None`/`[]` and the button never appears. When set, `cache`
must have one entry per rendered page — a shorter `cache` raises `ValueError` at
construction rather than failing with an `IndexError` mid-navigation.
## Constructor options
- `pages` — sequence of page content
- `cache` — optional per-page cache strings (enables the cache button)
- `author_id` — restrict interaction to this user
- `timeout` — seconds before the view stops (default 180)
- `delete_message_after` — delete the message on timeout
- `per_page` — entries per page (default 1)
- `mentions_allowed``AllowedMentions` for sends (default `.all()`)
- `ephemeral` — send/edit ephemerally
- `page_text` — format string for the jump button label
- `emojis` — override navigation emojis
## Subclassing
Override `format_page(page)` to transform pages before rendering (e.g. wrap raw data
in an embed). It may be sync or async.
## Versioning
Tagged `vX.Y.Z`. Pin the tag in `requirements.txt`.