aboutsummaryrefslogtreecommitdiff
path: root/lib/common/ButtonPaginator.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/common/ButtonPaginator.ts')
-rw-r--r--lib/common/ButtonPaginator.ts224
1 files changed, 224 insertions, 0 deletions
diff --git a/lib/common/ButtonPaginator.ts b/lib/common/ButtonPaginator.ts
new file mode 100644
index 0000000..92f3796
--- /dev/null
+++ b/lib/common/ButtonPaginator.ts
@@ -0,0 +1,224 @@
+import { DeleteButton, type CommandMessage, type SlashMessage } from '#lib';
+import { CommandUtil } from 'discord-akairo';
+import {
+ ActionRowBuilder,
+ ButtonBuilder,
+ ButtonStyle,
+ EmbedBuilder,
+ type APIEmbed,
+ type Message,
+ type MessageComponentInteraction
+} from 'discord.js';
+
+/**
+ * Sends multiple embeds with controls to switch between them
+ */
+export class ButtonPaginator {
+ /**
+ * The current page of the paginator
+ */
+ protected curPage: number;
+
+ /**
+ * The paginator message
+ */
+ protected sentMessage: Message | undefined;
+
+ /**
+ * @param message The message that triggered the command
+ * @param embeds The embeds to switch between
+ * @param text The optional text to send with the paginator
+ * @param {} [deleteOnExit=true] Whether the paginator message gets deleted when the exit button is pressed
+ * @param startOn The page to start from (**not** the index)
+ */
+ protected constructor(
+ protected message: CommandMessage | SlashMessage,
+ protected embeds: EmbedBuilder[] | APIEmbed[],
+ protected text: string | null,
+ protected deleteOnExit: boolean,
+ startOn: number
+ ) {
+ this.curPage = startOn - 1;
+
+ // add footers
+ for (let i = 0; i < embeds.length; i++) {
+ if (embeds[i] instanceof EmbedBuilder) {
+ (embeds[i] as EmbedBuilder).setFooter({ text: `Page ${(i + 1).toLocaleString()}/${embeds.length.toLocaleString()}` });
+ } else {
+ (embeds[i] as APIEmbed).footer = {
+ text: `Page ${(i + 1).toLocaleString()}/${embeds.length.toLocaleString()}`
+ };
+ }
+ }
+ }
+
+ /**
+ * The number of pages in the paginator
+ */
+ protected get numPages(): number {
+ return this.embeds.length;
+ }
+
+ /**
+ * Sends the paginator message
+ */
+ protected async send() {
+ this.sentMessage = await this.message.util.reply({
+ content: this.text,
+ embeds: [this.embeds[this.curPage]],
+ components: [this.getPaginationRow()]
+ });
+
+ const collector = this.sentMessage.createMessageComponentCollector({
+ filter: (i) => i.customId.startsWith('paginate_'),
+ time: 300_000
+ });
+ collector.on('collect', (i) => void this.collect(i));
+ collector.on('end', () => void this.end());
+ }
+
+ /**
+ * Handles interactions with the paginator
+ * @param interaction The interaction received
+ */
+ protected async collect(interaction: MessageComponentInteraction) {
+ if (interaction.user.id !== this.message.author.id && !this.message.client.config.owners.includes(interaction.user.id))
+ return await interaction?.deferUpdate().catch(() => null);
+
+ switch (interaction.customId) {
+ case 'paginate_beginning':
+ this.curPage = 0;
+ await this.edit(interaction);
+ break;
+ case 'paginate_back':
+ this.curPage--;
+ await this.edit(interaction);
+ break;
+ case 'paginate_stop':
+ if (this.deleteOnExit) {
+ await interaction.deferUpdate().catch(() => null);
+ await this.sentMessage!.delete().catch(() => null);
+ break;
+ } else {
+ await interaction
+ ?.update({
+ content: `${
+ this.text
+ ? `${this.text}
+`
+ : ''
+ }Command closed by user.`,
+ embeds: [],
+ components: []
+ })
+ .catch(() => null);
+ break;
+ }
+ case 'paginate_next':
+ this.curPage++;
+ await this.edit(interaction);
+ break;
+ case 'paginate_end':
+ this.curPage = this.embeds.length - 1;
+ await this.edit(interaction);
+ break;
+ }
+ }
+
+ /**
+ * Ends the paginator
+ */
+ protected async end() {
+ if (this.sentMessage && !CommandUtil.deletedMessages.has(this.sentMessage.id))
+ await this.sentMessage
+ .edit({
+ content: this.text,
+ embeds: [this.embeds[this.curPage]],
+ components: [this.getPaginationRow(true)]
+ })
+ .catch(() => null);
+ }
+
+ /**
+ * Edits the paginator message
+ * @param interaction The interaction received
+ */
+ protected async edit(interaction: MessageComponentInteraction) {
+ await interaction
+ ?.update({
+ content: this.text,
+ embeds: [this.embeds[this.curPage]],
+ components: [this.getPaginationRow()]
+ })
+ .catch(() => null);
+ }
+
+ /**
+ * Generates the pagination row based on the class properties
+ * @param disableAll Whether to disable all buttons
+ * @returns The generated {@link ActionRow}
+ */
+ protected getPaginationRow(disableAll = false) {
+ return new ActionRowBuilder<ButtonBuilder>().addComponents(
+ new ButtonBuilder({
+ style: ButtonStyle.Primary,
+ customId: 'paginate_beginning',
+ emoji: PaginateEmojis.BEGINNING,
+ disabled: disableAll || this.curPage === 0
+ }),
+ new ButtonBuilder({
+ style: ButtonStyle.Primary,
+ customId: 'paginate_back',
+ emoji: PaginateEmojis.BACK,
+ disabled: disableAll || this.curPage === 0
+ }),
+ new ButtonBuilder({
+ style: ButtonStyle.Primary,
+ customId: 'paginate_stop',
+ emoji: PaginateEmojis.STOP,
+ disabled: disableAll
+ }),
+ new ButtonBuilder({
+ style: ButtonStyle.Primary,
+ customId: 'paginate_next',
+ emoji: PaginateEmojis.FORWARD,
+ disabled: disableAll || this.curPage === this.numPages - 1
+ }),
+ new ButtonBuilder({
+ style: ButtonStyle.Primary,
+ customId: 'paginate_end',
+ emoji: PaginateEmojis.END,
+ disabled: disableAll || this.curPage === this.numPages - 1
+ })
+ );
+ }
+
+ /**
+ * Sends multiple embeds with controls to switch between them
+ * @param message The message to respond to
+ * @param embeds The embeds to switch between
+ * @param text The text send with the embeds (optional)
+ * @param deleteOnExit Whether to delete the message when the exit button is clicked (defaults to true)
+ * @param startOn The page to start from (**not** the index)
+ */
+ public static async send(
+ message: CommandMessage | SlashMessage,
+ embeds: EmbedBuilder[] | APIEmbed[],
+ text: string | null = null,
+ deleteOnExit = true,
+ startOn = 1
+ ) {
+ // no need to paginate if there is only one page
+ if (embeds.length === 1) return DeleteButton.send(message, { embeds: embeds });
+
+ return await new ButtonPaginator(message, embeds, text, deleteOnExit, startOn).send();
+ }
+}
+
+export const PaginateEmojis = {
+ BEGINNING: { id: '853667381335162910', name: 'w_paginate_beginning', animated: false } as const,
+ BACK: { id: '853667410203770881', name: 'w_paginate_back', animated: false } as const,
+ STOP: { id: '853667471110570034', name: 'w_paginate_stop', animated: false } as const,
+ FORWARD: { id: '853667492680564747', name: 'w_paginate_next', animated: false } as const,
+ END: { id: '853667514915225640', name: 'w_paginate_end', animated: false } as const
+} as const;