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

Annotation of /branches/4.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: 9362 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-2022 OSN Inc.
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 DiscordClient from "../client/Client";
21     import MessageEmbed from "../client/MessageEmbed";
22     import { v4 as uuid } from 'uuid';
23     import { ButtonInteraction, InteractionCollector, InteractionReplyOptions, Message, MessageActionRow, MessageButton, MessageEditOptions, MessageOptions, ReplyMessageOptions } from "discord.js";
24     import { emoji } from "./Emoji";
25    
26     export interface EmbedBuilderOptions<T> {
27     data: Array<T>;
28     currentPage: number;
29     maxPages: number;
30     }
31    
32     export interface FetchDataOption {
33     currentPage: number;
34     offset: number;
35     limit: number;
36     }
37    
38     export interface PaginationOptions<T> {
39     limit: number;
40     guild_id: string;
41     channel_id: string;
42     user_id?: string;
43     timeout?: number;
44     maxData?: (options: FetchDataOption) => Promise<number>;
45     fetchData?: (options: FetchDataOption) => Promise<T[]>;
46     messageOptions?: ReplyMessageOptions & MessageOptions & InteractionReplyOptions & MessageEditOptions;
47     embedBuilder: (options: EmbedBuilderOptions<T>) => MessageEmbed;
48     actionRowBuilder?: (options: { first: boolean, last: boolean, next: boolean, back: boolean }, id: string) => MessageActionRow<MessageButton>;
49     }
50    
51     export default class Pagination<T> {
52     protected readonly client = DiscordClient.client;
53     protected readonly id: string;
54     protected maxPage: number = 0;
55     protected currentPage: number = 1;
56     protected currentData: T[] = [];
57    
58     constructor(protected readonly data: Array<T> | null = [], protected readonly options: PaginationOptions<T>) {
59     this.id = uuid();
60     }
61    
62     getOffset(page: number = 1) {
63     return (page - 1) * this.options.limit;
64     }
65    
66     async getPaginatedData(page: number = 1) {
67     console.log(page, this.getOffset(page));
68    
69     if (this.options.fetchData)
70     this.currentData = await this.options.fetchData({
71     currentPage: page,
72     limit: this.options.limit,
73     offset: this.getOffset(page)
74     });
75    
76     return this.data ? this.data.slice(this.getOffset(page), this.getOffset(page) + this.options.limit) : this.currentData;
77     }
78    
79     async getEmbed(page: number = 1): Promise<MessageEmbed> {
80     const data = await this.getPaginatedData(page);
81    
82     return this.options.embedBuilder({
83     data: this.data ? data : this.currentData,
84     currentPage: this.currentPage,
85     maxPages: Math.ceil((this.data?.length ?? this.maxPage) / this.options.limit),
86     });
87     }
88    
89     async getMessageOptions(page: number = 1, actionRowOptions: { first: boolean, last: boolean, next: boolean, back: boolean } | undefined = undefined, optionsToMerge: ReplyMessageOptions & MessageOptions & InteractionReplyOptions & MessageEditOptions = {}) {
90     const options = {...this.options.messageOptions, ...optionsToMerge};
91     const actionRowOptionsDup = actionRowOptions ? {...actionRowOptions} : { first: true, last: true, next: true, back: true };
92    
93     if (this.options.maxData && this.maxPage === 0)
94     this.maxPage = await this.options.maxData({
95     currentPage: page,
96     limit: this.options.limit,
97     offset: this.getOffset(page)
98     });
99    
100     console.log("Max Page", this.maxPage);
101    
102     if (actionRowOptionsDup && page <= 1) {
103     actionRowOptionsDup.back = false;
104     actionRowOptionsDup.first = false;
105     }
106    
107     if (actionRowOptionsDup && page >= Math.ceil((this.data?.length ?? this.maxPage) / this.options.limit)) {
108     actionRowOptionsDup.last = false
109     actionRowOptionsDup.next = false;
110     }
111    
112     options.embeds ??= [];
113     options.embeds.push(await this.getEmbed(page));
114    
115     options.components ??= [];
116     options.components = [this.getActionRow(actionRowOptionsDup), ...options.components];
117    
118     return options;
119     }
120    
121     getActionRow({ first, last, next, back }: { first: boolean, last: boolean, next: boolean, back: boolean } = { first: true, last: true, next: true, back: true }) {
122     if (this.options.actionRowBuilder) {
123     return this.options.actionRowBuilder({ first, last, next, back }, this.id);
124     }
125    
126     const actionRow = new MessageActionRow<MessageButton>();
127    
128     actionRow.addComponents(
129     new MessageButton()
130     .setCustomId(`pagination_first_${this.id}`)
131     .setStyle("PRIMARY")
132     .setDisabled(!first)
133     .setEmoji(emoji('ArrowLeft')!),
134     new MessageButton()
135     .setCustomId(`pagination_back_${this.id}`)
136     .setStyle("PRIMARY")
137     .setDisabled(!back)
138     .setEmoji(emoji('ChevronLeft')!),
139     new MessageButton()
140     .setCustomId(`pagination_next_${this.id}`)
141     .setStyle("PRIMARY")
142     .setDisabled(!next)
143     .setEmoji(emoji('ChevronRight')!),
144     new MessageButton()
145     .setCustomId(`pagination_last_${this.id}`)
146     .setStyle("PRIMARY")
147     .setDisabled(!last)
148     .setEmoji(emoji('ArrowRight')!)
149     );
150    
151     return actionRow;
152     }
153    
154     async start(message: Message) {
155     const collector = new InteractionCollector(this.client, {
156     guild: this.options.guild_id,
157     channel: this.options.channel_id,
158     interactionType: 'MESSAGE_COMPONENT',
159     componentType: 'BUTTON',
160     message,
161     time: this.options.timeout ?? 60_000,
162     filter: interaction => {
163     if (interaction.inGuild() && (!this.options.user_id || interaction.user.id === this.options.user_id)) {
164     return true;
165     }
166    
167     if (interaction.isRepliable()) {
168     interaction.reply({ content: 'That\'s not under your control or the button controls are expired', ephemeral: true });
169     }
170    
171     return false;
172     },
173     });
174    
175     collector.on("collect", async (interaction: ButtonInteraction) => {
176     if (!interaction.customId.endsWith(this.id)) {
177     return;
178     }
179    
180     // await interaction.deferUpdate();
181    
182     const maxPage = Math.ceil((this.data?.length ?? this.maxPage) / this.options.limit);
183     const componentOptions = { first: true, last: true, next: true, back: true };
184    
185     if ([`pagination_next_${this.id}`, `pagination_back_${this.id}`].includes(interaction.customId)) {
186     console.log('here');
187    
188     if (this.currentPage >= maxPage && interaction.customId === `pagination_next_${this.id}`) {
189     console.log('here');
190     await interaction.reply({ content: maxPage === 1 ? "This is the only page!" : "You've reached the last page!", ephemeral: true });
191     return;
192     }
193    
194     if (this.currentPage <= 1 && interaction.customId === `pagination_back_${this.id}`) {
195     console.log('here');
196     await interaction.reply({ content: maxPage === 1 ? "This is the only page!" : "You're in the very first page!", ephemeral: true });
197     return;
198     }
199     }
200    
201     if (interaction.customId === `pagination_first_${this.id}`)
202     this.currentPage = 1;
203     else if (interaction.customId === `pagination_last_${this.id}`)
204     this.currentPage = maxPage;
205    
206     await interaction.update(await this.getMessageOptions(
207     interaction.customId === `pagination_first_${this.id}` ? 1 :
208     interaction.customId === `pagination_last_${this.id}` ? maxPage :
209     (interaction.customId === `pagination_next_${this.id}` ? (this.currentPage >= maxPage ? this.currentPage : ++this.currentPage) : --this.currentPage),
210     componentOptions,
211     {
212     embeds: [],
213     ...(this.options.messageOptions ?? {})
214     }
215     ));
216     });
217    
218     collector.on("end", async () => {
219     const [component, ...components] = message.components!; // this.getActionRow({ first: false, last: false, next: false, back: false })
220    
221     for (const i in component.components) {
222     component.components[i].disabled = true;
223     }
224    
225     try {
226     await message.edit({ components: [component, ...components] });
227     }
228     catch (e) {
229     console.log(e);
230     }
231     });
232     }
233     }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26