diff --git a/docs/COMMAND-WIKI.md b/docs/COMMAND-WIKI.md index c2cc7eca..410edfad 100644 --- a/docs/COMMAND-WIKI.md +++ b/docs/COMMAND-WIKI.md @@ -258,6 +258,13 @@ - ``uwid``: The Quest ID of the user. - **Subcommands:** None +## pg +- **Aliases:** `pagination`, `paginationtest`, `pgtest` +- **Description:** Test the pagination feature. +- **Examples:**
`.pg`
`.pagination`
`.pagination`
`.pgtest` +- **Options:** None +- **Subcommands:** None + ## ping - **Aliases:** `pong` - **Description:** Ping the bot to see if it is alive. :ping_pong: diff --git a/src/commandDetails/miscellaneous/pagination-test.ts b/src/commandDetails/miscellaneous/pagination-test.ts new file mode 100644 index 00000000..08214ce1 --- /dev/null +++ b/src/commandDetails/miscellaneous/pagination-test.ts @@ -0,0 +1,159 @@ +import { container } from '@sapphire/framework'; +import { CodeyCommandDetails, getUserFromMessage } from '../../codeyCommand'; +import { EmbedBuilder } from 'discord.js'; +import { PaginationBuilder, PaginationBuilderFromText } from '../../utils/pagination'; +import { SapphireMessageExecuteType, SapphireMessageResponse } from '../../codeyCommand'; +import { Colors } from 'discord.js'; + +const PaginationTestExecuteCommand: SapphireMessageExecuteType = async ( + _client, + messageFromUser, + _args, +): Promise => { + const message = messageFromUser; + const author = getUserFromMessage(message).id; + + const embedData: { title: string; description: string; color: number }[] = [ + { + title: 'Page 1', + description: 'Hey there! This is the content of Page 1 💙', + color: 0xff0000, + }, + { + title: 'Page 2', + description: 'Hey there! This is the content of Page 2 🎉', + color: 0x00ff00, + }, + { + title: 'Page 3', + description: 'Hey there! This is the content of Page 3 👽', + color: 0x0000ff, + }, + { + title: 'Page 4', + description: 'Hey there! This is the content of Page 4 🎃', + color: 0xff00ff, + }, + { + title: 'Page 5', + description: 'Hey there! This is the content of Page 5 🎄', + color: 0xffff00, + }, + ]; + + const embeds: EmbedBuilder[] = embedData.map((data) => + new EmbedBuilder() + .setColor(Colors.Blue) + .setTitle(data.title) + .setDescription(data.description) + .setColor(data.color), + ); + + try { + await PaginationBuilder(message, author, embeds); // 1. Test Embed List Pagination + + // 2. Test Large Text Pagination + // await PaginationBuilderFromText( + // message, + // author, + // `Lorem Ipsum is simply dummy text of the printing and typesetting industry. \ + // Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make \ + // a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, \ + // remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \ + // of Lorem Ipsum. Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, \ + // consectetur, from a \ + // Lorem \ + // Ipsum passage, and going through the cites of the word in classical literature, \ + // discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and \ + // 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, \ + // written in 45 BC. This book is a treatise on the theory of ethics, \ + // very popular \ + // during the Renaissance. The first \ + // line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.`, + // ); + + // 3. Test Empty String Case + // await PaginationBuilderFromText(message, author, "") // no content test case + + // 4. Test Large Text Pagination without ignoreNewLines (Spaces needed after \) + await PaginationBuilderFromText( + message, + author, + `Lorem Ipsum is simply dummy text of the printing and typesetting industry. \ + Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make \ + a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, \ + remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \ + remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \ + remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \ + remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \ + remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \ + remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \ + remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \ + remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \ + remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \ + remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \ + remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \ + of Lorem Ipsum. Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, \ + consectetur, from a \ + Lorem \ + Ipsum passage, and going through the cites of the word in classical literature, \ + discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and \ + 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, \ + written in 45 BC. This book is a treatise on the theory of ethics, \ + very popular \ + during the Renaissance. The first \ + line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.`, + ); + + // 5. Test Leaderboard Text Pagination with ignoreNewLines + await PaginationBuilderFromText( + message, + author, + `1. palepinkroses#0 - 69749 :codey_coin: \ +2. cho_c0.#0 - 49700 :codey_coin: \ +3. infinit3e#0 - 47952 :codey_coin: \ +4. picowchew#0 - 29696 :codey_coin: \ +5. redapple410#0 - 20237 :codey_coin: \ +6. mcpenguin6194#0 - 19240 :codey_coin: \ +7. fylixz#0 - 18580 :codey_coin: \ +8. antangelo#0 - 16037 :codey_coin: \ +9. elegy2333#0 - 15842 :codey_coin: \ +10. icanc#0 - 15828 :codey_coin: \ +11. sagar1#0 - 15700 :codey_coin: \ +12. sagar2#0 - 15600 :codey_coin: \ +13. sagar3#0 - 15500 :codey_coin: \ +14. sagar4#0 - 15400 :codey_coin: \ +15. sagar5#0 - 15300 :codey_coin: \ +16. sagar6#0 - 15200 :codey_coin: \ +17. sagar7#0 - 15100 :codey_coin: \ +18. sagar8#0 - 15000 :codey_coin: \ +19. sagar9#0 - 14900 :codey_coin: \ +20. sagar10#0 - 14800 :codey_coin: \ +21. sagar11#0 - 14700 :codey_coin: \ +22. sagar12#0 - 14600 :codey_coin: \ +Your Position \ +You are currently #213 in the leaderboard with 553 :codey_coin:.`, + true, + ); + } catch (error) { + await message.reply('Error or timeout occurred during navigation.'); + } +}; + +export const paginationTestCommandDetails: CodeyCommandDetails = { + name: 'pg', + aliases: ['pagination', 'paginationtest', 'pgtest'], + description: 'Test the pagination feature.', + detailedDescription: `**Examples:** + \`${container.botPrefix}pg\` + \`${container.botPrefix}pagination\` + \`${container.botPrefix}pagination\` + \`${container.botPrefix}pgtest\``, + + isCommandResponseEphemeral: false, + messageWhenExecutingCommand: 'Testing pagination...', + executeCommand: PaginationTestExecuteCommand, + messageIfFailure: 'Could not test pagination.', + options: [], + subcommandDetails: {}, +}; diff --git a/src/commands/miscellaneous/pagination-test.ts b/src/commands/miscellaneous/pagination-test.ts new file mode 100644 index 00000000..a0d3e7dc --- /dev/null +++ b/src/commands/miscellaneous/pagination-test.ts @@ -0,0 +1,16 @@ +import { Command } from '@sapphire/framework'; +import { CodeyCommand } from '../../codeyCommand'; +import { paginationTestCommandDetails } from '../../commandDetails/miscellaneous/pagination-test'; + +export class MiscellaneousPaginationTestCommand extends CodeyCommand { + details = paginationTestCommandDetails; + + public constructor(context: Command.Context, options: Command.Options) { + super(context, { + ...options, + aliases: paginationTestCommandDetails.aliases, + description: paginationTestCommandDetails.description, + detailedDescription: paginationTestCommandDetails.detailedDescription, + }); + } +} diff --git a/src/utils/pagination.ts b/src/utils/pagination.ts new file mode 100644 index 00000000..3c9399a9 --- /dev/null +++ b/src/utils/pagination.ts @@ -0,0 +1,205 @@ +import { + EmbedBuilder, + ButtonBuilder, + ButtonStyle, + ActionRowBuilder, + ButtonInteraction, + ComponentType, + Message, + ChatInputCommandInteraction, + CacheType, +} from 'discord.js'; + +const COLLECTOR_TIMEOUT = 300000; +const MAX_CHARS_PER_PAGE = 2048; +const MAX_PAGES = 25; +const MAX_NEWLINES_PER_PAGE = 10; +const getRandomColor = (): number => Math.floor(Math.random() * 16777215); + +const textToPages = (text: string, maxChars: number, ignoreNewLines: boolean): string[] => { + const pages: string[] = []; + let currentPage = ''; + let newLineCount = 0; + let charCount = 0; + + for (let i = 0; i < text.length; i++) { + currentPage += text[i]; + charCount++; + if (text[i] === '\n') { + newLineCount++; + } + + if ( + (text[i] === '\n' && !ignoreNewLines) || + charCount >= maxChars || + newLineCount === MAX_NEWLINES_PER_PAGE + ) { + pages.push(currentPage.trim()); + currentPage = ''; + charCount = 0; + newLineCount = 0; + } + } + + if (currentPage.trim()) { + pages.push(currentPage.trim()); + } + return pages; +}; + +export const PaginationBuilder = async ( + originalMessage: Message | ChatInputCommandInteraction, + author: string, + embedPages: EmbedBuilder[], + timeout: number = COLLECTOR_TIMEOUT, +): Promise | undefined> => { + try { + if (!embedPages || !embedPages.length) { + await originalMessage.reply({ + embeds: [new EmbedBuilder().setColor(0xff0000).setDescription('No pages to display.')], + }); + return; + } + if (embedPages.length > MAX_PAGES) { + await originalMessage.reply({ + embeds: [ + new EmbedBuilder() + .setColor(0xff0000) + .setDescription( + `Too much content to display. Limit is ${MAX_PAGES} pages. \nCurrent content produces ${embedPages.length} pages.`, + ), + ], + }); + return; + } + let currentPage = 0; + const firstButton = new ButtonBuilder() + .setCustomId('first') + .setEmoji('⏮️') + // .setLabel('First') + .setStyle(ButtonStyle.Primary) + .setDisabled(true); + const previousButton = new ButtonBuilder() + .setCustomId('previous') + .setEmoji('⬅️') + // .setLabel('Previous') + .setStyle(ButtonStyle.Primary) + .setDisabled(true); + const pageCount = new ButtonBuilder() + .setCustomId('pagecount') + .setLabel(`${currentPage + 1}/${embedPages.length}`) + .setStyle(ButtonStyle.Secondary) + .setDisabled(true); + const nextButton = new ButtonBuilder() + .setCustomId('next') + .setEmoji('➡️') + // .setLabel('Next') + .setStyle(ButtonStyle.Primary) + .setDisabled(embedPages.length <= 1); + const lastButton = new ButtonBuilder() + .setCustomId('last') + .setEmoji('⏭️') + // .setLabel('Last') + .setStyle(ButtonStyle.Primary) + .setDisabled(embedPages.length <= 1); + const actionRow = new ActionRowBuilder().addComponents( + firstButton, + previousButton, + pageCount, + nextButton, + lastButton, + ); + + const message = await originalMessage.reply({ + embeds: [embedPages[currentPage]], + components: [actionRow], + fetchReply: true, + }); + + await message.edit({ + embeds: [embedPages[currentPage]], + components: [actionRow], + }); + + const collector = message.createMessageComponentCollector({ + filter: (interaction) => interaction.user.id === author, + componentType: ComponentType.Button, + idle: timeout, + }); + + collector.on('collect', async (buttonInteraction: ButtonInteraction) => { + await buttonInteraction.deferUpdate(); + + switch (buttonInteraction.customId) { + case 'first': + currentPage = 0; + break; + case 'previous': + currentPage = Math.max(0, currentPage - 1); + break; + case 'next': + currentPage = Math.min(embedPages.length - 1, currentPage + 1); + break; + case 'last': + currentPage = embedPages.length - 1; + break; + } + + firstButton.setDisabled(currentPage === 0); + previousButton.setDisabled(currentPage === 0); + nextButton.setDisabled(currentPage === embedPages.length - 1); + lastButton.setDisabled(currentPage === embedPages.length - 1); + pageCount.setLabel(`${currentPage + 1}/${embedPages.length}`); + + await message.edit({ + embeds: [embedPages[currentPage]], + components: [actionRow], + }); + }); + + collector.on('end', async () => { + firstButton.setDisabled(true); + previousButton.setDisabled(true); + nextButton.setDisabled(true); + lastButton.setDisabled(true); + pageCount.setDisabled(true); + + await message.edit({ + components: [actionRow], + }); + + setTimeout(async () => { + await message.edit({ + components: [], + }); + }, 3000); + }); + + return message; + } catch (error) { + return undefined; + } +}; + +export const PaginationBuilderFromText = async ( + originalMessage: Message | ChatInputCommandInteraction, + author: string, + text: string, + ignoreNewLines = false, + textPageSize: number = MAX_CHARS_PER_PAGE, + timeout: number = COLLECTOR_TIMEOUT, +): Promise | undefined> => { + try { + const textPages = textToPages(text, textPageSize, ignoreNewLines); + const embedPages = textPages.map((text, index) => + new EmbedBuilder() + .setColor(getRandomColor()) + .setTitle('Page ' + (index + 1)) + .setDescription(text), + ); + + return PaginationBuilder(originalMessage, author, embedPages, timeout); + } catch (error) { + return undefined; + } +};