diff --git a/.idea/misc.xml b/.idea/misc.xml index ed2437ab..bdec65e5 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -19,7 +19,7 @@ - + diff --git a/.idea/modules.xml b/.idea/modules.xml index bb04ac9d..b0a65603 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -7,11 +7,8 @@ - - - diff --git a/gradle.properties b/gradle.properties index b622645e..2a7f8df9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled javaVersion=21 mcVersion=1.21.8 group=dev.slne.surf -version=1.21.8-2.38.0 +version=1.21.8-2.39.0 relocationPrefix=dev.slne.surf.surfapi.libs snapshot=true \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-api/api/surf-api-bukkit-api.api b/surf-api-bukkit/surf-api-bukkit-api/api/surf-api-bukkit-api.api index a136485f..6b5ba226 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/api/surf-api-bukkit-api.api +++ b/surf-api-bukkit/surf-api-bukkit-api/api/surf-api-bukkit-api.api @@ -1272,6 +1272,43 @@ public final class dev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogInputBu public static final fun dialogInput (Lkotlin/jvm/functions/Function1;)Lit/unimi/dsi/fastutil/objects/ObjectList; } +public abstract interface class dev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction { + public abstract fun newPage (II)I +} + +public final class dev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPaginationBuilder { + public fun ()V + public final fun addElement (Ljava/lang/Object;)V + public final fun addElements (Ljava/util/Collection;)V + public final fun addElements ([Ljava/lang/Object;)V + public final fun base (Lkotlin/jvm/functions/Function3;)V + public final fun build ()Lio/papermc/paper/dialog/Dialog; + public final fun buttonBuilder (Lkotlin/jvm/functions/Function2;)V + public final fun currentPageButton (Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction;Lkotlin/jvm/functions/Function3;)V + public static synthetic fun currentPageButton$default (Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPaginationBuilder;Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)V + public final fun elementsPerPage (I)V + public final fun exitAction (Lkotlin/jvm/functions/Function1;)V + public final fun firstPageButton (Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction;Lkotlin/jvm/functions/Function3;)V + public static synthetic fun firstPageButton$default (Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPaginationBuilder;Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)V + public final fun getCurrentPage ()I + public final fun getHasNextPage ()Z + public final fun getHasPreviousPage ()Z + public final fun getMaxPages ()I + public final fun lastPageButton (Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction;Lkotlin/jvm/functions/Function3;)V + public static synthetic fun lastPageButton$default (Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPaginationBuilder;Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)V + public final fun minElementButtonWidth (I)V + public final fun nextPageButton (Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction;Lkotlin/jvm/functions/Function3;)V + public static synthetic fun nextPageButton$default (Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPaginationBuilder;Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)V + public final fun paginationButtonWidth (I)V + public final fun previousPageButton (Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction;Lkotlin/jvm/functions/Function3;)V + public static synthetic fun previousPageButton$default (Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPaginationBuilder;Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)V +} + +public final class dev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPaginationBuilderKt { + public static final fun PaginatedDialog (Lkotlin/jvm/functions/Function1;)Lio/papermc/paper/dialog/Dialog; + public static final fun paginatedDialog (Lkotlin/jvm/functions/Function1;)Lio/papermc/paper/dialog/Dialog; +} + public final class dev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogTypeBuilder { public fun ()V public final fun confirmation (Lio/papermc/paper/registry/data/dialog/ActionButton;Lio/papermc/paper/registry/data/dialog/ActionButton;)V diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPaginationBuilder.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPaginationBuilder.kt new file mode 100644 index 00000000..97de9d14 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPaginationBuilder.kt @@ -0,0 +1,303 @@ +@file:Suppress("UnstableApiUsage") + +package dev.slne.surf.surfapi.bukkit.api.dialog.builder + +import dev.slne.surf.surfapi.bukkit.api.dialog.base +import dev.slne.surf.surfapi.bukkit.api.dialog.dialog +import dev.slne.surf.surfapi.bukkit.api.dialog.type +import dev.slne.surf.surfapi.core.api.messages.adventure.buildText +import dev.slne.surf.surfapi.core.api.messages.adventure.text +import io.papermc.paper.registry.data.dialog.ActionButton +import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet +import net.kyori.adventure.text.Component + +private const val PAGINATION_BUTTON_DEFAULT_WIDTH = 50 + +fun PaginatedDialog( + block: DialogPaginationBuilder.() -> Unit, +) = DialogPaginationBuilder().apply(block).build() + +fun paginatedDialog( + block: DialogPaginationBuilder.() -> Unit, +) = PaginatedDialog(block) + +fun interface DialogPageAction { + fun newPage(currentPage: Int, maxPages: Int): Int +} + +private enum class DialogPaginationBaseAction( + val label: (Int, Int) -> Component, + val tooltip: (Int, Int) -> Component, + val pageAction: DialogPageAction, +) { + FIRST( + label = { _, _ -> text("«") }, + tooltip = { _, _ -> buildText { info("Erste Seite") } }, + pageAction = { _, _ -> 0 } + ), + BACK( + label = { _, _ -> text("‹") }, + tooltip = { _, _ -> buildText { info("Vorherige Seite") } }, + pageAction = { currentPage, _ -> (currentPage - 1).coerceAtLeast(0) } + ), + CURRENT( + label = { currentPage, maxPages -> text("${currentPage + 1} / $maxPages") }, + tooltip = { currentPage, maxPages -> buildText { info("Seite ${currentPage + 1} von $maxPages") } }, + pageAction = { currentPage, _ -> currentPage }, + ), + NEXT( + label = { _, _ -> text("›") }, + tooltip = { _, _ -> buildText { info("Nächste Seite") } }, + pageAction = { currentPage, maxPages -> (currentPage + 1).coerceAtMost(maxPages - 1) } + ), + LAST( + label = { _, _ -> text("»") }, + tooltip = { _, _ -> buildText { info("Letzte Seite") } }, + pageAction = { _, maxPages -> maxPages - 1 } + ); + + fun actionButton( + currentPage: Int, + maxPages: Int, + width: Int, + ) = actionButton { + label(this@DialogPaginationBaseAction.label(currentPage, maxPages)) + tooltip(this@DialogPaginationBaseAction.tooltip(currentPage, maxPages)) + width(width) + } +} + +class DialogPaginationBuilder { + + private var base: (DialogBaseBuilder.() -> Unit)? = null + private var exitAction: ActionButton? = null + private var buttonBuilder: (DialogActionButtonBuilder.(T) -> Unit)? = null + + private val elements = ObjectLinkedOpenHashSet() + private var elementsPerPage = 10 + private val currentPageElements + get() = elements.drop(currentPage * elementsPerPage).take(elementsPerPage) + + private var paginationButtonWidth = PAGINATION_BUTTON_DEFAULT_WIDTH + + @Suppress("SuspiciousVarProperty") + private var minElementButtonWidth = 200 + + private fun calculateElementButtonWidth(): Int { + var width = 0 + + if (hasPreviousPage) { + width += paginationButtonWidth * 2 // first + back + } + + if (hasNextPage) { + width += paginationButtonWidth * 2 // next + last + } + + width += paginationButtonWidth // current page button + + return width.coerceAtLeast(minElementButtonWidth) + } + + var currentPage = 0 + private set + + val maxPages: Int + get() = (elements.size + elementsPerPage - 1) / elementsPerPage + + val hasNextPage: Boolean + get() = currentPage < maxPages - 1 + + val hasPreviousPage: Boolean + get() = currentPage > 0 + + private var firstPageButton = DialogPaginationBaseAction.FIRST.actionButton( + currentPage, maxPages, paginationButtonWidth + ) + + private var backButton = DialogPaginationBaseAction.BACK.actionButton( + currentPage, maxPages, paginationButtonWidth + ) + + private var currentPageButton = DialogPaginationBaseAction.CURRENT.actionButton( + currentPage, maxPages, paginationButtonWidth + ) + + private var nextButton = DialogPaginationBaseAction.NEXT.actionButton( + currentPage, maxPages, paginationButtonWidth + ) + + private var lastPageButton = DialogPaginationBaseAction.LAST.actionButton( + currentPage, maxPages, paginationButtonWidth + ) + + fun base(block: DialogBaseBuilder.(Int, Int) -> Unit) { + base = { + block(currentPage, maxPages) + } + } + + private fun buildPageButtonAction(pageAction: DialogPageAction) = dialogAction { + playerCallback { player -> + val newPage = pageAction.newPage(currentPage, maxPages) + + if (newPage == currentPage) return@playerCallback + + currentPage = newPage + + player.showDialog(this@DialogPaginationBuilder.build()) + } + } + + fun firstPageButton( + pageAction: DialogPageAction = DialogPaginationBaseAction.FIRST.pageAction, + block: DialogActionButtonBuilder.(Int, Int) -> Unit, + ) { + firstPageButton = actionButton { + block(currentPage, maxPages) + + width(paginationButtonWidth) + action(buildPageButtonAction(pageAction)) + } + } + + fun previousPageButton( + pageAction: DialogPageAction = DialogPaginationBaseAction.BACK.pageAction, + block: DialogActionButtonBuilder.(Int, Int) -> Unit, + ) { + backButton = actionButton { + block(currentPage, maxPages) + + width(paginationButtonWidth) + action(buildPageButtonAction(pageAction)) + } + } + + fun currentPageButton( + pageAction: DialogPageAction = DialogPaginationBaseAction.CURRENT.pageAction, + block: DialogActionButtonBuilder.(Int, Int) -> Unit, + ) { + currentPageButton = actionButton { + block(currentPage, maxPages) + + width(paginationButtonWidth) + action(buildPageButtonAction(pageAction)) + } + } + + fun nextPageButton( + pageAction: DialogPageAction = DialogPaginationBaseAction.NEXT.pageAction, + block: DialogActionButtonBuilder.(Int, Int) -> Unit, + ) { + nextButton = actionButton { + block(currentPage, maxPages) + + width(paginationButtonWidth) + action(buildPageButtonAction(pageAction)) + } + } + + fun lastPageButton( + pageAction: DialogPageAction = DialogPaginationBaseAction.LAST.pageAction, + block: DialogActionButtonBuilder.(Int, Int) -> Unit, + ) { + lastPageButton = actionButton { + block(currentPage, maxPages) + + width(paginationButtonWidth) + action(buildPageButtonAction(pageAction)) + } + } + + fun elementsPerPage(elementsPerPage: Int) { + require(elementsPerPage > 0) { "elementsPerPage must be greater than 0" } + + this.elementsPerPage = elementsPerPage + } + + fun addElement(element: T) { + elements.add(element) + } + + fun addElements(elements: Collection) { + this.elements.addAll(elements) + } + + fun addElements(vararg elements: T) { + this.elements.addAll(elements) + } + + fun paginationButtonWidth(width: Int) { + require(width in 1..1024) { + "Pagination button width must be between 1 and 1024" + } + + paginationButtonWidth = width + } + + fun minElementButtonWidth(width: Int) { + require(width in 1..1024) { + "Element button minimum width must be between 1 and 1024" + } + + minElementButtonWidth = width + } + + fun exitAction(block: DialogActionButtonBuilder.() -> Unit) { + exitAction = actionButton(block) + } + + fun buttonBuilder(builder: DialogActionButtonBuilder.(T) -> Unit) { + buttonBuilder = builder + } + + fun build() = dialog { + val baseBuilder = base + val buttonBuilder = buttonBuilder + val exitAction = exitAction + + require(baseBuilder != null) { + "Dialog base must be set before building the paginated dialog" + } + + require(buttonBuilder != null) { + "Dialog button builder must be set before building the paginated dialog" + } + + base(baseBuilder) + + val elementButtonWidth = calculateElementButtonWidth() + val elementButtons = currentPageElements.map { + actionButton { + buttonBuilder(it) + + width(elementButtonWidth) + } + } + + type { + multiAction { + columns(1) + + elementButtons.forEach { action(it) } + + if (hasPreviousPage) { + action(firstPageButton) + action(backButton) + } + + action(currentPageButton) + + if (hasNextPage) { + action(nextButton) + action(lastPageButton) + } + + if (exitAction != null) { + exitAction(exitAction) + } + } + } + } + +} \ No newline at end of file