From 0a9db16f86a6685675500b4459918063570516bc Mon Sep 17 00:00:00 2001 From: Peter Willemsen Date: Thu, 15 Sep 2022 13:28:25 +0200 Subject: [PATCH] optimized pagination with realtime database-searches --- .../commands/chapters/ListChaptersCommand.kt | 37 ++++----- .../commands/chapters/RollbackCommand.kt | 16 ++-- .../{SetBackground.kt => EditProfile.kt} | 30 +------- .../kotlin/commands/variate/VariateCommand.kt | 1 + .../kotlin/database/models/UserChapter.kt | 11 ++- src/main/kotlin/ui/PaginationUI.kt | 50 +++++------- src/main/kotlin/ui/RealPaginator.kt | 76 ++++--------------- 7 files changed, 72 insertions(+), 149 deletions(-) rename src/main/kotlin/commands/social/{SetBackground.kt => EditProfile.kt} (83%) diff --git a/src/main/kotlin/commands/chapters/ListChaptersCommand.kt b/src/main/kotlin/commands/chapters/ListChaptersCommand.kt index ad0ca9c..4af3700 100644 --- a/src/main/kotlin/commands/chapters/ListChaptersCommand.kt +++ b/src/main/kotlin/commands/chapters/ListChaptersCommand.kt @@ -2,6 +2,7 @@ package commands.chapters import commands.make.DiffusionParameters import database.chapterDao +import database.models.UserChapter import database.userDao import dev.minn.jda.ktx.events.onCommand import dev.minn.jda.ktx.interactions.components.button @@ -13,6 +14,7 @@ import net.dv8tion.jda.api.JDA import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle import net.dv8tion.jda.api.utils.messages.MessageEditData import replyPaginator +import ui.GetImageCallback import ui.ImageSliderEntry import ui.sendImageSlider import java.net.URL @@ -27,36 +29,37 @@ fun listChaptersCommand(jda: JDA) { .setEphemeral(true).queue() return@onCommand } - event.deferReply(true).queue() - val possibleChapters = - chapterDao.queryBuilder().orderBy("creationTimestamp", false).selectColumns().where() - .eq("userID", user.id).query() - if (possibleChapters.isEmpty()) { + val chaptersCount = + chapterDao.queryBuilder().selectColumns("id").where() + .eq("userID", user.id).countOf() + if (chaptersCount == 0L) { event.reply_("Sorry, we couldn't find any chapters! $miniManual") .setEphemeral(true).queue() return@onCommand } - val chapterEntries = possibleChapters.map { - val latestEntry = it.getLatestEntry() + var lastSelectedChapter: UserChapter? = null + val onImage: GetImageCallback = { index -> + lastSelectedChapter = chapterDao.queryBuilder().selectColumns().limit(1).offset(index).orderBy("creationTimestamp", false).where() + .eq("userID", user.id).queryForFirst() + val latestEntry = lastSelectedChapter!!.getLatestEntry() val parameters = gson.fromJson(latestEntry.parameters, Array::class.java) ImageSliderEntry( description = parameters.first().getPrompt() ?: "No prompt", image = URL(latestEntry.imageURL) ) } - val slider = sendImageSlider("My Chapters", chapterEntries) + val slider = sendImageSlider("My Chapters", chaptersCount, onImage) slider.customActionComponents = listOf(jda.button( label = "Select", style = ButtonStyle.PRIMARY, user = event.user ) { - val chapter = possibleChapters[slider.getIndex()] val parameters = - gson.fromJson(chapter.getLatestEntry().parameters, Array::class.java) + gson.fromJson(lastSelectedChapter!!.getLatestEntry().parameters, Array::class.java) val updateBuilder = userDao.updateBuilder() - updateBuilder.where().eq("id", chapter.userID) - updateBuilder.updateColumnValue("currentChapterId", chapter.id) + updateBuilder.where().eq("id", lastSelectedChapter!!.userID) + updateBuilder.updateColumnValue("currentChapterId", lastSelectedChapter!!.id) updateBuilder.update() it.reply_( @@ -69,9 +72,8 @@ fun listChaptersCommand(jda: JDA) { style = ButtonStyle.DANGER, user = event.user ) { deleteEvent -> - val chapter = possibleChapters[slider.getIndex()] val parameters = - gson.fromJson(chapter.getLatestEntry().parameters, Array::class.java) + gson.fromJson(lastSelectedChapter!!.getLatestEntry().parameters, Array::class.java) deleteEvent.reply_("**Are you sure to delete this chapter?** *${parameters.first().getPrompt()}*") .setEphemeral(true).addActionRow(listOf( @@ -80,8 +82,8 @@ fun listChaptersCommand(jda: JDA) { style = ButtonStyle.DANGER, user = event.user ) { - chapter.delete() - deleteEvent.hook.editMessage(content = "*Delete!*").setComponents().queue() + lastSelectedChapter!!.delete() + deleteEvent.hook.editMessage(content = "*Deleted!*").setComponents().queue() }, jda.button( label = "Keep!", @@ -92,8 +94,7 @@ fun listChaptersCommand(jda: JDA) { } )).queue() }) - event.hook.editOriginal(MessageEditData.fromCreateData(slider.also { jda.addEventListener(it) }.pages[0])) - .setComponents(slider.getControls()).queue() + event.replyPaginator(slider).queue() } catch (e: Exception) { e.printStackTrace() event.reply_("**Error!** $e").setEphemeral(true).queue() diff --git a/src/main/kotlin/commands/chapters/RollbackCommand.kt b/src/main/kotlin/commands/chapters/RollbackCommand.kt index e6b0328..eac477d 100644 --- a/src/main/kotlin/commands/chapters/RollbackCommand.kt +++ b/src/main/kotlin/commands/chapters/RollbackCommand.kt @@ -3,6 +3,7 @@ package commands.chapters import commands.make.DiffusionParameters import database.chapterDao import database.chapterEntryDao +import database.models.ChapterEntry import database.userDao import dev.minn.jda.ktx.events.onCommand import dev.minn.jda.ktx.interactions.components.button @@ -12,6 +13,7 @@ import miniManual import net.dv8tion.jda.api.JDA import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle import replyPaginator +import ui.GetImageCallback import ui.ImageSliderEntry import ui.sendImageSlider import java.net.URL @@ -38,21 +40,23 @@ fun rollbackChapterCommand(jda: JDA) { return@onCommand } - val chapterEntries = usingChapter.getEntries() - val slides = chapterEntries.map { entry -> - val parameters = gson.fromJson(entry.parameters, Array::class.java) + val entryCount = usingChapter.getEntryCount() + var lastEntry: ChapterEntry? = null + val onImage: GetImageCallback = { index -> + lastEntry = usingChapter.getEntryAtIndex(index) + val parameters = gson.fromJson(lastEntry!!.parameters, Array::class.java) ImageSliderEntry( description = parameters.first().getPrompt() ?: "No prompt", - image = URL(entry.imageURL) + image = URL(lastEntry!!.imageURL) ) } - val slider = sendImageSlider("Rollback to", slides) + val slider = sendImageSlider("Rollback to", entryCount, onImage) slider.customActionComponents = listOf(jda.button( label = "Rollback", style = ButtonStyle.PRIMARY, user = event.user ) { - val entryToRollBackTo = chapterEntries[slider.getIndex()] + val entryToRollBackTo = lastEntry!! val db = chapterEntryDao.deleteBuilder() db.where().eq("chapterID", entryToRollBackTo.chapterID).and().gt("creationTimestamp", entryToRollBackTo.creationTimestamp) chapterEntryDao.delete(db.prepare()) diff --git a/src/main/kotlin/commands/social/SetBackground.kt b/src/main/kotlin/commands/social/EditProfile.kt similarity index 83% rename from src/main/kotlin/commands/social/SetBackground.kt rename to src/main/kotlin/commands/social/EditProfile.kt index 8681937..5cd126e 100644 --- a/src/main/kotlin/commands/social/SetBackground.kt +++ b/src/main/kotlin/commands/social/EditProfile.kt @@ -2,8 +2,6 @@ package commands.social import commands.make.DiffusionParameters import database.chapterDao -import database.models.SharedArtCacheEntry -import database.sharedArtCacheEntryDao import database.userDao import dev.minn.jda.ktx.events.onCommand import dev.minn.jda.ktx.interactions.components.button @@ -13,7 +11,6 @@ import gson import miniManual import net.dv8tion.jda.api.JDA import net.dv8tion.jda.api.entities.Message -import net.dv8tion.jda.api.entities.User import net.dv8tion.jda.api.entities.emoji.Emoji import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent @@ -24,39 +21,14 @@ import net.dv8tion.jda.api.interactions.components.selections.SelectMenu import net.dv8tion.jda.api.interactions.components.selections.SelectOption import net.dv8tion.jda.api.requests.restaction.WebhookMessageEditAction import net.dv8tion.jda.api.utils.FileUpload -import org.atteo.evo.inflector.English import ui.makeSelectImageFromQuilt import utils.* -import java.awt.Color -import java.awt.Font -import java.awt.Graphics import java.awt.image.BufferedImage import java.net.URL import javax.imageio.ImageIO -object DropdownBot : ListenerAdapter() { - override fun onSlashCommandInteraction(event: SlashCommandInteractionEvent) { - if (event.name == "food") { - val selectMenu = SelectMenu.create("choose-food") - .addOption("Pizza", "pizza", "Classic") // SelectOption with only the label, value, and description - .addOptions( - SelectOption.of("Hamburger", "hamburger") // another way to create a SelectOption - .withDescription("Tasty") // this time with a description - .withEmoji(Emoji.fromUnicode("\uD83C\uDF54")) // and an emoji - .withDefault(true)) // while also being the default option - .build() - } - } - - override fun onSelectMenuInteraction(event: SelectMenuInteractionEvent) { - if (event.componentId == "choose-food") { - event.reply("You chose " + event.values[0]).queue() - } - } -} - fun setBackgroundCommand(jda: JDA) { - jda.onCommand("set_background") { event -> + jda.onCommand("edit_profile") { event -> try { val user = userDao.queryBuilder().selectColumns("id", "currentChapterId").where() .eq("discordUserID", event.user.id).queryForFirst() diff --git a/src/main/kotlin/commands/variate/VariateCommand.kt b/src/main/kotlin/commands/variate/VariateCommand.kt index 7a1e582..35c9d0b 100644 --- a/src/main/kotlin/commands/variate/VariateCommand.kt +++ b/src/main/kotlin/commands/variate/VariateCommand.kt @@ -37,6 +37,7 @@ fun variateCommand(jda: JDA) { val usingChapter = chapterDao.queryBuilder().selectColumns().where().eq("id", user.currentChapterId).and() .eq("userID", user.id).queryForFirst() + if (usingChapter == null) { event.reply_("Sorry, we couldn't find any chapters! $miniManual") .setEphemeral(true).queue() diff --git a/src/main/kotlin/database/models/UserChapter.kt b/src/main/kotlin/database/models/UserChapter.kt index 4a65d66..a32ec46 100644 --- a/src/main/kotlin/database/models/UserChapter.kt +++ b/src/main/kotlin/database/models/UserChapter.kt @@ -35,9 +35,14 @@ class UserChapter { this.userID = userID } - fun getEntries(): Array { - return chapterEntryDao.queryBuilder().orderBy("creationTimestamp", false).selectColumns().where() - .eq("chapterID", this.id).query().toTypedArray() + fun getEntryAtIndex(index: Long): ChapterEntry { + return chapterEntryDao.queryBuilder().offset(index).limit(1).orderBy("creationTimestamp", false).selectColumns().where() + .eq("chapterID", this.id).queryForFirst() + } + + fun getEntryCount(): Long { + return chapterEntryDao.queryBuilder().selectColumns().where() + .eq("chapterID", this.id).countOf() } fun getLatestEntry(): ChapterEntry { diff --git a/src/main/kotlin/ui/PaginationUI.kt b/src/main/kotlin/ui/PaginationUI.kt index b90bd18..840b265 100644 --- a/src/main/kotlin/ui/PaginationUI.kt +++ b/src/main/kotlin/ui/PaginationUI.kt @@ -1,7 +1,9 @@ package ui +import GetPageCallback import Paginator import dev.minn.jda.ktx.interactions.components.button +import dev.minn.jda.ktx.messages.MessageCreate import getAverageColor import net.dv8tion.jda.api.EmbedBuilder import net.dv8tion.jda.api.entities.User @@ -23,34 +25,37 @@ data class ImageSliderEntry( val image: URL ) -fun sendImageSlider(title: String, items: List): Paginator { - val pagesAmount = items.size - val messages = items.mapIndexed { index, entry -> +typealias GetImageCallback = ((index: Long) -> ImageSliderEntry) + +fun sendImageSlider(title: String, amountOfImages: Long, onImage: GetImageCallback): Paginator { + val getPageCallback: GetPageCallback = { index -> val page = EmbedBuilder() page.setTitle(title) - page.setFooter("Page ${index + 1} / $pagesAmount") + page.setFooter("Page ${index + 1} / $amountOfImages") + val entry = onImage(index) page.setImage(entry.image.toString()) page.setDescription(entry.description) page.setColor(0x33cc33) - page.build() - }.toTypedArray() - return paginator(*messages, expireAfter = 10.minutes) + MessageCreate(embeds = listOf(page.build())) + } + return paginator(amountOfImages, getPage = getPageCallback, expireAfter = 10.minutes) } fun makeSelectImageFromQuilt(event: GenericCommandInteractionEvent, user: User, title: String, quilt: BufferedImage, totalImages: Int, callback: (btnEvent: ButtonInteractionEvent, index: Int) -> Unit): ReplyCallbackAction { - val messages = (0 until totalImages).map { index -> + val getPageCallback: GetPageCallback = { index -> val page = EmbedBuilder() page.setTitle(title) page.setFooter("Image ${index + 1} / $totalImages") page.setImage("attachment://${index + 1}.jpg") - val imageSlice = takeSlice(quilt, totalImages, index) + val imageSlice = takeSlice(quilt, totalImages, index.toInt()) val avgColor = getAverageColor(imageSlice, 0, 0, imageSlice.width, imageSlice.height) page.setColor(avgColor) page.build() - }.toTypedArray() - val imageSelector = paginator(*messages, expireAfter = 10.minutes) + MessageCreate(embeds = listOf(page.build())) + } + val imageSelector = paginator(amountOfPages = totalImages.toLong(), getPage = getPageCallback, expireAfter = 10.minutes) imageSelector.injectMessageCallback = { idx, msgEdit -> - val imageSlice = takeSlice(quilt, totalImages, idx) + val imageSlice = takeSlice(quilt, totalImages, idx.toInt()) msgEdit.setFiles(FileUpload.fromData(bufferedImageToByteArray(imageSlice, "jpg"), "${idx + 1}.jpg")) } imageSelector.customActionComponents = listOf(user.jda.button( @@ -58,27 +63,8 @@ fun makeSelectImageFromQuilt(event: GenericCommandInteractionEvent, user: User, style = ButtonStyle.PRIMARY, user = user ) { btnEvent -> - callback(btnEvent, imageSelector.getIndex()) + callback(btnEvent, imageSelector.getIndex().toInt()) }) val firstSlice = takeSlice(quilt, totalImages, 0) return event.replyPaginator(imageSelector).setFiles(FileUpload.fromData(bufferedImageToByteArray(firstSlice, "jpg"), "1.jpg")) -} - -fun sendPagination( - event: GenericCommandInteractionEvent, - title: String, - items: List, - itemsPerPage: Int -): Paginator { - val chunks = items.chunked(itemsPerPage) - val pagesAmount = chunks.size - val messages = (0 until pagesAmount).map { pageNumber -> - val page = EmbedBuilder() - page.setTitle(title) - page.setFooter("Page ${pageNumber + 1} / $pagesAmount") - page.setDescription(chunks[pageNumber].joinToString("\n")) - page.setColor(0x33cc33) - page.build() - }.toTypedArray() - return paginator(*messages, expireAfter = 10.minutes) } \ No newline at end of file diff --git a/src/main/kotlin/ui/RealPaginator.kt b/src/main/kotlin/ui/RealPaginator.kt index c968375..30ae291 100644 --- a/src/main/kotlin/ui/RealPaginator.kt +++ b/src/main/kotlin/ui/RealPaginator.kt @@ -14,10 +14,8 @@ * limitations under the License. */ -import dev.minn.jda.ktx.messages.MessageCreate import net.dv8tion.jda.api.JDA import net.dv8tion.jda.api.entities.MessageChannel -import net.dv8tion.jda.api.entities.MessageEmbed import net.dv8tion.jda.api.entities.emoji.Emoji import net.dv8tion.jda.api.events.GenericEvent import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent @@ -50,25 +48,26 @@ object PaginatorDefaults { var NEXT: Button = Button.secondary("next", Emoji.fromUnicode("➡️")) } -class Paginator internal constructor(private val nonce: String, private val ttl: Duration): EventListener { +typealias GetPageCallback = ((index: Long) -> MessageCreateData) + +class Paginator internal constructor(private val nonce: String, private val amountOfPages: Long, internal val getPage: GetPageCallback, private val ttl: Duration): EventListener { private var expiresAt: Long = Math.addExact(System.currentTimeMillis(), ttl.inWholeMilliseconds) - private var index = 0 - private val pageCache = mutableListOf() + private var index = 0L private val nextPage: MessageCreateData get() { - index = (index + 1) % pageCache.size - return pageCache[index] + index = (index + 1) % amountOfPages + return getPage(index) } private val prevPage: MessageCreateData get() { index = (index - 1) if(index < 0) { - index = pageCache.size - 1 + index = amountOfPages - 1 } - return pageCache[index] + return getPage(index) } var customActionComponents: List? = null var filter: (ButtonInteraction) -> Boolean = { true } - var injectMessageCallback: ((index: Int, messageEdit: MessageEditCallbackAction) -> Unit)? = null + var injectMessageCallback: ((index: Long, messageEdit: MessageEditCallbackAction) -> Unit)? = null fun filterBy(filter: (ButtonInteraction) -> Boolean): Paginator { this.filter = filter @@ -77,7 +76,6 @@ class Paginator internal constructor(private val nonce: String, private val ttl: var prev: Button = PaginatorDefaults.PREV var next: Button = PaginatorDefaults.NEXT - val pages: List get() = pageCache.toList() internal fun getControls(): ActionRow { val controls: MutableList = mutableListOf( @@ -90,15 +88,7 @@ class Paginator internal constructor(private val nonce: String, private val ttl: return ActionRow.of(controls) } - fun addPages(vararg page: MessageCreateData) { - pageCache.addAll(page) - } - - fun addPages(vararg page: MessageEmbed) { - addPages(*page.map { MessageCreate(embeds=listOf(it)) }.toTypedArray()) - } - - fun getIndex(): Int { + fun getIndex(): Long { return this.index } @@ -134,53 +124,17 @@ class Paginator internal constructor(private val nonce: String, private val ttl: } } -fun paginator(vararg pages: MessageCreateData, expireAfter: Duration): Paginator { +fun paginator(amountOfPages: Long, getPage: GetPageCallback, expireAfter: Duration): Paginator { val nonce = ByteArray(32) SecureRandom().nextBytes(nonce) - return Paginator(Base64.getEncoder().encodeToString(nonce), expireAfter).also { it.addPages(*pages) } + return Paginator(Base64.getEncoder().encodeToString(nonce), amountOfPages, getPage, expireAfter) } -fun paginator(vararg pages: MessageEmbed, expireAfter: Duration): Paginator - = paginator(*pages.map { MessageCreate(embeds=listOf(it)) }.toTypedArray(), expireAfter=expireAfter) - fun MessageChannel.sendPaginator(paginator: Paginator) - = sendMessage(paginator.also { jda.addEventListener(it) }.pages[0]).setComponents(paginator.getControls()) + = sendMessage(paginator.also { jda.addEventListener(it) }.getPage(0)).setComponents(paginator.getControls()) fun InteractionHook.sendPaginator(paginator: Paginator) - = sendMessage(paginator.also { jda.addEventListener(it) }.pages[0]).setComponents(paginator.getControls()) + = sendMessage(paginator.also { jda.addEventListener(it) }.getPage(0)).setComponents(paginator.getControls()) fun IReplyCallback.replyPaginator(paginator: Paginator) - = reply(paginator.also { user.jda.addEventListener(it) }.pages[0]).setComponents(paginator.getControls()) - -fun MessageChannel.sendPaginator( - vararg pages: MessageCreateData, - expireAfter: Duration, - filter: (ButtonInteraction) -> Boolean = {true} -) = sendPaginator(paginator(*pages, expireAfter=expireAfter).filterBy(filter)) -fun MessageChannel.sendPaginator( - vararg pages: MessageEmbed, - expireAfter: Duration, - filter: (ButtonInteraction) -> Boolean = {true} -) = sendPaginator(paginator(*pages, expireAfter=expireAfter).filterBy(filter)) - -fun InteractionHook.sendPaginator( - vararg pages: MessageCreateData, - expireAfter: Duration, - filter: (ButtonInteraction) -> Boolean = {true} -) = sendPaginator(paginator(*pages, expireAfter=expireAfter).filterBy(filter)) -fun InteractionHook.sendPaginator( - vararg pages: MessageEmbed, - expireAfter: Duration, - filter: (ButtonInteraction) -> Boolean = {true} -) = sendPaginator(paginator(*pages, expireAfter=expireAfter).filterBy(filter)) - -fun IReplyCallback.replyPaginator( - vararg pages: MessageCreateData, - expireAfter: Duration, - filter: (ButtonInteraction) -> Boolean = {true} -) = replyPaginator(paginator(*pages, expireAfter=expireAfter).filterBy(filter)) -fun IReplyCallback.replyPaginator( - vararg pages: MessageEmbed, - expireAfter: Duration, - filter: (ButtonInteraction) -> Boolean = {true} -) = replyPaginator(paginator(*pages, expireAfter=expireAfter).filterBy(filter)) \ No newline at end of file + = reply(paginator.also { user.jda.addEventListener(it) }.getPage(0)).setComponents(paginator.getControls()) \ No newline at end of file