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

Annotation of /branches/6.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: 10196 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     ButtonBuilder,
23     ButtonInteraction,
24     ButtonStyle,
25     ComponentType,
26     EmbedBuilder,
27     InteractionCollector,
28     InteractionReplyOptions,
29     InteractionType,
30     Message,
31     MessageEditOptions,
32     MessageReplyOptions
33     } from "discord.js";
34     import * as uuid from "uuid";
35     import Client from "../core/Client";
36     import { log } from "./logger";
37     import { getComponentEmojiResolvable } from "./utils";
38    
39     export default class Pagination<T> {
40     protected readonly id: string;
41     protected readonly client: Client<true>;
42     protected maxPage: number = 0;
43     protected currentPage: number = 1;
44     protected currentData: T[] = [];
45    
46     constructor(protected readonly data: Array<T> | null = [], protected readonly options: PaginationOptions<T>) {
47     this.id = uuid.v4();
48     this.client = options.client;
49     }
50    
51     getOffset(page: number = 1) {
52     return (page - 1) * this.options.limit;
53     }
54    
55     async getPaginatedData(page: number = 1) {
56     if (this.options.fetchData)
57     this.currentData = await this.options.fetchData({
58     currentPage: page,
59     limit: this.options.limit,
60     offset: this.getOffset(page)
61     });
62    
63     return this.data ? this.data.slice(this.getOffset(page), this.getOffset(page) + this.options.limit) : this.currentData;
64     }
65    
66     async getEmbed(page: number = 1): Promise<EmbedBuilder> {
67     const data = await this.getPaginatedData(page);
68    
69     return this.options.embedBuilder({
70     data: this.data ? data : this.currentData,
71     currentPage: this.currentPage,
72     maxPages: Math.max(Math.ceil((this.data?.length ?? this.maxPage) / this.options.limit), 1)
73     });
74     }
75    
76     async getMessageOptions(
77     page: number = 1,
78     actionRowOptions: { first: boolean; last: boolean; next: boolean; back: boolean } | undefined = undefined,
79     optionsToMerge: MessageOptions = {}
80     ) {
81     const options = { ...this.options.messageOptions, ...optionsToMerge };
82     const actionRowOptionsDup = actionRowOptions
83     ? { ...actionRowOptions }
84     : { first: true, last: true, next: true, back: true };
85    
86     if (this.options.maxData && this.maxPage === 0)
87     this.maxPage = await this.options.maxData({
88     currentPage: page,
89     limit: this.options.limit,
90     offset: this.getOffset(page)
91     });
92    
93     if (actionRowOptionsDup && page <= 1) {
94     actionRowOptionsDup.back = false;
95     actionRowOptionsDup.first = false;
96     }
97    
98     if (actionRowOptionsDup && page >= Math.ceil((this.data?.length ?? this.maxPage) / this.options.limit)) {
99     actionRowOptionsDup.last = false;
100     actionRowOptionsDup.next = false;
101     }
102    
103     options.embeds ??= [];
104     options.embeds.push(await this.getEmbed(page));
105    
106     options.components ??= [];
107     options.components = [this.getActionRow(actionRowOptionsDup), ...options.components];
108    
109     return options;
110     }
111    
112     getActionRow(
113     { first, last, next, back }: { first: boolean; last: boolean; next: boolean; back: boolean } = {
114     first: true,
115     last: true,
116     next: true,
117     back: true
118     }
119     ) {
120     if (this.options.actionRowBuilder) {
121     return this.options.actionRowBuilder({ first, last, next, back }, this.id);
122     }
123    
124     const actionRow = new ActionRowBuilder<ButtonBuilder>();
125    
126     actionRow.addComponents(
127     new ButtonBuilder()
128     .setCustomId(`pagination_first_${this.id}`)
129     .setStyle(ButtonStyle.Secondary)
130     .setDisabled(!first)
131     .setEmoji(getComponentEmojiResolvable(this.client, "ArrowLeft") ?? "⏮️"),
132     new ButtonBuilder()
133     .setCustomId(`pagination_back_${this.id}`)
134     .setStyle(ButtonStyle.Secondary)
135     .setDisabled(!back)
136     .setEmoji(getComponentEmojiResolvable(this.client, "ChevronLeft") ?? "◀️"),
137     new ButtonBuilder()
138     .setCustomId(`pagination_next_${this.id}`)
139     .setStyle(ButtonStyle.Secondary)
140     .setDisabled(!next)
141     .setEmoji(getComponentEmojiResolvable(this.client, "ChevronRight") ?? "▶️"),
142     new ButtonBuilder()
143     .setCustomId(`pagination_last_${this.id}`)
144     .setStyle(ButtonStyle.Secondary)
145     .setDisabled(!last)
146     .setEmoji(getComponentEmojiResolvable(this.client, "ArrowRight") ?? "⏭️")
147     );
148    
149     return actionRow;
150     }
151    
152     async start(message: Message) {
153     if (this.data?.length === 0) {
154     return;
155     }
156    
157     const collector = new InteractionCollector(this.client, {
158     guild: this.options.guildId,
159     channel: this.options.channelId,
160     interactionType: InteractionType.MessageComponent,
161     componentType: ComponentType.Button,
162     message,
163     time: this.options.timeout ?? 60_000,
164     filter: interaction => {
165     if (interaction.inGuild() && (!this.options.userId || interaction.user.id === this.options.userId)) {
166     return true;
167     }
168    
169     if (interaction.isRepliable()) {
170     interaction.reply({
171     content: "That's not under your control or the button controls are expired",
172     ephemeral: true
173     });
174     }
175    
176     return false;
177     }
178     });
179    
180     collector.on("collect", async (interaction: ButtonInteraction) => {
181     if (!interaction.customId.endsWith(this.id)) {
182     return;
183     }
184    
185     const maxPage = Math.ceil((this.data?.length ?? this.maxPage) / this.options.limit);
186     const componentOptions = { first: true, last: true, next: true, back: true };
187    
188     if ([`pagination_next_${this.id}`, `pagination_back_${this.id}`].includes(interaction.customId)) {
189     if (this.currentPage >= maxPage && interaction.customId === `pagination_next_${this.id}`) {
190     await interaction.reply({
191     content: maxPage === 1 ? "This is the only page!" : "You've reached the last page!",
192     ephemeral: true
193     });
194    
195     return;
196     }
197    
198     if (this.currentPage <= 1 && interaction.customId === `pagination_back_${this.id}`) {
199     await interaction.reply({
200     content: maxPage === 1 ? "This is the only page!" : "You're in the very first page!",
201     ephemeral: true
202     });
203    
204     return;
205     }
206     }
207    
208     if (interaction.customId === `pagination_first_${this.id}`) this.currentPage = 1;
209     else if (interaction.customId === `pagination_last_${this.id}`) this.currentPage = maxPage;
210    
211     await interaction.update(
212     await this.getMessageOptions(
213     interaction.customId === `pagination_first_${this.id}`
214     ? 1
215     : interaction.customId === `pagination_last_${this.id}`
216     ? maxPage
217     : interaction.customId === `pagination_next_${this.id}`
218     ? this.currentPage >= maxPage
219     ? this.currentPage
220     : ++this.currentPage
221     : --this.currentPage,
222     componentOptions,
223     {
224     embeds: [],
225     ...(this.options.messageOptions ?? {})
226     }
227     )
228     );
229     });
230    
231     collector.on("end", async () => {
232     const [, ...components] = message.components!; // this.getActionRow({ first: false, last: false, next: false, back: false })
233    
234     try {
235     await message.edit({
236     components: [
237     this.getActionRow({
238     back: false,
239     first: false,
240     last: false,
241     next: false
242     }),
243     ...components
244     ]
245     });
246     } catch (e) {
247     log(e);
248     }
249     });
250     }
251     }
252    
253     export interface EmbedBuilderOptions<T> {
254     data: Array<T>;
255     currentPage: number;
256     maxPages: number;
257     }
258    
259     export interface FetchDataOption {
260     currentPage: number;
261     offset: number;
262     limit: number;
263     }
264    
265     export type MessageOptions = MessageReplyOptions & InteractionReplyOptions & MessageEditOptions;
266    
267     export interface PaginationOptions<T> {
268     client: Client<true>;
269     limit: number;
270     guildId: string;
271     channelId: string;
272     userId?: string;
273     timeout?: number;
274     maxData?: (options: FetchDataOption) => Promise<number>;
275     fetchData?: (options: FetchDataOption) => Promise<T[]>;
276     messageOptions?: MessageOptions;
277     embedBuilder: (options: EmbedBuilderOptions<T>) => EmbedBuilder;
278     actionRowBuilder?: (
279     options: { first: boolean; last: boolean; next: boolean; back: boolean },
280     id: string
281     ) => ActionRowBuilder<ButtonBuilder>;
282     }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26