/[sudobot]/branches/8.x/src/utils/Pagination.ts
ViewVC logotype

Annotation of /branches/8.x/src/utils/Pagination.ts

Parent Directory Parent Directory | Revision Log Revision Log


Revision 577 - (hide annotations)
Mon Jul 29 18:52:37 2024 UTC (8 months ago) by rakinar2
File MIME type: application/typescript
File size: 12322 byte(s)
chore: add old version archive branches (2.x to 9.x-dev)
1 rakinar2 577 /*
2     * This file is part of SudoBot.
3     *
4     * Copyright (C) 2021-2023 OSN Developers.
5     *
6     * SudoBot is free software; you can redistribute it and/or modify it
7     * under the terms of the GNU Affero General Public License as published by
8     * the Free Software Foundation, either version 3 of the License, or
9     * (at your option) any later version.
10     *
11     * SudoBot is distributed in the hope that it will be useful, but
12     * WITHOUT ANY WARRANTY; without even the implied warranty of
13     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14     * GNU Affero General Public License for more details.
15     *
16     * You should have received a copy of the GNU Affero General Public License
17     * along with SudoBot. If not, see <https://www.gnu.org/licenses/>.
18     */
19    
20     import {
21     ActionRowBuilder,
22     AnySelectMenuInteraction,
23     ButtonBuilder,
24     ButtonInteraction,
25     ButtonStyle,
26     EmbedBuilder,
27     Interaction,
28     InteractionCollector,
29     InteractionReplyOptions,
30     InteractionType,
31     Message,
32     MessageEditOptions,
33     MessageReplyOptions
34     } from "discord.js";
35     import * as uuid from "uuid";
36     import Client from "../core/Client";
37     import { log } from "./Logger";
38     import { getComponentEmojiResolvable } from "./utils";
39    
40     export default class Pagination<T> {
41     protected readonly id: string;
42     protected readonly client: Client<true>;
43     protected maxPage: number = 0;
44     protected currentPage: number = 1;
45     protected currentData: T[] = [];
46     protected metadata: Record<string, unknown>;
47    
48     constructor(protected readonly data: Array<T> | null = [], protected readonly options: PaginationOptions<T>) {
49     this.id = uuid.v4();
50     this.client = options.client;
51     this.metadata = this.options.metadata ?? {};
52     }
53    
54     getOffset(page: number = 1) {
55     return (page - 1) * this.options.limit;
56     }
57    
58     getMetadata<T>(key: string) {
59     return this.metadata[key] as T;
60     }
61    
62     async getPaginatedData(page: number = 1) {
63     if (this.options.fetchData)
64     this.currentData = await this.options.fetchData({
65     currentPage: page,
66     limit: this.options.limit,
67     offset: this.getOffset(page)
68     });
69    
70     return this.data ? this.data.slice(this.getOffset(page), this.getOffset(page) + this.options.limit) : this.currentData;
71     }
72    
73     async getEmbed(page: number = 1): Promise<EmbedBuilder> {
74     const data = await this.getPaginatedData(page);
75    
76     return this.options.embedBuilder({
77     data: this.data ? data : this.currentData,
78     currentPage: this.currentPage,
79     maxPages: Math.max(Math.ceil((this.data?.length ?? this.maxPage) / this.options.limit), 1)
80     });
81     }
82    
83     async getMessageOptions(
84     page: number = 1,
85     actionRowOptions: { first: boolean; last: boolean; next: boolean; back: boolean } | undefined = undefined,
86     optionsToMerge: MessageOptions = {},
87     interaction?: Interaction
88     ) {
89     const options = { ...this.options.messageOptions, ...optionsToMerge };
90     const actionRowOptionsDup = actionRowOptions
91     ? { ...actionRowOptions }
92     : { first: true, last: true, next: true, back: true };
93    
94     if (this.options.maxData && this.maxPage === 0)
95     this.maxPage = await this.options.maxData({
96     currentPage: page,
97     limit: this.options.limit,
98     offset: this.getOffset(page)
99     });
100    
101     if (actionRowOptionsDup && page <= 1) {
102     actionRowOptionsDup.back = false;
103     actionRowOptionsDup.first = false;
104     }
105    
106     if (actionRowOptionsDup && page >= Math.ceil((this.data?.length ?? this.maxPage) / this.options.limit)) {
107     actionRowOptionsDup.last = false;
108     actionRowOptionsDup.next = false;
109     }
110    
111     options.embeds ??= [];
112     options.embeds.push(await this.getEmbed(page));
113    
114     options.components ??= [];
115     options.components = [
116     this.getActionRow(actionRowOptionsDup),
117     ...(this.options.extraActionRows
118     ? (this.options.extraActionRows(interaction) as ActionRowBuilder<ButtonBuilder>[]) ?? []
119     : []),
120     ...options.components
121     ];
122    
123     return options;
124     }
125    
126     getEntryCount() {
127     return this.maxPage;
128     }
129    
130     getActionRow(
131     { first, last, next, back, custom }: { first: boolean; last: boolean; next: boolean; back: boolean; custom?: boolean } = {
132     first: true,
133     last: true,
134     next: true,
135     back: true,
136     custom: true
137     }
138     ) {
139     if (this.options.actionRowBuilder) {
140     return this.options.actionRowBuilder({ first, last, next, back, custom }, this.id);
141     }
142    
143     const actionRow = new ActionRowBuilder<ButtonBuilder>();
144    
145     actionRow.addComponents(
146     new ButtonBuilder()
147     .setCustomId(`pagination_first_${this.id}`)
148     .setStyle(ButtonStyle.Secondary)
149     .setDisabled(!first)
150     .setEmoji(getComponentEmojiResolvable(this.client, "ArrowLeft") ?? "⏮️"),
151     new ButtonBuilder()
152     .setCustomId(`pagination_back_${this.id}`)
153     .setStyle(ButtonStyle.Secondary)
154     .setDisabled(!back)
155     .setEmoji(getComponentEmojiResolvable(this.client, "ChevronLeft") ?? "◀️"),
156     new ButtonBuilder()
157     .setCustomId(`pagination_next_${this.id}`)
158     .setStyle(ButtonStyle.Secondary)
159     .setDisabled(!next)
160     .setEmoji(getComponentEmojiResolvable(this.client, "ChevronRight") ?? "▶️"),
161     new ButtonBuilder()
162     .setCustomId(`pagination_last_${this.id}`)
163     .setStyle(ButtonStyle.Secondary)
164     .setDisabled(!last)
165     .setEmoji(getComponentEmojiResolvable(this.client, "ArrowRight") ?? "⏭️")
166     );
167    
168     return actionRow;
169     }
170    
171     async start(message: Message) {
172     if (this.data?.length === 0) {
173     return;
174     }
175    
176     const collector = new InteractionCollector(this.client, {
177     guild: this.options.guildId,
178     channel: this.options.channelId,
179     interactionType: InteractionType.MessageComponent,
180     message,
181     time: this.options.timeout ?? 60_000,
182     filter: interaction => {
183     if (interaction.inGuild() && (!this.options.userId || interaction.user.id === this.options.userId)) {
184     this.options.onInteraction?.(interaction);
185     return interaction.isButton() && interaction.customId.startsWith("pagination_");
186     }
187    
188     if (interaction.isRepliable()) {
189     interaction.reply({
190     content: "That's not under your control or the button controls are expired",
191     ephemeral: true
192     });
193     }
194    
195     return false;
196     }
197     });
198    
199     collector.on("collect", async (interaction: ButtonInteraction) => {
200     if (!interaction.customId.endsWith(this.id)) {
201     return;
202     }
203    
204     const maxPage = Math.ceil((this.data?.length ?? this.maxPage) / this.options.limit);
205     const componentOptions = { first: true, last: true, next: true, back: true };
206    
207     if ([`pagination_next_${this.id}`, `pagination_back_${this.id}`].includes(interaction.customId)) {
208     if (this.currentPage >= maxPage && interaction.customId === `pagination_next_${this.id}`) {
209     await interaction.reply({
210     content: maxPage === 1 ? "This is the only page!" : "You've reached the last page!",
211     ephemeral: true
212     });
213    
214     return;
215     }
216    
217     if (this.currentPage <= 1 && interaction.customId === `pagination_back_${this.id}`) {
218     await interaction.reply({
219     content: maxPage === 1 ? "This is the only page!" : "You're in the very first page!",
220     ephemeral: true
221     });
222    
223     return;
224     }
225     }
226    
227     if (interaction.customId === `pagination_first_${this.id}`) this.currentPage = 1;
228     else if (interaction.customId === `pagination_last_${this.id}`) this.currentPage = maxPage;
229    
230     await interaction.update(
231     await this.getMessageOptions(
232     interaction.customId === `pagination_first_${this.id}`
233     ? 1
234     : interaction.customId === `pagination_last_${this.id}`
235     ? maxPage
236     : interaction.customId === `pagination_next_${this.id}`
237     ? this.currentPage >= maxPage
238     ? this.currentPage
239     : ++this.currentPage
240     : --this.currentPage,
241     componentOptions,
242     {
243     embeds: [],
244     ...(this.options.messageOptions ?? {})
245     },
246     interaction
247     )
248     );
249     });
250    
251     collector.on("end", async () => {
252     const [, ...components] = message.components!;
253    
254     try {
255     await message.edit({
256     components: this.options.removeComponentsOnDisable
257     ? []
258     : [
259     this.getActionRow({
260     back: false,
261     first: false,
262     last: false,
263     next: false,
264     custom: false
265     }),
266     ...components
267     ]
268     });
269     } catch (e) {
270     log(e);
271     }
272    
273     this.options.onDisable?.(message);
274     });
275     }
276    
277     getCurrentPage() {
278     return this.currentPage;
279     }
280    
281     async update(interaction: AnySelectMenuInteraction | ButtonInteraction, metadata: Record<string, unknown> = {}) {
282     this.metadata = { ...this.metadata, ...metadata };
283    
284     if (this.options.maxData) {
285     this.maxPage = await this.options.maxData({
286     currentPage: this.currentPage,
287     limit: this.options.limit,
288     offset: this.getOffset(this.currentPage)
289     });
290     }
291    
292     return interaction.update(
293     await this.getMessageOptions(
294     this.getCurrentPage(),
295     { first: true, last: true, next: true, back: true },
296     {
297     embeds: [],
298     ...(this.options.messageOptions ?? {})
299     },
300     interaction
301     )
302     );
303     }
304    
305     extraActionRows(interaction: Interaction) {
306     return this.options.extraActionRows?.(interaction) ?? [];
307     }
308     }
309    
310     export interface EmbedBuilderOptions<T> {
311     data: Array<T>;
312     currentPage: number;
313     maxPages: number;
314     }
315    
316     export interface FetchDataOption {
317     currentPage: number;
318     offset: number;
319     limit: number;
320     }
321    
322     export type MessageOptions = MessageReplyOptions & InteractionReplyOptions & MessageEditOptions;
323    
324     export interface PaginationOptions<T> {
325     client: Client<true>;
326     limit: number;
327     guildId: string;
328     channelId: string;
329     userId?: string;
330     timeout?: number;
331     metadata?: Record<string, unknown>;
332     maxData?: (options: FetchDataOption) => Promise<number>;
333     fetchData?: (options: FetchDataOption) => Promise<T[]>;
334     messageOptions?: MessageOptions;
335     embedBuilder: (options: EmbedBuilderOptions<T>) => EmbedBuilder;
336     actionRowBuilder?: (
337     options: { first: boolean; last: boolean; next: boolean; back: boolean; custom?: boolean },
338     id: string
339     ) => ActionRowBuilder<ButtonBuilder>;
340     onDisable?: (message: Message) => unknown;
341     onInteraction?: (interaction: Interaction) => unknown;
342     removeComponentsOnDisable?: boolean;
343     extraActionRows?: (interaction?: Interaction) => ActionRowBuilder[];
344     }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26