-
Notifications
You must be signed in to change notification settings - Fork 257
/
paginator.py
306 lines (253 loc) · 12.5 KB
/
paginator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
from __future__ import annotations
import asyncio
from typing import TYPE_CHECKING, Any, Dict, Optional
import discord
import traceback
from discord.ext import commands
from discord.ext.commands import Paginator as CommandPaginator
from discord.ext import menus
if TYPE_CHECKING:
from .context import Context
class NumberedPageModal(discord.ui.Modal, title='Go to page'):
page = discord.ui.TextInput(label='Page', placeholder='Enter a number', min_length=1)
def __init__(self, max_pages: Optional[int]) -> None:
super().__init__()
if max_pages is not None:
as_string = str(max_pages)
self.page.placeholder = f'Enter a number between 1 and {as_string}'
self.page.max_length = len(as_string)
async def on_submit(self, interaction: discord.Interaction) -> None:
self.interaction = interaction
self.stop()
class RoboPages(discord.ui.View):
def __init__(
self,
source: menus.PageSource,
*,
ctx: Context,
check_embeds: bool = True,
compact: bool = False,
):
super().__init__()
self.source: menus.PageSource = source
self.check_embeds: bool = check_embeds
self.ctx: Context = ctx
self.message: Optional[discord.Message] = None
self.current_page: int = 0
self.compact: bool = compact
self.clear_items()
self.fill_items()
def fill_items(self) -> None:
if not self.compact:
self.numbered_page.row = 1
self.stop_pages.row = 1
if self.source.is_paginating():
max_pages = self.source.get_max_pages()
use_last_and_first = max_pages is not None and max_pages >= 2
if use_last_and_first:
self.add_item(self.go_to_first_page)
self.add_item(self.go_to_previous_page)
if not self.compact:
self.add_item(self.go_to_current_page)
self.add_item(self.go_to_next_page)
if use_last_and_first:
self.add_item(self.go_to_last_page)
if not self.compact:
self.add_item(self.numbered_page)
self.add_item(self.stop_pages)
async def _get_kwargs_from_page(self, page: int) -> Dict[str, Any]:
value = await discord.utils.maybe_coroutine(self.source.format_page, self, page)
if isinstance(value, dict):
return value
elif isinstance(value, str):
return {'content': value, 'embed': None}
elif isinstance(value, discord.Embed):
return {'embed': value, 'content': None}
else:
return {}
async def show_page(self, interaction: discord.Interaction, page_number: int) -> None:
page = await self.source.get_page(page_number)
self.current_page = page_number
kwargs = await self._get_kwargs_from_page(page)
self._update_labels(page_number)
if kwargs:
if interaction.response.is_done():
if self.message:
await self.message.edit(**kwargs, view=self)
else:
await interaction.response.edit_message(**kwargs, view=self)
def _update_labels(self, page_number: int) -> None:
self.go_to_first_page.disabled = page_number == 0
if self.compact:
max_pages = self.source.get_max_pages()
self.go_to_last_page.disabled = max_pages is None or (page_number + 1) >= max_pages
self.go_to_next_page.disabled = max_pages is not None and (page_number + 1) >= max_pages
self.go_to_previous_page.disabled = page_number == 0
return
self.go_to_current_page.label = str(page_number + 1)
self.go_to_previous_page.label = str(page_number)
self.go_to_next_page.label = str(page_number + 2)
self.go_to_next_page.disabled = False
self.go_to_previous_page.disabled = False
self.go_to_first_page.disabled = False
max_pages = self.source.get_max_pages()
if max_pages is not None:
self.go_to_last_page.disabled = (page_number + 1) >= max_pages
if (page_number + 1) >= max_pages:
self.go_to_next_page.disabled = True
self.go_to_next_page.label = '…'
if page_number == 0:
self.go_to_previous_page.disabled = True
self.go_to_previous_page.label = '…'
async def show_checked_page(self, interaction: discord.Interaction, page_number: int) -> None:
max_pages = self.source.get_max_pages()
try:
if max_pages is None:
# If it doesn't give maximum pages, it cannot be checked
await self.show_page(interaction, page_number)
elif max_pages > page_number >= 0:
await self.show_page(interaction, page_number)
except IndexError:
# An error happened that can be handled, so ignore it.
pass
async def interaction_check(self, interaction: discord.Interaction) -> bool:
if interaction.user and interaction.user.id in (self.ctx.bot.owner_id, self.ctx.author.id):
return True
await interaction.response.send_message('This pagination menu cannot be controlled by you, sorry!', ephemeral=True)
return False
async def on_timeout(self) -> None:
if self.message:
await self.message.edit(view=None)
async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item) -> None:
if interaction.response.is_done():
await interaction.followup.send('An unknown error occurred, sorry', ephemeral=True)
else:
await interaction.response.send_message('An unknown error occurred, sorry', ephemeral=True)
try:
exc = ''.join(traceback.format_exception(type(error), error, error.__traceback__, chain=False))
embed = discord.Embed(
title=f'{self.source.__class__.__name__} Error',
description=f'```py\n{exc}\n```',
timestamp=interaction.created_at,
colour=0xCC3366,
)
embed.add_field(name='User', value=f'{interaction.user} ({interaction.user.id})')
embed.add_field(name='Guild', value=f'{interaction.guild} ({interaction.guild_id})')
embed.add_field(name='Channel', value=f'{interaction.channel} ({interaction.channel_id})')
await self.ctx.bot.stats_webhook.send(embed=embed)
except discord.HTTPException:
pass
async def start(self, *, content: Optional[str] = None, ephemeral: bool = False) -> None:
if self.check_embeds and not self.ctx.channel.permissions_for(self.ctx.me).embed_links: # type: ignore
await self.ctx.send('Bot does not have embed links permission in this channel.', ephemeral=True)
return
await self.source._prepare_once()
page = await self.source.get_page(0)
kwargs = await self._get_kwargs_from_page(page)
if content:
kwargs.setdefault('content', content)
self._update_labels(0)
self.message = await self.ctx.send(**kwargs, view=self, ephemeral=ephemeral)
@discord.ui.button(label='≪', style=discord.ButtonStyle.grey)
async def go_to_first_page(self, interaction: discord.Interaction, button: discord.ui.Button):
"""go to the first page"""
await self.show_page(interaction, 0)
@discord.ui.button(label='Back', style=discord.ButtonStyle.blurple)
async def go_to_previous_page(self, interaction: discord.Interaction, button: discord.ui.Button):
"""go to the previous page"""
await self.show_checked_page(interaction, self.current_page - 1)
@discord.ui.button(label='Current', style=discord.ButtonStyle.grey, disabled=True)
async def go_to_current_page(self, interaction: discord.Interaction, button: discord.ui.Button):
pass
@discord.ui.button(label='Next', style=discord.ButtonStyle.blurple)
async def go_to_next_page(self, interaction: discord.Interaction, button: discord.ui.Button):
"""go to the next page"""
await self.show_checked_page(interaction, self.current_page + 1)
@discord.ui.button(label='≫', style=discord.ButtonStyle.grey)
async def go_to_last_page(self, interaction: discord.Interaction, button: discord.ui.Button):
"""go to the last page"""
# The call here is safe because it's guarded by skip_if
await self.show_page(interaction, self.source.get_max_pages() - 1) # type: ignore
@discord.ui.button(label='Skip to page...', style=discord.ButtonStyle.grey)
async def numbered_page(self, interaction: discord.Interaction, button: discord.ui.Button):
"""lets you type a page number to go to"""
if self.message is None:
return
modal = NumberedPageModal(self.source.get_max_pages())
await interaction.response.send_modal(modal)
timed_out = await modal.wait()
if timed_out:
await interaction.followup.send('Took too long', ephemeral=True)
return
elif self.is_finished():
await modal.interaction.response.send_message('Took too long', ephemeral=True)
return
value = str(modal.page.value)
if not value.isdigit():
await modal.interaction.response.send_message(f'Expected a number not {value!r}', ephemeral=True)
return
value = int(value)
await self.show_checked_page(modal.interaction, value - 1)
if not modal.interaction.response.is_done():
error = modal.page.placeholder.replace('Enter', 'Expected') # type: ignore # Can't be None
await modal.interaction.response.send_message(error, ephemeral=True)
@discord.ui.button(label='Quit', style=discord.ButtonStyle.red)
async def stop_pages(self, interaction: discord.Interaction, button: discord.ui.Button):
"""stops the pagination session."""
await interaction.response.defer()
await interaction.delete_original_response()
self.stop()
class FieldPageSource(menus.ListPageSource):
"""A page source that requires (field_name, field_value) tuple items."""
def __init__(
self,
entries: list[tuple[Any, Any]],
*,
per_page: int = 12,
inline: bool = False,
clear_description: bool = True,
) -> None:
super().__init__(entries, per_page=per_page)
self.embed: discord.Embed = discord.Embed(colour=discord.Colour.blurple())
self.clear_description: bool = clear_description
self.inline: bool = inline
async def format_page(self, menu: RoboPages, entries: list[tuple[Any, Any]]) -> discord.Embed:
self.embed.clear_fields()
if self.clear_description:
self.embed.description = None
for key, value in entries:
self.embed.add_field(name=key, value=value, inline=self.inline)
maximum = self.get_max_pages()
if maximum > 1:
text = f'Page {menu.current_page + 1}/{maximum} ({len(self.entries)} entries)'
self.embed.set_footer(text=text)
return self.embed
class TextPageSource(menus.ListPageSource):
def __init__(self, text, *, prefix='```', suffix='```', max_size=2000):
pages = CommandPaginator(prefix=prefix, suffix=suffix, max_size=max_size - 200)
for line in text.split('\n'):
pages.add_line(line)
super().__init__(entries=pages.pages, per_page=1)
async def format_page(self, menu, content):
maximum = self.get_max_pages()
if maximum > 1:
return f'{content}\nPage {menu.current_page + 1}/{maximum}'
return content
class SimplePageSource(menus.ListPageSource):
async def format_page(self, menu, entries):
pages = []
for index, entry in enumerate(entries, start=menu.current_page * self.per_page):
pages.append(f'{index + 1}. {entry}')
maximum = self.get_max_pages()
if maximum > 1:
footer = f'Page {menu.current_page + 1}/{maximum} ({len(self.entries)} entries)'
menu.embed.set_footer(text=footer)
menu.embed.description = '\n'.join(pages)
return menu.embed
class SimplePages(RoboPages):
"""A simple pagination session reminiscent of the old Pages interface.
Basically an embed with some normal formatting.
"""
def __init__(self, entries, *, ctx: Context, per_page: int = 12):
super().__init__(SimplePageSource(entries, per_page=per_page), ctx=ctx)
self.embed = discord.Embed(colour=discord.Colour.blurple())