feat(canary/ComponentsV2): init @ LavaDesu/Aliucord@8ee06ba700
This commit is contained in:
parent
78022652d7
commit
02ac3cb652
48 changed files with 2223 additions and 6 deletions
|
|
@ -6,6 +6,7 @@ buildscript {
|
|||
google()
|
||||
mavenCentral()
|
||||
maven("https://maven.aliucord.com/snapshots")
|
||||
gradlePluginPortal() // remove when gradle 8
|
||||
maven("https://jitpack.io")
|
||||
}
|
||||
|
||||
|
|
@ -13,6 +14,8 @@ buildscript {
|
|||
classpath("com.android.tools.build:gradle:7.1.3")
|
||||
classpath("com.aliucord:gradle:main-SNAPSHOT")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21")
|
||||
// classpath("com.gradleup.shadow:shadow-gradle-plugin:8.3.8")
|
||||
classpath("com.github.johnrengelman.shadow:com.github.johnrengelman.shadow.gradle.plugin:7.1.2") // For Gradle 7 compat
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
54
canary/ComponentsV2/build.gradle.kts
Normal file
54
canary/ComponentsV2/build.gradle.kts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
|
||||
version = "7.15.0-8ee06ba"
|
||||
description = "Beta backport of ComponentsV2"
|
||||
|
||||
aliucord {
|
||||
// Changelog of your plugin
|
||||
changelog.set("""
|
||||
TODO {fixed}
|
||||
======================
|
||||
* File component
|
||||
* SelectV2: searching
|
||||
* SelectV2: showing selected items in chat list
|
||||
|
||||
Changelog {added marginTop}
|
||||
======================
|
||||
# 7.15.0
|
||||
* Initial release >w<
|
||||
""".trimIndent())
|
||||
|
||||
excludeFromUpdaterJson.set(false)
|
||||
}
|
||||
|
||||
//apply(plugin = "com.gradleup.shadow")
|
||||
apply(plugin = "com.github.johnrengelman.shadow") // remove when gradle 8
|
||||
|
||||
val shadowDir = File(buildDir, "intermediates/shadowed")
|
||||
|
||||
tasks.register<ShadowJar>("relocateJar") {
|
||||
val task = tasks.findByName("compileDebugKotlin")!!
|
||||
from(task.outputs)
|
||||
// relocate("com.discord.api.botuikit", "moe.lava.awoocanary.componentsv2.botuikit") {
|
||||
// exclude("com.discord.api.botuikit.ComponentType")
|
||||
// }
|
||||
relocate("com.aliucord.coreplugins.componentsv2", "moe.lava.corenary.componentsv2")
|
||||
relocate("com.aliucord.coreplugins.ComponentsV2", "moe.lava.corenary.ComponentsV2")
|
||||
archiveClassifier.set("shadowed")
|
||||
destinationDirectory.set(File(buildDir, "intermediates"))
|
||||
}
|
||||
|
||||
tasks.register<Sync>("copyShadowed") {
|
||||
val reloc = tasks.findByName("relocateJar")!! as ShadowJar
|
||||
dependsOn(reloc)
|
||||
from(zipTree(reloc.archiveFile))
|
||||
into(shadowDir)
|
||||
}
|
||||
|
||||
project.afterEvaluate {
|
||||
tasks.compileDex {
|
||||
val copyShadowed = tasks.findByName("copyShadowed")!! as Sync
|
||||
dependsOn(copyShadowed)
|
||||
input.setFrom(shadowDir)
|
||||
}
|
||||
}
|
||||
2
canary/ComponentsV2/src/main/AndroidManifest.xml
Normal file
2
canary/ComponentsV2/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="moe.lava.awoocanary.componentsv2" />
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
package com.aliucord.coreplugins
|
||||
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.aliucord.Utils
|
||||
import com.aliucord.annotations.AliucordPlugin
|
||||
import com.aliucord.coreplugins.componentsv2.ComponentV2Type
|
||||
import com.aliucord.coreplugins.componentsv2.models.*
|
||||
import com.aliucord.coreplugins.componentsv2.patchMessageItems
|
||||
import com.aliucord.coreplugins.componentsv2.views.*
|
||||
import com.aliucord.entities.Plugin
|
||||
import com.aliucord.patcher.*
|
||||
import com.aliucord.utils.ReflectUtils
|
||||
import com.discord.api.botuikit.*
|
||||
import com.discord.api.botuikit.gson.ComponentRuntimeTypeAdapter
|
||||
import com.discord.api.botuikit.gson.ComponentTypeTypeAdapter
|
||||
import com.discord.api.message.attachment.MessageAttachment
|
||||
import com.discord.models.botuikit.*
|
||||
import com.discord.stores.StoreApplicationInteractions.InteractionSendState
|
||||
import com.discord.utilities.view.extensions.ViewExtensions
|
||||
import com.discord.widgets.botuikit.*
|
||||
import com.discord.widgets.botuikit.ComponentChatListState.ComponentStoreState
|
||||
import com.discord.widgets.botuikit.views.*
|
||||
import com.discord.widgets.botuikit.views.select.SelectComponentView
|
||||
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapter
|
||||
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemBotComponentRow
|
||||
import com.discord.widgets.chat.list.entries.BotUiComponentEntry
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.lytefast.flexinput.R
|
||||
import de.robv.android.xposed.XposedBridge
|
||||
|
||||
@AliucordPlugin(requiresRestart = true)
|
||||
@Suppress("unused")
|
||||
class ComponentsV2 : Plugin() {
|
||||
companion object {
|
||||
/** Creates a new [MessageAttachment] */
|
||||
fun createAttachment(
|
||||
filename: String,
|
||||
filesize: Long,
|
||||
proxyUrl: String,
|
||||
url: String,
|
||||
width: Int,
|
||||
height: Int,
|
||||
): MessageAttachment {
|
||||
val inst = ReflectUtils.allocateInstance(clazz)
|
||||
filenameField.set(inst, filename)
|
||||
filesizeField.set(inst, filesize)
|
||||
proxyUrlField.set(inst, proxyUrl)
|
||||
urlField.set(inst, url)
|
||||
widthField.set(inst, width)
|
||||
heightField.set(inst, height)
|
||||
return inst
|
||||
}
|
||||
|
||||
private val clazz = MessageAttachment::class.java
|
||||
private val filenameField = clazz.getDeclaredField("filename").apply { isAccessible = true }
|
||||
private val filesizeField = clazz.getDeclaredField("size").apply { isAccessible = true }
|
||||
private val proxyUrlField = clazz.getDeclaredField("proxyUrl").apply { isAccessible = true }
|
||||
private val urlField = clazz.getDeclaredField("url").apply { isAccessible = true }
|
||||
private val widthField = clazz.getDeclaredField("width").apply { isAccessible = true }
|
||||
private val heightField = clazz.getDeclaredField("height").apply { isAccessible = true }
|
||||
}
|
||||
|
||||
override fun start(context: Context) {
|
||||
XposedBridge.makeClassInheritable(BotUiComponentEntry::class.java)
|
||||
ComponentV2Type.make()
|
||||
patchGson()
|
||||
// https://github.com/LSPosed/LSPlant/issues/41
|
||||
patchMessageItems(patcher)
|
||||
|
||||
patcher.instead<ComponentStateMapper>(
|
||||
"toMessageLayoutComponent",
|
||||
LayoutComponent::class.java,
|
||||
Int::class.javaPrimitiveType!!,
|
||||
List::class.java,
|
||||
ComponentExperiments::class.java
|
||||
) { (_, layout: LayoutComponent, index: Int, components: List<MessageComponent>) ->
|
||||
when (layout) {
|
||||
is ActionRowComponent ->
|
||||
ActionRowMessageComponent(layout.type, index, components)
|
||||
is SectionComponent ->
|
||||
SectionMessageComponent.mergeToMessageComponent(layout, index, components)
|
||||
is TextDisplayComponent ->
|
||||
TextDisplayMessageComponent.mergeToMessageComponent(layout, index)
|
||||
is ThumbnailComponent ->
|
||||
ThumbnailMessageComponent.mergeToMessageComponent(layout, index)
|
||||
is MediaGalleryComponent ->
|
||||
MediaGalleryMessageComponent.mergeToMessageComponent(layout, index)
|
||||
is FileComponent ->
|
||||
ActionRowMessageComponent(layout.type, index, components)
|
||||
is SeparatorComponent ->
|
||||
SeparatorMessageComponent.mergeToMessageComponent(layout, index)
|
||||
is ContainerComponent ->
|
||||
ContainerMessageComponent.mergeToMessageComponent(layout, index, components)
|
||||
else ->
|
||||
throw IllegalArgumentException("Unknown layout component ${layout::class.java.name} (${layout.type.type}:${layout.type.name})")
|
||||
}
|
||||
}
|
||||
|
||||
patcher.instead<ComponentProvider>("configureView", ComponentActionListener::class.java, MessageComponent::class.java, ComponentView::class.java)
|
||||
{ (_, listener: ComponentActionListener, component: MessageComponent, view: ComponentView<MessageComponent>?) ->
|
||||
view?.configure(component, this, listener)
|
||||
}
|
||||
|
||||
patcher.instead<ComponentInflater>("inflateComponent", ComponentType::class.java, ViewGroup::class.java)
|
||||
{ (_, type: ComponentType, viewGroup: ViewGroup) ->
|
||||
when (type) {
|
||||
ComponentType.ACTION_ROW ->
|
||||
ActionRowComponentView.Companion!!.inflateComponent(this.context, viewGroup)
|
||||
ComponentType.BUTTON ->
|
||||
ButtonComponentView.Companion!!.inflateComponent(this.context, viewGroup)
|
||||
ComponentType.SELECT ->
|
||||
SelectComponentView.Companion!!.inflateComponent(this.context, viewGroup)
|
||||
ComponentV2Type.USER_SELECT,
|
||||
ComponentV2Type.ROLE_SELECT,
|
||||
ComponentV2Type.MENTIONABLE_SELECT,
|
||||
ComponentV2Type.CHANNEL_SELECT ->
|
||||
SelectV2ComponentView(this.context, type)
|
||||
ComponentV2Type.SECTION ->
|
||||
SectionComponentView(this.context)
|
||||
ComponentV2Type.TEXT_DISPLAY ->
|
||||
TextDisplayComponentView(this.context)
|
||||
ComponentV2Type.THUMBNAIL ->
|
||||
ThumbnailComponentView(this.context)
|
||||
ComponentV2Type.MEDIA_GALLERY ->
|
||||
MediaGalleryComponentView(this.context)
|
||||
ComponentV2Type.FILE ->
|
||||
null
|
||||
ComponentV2Type.SEPARATOR ->
|
||||
SeparatorComponentView(this.context)
|
||||
ComponentV2Type.CONTAINER ->
|
||||
ContainerComponentView(this.context)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
patcher.after<WidgetChatListAdapterItemBotComponentRow>(WidgetChatListAdapter::class.java)
|
||||
{
|
||||
val rootLayout = itemView.findViewById<LinearLayout>(Utils.getResId("chat_list_adapter_item_component_root", "id"))
|
||||
rootLayout.layoutParams = (rootLayout.layoutParams as ConstraintLayout.LayoutParams).apply {
|
||||
marginEnd = adapter.context.resources.getDimension(R.d.chat_cell_horizontal_spacing_padding).toInt()
|
||||
}
|
||||
|
||||
ViewExtensions.setOnLongClickListenerConsumeClick(itemView) {
|
||||
adapter.eventHandler.onMessageLongClicked(entry.message, "", false)
|
||||
}
|
||||
itemView.setOnClickListener {
|
||||
adapter.eventHandler.onMessageClicked(entry.message, false)
|
||||
}
|
||||
}
|
||||
|
||||
patcher.instead<ComponentStateMapper>(
|
||||
"createActionMessageComponent",
|
||||
ActionComponent::class.java,
|
||||
Int::class.javaPrimitiveType!!,
|
||||
ComponentStoreState::class.java,
|
||||
ComponentExperiments::class.java,
|
||||
) { (
|
||||
_,
|
||||
actionComponent: ActionComponent,
|
||||
index: Int,
|
||||
componentStoreState: ComponentStoreState,
|
||||
) ->
|
||||
val interactionState: Map<Int, InteractionSendState>? = componentStoreState.interactionState;
|
||||
val num = interactionState?.entries?.find { it.value is InteractionSendState.Loading }?.key
|
||||
|
||||
val state = interactionState?.get(index)
|
||||
val comState: ActionInteractionComponentState = when {
|
||||
state is InteractionSendState.Failed -> ActionInteractionComponentState.Failed(state.errorMessage)
|
||||
num == null -> ActionInteractionComponentState.Enabled.INSTANCE
|
||||
num == index -> ActionInteractionComponentState.Loading.INSTANCE
|
||||
else -> ActionInteractionComponentState.Disabled.INSTANCE
|
||||
}
|
||||
|
||||
when (actionComponent) {
|
||||
is ButtonComponent ->
|
||||
ButtonMessageComponentKt.mergeToMessageComponent(actionComponent, index, comState, componentStoreState)
|
||||
is SelectComponent ->
|
||||
SelectMessageComponentKt.mergeToMessageComponent(actionComponent, index, comState, componentStoreState)
|
||||
is SelectV2Component ->
|
||||
SelectV2MessageComponent.mergeToMessageComponent(actionComponent, index, comState, componentStoreState)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun stop(context: Context) {
|
||||
patcher.unpatchAll()
|
||||
unpatchGson()
|
||||
ComponentV2Type.unmake(logger)
|
||||
}
|
||||
|
||||
private fun patchGson() {
|
||||
val factory = ComponentRuntimeTypeAdapter.INSTANCE.a()
|
||||
val typeToClass = factory.l
|
||||
val classToType = factory.m
|
||||
ComponentV2Type.newValues?.forEach {
|
||||
typeToClass[it.type.toString()] = it.clazz
|
||||
classToType[it.clazz] = it.type.toString()
|
||||
}
|
||||
|
||||
patcher.instead<ComponentTypeTypeAdapter>("read", JsonReader::class.java)
|
||||
{ (_, jsonReader: JsonReader) ->
|
||||
val type: Int = b.c.a.a0.d.n1(jsonReader)
|
||||
ComponentType.values().find { it.type == type } ?: ComponentType.UNKNOWN
|
||||
}
|
||||
}
|
||||
private fun unpatchGson() {
|
||||
val factory = ComponentRuntimeTypeAdapter.INSTANCE.a()
|
||||
val typeToClass = factory.l
|
||||
val classToType = factory.m
|
||||
ComponentV2Type.newValues?.forEach {
|
||||
typeToClass.remove(it.type.toString())
|
||||
classToType.remove(it.clazz)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.aliucord.coreplugins.componentsv2
|
||||
|
||||
import com.discord.api.channel.Channel
|
||||
import com.discord.api.role.GuildRole
|
||||
import com.discord.models.botuikit.MessageComponent
|
||||
import com.discord.models.member.GuildMember
|
||||
import com.discord.models.message.Message
|
||||
import com.discord.stores.StoreMessageState
|
||||
import com.discord.widgets.chat.list.entries.BotUiComponentEntry
|
||||
|
||||
@Suppress("EqualsOrHashCode")
|
||||
class BotUiComponentV2Entry(
|
||||
message: Message, appId: Long, guildId: Long?, components: MutableList<out MessageComponent>,
|
||||
private val v2Fields: V2Fields
|
||||
) : BotUiComponentEntry(message, appId, guildId, components) {
|
||||
data class V2Fields(
|
||||
val state: StoreMessageState.State?,
|
||||
val meId: Long,
|
||||
val channel: Channel,
|
||||
val guildMembers: Map<Long, GuildMember>,
|
||||
val guildRoles: Map<Long, GuildRole>,
|
||||
// val channelNames: Map<Long, String>,
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun fromV1(entry: BotUiComponentEntry, fields: V2Fields) =
|
||||
entry.run { BotUiComponentV2Entry(message, applicationId, guildId, messageComponents, fields) }
|
||||
}
|
||||
|
||||
val state get() = v2Fields.state
|
||||
val meId get() = v2Fields.meId
|
||||
val channel get() = v2Fields.channel
|
||||
val guildMembers get() = v2Fields.guildMembers
|
||||
val guildRoles get() = v2Fields.guildRoles
|
||||
|
||||
override fun equals(other: Any?) =
|
||||
super.equals(other) && if (other is BotUiComponentV2Entry) this.v2Fields == other.v2Fields else true
|
||||
|
||||
override fun toString() =
|
||||
"AliuV2" + super.toString() + "& " + v2Fields.toString()
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.aliucord.coreplugins.componentsv2
|
||||
|
||||
import com.aliucord.Logger
|
||||
import com.discord.api.botuikit.*
|
||||
|
||||
// Values added by smali patch
|
||||
object ComponentV2Type {
|
||||
lateinit var USER_SELECT: ComponentType
|
||||
lateinit var ROLE_SELECT: ComponentType
|
||||
lateinit var MENTIONABLE_SELECT: ComponentType
|
||||
lateinit var CHANNEL_SELECT: ComponentType
|
||||
lateinit var SECTION: ComponentType
|
||||
lateinit var TEXT_DISPLAY: ComponentType
|
||||
lateinit var THUMBNAIL: ComponentType
|
||||
lateinit var MEDIA_GALLERY: ComponentType
|
||||
lateinit var FILE: ComponentType
|
||||
lateinit var SEPARATOR: ComponentType
|
||||
lateinit var CONTAINER: ComponentType
|
||||
|
||||
var newValues: Array<ComponentType>? = null
|
||||
private var oldValues: Array<ComponentType>? = null
|
||||
@Suppress("UNCHECKED_CAST", "UNUSED_CHANGED_VALUE")
|
||||
fun make() {
|
||||
if (oldValues != null)
|
||||
return
|
||||
oldValues = ComponentType.values()
|
||||
|
||||
val cls = ComponentType::class.java
|
||||
val constructor = cls.declaredConstructors[0]
|
||||
constructor.isAccessible = true
|
||||
|
||||
val field = cls.getDeclaredField("\$VALUES")
|
||||
field.isAccessible = true
|
||||
val values = ComponentType.values()
|
||||
var nextIdx = values.size
|
||||
|
||||
USER_SELECT = constructor.newInstance("USER_SELECT", nextIdx++, 5, UserSelectComponent::class.java) as ComponentType
|
||||
ROLE_SELECT = constructor.newInstance("ROLE_SELECT", nextIdx++, 6, RoleSelectComponent::class.java) as ComponentType
|
||||
MENTIONABLE_SELECT = constructor.newInstance("MENTIONABLE_SELECT", nextIdx++, 7, MentionableSelectComponent::class.java) as ComponentType
|
||||
CHANNEL_SELECT = constructor.newInstance("CHANNEL_SELECT", nextIdx++, 8, ChannelSelectComponent::class.java) as ComponentType
|
||||
SECTION = constructor.newInstance("SECTION", nextIdx++, 9, SectionComponent::class.java) as ComponentType
|
||||
TEXT_DISPLAY = constructor.newInstance("TEXT_DISPLAY", nextIdx++, 10, TextDisplayComponent::class.java) as ComponentType
|
||||
THUMBNAIL = constructor.newInstance("THUMBNAIL", nextIdx++, 11, ThumbnailComponent::class.java) as ComponentType
|
||||
MEDIA_GALLERY = constructor.newInstance("MEDIA_GALLERY", nextIdx++, 12, MediaGalleryComponent::class.java) as ComponentType
|
||||
FILE = constructor.newInstance("FILE", nextIdx++, 13, FileComponent::class.java) as ComponentType
|
||||
SEPARATOR = constructor.newInstance("SEPARATOR", nextIdx++, 14, SeparatorComponent::class.java) as ComponentType
|
||||
CONTAINER = constructor.newInstance("CONTAINER", nextIdx++, 17, ContainerComponent::class.java) as ComponentType
|
||||
|
||||
newValues = arrayOf(USER_SELECT, ROLE_SELECT, MENTIONABLE_SELECT, CHANNEL_SELECT, SECTION, TEXT_DISPLAY, THUMBNAIL, MEDIA_GALLERY, FILE, SEPARATOR, CONTAINER)
|
||||
field.set(null, values + newValues!!)
|
||||
}
|
||||
|
||||
fun unmake(logger: Logger) {
|
||||
if (oldValues == null)
|
||||
return logger.error("No unpatched component types?", null)
|
||||
|
||||
val cls = ComponentType::class.java
|
||||
val field = cls.getDeclaredField("\$VALUES")
|
||||
field.isAccessible = true
|
||||
field.set(null, oldValues)
|
||||
oldValues = null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.aliucord.coreplugins.componentsv2
|
||||
|
||||
import com.aliucord.api.PatcherAPI
|
||||
import com.aliucord.patcher.*
|
||||
import com.discord.api.channel.Channel
|
||||
import com.discord.api.role.GuildRole
|
||||
import com.discord.models.member.GuildMember
|
||||
import com.discord.models.message.Message
|
||||
import com.discord.stores.StoreMessageReplies.MessageState
|
||||
import com.discord.stores.StoreMessageState
|
||||
import com.discord.stores.StoreThreadMessages
|
||||
import com.discord.widgets.chat.list.entries.BotUiComponentEntry
|
||||
import com.discord.widgets.chat.list.entries.ChatListEntry
|
||||
import com.discord.widgets.chat.list.model.WidgetChatListModelMessages
|
||||
|
||||
fun patchMessageItems(patcher: PatcherAPI) {
|
||||
@Suppress("UNUSED_DESTRUCTURED_PARAMETER_ENTRY", "LocalVariableName", "UnusedVariable")
|
||||
patcher.patch(WidgetChatListModelMessages.Companion::class.java.declaredMethods.find { it.name == "getMessageItems" }!!)
|
||||
{(
|
||||
param,
|
||||
channel: Channel,
|
||||
guildMembers: Map<Long, GuildMember>,
|
||||
guildRoles: Map<Long, GuildRole>,
|
||||
_blockedRelationships: Map<Long, Int>?,
|
||||
_referencedChannel: Channel?,
|
||||
_threadStoreState: StoreThreadMessages.ThreadState?,
|
||||
_message: Message,
|
||||
state: StoreMessageState.State?,
|
||||
_repliedMessages: Map<Long, MessageState>?,
|
||||
_isBlockedExpanded: Boolean,
|
||||
_isMinimal: Boolean,
|
||||
) ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val result = (param.result as MutableList<ChatListEntry>)
|
||||
val meId = param.args[15] as Long
|
||||
result.forEachIndexed { index, entry ->
|
||||
if (entry is BotUiComponentEntry && ((entry.message.flags shr 15) and 1 == 1L)) {
|
||||
val fields = BotUiComponentV2Entry.V2Fields(state, meId, channel, guildMembers, guildRoles)
|
||||
result[index] = BotUiComponentV2Entry.fromV1(entry, fields)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.aliucord.coreplugins.componentsv2.models
|
||||
|
||||
import com.discord.api.botuikit.ComponentType
|
||||
import com.discord.api.botuikit.ContainerComponent
|
||||
import com.discord.models.botuikit.MessageComponent
|
||||
|
||||
data class ContainerMessageComponent(
|
||||
private val type: ComponentType,
|
||||
private val index: Int,
|
||||
|
||||
override val id: Int,
|
||||
val components: List<MessageComponent>,
|
||||
val accentColor: Int?,
|
||||
override val spoiler: Boolean,
|
||||
) : SpoilableMessageComponent {
|
||||
override fun getType() = type
|
||||
override fun getIndex() = index
|
||||
|
||||
companion object {
|
||||
fun mergeToMessageComponent(
|
||||
component: ContainerComponent,
|
||||
index: Int,
|
||||
components: List<MessageComponent>,
|
||||
): ContainerMessageComponent {
|
||||
components.forEach {
|
||||
if (it is MediaGalleryMessageComponent)
|
||||
it.markedContained = true
|
||||
}
|
||||
return component.run {
|
||||
ContainerMessageComponent(
|
||||
type,
|
||||
index,
|
||||
id,
|
||||
components,
|
||||
accentColor,
|
||||
spoiler,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.aliucord.coreplugins.componentsv2.models
|
||||
|
||||
import com.discord.api.botuikit.*
|
||||
import com.discord.models.botuikit.MessageComponent
|
||||
|
||||
data class MediaGalleryMessageComponent(
|
||||
private val type: ComponentType,
|
||||
private val index: Int,
|
||||
|
||||
val id: Int,
|
||||
val items: List<MediaGalleryItem>,
|
||||
// Set by ContainerComponentView to tell MediaGalleryComponentView it is contained
|
||||
var markedContained: Boolean = false,
|
||||
) : MessageComponent {
|
||||
override fun getType() = type
|
||||
override fun getIndex() = index
|
||||
|
||||
companion object {
|
||||
fun mergeToMessageComponent(
|
||||
component: MediaGalleryComponent,
|
||||
index: Int
|
||||
): MediaGalleryMessageComponent {
|
||||
return component.run {
|
||||
MediaGalleryMessageComponent(
|
||||
type,
|
||||
index,
|
||||
id,
|
||||
items,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.aliucord.coreplugins.componentsv2.models
|
||||
|
||||
import com.discord.api.botuikit.ComponentType
|
||||
import com.discord.api.botuikit.SectionComponent
|
||||
import com.discord.models.botuikit.MessageComponent
|
||||
|
||||
data class SectionMessageComponent(
|
||||
private val type: ComponentType,
|
||||
private val index: Int,
|
||||
|
||||
val id: Int,
|
||||
val components: List<MessageComponent>,
|
||||
val accessory: MessageComponent?,
|
||||
) : MessageComponent {
|
||||
override fun getType() = type
|
||||
override fun getIndex() = index
|
||||
|
||||
companion object {
|
||||
fun mergeToMessageComponent(
|
||||
component: SectionComponent,
|
||||
index: Int,
|
||||
components: List<MessageComponent>,
|
||||
): SectionMessageComponent {
|
||||
return component.run {
|
||||
val realComponents = components.toMutableList()
|
||||
val accessory = realComponents.removeAt(realComponents.lastIndex)
|
||||
SectionMessageComponent(
|
||||
type,
|
||||
index,
|
||||
id,
|
||||
realComponents,
|
||||
accessory,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package com.aliucord.coreplugins.componentsv2.models
|
||||
|
||||
import com.discord.api.botuikit.*
|
||||
import com.discord.models.botuikit.ActionInteractionComponentState
|
||||
import com.discord.models.botuikit.ActionMessageComponent
|
||||
import com.discord.widgets.botuikit.ComponentChatListState
|
||||
|
||||
data class SelectV2MessageComponent(
|
||||
private val type: ComponentType,
|
||||
private val index: Int,
|
||||
private val stateInteraction: ActionInteractionComponentState,
|
||||
|
||||
val id: Int,
|
||||
val customId: String,
|
||||
val placeholder: String,
|
||||
val minValues: Int,
|
||||
val maxValues: Int,
|
||||
val defaultValues: List<SelectV2DefaultValue>,
|
||||
val emojiAnimationsEnabled: Boolean,
|
||||
) : ActionMessageComponent() {
|
||||
override fun getType() = type
|
||||
override fun getIndex() = index
|
||||
override fun getStateInteraction() = stateInteraction
|
||||
|
||||
companion object {
|
||||
fun mergeToMessageComponent(
|
||||
selectComponent: SelectV2Component,
|
||||
index: Int,
|
||||
stateInteraction: ActionInteractionComponentState,
|
||||
componentStoreState: ComponentChatListState.ComponentStoreState
|
||||
): SelectV2MessageComponent {
|
||||
return selectComponent.run {
|
||||
SelectV2MessageComponent(
|
||||
type,
|
||||
index,
|
||||
stateInteraction,
|
||||
id,
|
||||
customId,
|
||||
placeholder,
|
||||
minValues,
|
||||
maxValues,
|
||||
defaultValues ?: listOf(),
|
||||
componentStoreState.animateEmojis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.aliucord.coreplugins.componentsv2.models
|
||||
|
||||
import com.discord.api.botuikit.ComponentType
|
||||
import com.discord.api.botuikit.SeparatorComponent
|
||||
import com.discord.models.botuikit.MessageComponent
|
||||
|
||||
data class SeparatorMessageComponent(
|
||||
private val type: ComponentType,
|
||||
private val index: Int,
|
||||
|
||||
val divider: Boolean,
|
||||
val spacing: Int, // 1 = small padding, 2 = large padding
|
||||
) : MessageComponent {
|
||||
override fun getType() = type
|
||||
override fun getIndex() = index
|
||||
|
||||
companion object {
|
||||
fun mergeToMessageComponent(
|
||||
component: SeparatorComponent,
|
||||
index: Int
|
||||
): SeparatorMessageComponent {
|
||||
return component.run {
|
||||
SeparatorMessageComponent(
|
||||
type,
|
||||
index,
|
||||
divider,
|
||||
spacing,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.aliucord.coreplugins.componentsv2.models
|
||||
|
||||
import com.discord.models.botuikit.MessageComponent
|
||||
|
||||
interface SpoilableMessageComponent : MessageComponent {
|
||||
val id: Int
|
||||
val spoiler: Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.aliucord.coreplugins.componentsv2.models
|
||||
|
||||
import com.discord.api.botuikit.ComponentType
|
||||
import com.discord.api.botuikit.TextDisplayComponent
|
||||
import com.discord.models.botuikit.MessageComponent
|
||||
|
||||
data class TextDisplayMessageComponent(
|
||||
private val type: ComponentType,
|
||||
private val index: Int,
|
||||
|
||||
val id: Int,
|
||||
val content: String,
|
||||
) : MessageComponent {
|
||||
override fun getType() = type
|
||||
override fun getIndex() = index
|
||||
|
||||
companion object {
|
||||
fun mergeToMessageComponent(
|
||||
component: TextDisplayComponent,
|
||||
index: Int
|
||||
): TextDisplayMessageComponent {
|
||||
return component.run {
|
||||
TextDisplayMessageComponent(
|
||||
type,
|
||||
index,
|
||||
id,
|
||||
content
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.aliucord.coreplugins.componentsv2.models
|
||||
|
||||
import com.discord.api.botuikit.*
|
||||
|
||||
data class ThumbnailMessageComponent(
|
||||
private val type: ComponentType,
|
||||
private val index: Int,
|
||||
|
||||
override val id: Int,
|
||||
val media: UnfurledMediaItem,
|
||||
val description: String?,
|
||||
override val spoiler: Boolean,
|
||||
) : SpoilableMessageComponent {
|
||||
override fun getType() = type
|
||||
override fun getIndex() = index
|
||||
|
||||
companion object {
|
||||
fun mergeToMessageComponent(
|
||||
component: ThumbnailComponent,
|
||||
index: Int
|
||||
): ThumbnailMessageComponent {
|
||||
return component.run {
|
||||
ThumbnailMessageComponent(
|
||||
type,
|
||||
index,
|
||||
id,
|
||||
media,
|
||||
description,
|
||||
spoiler,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
@file:Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS")
|
||||
|
||||
package com.aliucord.coreplugins.componentsv2.selectsheet
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.aliucord.Utils
|
||||
import com.aliucord.coreplugins.componentsv2.BotUiComponentV2Entry
|
||||
import com.aliucord.coreplugins.componentsv2.models.SelectV2MessageComponent
|
||||
import com.discord.app.AppBottomSheet
|
||||
import com.discord.utilities.view.extensions.ViewExtensions
|
||||
import com.discord.utilities.view.recycler.MaxHeightRecyclerView
|
||||
import com.discord.widgets.botuikit.views.select.`SelectComponentBottomSheet$binding$2`
|
||||
import com.lytefast.flexinput.R
|
||||
|
||||
internal class SelectSheet : AppBottomSheet {
|
||||
val entry: BotUiComponentV2Entry?
|
||||
val component: SelectV2MessageComponent?
|
||||
|
||||
private lateinit var header: ConstraintLayout
|
||||
private lateinit var placeholder: TextView
|
||||
private lateinit var recycler: MaxHeightRecyclerView
|
||||
private lateinit var select: TextView
|
||||
private lateinit var subtitle: TextView
|
||||
|
||||
private lateinit var adapter: SelectSheetAdapter
|
||||
|
||||
constructor(entry: BotUiComponentV2Entry, component: SelectV2MessageComponent) {
|
||||
this.entry = entry
|
||||
this.component = component
|
||||
}
|
||||
constructor() {
|
||||
this.entry = null
|
||||
this.component = null
|
||||
}
|
||||
|
||||
override fun getContentViewResId() = Utils.getResId("widget_select_component_bottom_sheet", "layout")
|
||||
|
||||
override fun onViewCreated(view: View, bundle: Bundle?) {
|
||||
super.onViewCreated(view, bundle)
|
||||
|
||||
val viewModel = ViewModelProvider(this).get(SelectSheetViewModel::class.java)
|
||||
|
||||
`SelectComponentBottomSheet$binding$2`.INSTANCE.invoke(view).run {
|
||||
header = a
|
||||
placeholder = b
|
||||
recycler = c
|
||||
select = d
|
||||
subtitle = e
|
||||
}
|
||||
adapter = SelectSheetAdapter(recycler, viewModel)
|
||||
recycler.adapter = adapter
|
||||
(recycler.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
|
||||
select.setOnClickListener { viewModel.submit() }
|
||||
viewModel.onUpdate = ::configureUI
|
||||
viewModel.onRequestDismiss = ::dismiss
|
||||
if (entry != null && component != null)
|
||||
viewModel.configure(entry, component)
|
||||
else
|
||||
viewModel.state?.let { configureUI(it) }
|
||||
}
|
||||
|
||||
private fun configureUI(state: SelectSheetViewModel.ViewState) {
|
||||
placeholder.text = state.placeholder
|
||||
subtitle.visibility = if (state.isMultiSelect) View.VISIBLE else View.GONE
|
||||
|
||||
if (state.isMultiSelect) {
|
||||
subtitle.text =
|
||||
b.a.k.b.k(
|
||||
this,
|
||||
R.h.message_select_component_select_requirement,
|
||||
arrayOf(state.minSelections),
|
||||
null,
|
||||
4
|
||||
)
|
||||
}
|
||||
select.visibility = if (state.isMultiSelect) View.VISIBLE else View.INVISIBLE
|
||||
select.isClickable = state.isValidSelection
|
||||
ViewExtensions.setEnabledAlpha(select, state.isValidSelection, 0.3f)
|
||||
adapter.setData(state.items)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.aliucord.coreplugins.componentsv2.selectsheet
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.discord.utilities.mg_recycler.MGRecyclerAdapterSimple
|
||||
import com.discord.utilities.mg_recycler.MGRecyclerViewHolder
|
||||
|
||||
internal class SelectSheetAdapter(recycler: RecyclerView, val viewModel: SelectSheetViewModel)
|
||||
: MGRecyclerAdapterSimple<SelectSheetItem>(recycler) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MGRecyclerViewHolder<*, SelectSheetItem> {
|
||||
return SelectSheetItemViewHolder(this)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.aliucord.coreplugins.componentsv2.selectsheet
|
||||
|
||||
import com.aliucord.wrappers.ChannelWrapper.Companion.id
|
||||
import com.discord.api.channel.Channel
|
||||
import com.discord.api.role.GuildRole
|
||||
import com.discord.models.member.GuildMember
|
||||
import com.discord.models.user.User
|
||||
import com.discord.utilities.mg_recycler.MGRecyclerDataPayload
|
||||
|
||||
sealed class SelectSheetItem(
|
||||
private val type: Int,
|
||||
val id: Long,
|
||||
) : MGRecyclerDataPayload {
|
||||
override fun getKey() = id.toString()
|
||||
override fun getType() = type
|
||||
|
||||
abstract val checked: Boolean
|
||||
abstract val disabled: Boolean
|
||||
abstract fun copy(checked: Boolean = this.checked, disabled: Boolean = this.disabled) : SelectSheetItem
|
||||
|
||||
internal data class UserSelectItem(
|
||||
val user: User,
|
||||
val member: GuildMember,
|
||||
override val checked: Boolean,
|
||||
override val disabled: Boolean = false,
|
||||
) : SelectSheetItem(1, user.id) {
|
||||
override fun copy(checked: Boolean, disabled: Boolean): SelectSheetItem = copy(checked = checked, disabled = disabled, user = user)
|
||||
}
|
||||
|
||||
internal data class RoleSelectItem(
|
||||
val role: GuildRole,
|
||||
override val checked: Boolean,
|
||||
override val disabled: Boolean = false,
|
||||
) : SelectSheetItem(2, role.id) {
|
||||
override fun copy(checked: Boolean, disabled: Boolean): SelectSheetItem = copy(checked = checked, disabled = disabled, role = role)
|
||||
}
|
||||
|
||||
internal data class ChannelSelectItem(
|
||||
val channel: Channel,
|
||||
override val checked: Boolean,
|
||||
override val disabled: Boolean = false,
|
||||
) : SelectSheetItem(3, channel.id) {
|
||||
override fun copy(checked: Boolean, disabled: Boolean): SelectSheetItem = copy(checked = checked, disabled = disabled, channel = channel)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
package com.aliucord.coreplugins.componentsv2.selectsheet
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.aliucord.Utils
|
||||
import com.aliucord.utils.DimenUtils.dp
|
||||
import com.aliucord.wrappers.ChannelWrapper.Companion.name
|
||||
import com.aliucord.wrappers.ChannelWrapper.Companion.type
|
||||
import com.aliucord.wrappers.GuildRoleWrapper.Companion.name
|
||||
import com.discord.api.channel.Channel
|
||||
import com.discord.models.member.GuildMember
|
||||
import com.discord.utilities.color.ColorCompat
|
||||
import com.discord.utilities.drawable.DrawableCompat
|
||||
import com.discord.utilities.guilds.RoleUtils
|
||||
import com.discord.utilities.icon.IconUtils
|
||||
import com.discord.utilities.images.MGImages
|
||||
import com.discord.utilities.mg_recycler.MGRecyclerViewHolder
|
||||
import com.discord.utilities.user.UserUtils
|
||||
import com.discord.utilities.view.extensions.ViewExtensions
|
||||
import com.facebook.drawee.view.SimpleDraweeView
|
||||
import com.google.android.material.checkbox.MaterialCheckBox
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
import com.lytefast.flexinput.R
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
internal class SelectSheetItemViewHolder(adapter: SelectSheetAdapter)
|
||||
: MGRecyclerViewHolder<SelectSheetAdapter, SelectSheetItem>(Utils.getResId("widget_select_component_bottom_sheet_item", "layout"), adapter) {
|
||||
|
||||
private val description = itemView.findViewById<MaterialTextView>(Utils.getResId("select_component_sheet_item_description", "id"))!!
|
||||
private val divider = itemView.findViewById<View>(Utils.getResId("select_component_sheet_item_divider", "id"))!!
|
||||
private val dividerWithIcon = itemView.findViewById<View>(Utils.getResId("select_component_sheet_item_divider_icon", "id"))!!
|
||||
private val icon = itemView.findViewById<SimpleDraweeView>(Utils.getResId("select_component_sheet_item_icon", "id"))!!
|
||||
private val checkbox = itemView.findViewById<MaterialCheckBox>(Utils.getResId("select_component_sheet_item_selected", "id"))!!
|
||||
private val title = itemView.findViewById<MaterialTextView>(Utils.getResId("select_component_sheet_item_title", "id"))!!
|
||||
|
||||
init {
|
||||
(itemView as ConstraintLayout).minHeight = 62.dp
|
||||
divider.visibility = View.GONE
|
||||
dividerWithIcon.visibility = View.VISIBLE
|
||||
dividerWithIcon.layoutParams = (dividerWithIcon.layoutParams as ConstraintLayout.LayoutParams).apply {
|
||||
marginStart = 56.dp
|
||||
}
|
||||
description.setPadding(0, 0, 0, 12.dp)
|
||||
icon.visibility = View.VISIBLE
|
||||
icon.layoutParams = (icon.layoutParams as ConstraintLayout.LayoutParams).apply {
|
||||
width = 24.dp
|
||||
height = 24.dp
|
||||
}
|
||||
MGImages.setRoundingParams(
|
||||
icon,
|
||||
Float.MAX_VALUE,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onConfigure(viewType: Int, item: SelectSheetItem) {
|
||||
super.onConfigure(viewType, item)
|
||||
|
||||
description.visibility = View.GONE
|
||||
checkbox.visibility = if (adapter.viewModel.state?.isMultiSelect == true) VISIBLE else GONE
|
||||
checkbox.isChecked = item.checked
|
||||
title.setPadding(0, 12.dp, 0, 12.dp)
|
||||
|
||||
when (item) {
|
||||
is SelectSheetItem.ChannelSelectItem -> configureChannel(item)
|
||||
is SelectSheetItem.RoleSelectItem -> configureRole(item)
|
||||
is SelectSheetItem.UserSelectItem -> configureUser(item)
|
||||
}
|
||||
itemView.setOnClickListener { adapter.viewModel.onItemSelect(item) }
|
||||
ViewExtensions.setEnabledAlpha(itemView, !item.disabled, 0.3f);
|
||||
itemView.isEnabled = !item.disabled
|
||||
}
|
||||
|
||||
private fun configureChannel(item: SelectSheetItem.ChannelSelectItem) {
|
||||
title.text = "#${item.channel.name}"
|
||||
val res = when (item.channel.type) {
|
||||
Channel.GUILD_ANNOUNCEMENT -> R.e.ic_channel_announcements
|
||||
Channel.GUILD_VOICE -> R.e.ic_channel_voice
|
||||
Channel.CATEGORY -> DrawableCompat.getThemedDrawableRes(adapter.context, R.b.ic_category)
|
||||
else -> DrawableCompat.getThemedDrawableRes(adapter.context, R.b.ic_channel_text)
|
||||
}
|
||||
icon.setImageResource(res)
|
||||
}
|
||||
|
||||
private fun configureRole(item: SelectSheetItem.RoleSelectItem) {
|
||||
title.text = item.role.name
|
||||
|
||||
val opaqueColor: Int = RoleUtils.getOpaqueColor(item.role, ColorCompat.getColor(adapter.context, R.c.status_grey_500))
|
||||
icon.setImageResource(R.e.ic_role_24dp)
|
||||
icon.setColorFilter(opaqueColor)
|
||||
}
|
||||
|
||||
private fun configureUser(item: SelectSheetItem.UserSelectItem) {
|
||||
IconUtils.`setIcon$default`(
|
||||
icon,
|
||||
item.user,
|
||||
R.d.avatar_size_standard,
|
||||
null,
|
||||
null,
|
||||
item.member,
|
||||
24,
|
||||
null
|
||||
)
|
||||
title.text = GuildMember.Companion!!.getNickOrUsername(item.member, item.user)
|
||||
val descText = item.user.username + if (item.user.discriminator != 0)
|
||||
UserUtils.INSTANCE.getDiscriminatorWithPadding(item.user)
|
||||
else
|
||||
""
|
||||
if (title.text != descText) {
|
||||
title.setPadding(0, 12.dp, 0, 0)
|
||||
description.visibility = View.VISIBLE
|
||||
description.text = descText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
@file:Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS")
|
||||
|
||||
package com.aliucord.coreplugins.componentsv2.selectsheet
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.aliucord.coreplugins.componentsv2.BotUiComponentV2Entry
|
||||
import com.aliucord.coreplugins.componentsv2.ComponentV2Type
|
||||
import com.aliucord.coreplugins.componentsv2.models.SelectV2MessageComponent
|
||||
import com.aliucord.wrappers.ChannelWrapper.Companion.id
|
||||
import com.discord.api.botuikit.ComponentType
|
||||
import com.discord.restapi.RestAPIParams.ComponentInteractionData.SelectComponentInteractionData
|
||||
import com.discord.stores.StoreStream
|
||||
|
||||
const val ENTRY_LIMIT = 15
|
||||
|
||||
internal class SelectSheetViewModel() : ViewModel() {
|
||||
data class ViewState(
|
||||
val placeholder: String,
|
||||
val items: List<SelectSheetItem>,
|
||||
val isMultiSelect: Boolean,
|
||||
val minSelections: Int,
|
||||
val maxSelections: Int,
|
||||
val isValidSelection: Boolean,
|
||||
)
|
||||
|
||||
private data class SubmissionData(
|
||||
val applicationId: Long,
|
||||
val guildId: Long?,
|
||||
val channelId: Long,
|
||||
val messageId: Long,
|
||||
val messageFlags: Long,
|
||||
val index: Int,
|
||||
val customId: String,
|
||||
val type: ComponentType,
|
||||
)
|
||||
|
||||
var onUpdate: ((ViewState) -> Unit)? = null
|
||||
var onRequestDismiss: (() -> Unit)? = null
|
||||
|
||||
var state: ViewState? = null
|
||||
set(value) {
|
||||
field = value
|
||||
value?.let { onUpdate?.invoke(it) }
|
||||
}
|
||||
|
||||
private var submissionData: SubmissionData? = null
|
||||
|
||||
fun configure(entry: BotUiComponentV2Entry, component: SelectV2MessageComponent) {
|
||||
var entryCount = 0
|
||||
val items = mutableListOf<SelectSheetItem>()
|
||||
val users = StoreStream.getUsers().users
|
||||
if (component.type in listOf(ComponentV2Type.USER_SELECT, ComponentV2Type.MENTIONABLE_SELECT)) {
|
||||
for (member in entry.guildMembers.values) {
|
||||
entryCount += 1
|
||||
if (entryCount > ENTRY_LIMIT)
|
||||
break
|
||||
val user = users[member.userId]!!
|
||||
val isDefault = component.defaultValues.any { it.id == member.userId }
|
||||
items.add(SelectSheetItem.UserSelectItem(user, member, isDefault))
|
||||
}
|
||||
}
|
||||
if (component.type in listOf(ComponentV2Type.ROLE_SELECT, ComponentV2Type.MENTIONABLE_SELECT)) {
|
||||
for (role in entry.guildRoles.values) {
|
||||
entryCount += 1
|
||||
if (entryCount > ENTRY_LIMIT)
|
||||
break
|
||||
val isDefault = component.defaultValues.any { it.id == role.id }
|
||||
items.add(SelectSheetItem.RoleSelectItem(role, isDefault))
|
||||
}
|
||||
}
|
||||
// TODO: is the guildID check needed? as in, can server side allow this component?
|
||||
if (component.type == ComponentV2Type.CHANNEL_SELECT && entry.guildId != null) {
|
||||
val channels = StoreStream.getChannels().getChannelsForGuild(entry.guildId!!)!!
|
||||
for (channel in channels.values) {
|
||||
entryCount += 1
|
||||
if (entryCount > ENTRY_LIMIT)
|
||||
break
|
||||
val isDefault = component.defaultValues.any { it.id == channel.id }
|
||||
items.add(SelectSheetItem.ChannelSelectItem(channel, isDefault))
|
||||
}
|
||||
}
|
||||
|
||||
val min = component.minValues
|
||||
val max = component.maxValues
|
||||
state = ViewState(
|
||||
component.placeholder,
|
||||
items,
|
||||
isMultiSelect = max > 1,
|
||||
minSelections = min,
|
||||
maxSelections = max,
|
||||
isValidSelection = false,
|
||||
)
|
||||
submissionData = SubmissionData(
|
||||
entry.applicationId,
|
||||
entry.guildId,
|
||||
entry.message.channelId,
|
||||
entry.message.id,
|
||||
entry.message.flags,
|
||||
component.index,
|
||||
component.customId,
|
||||
component.type,
|
||||
)
|
||||
}
|
||||
|
||||
fun onItemSelect(item: SelectSheetItem) {
|
||||
val state = state ?: return
|
||||
var checkedCount = 0
|
||||
var newItems = state.items.map {
|
||||
val res = if (it == item)
|
||||
item.copy(checked = !item.checked)
|
||||
else
|
||||
it
|
||||
if (res.checked)
|
||||
checkedCount += 1
|
||||
res
|
||||
}
|
||||
val isMaxed = checkedCount == state.maxSelections
|
||||
newItems = newItems.map {
|
||||
it.copy(disabled = isMaxed && !it.checked)
|
||||
}
|
||||
this.state = state.copy(
|
||||
items = newItems,
|
||||
isValidSelection = checkedCount in state.minSelections..state.maxSelections
|
||||
)
|
||||
|
||||
if (!state.isMultiSelect)
|
||||
submit()
|
||||
}
|
||||
|
||||
fun submit() {
|
||||
// val companion = StoreStream.Companion
|
||||
// companion.localActionComponentState.setSelectComponentSelection(this.componentContext.getMessageId(), this.componentIndex, u.toList(set))
|
||||
val state = state ?: return
|
||||
val submissionData = submissionData ?: return
|
||||
|
||||
val selected = state.items.filter { it.checked }.map { it.id.toString() }
|
||||
submissionData.run {
|
||||
StoreStream.getInteractions().sendComponentInteraction(
|
||||
applicationId,
|
||||
guildId,
|
||||
channelId,
|
||||
messageId,
|
||||
index,
|
||||
SelectComponentInteractionData(
|
||||
type,
|
||||
customId,
|
||||
selected,
|
||||
),
|
||||
messageFlags
|
||||
)
|
||||
}
|
||||
onRequestDismiss?.invoke()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
@file:Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS")
|
||||
|
||||
package com.aliucord.coreplugins.componentsv2.views
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import com.aliucord.Logger
|
||||
import com.aliucord.coreplugins.componentsv2.BotUiComponentV2Entry
|
||||
import com.aliucord.coreplugins.componentsv2.ComponentV2Type
|
||||
import com.aliucord.coreplugins.componentsv2.models.ContainerMessageComponent
|
||||
import com.aliucord.utils.DimenUtils.dp
|
||||
import com.aliucord.utils.ViewUtils.addTo
|
||||
import com.aliucord.widgets.LinearLayout
|
||||
import com.discord.utilities.color.ColorCompat
|
||||
import com.discord.widgets.botuikit.ComponentProvider
|
||||
import com.discord.widgets.botuikit.views.ComponentActionListener
|
||||
import com.discord.widgets.botuikit.views.ComponentView
|
||||
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemBotComponentRow
|
||||
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemBotComponentRowKt
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.lytefast.flexinput.R
|
||||
|
||||
class ContainerComponentView(ctx: Context) : ConstraintLayout(ctx), ComponentView<ContainerMessageComponent> {
|
||||
override fun type() = ComponentV2Type.CONTAINER
|
||||
|
||||
companion object {
|
||||
private val accentDividerId = View.generateViewId()
|
||||
}
|
||||
|
||||
private lateinit var accentDivider: View
|
||||
private lateinit var contentView: LinearLayout
|
||||
private lateinit var spoilerView: SpoilerView
|
||||
|
||||
init {
|
||||
MaterialCardView(ctx).addTo(this) {
|
||||
radius = 8.dp.toFloat()
|
||||
elevation = 0f
|
||||
setCardBackgroundColor(ColorCompat.getThemedColor(ctx, R.b.colorBackgroundSecondary))
|
||||
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
||||
topToTop = PARENT_ID
|
||||
bottomToBottom = PARENT_ID
|
||||
startToStart = PARENT_ID
|
||||
}
|
||||
ConstraintLayout(ctx).addTo(this) {
|
||||
accentDivider = View(ctx).addTo(this) {
|
||||
id = accentDividerId
|
||||
layoutParams = LayoutParams(3.dp, 0).apply {
|
||||
bottomToBottom = PARENT_ID
|
||||
startToStart = PARENT_ID
|
||||
topToTop = PARENT_ID
|
||||
}
|
||||
}
|
||||
contentView = LinearLayout(ctx).addTo(this) {
|
||||
setPadding(8.dp, 8.dp, 8.dp, 8.dp)
|
||||
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
||||
startToEnd = accentDividerId
|
||||
endToEnd = PARENT_ID
|
||||
topToTop = PARENT_ID
|
||||
constrainedWidth = true
|
||||
}
|
||||
}
|
||||
spoilerView = SpoilerView(ctx, 1).addTo(this) {
|
||||
layoutParams = SpoilerView.constraintLayoutParamsAround(PARENT_ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun configure(component: ContainerMessageComponent, provider: ComponentProvider, listener: ComponentActionListener) {
|
||||
val item = listener as WidgetChatListAdapterItemBotComponentRow
|
||||
val entry = item.entry
|
||||
if (entry !is BotUiComponentV2Entry) {
|
||||
Logger("ComponentsV2").warn("configured container with non-v2 entry")
|
||||
return
|
||||
}
|
||||
|
||||
val configuredViews = component.components.mapIndexed { index, child ->
|
||||
provider.getConfiguredComponentView(listener, child, contentView, index)
|
||||
}.filterNotNull()
|
||||
WidgetChatListAdapterItemBotComponentRowKt.replaceViews(contentView, configuredViews)
|
||||
|
||||
val color = component.accentColor?.let { ColorUtils.setAlphaComponent(it, 255) }
|
||||
?: ColorCompat.getThemedColor(context, R.b.colorBackgroundModifierAccent)
|
||||
accentDivider.setBackgroundColor(color)
|
||||
|
||||
spoilerView.configure(entry, component)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
@file:Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS")
|
||||
|
||||
package com.aliucord.coreplugins.componentsv2.views
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.FrameLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
|
||||
import com.aliucord.Logger
|
||||
import com.aliucord.coreplugins.ComponentsV2
|
||||
import com.aliucord.coreplugins.componentsv2.BotUiComponentV2Entry
|
||||
import com.aliucord.coreplugins.componentsv2.ComponentV2Type
|
||||
import com.aliucord.coreplugins.componentsv2.models.MediaGalleryMessageComponent
|
||||
import com.aliucord.utils.DimenUtils.dp
|
||||
import com.aliucord.utils.ViewUtils.addTo
|
||||
import com.aliucord.widgets.LinearLayout
|
||||
import com.aliucord.wrappers.messages.AttachmentWrapper.Companion.height
|
||||
import com.aliucord.wrappers.messages.AttachmentWrapper.Companion.width
|
||||
import com.discord.api.message.attachment.MessageAttachment
|
||||
import com.discord.utilities.color.ColorCompat
|
||||
import com.discord.utilities.display.DisplayUtils
|
||||
import com.discord.utilities.embed.EmbedResourceUtils
|
||||
import com.discord.widgets.botuikit.ComponentProvider
|
||||
import com.discord.widgets.botuikit.views.ComponentActionListener
|
||||
import com.discord.widgets.botuikit.views.ComponentView
|
||||
import com.discord.widgets.chat.list.InlineMediaView
|
||||
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemBotComponentRow
|
||||
import com.discord.widgets.media.WidgetMedia
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.lytefast.flexinput.R
|
||||
|
||||
class MediaGalleryComponentView(ctx: Context) : ConstraintLayout(ctx), ComponentView<MediaGalleryMessageComponent> {
|
||||
override fun type() = ComponentV2Type.MEDIA_GALLERY
|
||||
|
||||
companion object {
|
||||
private val mediaViewId = View.generateViewId()
|
||||
private val maxEmbedHeight = EmbedResourceUtils.INSTANCE.maX_IMAGE_VIEW_HEIGHT_PX
|
||||
}
|
||||
|
||||
private val layout = LinearLayout(ctx).addTo(this) {
|
||||
layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply {
|
||||
topToTop = PARENT_ID
|
||||
startToStart = PARENT_ID
|
||||
endToEnd = PARENT_ID
|
||||
}
|
||||
}
|
||||
private var mediaViews: List<Pair<MessageAttachment, InlineMediaView>>? = null
|
||||
|
||||
// This isn't pretty, but Discord actually does this in their code (EmbedResourceUtils.computeMaximumImageWidthPx)
|
||||
private fun calculateMaxWidth(contained: Boolean): Int {
|
||||
var maxPossibleWidth = DisplayUtils.getScreenSize(context).width() -
|
||||
resources.getDimensionPixelSize(R.d.uikit_guideline_chat) -
|
||||
resources.getDimensionPixelSize(R.d.chat_cell_horizontal_spacing_total)
|
||||
|
||||
if (contained)
|
||||
maxPossibleWidth -= 15.dp
|
||||
|
||||
return maxPossibleWidth.coerceAtMost(1440)
|
||||
}
|
||||
|
||||
// Reference: WidgetChatListAdapterItemAttachment.configureUI
|
||||
override fun configure(component: MediaGalleryMessageComponent, provider: ComponentProvider, listener: ComponentActionListener) {
|
||||
val item = listener as WidgetChatListAdapterItemBotComponentRow
|
||||
val entry = item.entry
|
||||
if (entry !is BotUiComponentV2Entry) {
|
||||
Logger("ComponentsV2").warn("configured media gallery with non-v2 entry")
|
||||
return
|
||||
}
|
||||
|
||||
val maxEmbedWidth = calculateMaxWidth(component.markedContained)
|
||||
layout.removeAllViews()
|
||||
val pendingViews = mutableListOf<Pair<MessageAttachment, InlineMediaView>>()
|
||||
component.items.forEachIndexed { index, it ->
|
||||
val media = it.media
|
||||
// TODO: there's probably a utility to extract filename from url
|
||||
val name = media.url.split("/").last().split("?").first()
|
||||
val attachment = ComponentsV2.createAttachment(
|
||||
name,
|
||||
0,
|
||||
media.proxyUrl,
|
||||
media.url,
|
||||
media.width,
|
||||
media.height,
|
||||
)
|
||||
|
||||
val (width, height) = EmbedResourceUtils.INSTANCE.calculateScaledSize(
|
||||
attachment.width!!,
|
||||
attachment.height!!,
|
||||
maxEmbedWidth,
|
||||
maxEmbedHeight,
|
||||
resources,
|
||||
0,
|
||||
)
|
||||
MaterialCardView(context).addTo(layout) {
|
||||
radius = 8.dp.toFloat()
|
||||
elevation = 0f
|
||||
setCardBackgroundColor(ColorCompat.getThemedColor(context, R.b.colorBackgroundPrimary))
|
||||
layoutParams = android.widget.LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
||||
topMargin = 8.dp
|
||||
}
|
||||
ConstraintLayout(context).addTo(this) {
|
||||
layoutParams = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
|
||||
val mediaView = InlineMediaView(context).addTo(this) {
|
||||
radius = 8.dp.toFloat()
|
||||
elevation = 0f
|
||||
setCardBackgroundColor(ColorCompat.getThemedColor(context, R.b.colorBackgroundPrimary))
|
||||
id = mediaViewId
|
||||
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
||||
topToTop = PARENT_ID
|
||||
startToStart = PARENT_ID
|
||||
}
|
||||
setOnClickListener {
|
||||
WidgetMedia.Companion!!.launch(context, attachment);
|
||||
}
|
||||
updateUIWithAttachment(attachment, width, height, true)
|
||||
}
|
||||
val spoilerView = SpoilerView(context, 1).addTo(this) {
|
||||
translationZ = 10f
|
||||
layoutParams = SpoilerView.constraintLayoutParamsAround(mediaViewId)
|
||||
}
|
||||
pendingViews.add(attachment to mediaView)
|
||||
spoilerView.configure(it.spoiler, entry.state, entry.message.id, Pair(component.id, "media:$index"))
|
||||
}
|
||||
}
|
||||
}
|
||||
mediaViews = pendingViews.toList()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
@file:Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS")
|
||||
|
||||
package com.aliucord.coreplugins.componentsv2.views
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.FrameLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
|
||||
import com.aliucord.coreplugins.componentsv2.ComponentV2Type
|
||||
import com.aliucord.coreplugins.componentsv2.models.SectionMessageComponent
|
||||
import com.aliucord.utils.DimenUtils.dp
|
||||
import com.aliucord.utils.ViewUtils.addTo
|
||||
import com.aliucord.widgets.LinearLayout
|
||||
import com.discord.widgets.botuikit.ComponentProvider
|
||||
import com.discord.widgets.botuikit.views.ComponentActionListener
|
||||
import com.discord.widgets.botuikit.views.ComponentView
|
||||
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemBotComponentRowKt
|
||||
|
||||
class SectionComponentView(ctx: Context) : ConstraintLayout(ctx), ComponentView<SectionMessageComponent> {
|
||||
override fun type() = ComponentV2Type.SECTION
|
||||
|
||||
companion object {
|
||||
private val accessoryViewId = View.generateViewId()
|
||||
}
|
||||
|
||||
private val mainView = LinearLayout(ctx).addTo(this) {
|
||||
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
||||
constrainedWidth = true
|
||||
horizontalBias = 0f
|
||||
topToTop = PARENT_ID
|
||||
startToStart = PARENT_ID
|
||||
endToStart = accessoryViewId
|
||||
marginEnd = 16.dp
|
||||
}
|
||||
}
|
||||
private var accessoryView = FrameLayout(ctx).addTo(this) {
|
||||
id = accessoryViewId
|
||||
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
||||
topToTop = PARENT_ID
|
||||
endToEnd = PARENT_ID
|
||||
}
|
||||
}
|
||||
|
||||
override fun configure(component: SectionMessageComponent, provider: ComponentProvider, listener: ComponentActionListener) {
|
||||
val configuredViews = component.components.mapIndexed { index, child ->
|
||||
provider.getConfiguredComponentView(listener, child, mainView, index)
|
||||
}.filterNotNull()
|
||||
WidgetChatListAdapterItemBotComponentRowKt.replaceViews(mainView, configuredViews)
|
||||
|
||||
val accessoryComponent = provider.getConfiguredComponentView(listener, component.accessory, accessoryView, 0)
|
||||
accessoryComponent?.let {
|
||||
WidgetChatListAdapterItemBotComponentRowKt.replaceViews(accessoryView, listOf(accessoryComponent))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
@file:Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS")
|
||||
|
||||
package com.aliucord.coreplugins.componentsv2.views
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.widget.ImageView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.aliucord.Logger
|
||||
import com.aliucord.coreplugins.componentsv2.BotUiComponentV2Entry
|
||||
import com.aliucord.coreplugins.componentsv2.models.SelectV2MessageComponent
|
||||
import com.aliucord.coreplugins.componentsv2.selectsheet.SelectSheet
|
||||
import com.aliucord.utils.ViewUtils.addTo
|
||||
import com.discord.api.botuikit.ComponentType
|
||||
import com.discord.models.botuikit.SelectMessageComponent
|
||||
import com.discord.views.typing.TypingDots
|
||||
import com.discord.widgets.botuikit.ComponentProvider
|
||||
import com.discord.widgets.botuikit.views.ComponentActionListener
|
||||
import com.discord.widgets.botuikit.views.ComponentView
|
||||
import com.discord.widgets.botuikit.views.select.SelectComponentView
|
||||
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemBotComponentRow
|
||||
import com.facebook.drawee.view.SimpleDraweeView
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
internal class SelectV2ComponentView(context: Context, private val type: ComponentType)
|
||||
: ConstraintLayout(context), ComponentView<SelectV2MessageComponent> {
|
||||
override fun type(): ComponentType = type
|
||||
|
||||
private val componentView: SelectComponentView
|
||||
private val chevron: ImageView
|
||||
private val loadingDots: TypingDots
|
||||
private val selectionIcon: SimpleDraweeView
|
||||
private val selectionText: MaterialTextView
|
||||
private val selectionsRoot: FlexboxLayout
|
||||
|
||||
init {
|
||||
val view = SelectComponentView.Companion!!.inflateComponent(context, this).addTo(this)
|
||||
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||
maxWidth = view.maxWidth
|
||||
b.a.i.b5.a(view).run {
|
||||
componentView = a
|
||||
chevron = b
|
||||
loadingDots = c
|
||||
selectionIcon = d
|
||||
selectionText = e
|
||||
selectionsRoot = f
|
||||
}
|
||||
}
|
||||
|
||||
override fun configure(
|
||||
component: SelectV2MessageComponent,
|
||||
provider: ComponentProvider,
|
||||
listener: ComponentActionListener,
|
||||
) {
|
||||
val item = listener as WidgetChatListAdapterItemBotComponentRow
|
||||
val entry = item.entry
|
||||
if (entry !is BotUiComponentV2Entry) {
|
||||
Logger("ComponentsV2").warn("configured v2 select with non-v2 entry")
|
||||
return
|
||||
}
|
||||
|
||||
val proxyComponent = component.run {
|
||||
SelectMessageComponent(
|
||||
type,
|
||||
index,
|
||||
stateInteraction,
|
||||
customId,
|
||||
placeholder,
|
||||
minValues,
|
||||
maxValues,
|
||||
listOf(),
|
||||
listOf(),
|
||||
emojiAnimationsEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
componentView.configure(proxyComponent, provider, listener)
|
||||
componentView.setOnClickListener {
|
||||
val sh = SelectSheet(entry, component)
|
||||
sh.show(item.adapter.fragmentManager, SelectSheet::class.java.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
@file:Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS")
|
||||
|
||||
package com.aliucord.coreplugins.componentsv2.views
|
||||
|
||||
import android.content.Context
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.aliucord.Logger
|
||||
import com.aliucord.coreplugins.componentsv2.BotUiComponentV2Entry
|
||||
import com.aliucord.coreplugins.componentsv2.ComponentV2Type
|
||||
import com.aliucord.coreplugins.componentsv2.models.SeparatorMessageComponent
|
||||
import com.aliucord.utils.DimenUtils.dp
|
||||
import com.aliucord.utils.ViewUtils.addTo
|
||||
import com.aliucord.views.Divider
|
||||
import com.discord.utilities.color.ColorCompat
|
||||
import com.discord.widgets.botuikit.ComponentProvider
|
||||
import com.discord.widgets.botuikit.views.ComponentActionListener
|
||||
import com.discord.widgets.botuikit.views.ComponentView
|
||||
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemBotComponentRow
|
||||
import com.lytefast.flexinput.R
|
||||
|
||||
class SeparatorComponentView(ctx: Context) : ConstraintLayout(ctx), ComponentView<SeparatorMessageComponent> {
|
||||
override fun type() = ComponentV2Type.SEPARATOR
|
||||
|
||||
private val divider = Divider(ctx).addTo(this) {
|
||||
setBackgroundColor(ColorCompat.getThemedColor(context, R.b.colorTextMuted));
|
||||
}
|
||||
|
||||
override fun configure(component: SeparatorMessageComponent, provider: ComponentProvider, listener: ComponentActionListener) {
|
||||
val item = listener as WidgetChatListAdapterItemBotComponentRow
|
||||
val entry = item.entry
|
||||
if (entry !is BotUiComponentV2Entry) {
|
||||
Logger("ComponentsV2").warn("configured separator with non-v2 entry")
|
||||
return
|
||||
}
|
||||
|
||||
divider.visibility = if (component.divider) VISIBLE else INVISIBLE
|
||||
divider.layoutParams = (divider.layoutParams as LayoutParams).apply {
|
||||
val padding = 6.dp * component.spacing
|
||||
setPadding(paddingLeft, padding, paddingRight, padding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
package com.aliucord.coreplugins.componentsv2.views
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.*
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
|
||||
import com.aliucord.coreplugins.componentsv2.BotUiComponentV2Entry
|
||||
import com.aliucord.coreplugins.componentsv2.models.SpoilableMessageComponent
|
||||
import com.aliucord.utils.DimenUtils.dp
|
||||
import com.aliucord.utils.ViewUtils.addTo
|
||||
import com.discord.stores.StoreMessageState
|
||||
import com.discord.stores.StoreStream
|
||||
import com.discord.utilities.color.ColorCompat
|
||||
import com.lytefast.flexinput.R
|
||||
|
||||
/**
|
||||
* A view that can be spoilered.
|
||||
*
|
||||
* @param ctx Context
|
||||
* @param type 1 for full (spoiler text and button), 2 for mini (eye icon)
|
||||
*/
|
||||
@SuppressLint("ViewConstructor")
|
||||
internal class SpoilerView(ctx: Context, type: Int) : ConstraintLayout(ctx) {
|
||||
companion object {
|
||||
fun constraintLayoutParamsAround(viewId: Int) =
|
||||
LayoutParams(0, 0).apply {
|
||||
topToTop = viewId
|
||||
bottomToBottom = viewId
|
||||
startToStart = viewId
|
||||
endToEnd = viewId
|
||||
}
|
||||
}
|
||||
|
||||
private val spoilerView = ConstraintLayout(ctx).addTo(this) {
|
||||
visibility = GONE
|
||||
setBackgroundColor(ColorCompat.getThemedColor(ctx, R.b.theme_chat_spoiler_bg))
|
||||
layoutParams = LayoutParams(0, 0).apply {
|
||||
bottomToBottom = PARENT_ID
|
||||
endToEnd = PARENT_ID
|
||||
startToStart = PARENT_ID
|
||||
topToTop = PARENT_ID
|
||||
}
|
||||
isClickable = true
|
||||
|
||||
when (type) {
|
||||
1 -> {
|
||||
CardView(ctx).addTo(this) {
|
||||
elevation = ctx.resources.getDimension(R.d.app_elevation)
|
||||
setCardBackgroundColor(ColorCompat.getThemedColor(ctx, R.b.colorBackgroundFloating))
|
||||
radius = 16.dp.toFloat()
|
||||
|
||||
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
||||
startToStart = PARENT_ID
|
||||
endToEnd = PARENT_ID
|
||||
topToTop = PARENT_ID
|
||||
bottomToBottom = PARENT_ID
|
||||
}
|
||||
|
||||
TextView(ctx, null, 0, R.i.UiKit_TextView_H2).addTo(this) {
|
||||
setText(R.h.spoiler)
|
||||
isAllCaps = true
|
||||
setPadding(8.dp, 4.dp, 8.dp, 4.dp)
|
||||
setTextColor(ColorCompat.getThemedColor(ctx, R.b.colorTextNormal))
|
||||
layoutParams = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
||||
marginStart = 4.dp
|
||||
marginEnd = 4.dp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2 -> {
|
||||
ImageView(ctx).addTo(this) {
|
||||
setImageResource(R.e.ic_spoiler)
|
||||
layoutParams = LayoutParams(0, 0).apply {
|
||||
startToStart = PARENT_ID
|
||||
endToEnd = PARENT_ID
|
||||
topToTop = PARENT_ID
|
||||
bottomToBottom = PARENT_ID
|
||||
dimensionRatio = "1:1"
|
||||
matchConstraintPercentWidth = 0.5f
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> throw IllegalArgumentException("Invalid spoiler view type")
|
||||
}
|
||||
}
|
||||
|
||||
fun configure(entry: BotUiComponentV2Entry, component: SpoilableMessageComponent, key: String? = null) {
|
||||
configure(component.spoiler, entry.state, entry.message.id, Pair(component.id, key))
|
||||
}
|
||||
|
||||
fun configure(
|
||||
isSpoiler: Boolean,
|
||||
state: StoreMessageState.State?,
|
||||
messageId: Long,
|
||||
key: Pair<Int, String?>,
|
||||
) {
|
||||
val (id, strKey) = key
|
||||
val spoiled = if (strKey != null)
|
||||
state?.visibleSpoilerEmbedMap?.get(id)?.contains(strKey) ?: false
|
||||
else
|
||||
state?.visibleSpoilerEmbedMap?.containsKey(id) ?: false
|
||||
|
||||
spoilerView.setOnClickListener {
|
||||
spoilerView.setOnClickListener(null)
|
||||
spoilerView.animate()
|
||||
.withEndAction {
|
||||
if (strKey != null)
|
||||
StoreStream.getMessageState().revealSpoilerEmbedData(messageId, id, strKey)
|
||||
else
|
||||
StoreStream.getMessageState().revealSpoilerEmbed(messageId, id)
|
||||
}
|
||||
.alpha(0f)
|
||||
}
|
||||
spoilerView.visibility = if (isSpoiler && !spoiled) VISIBLE else GONE
|
||||
spoilerView.alpha = 1f
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
@file:Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS")
|
||||
|
||||
package com.aliucord.coreplugins.componentsv2.views
|
||||
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.aliucord.Logger
|
||||
import com.aliucord.coreplugins.componentsv2.BotUiComponentV2Entry
|
||||
import com.aliucord.coreplugins.componentsv2.ComponentV2Type
|
||||
import com.aliucord.coreplugins.componentsv2.models.TextDisplayMessageComponent
|
||||
import com.aliucord.utils.DimenUtils.dp
|
||||
import com.aliucord.utils.ViewUtils.addTo
|
||||
import com.discord.stores.StoreStream
|
||||
import com.discord.utilities.color.ColorCompat
|
||||
import com.discord.utilities.message.MessageUtils
|
||||
import com.discord.utilities.textprocessing.*
|
||||
import com.discord.utilities.textprocessing.node.SpoilerNode
|
||||
import com.discord.utilities.view.text.LinkifiedTextView
|
||||
import com.discord.widgets.botuikit.ComponentProvider
|
||||
import com.discord.widgets.botuikit.views.ComponentActionListener
|
||||
import com.discord.widgets.botuikit.views.ComponentView
|
||||
import com.discord.widgets.chat.list.adapter.*
|
||||
import com.lytefast.flexinput.R
|
||||
|
||||
class TextDisplayComponentView(ctx: Context) : ConstraintLayout(ctx), ComponentView<TextDisplayMessageComponent> {
|
||||
override fun type() = ComponentV2Type.TEXT_DISPLAY
|
||||
|
||||
private val textView = LinkifiedTextView(ContextThemeWrapper(ctx, R.i.UiKit_Chat_Text)).addTo(this) {
|
||||
layoutParams = LayoutParams(0, WRAP_CONTENT).apply {
|
||||
topMargin = 2.dp
|
||||
bottomMargin = 2.dp
|
||||
}
|
||||
}
|
||||
|
||||
override fun configure(component: TextDisplayMessageComponent, provider: ComponentProvider, listener: ComponentActionListener) {
|
||||
val item = listener as WidgetChatListAdapterItemBotComponentRow
|
||||
val entry = item.entry
|
||||
if (entry !is BotUiComponentV2Entry) {
|
||||
Logger("ComponentsV2").warn("configured text display with non-v2 entry")
|
||||
return
|
||||
}
|
||||
|
||||
render(component.id, component.content, item.adapter, entry)
|
||||
}
|
||||
|
||||
private fun render(id: Int, content: String, adapter: WidgetChatListAdapter, entry: BotUiComponentV2Entry) {
|
||||
val data = adapter.data
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val spoilers = entry.state?.visibleSpoilerEmbedMap?.let {
|
||||
WidgetChatListAdapterItemEmbed.Companion.`access$getEmbedFieldVisibleIndices`(
|
||||
WidgetChatListAdapterItemEmbed.Companion,
|
||||
it,
|
||||
id,
|
||||
"comp"
|
||||
)
|
||||
} as List<Int>?
|
||||
val processor = MessagePreprocessor(entry.meId, spoilers, null, false, 50)
|
||||
val nickOrUsernames = MessageUtils.getNickOrUsernames(entry.message, entry.channel, entry.guildMembers, entry.channel.q())
|
||||
val parseChannelMessage = DiscordParser.parseChannelMessage(
|
||||
context,
|
||||
content,
|
||||
MessageRenderContext(
|
||||
context,
|
||||
entry.meId,
|
||||
false,
|
||||
nickOrUsernames,
|
||||
StoreStream.getChannels().channelNames, // TODO, does not change
|
||||
entry.guildRoles,
|
||||
R.b.colorTextLink,
|
||||
`WidgetChatListAdapterItemMessage$getMessageRenderContext$1`.INSTANCE,
|
||||
{ s: String -> adapter.eventHandler.onUrlLongClicked(s) },
|
||||
ColorCompat.getThemedColor(context, R.b.theme_chat_spoiler_bg),
|
||||
ColorCompat.getThemedColor(context, R.b.theme_chat_spoiler_bg_visible),
|
||||
{ node: SpoilerNode<*> -> StoreStream.getMessageState().revealSpoilerEmbedData(entry.message.id, id, "comp:${node.id}") },
|
||||
{ l: Long -> adapter.eventHandler.onUserMentionClicked(l, data.channelId, data.guildId) },
|
||||
`WidgetChatListAdapterItemMessage$getMessageRenderContext$4`(context)
|
||||
),
|
||||
processor,
|
||||
DiscordParser.ParserOptions.DEFAULT,
|
||||
false
|
||||
)
|
||||
textView.setDraweeSpanStringBuilder(parseChannelMessage);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
@file:Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS")
|
||||
|
||||
package com.aliucord.coreplugins.componentsv2.views
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.FrameLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.aliucord.Logger
|
||||
import com.aliucord.coreplugins.componentsv2.BotUiComponentV2Entry
|
||||
import com.aliucord.coreplugins.componentsv2.ComponentV2Type
|
||||
import com.aliucord.coreplugins.componentsv2.models.ThumbnailMessageComponent
|
||||
import com.aliucord.utils.DimenUtils.dp
|
||||
import com.aliucord.utils.ViewUtils.addTo
|
||||
import com.discord.utilities.color.ColorCompat
|
||||
import com.discord.utilities.embed.EmbedResourceUtils
|
||||
import com.discord.utilities.images.MGImages
|
||||
import com.discord.widgets.botuikit.ComponentProvider
|
||||
import com.discord.widgets.botuikit.views.ComponentActionListener
|
||||
import com.discord.widgets.botuikit.views.ComponentView
|
||||
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemBotComponentRow
|
||||
import com.facebook.drawee.view.SimpleDraweeView
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.lytefast.flexinput.R
|
||||
|
||||
class ThumbnailComponentView(ctx: Context) : ConstraintLayout(ctx), ComponentView<ThumbnailMessageComponent> {
|
||||
override fun type() = ComponentV2Type.THUMBNAIL
|
||||
|
||||
private val embedThumbnailMaxSize = (ctx.resources.getDimension(R.d.embed_thumbnail_max_size) * 1.5).toInt()
|
||||
|
||||
companion object {
|
||||
private val imageViewId = View.generateViewId()
|
||||
}
|
||||
private lateinit var imageView: SimpleDraweeView
|
||||
private lateinit var spoilerView: SpoilerView
|
||||
|
||||
init {
|
||||
MaterialCardView(ctx).addTo(this) {
|
||||
radius = 8.dp.toFloat()
|
||||
elevation = 0f
|
||||
setCardBackgroundColor(ColorCompat.getThemedColor(ctx, R.b.colorBackgroundPrimary))
|
||||
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
|
||||
ConstraintLayout(ctx).addTo(this) {
|
||||
layoutParams = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
|
||||
imageView = SimpleDraweeView(ctx, null, 0, R.i.UiKit_ImageView).addTo(this) {
|
||||
id = imageViewId
|
||||
}
|
||||
spoilerView = SpoilerView(ctx, 2).addTo(this) {
|
||||
layoutParams = SpoilerView.constraintLayoutParamsAround(imageViewId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reference: WidgetChatListAdapterItemEmbed.configureEmbedThumbnail
|
||||
override fun configure(component: ThumbnailMessageComponent, provider: ComponentProvider, listener: ComponentActionListener) {
|
||||
val item = listener as WidgetChatListAdapterItemBotComponentRow
|
||||
val entry = item.entry
|
||||
if (entry !is BotUiComponentV2Entry) {
|
||||
Logger("ComponentsV2").warn("configured thumbnail with non-v2 entry")
|
||||
return
|
||||
}
|
||||
|
||||
val (width, height) = EmbedResourceUtils.INSTANCE.calculateScaledSize(
|
||||
component.media.width,
|
||||
component.media.height,
|
||||
embedThumbnailMaxSize,
|
||||
embedThumbnailMaxSize,
|
||||
resources,
|
||||
0
|
||||
)
|
||||
imageView.apply {
|
||||
if (layoutParams.width != width || layoutParams.height != height)
|
||||
layoutParams = layoutParams.apply {
|
||||
this.width = width
|
||||
this.height = height
|
||||
}
|
||||
MGImages.`setImage$default`(
|
||||
this,
|
||||
EmbedResourceUtils.INSTANCE.getPreviewUrls(component.media.proxyUrl, width, height, true), // z2: shouldAnimate
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
252,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
spoilerView.configure(entry, component)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.aliucord.utils
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.discord.views.CheckedSetting
|
||||
|
||||
object ViewUtils {
|
||||
/**
|
||||
* Shorthand extension function to add a View into a ViewGroup, and then
|
||||
* run a scoped function
|
||||
*
|
||||
* @param group ViewGroup to add this View into
|
||||
* @param block A scoped function, with the View as its receiver
|
||||
* @return The View
|
||||
*/
|
||||
fun <T : View> T.addTo(group: ViewGroup, block: (T.() -> Unit)? = null): T = apply { group.addView(this); block?.invoke(this) }
|
||||
|
||||
/**
|
||||
* Shorthand extension function to add a View into a ViewGroup at specified
|
||||
* index, and then run a scoped function
|
||||
*
|
||||
* @param group ViewGroup to add this View into
|
||||
* @param index Index to insert this View at
|
||||
* @param block A scoped function, with the View as its receiver
|
||||
* @return The View
|
||||
*/
|
||||
fun <T : View> T.addTo(group: ViewGroup, index: Int, block: (T.() -> Unit)? = null): T = apply { group.addView(this, index); block?.invoke(this) }
|
||||
|
||||
/** Main layout of the setting */
|
||||
val CheckedSetting.layout get() = l.b() as ConstraintLayout
|
||||
|
||||
/** Main text/label of the setting */
|
||||
val CheckedSetting.label get() = l.a()
|
||||
|
||||
/** Checkbox button at the end of the setting */
|
||||
val CheckedSetting.checkbox get() = l.c()
|
||||
|
||||
/** Subtext of the setting */
|
||||
val CheckedSetting.subtext get() = l.f()
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.discord.api.botuikit
|
||||
|
||||
data class ChannelSelectComponent(
|
||||
private val type: ComponentType,
|
||||
override val id: Int,
|
||||
@b.i.d.p.b("custom_id") override val customId: String,
|
||||
override val placeholder: String,
|
||||
override val defaultValues: List<SelectV2DefaultValue>?,
|
||||
override val minValues: Int,
|
||||
override val maxValues: Int,
|
||||
override val disabled: Boolean,
|
||||
) : SelectV2Component() {
|
||||
override fun getType() = type
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.discord.api.botuikit
|
||||
|
||||
data class ContainerComponent(
|
||||
private val type: ComponentType,
|
||||
val id: Int,
|
||||
val components: List<Component>,
|
||||
@b.i.d.p.b("accent_color") val accentColor: Int?,
|
||||
val spoiler: Boolean,
|
||||
): LayoutComponent() {
|
||||
override fun getType() = type
|
||||
override fun a() = components
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.discord.api.botuikit
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
abstract class ContentComponent : LayoutComponent(), Serializable {
|
||||
final override fun a(): List<Component> = listOf()
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.discord.api.botuikit
|
||||
|
||||
data class FileComponent(
|
||||
private val type: ComponentType,
|
||||
val id: Int,
|
||||
val file: UnfurledMediaItem,
|
||||
val spoiler: Boolean,
|
||||
val name: String,
|
||||
val size: Int,
|
||||
) : ContentComponent() {
|
||||
override fun getType() = type
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.discord.api.botuikit
|
||||
|
||||
data class MediaGalleryComponent(
|
||||
private val type: ComponentType,
|
||||
val id: Int,
|
||||
val items: List<MediaGalleryItem>,
|
||||
) : ContentComponent() {
|
||||
override fun getType() = type
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.discord.api.botuikit
|
||||
|
||||
data class MediaGalleryItem(
|
||||
val media: UnfurledMediaItem,
|
||||
val description: String?,
|
||||
val spoiler: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.discord.api.botuikit
|
||||
|
||||
data class MentionableSelectComponent(
|
||||
private val type: ComponentType,
|
||||
override val id: Int,
|
||||
@b.i.d.p.b("custom_id") override val customId: String,
|
||||
override val placeholder: String,
|
||||
override val defaultValues: List<SelectV2DefaultValue>?,
|
||||
override val minValues: Int,
|
||||
override val maxValues: Int,
|
||||
override val disabled: Boolean,
|
||||
) : SelectV2Component() {
|
||||
override fun getType() = type
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.discord.api.botuikit
|
||||
|
||||
data class RoleSelectComponent(
|
||||
private val type: ComponentType,
|
||||
override val id: Int,
|
||||
@b.i.d.p.b("custom_id") override val customId: String,
|
||||
override val placeholder: String,
|
||||
override val defaultValues: List<SelectV2DefaultValue>?,
|
||||
override val minValues: Int,
|
||||
override val maxValues: Int,
|
||||
override val disabled: Boolean,
|
||||
) : SelectV2Component() {
|
||||
override fun getType() = type
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.discord.api.botuikit
|
||||
|
||||
data class SectionComponent(
|
||||
private val type: ComponentType,
|
||||
val id: Int,
|
||||
val components: List<Component>,
|
||||
val accessory: Component,
|
||||
): LayoutComponent() {
|
||||
override fun getType() = type
|
||||
|
||||
// This property will be accessed by ComponentStateMapper to be processed into MessageComponents,
|
||||
// so we pass in the accessory component to be processed too.
|
||||
// Back in SectionMessageComponent.mergeToMessageComponent, we will separate this back correctly.
|
||||
override fun a() = components + accessory
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.discord.api.botuikit
|
||||
|
||||
abstract class SelectV2Component() : ActionComponent() {
|
||||
abstract val id: Int
|
||||
abstract val customId: String
|
||||
abstract val placeholder: String
|
||||
abstract val defaultValues: List<SelectV2DefaultValue>?
|
||||
abstract val minValues: Int
|
||||
abstract val maxValues: Int
|
||||
abstract val disabled: Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.discord.api.botuikit
|
||||
|
||||
data class SelectV2DefaultValue(
|
||||
val id: Long,
|
||||
val type: SelectV2DefaultValueType,
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.discord.api.botuikit
|
||||
|
||||
enum class SelectV2DefaultValueType {
|
||||
@b.i.d.p.b("user") USER,
|
||||
@b.i.d.p.b("role") ROLE,
|
||||
@b.i.d.p.b("channel") CHANNEL,
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.discord.api.botuikit
|
||||
|
||||
data class SeparatorComponent(
|
||||
private val type: ComponentType,
|
||||
val id: Int,
|
||||
val divider: Boolean,
|
||||
val spacing: Int, // 1 = small padding, 2 = large padding
|
||||
): LayoutComponent() {
|
||||
override fun getType() = type
|
||||
override fun a(): List<Component> = listOf()
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.discord.api.botuikit
|
||||
|
||||
data class TextDisplayComponent(
|
||||
private val type: ComponentType,
|
||||
val id: Int,
|
||||
val content: String,
|
||||
) : ContentComponent() {
|
||||
override fun getType() = type
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.discord.api.botuikit
|
||||
|
||||
data class ThumbnailComponent(
|
||||
private val type: ComponentType,
|
||||
val id: Int,
|
||||
val media: UnfurledMediaItem,
|
||||
val description: String?,
|
||||
val spoiler: Boolean,
|
||||
) : ContentComponent() {
|
||||
override fun getType() = type
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.discord.api.botuikit
|
||||
|
||||
data class UnfurledMediaItem(
|
||||
val url: String,
|
||||
@b.i.d.p.b("proxy_url") val proxyUrl: String,
|
||||
val height: Int,
|
||||
val width: Int,
|
||||
@b.i.d.p.b("content_type") val contentType: String?,
|
||||
@b.i.d.p.b("attachment_id") val attachmentId: Long?,
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.discord.api.botuikit
|
||||
|
||||
data class UserSelectComponent(
|
||||
private val type: ComponentType,
|
||||
override val id: Int,
|
||||
@b.i.d.p.b("custom_id") override val customId: String,
|
||||
override val placeholder: String,
|
||||
override val defaultValues: List<SelectV2DefaultValue>?,
|
||||
override val minValues: Int,
|
||||
override val maxValues: Int,
|
||||
override val disabled: Boolean,
|
||||
) : SelectV2Component() {
|
||||
override fun getType() = type
|
||||
}
|
||||
172
canary/LICENSE
Normal file
172
canary/LICENSE
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
Open Software License ("OSL") v. 3.0
|
||||
|
||||
This Open Software License (the "License") applies to any original work of
|
||||
authorship (the "Original Work") whose owner (the "Licensor") has placed the
|
||||
following licensing notice adjacent to the copyright notice for the Original
|
||||
Work:
|
||||
|
||||
Licensed under the Open Software License version 3.0
|
||||
|
||||
1) Grant of Copyright License. Licensor grants You a worldwide, royalty-free,
|
||||
non-exclusive, sublicensable license, for the duration of the copyright, to do
|
||||
the following:
|
||||
|
||||
a) to reproduce the Original Work in copies, either alone or as part of a
|
||||
collective work;
|
||||
|
||||
b) to translate, adapt, alter, transform, modify, or arrange the Original
|
||||
Work, thereby creating derivative works ("Derivative Works") based upon the
|
||||
Original Work;
|
||||
|
||||
c) to distribute or communicate copies of the Original Work and Derivative
|
||||
Works to the public, with the proviso that copies of Original Work or
|
||||
Derivative Works that You distribute or communicate shall be licensed under
|
||||
this Open Software License;
|
||||
|
||||
d) to perform the Original Work publicly; and
|
||||
|
||||
e) to display the Original Work publicly.
|
||||
|
||||
2) Grant of Patent License. Licensor grants You a worldwide, royalty-free,
|
||||
non-exclusive, sublicensable license, under patent claims owned or controlled
|
||||
by the Licensor that are embodied in the Original Work as furnished by the
|
||||
Licensor, for the duration of the patents, to make, use, sell, offer for sale,
|
||||
have made, and import the Original Work and Derivative Works.
|
||||
|
||||
3) Grant of Source Code License. The term "Source Code" means the preferred
|
||||
form of the Original Work for making modifications to it and all available
|
||||
documentation describing how to modify the Original Work. Licensor agrees to
|
||||
provide a machine-readable copy of the Source Code of the Original Work along
|
||||
with each copy of the Original Work that Licensor distributes. Licensor
|
||||
reserves the right to satisfy this obligation by placing a machine-readable
|
||||
copy of the Source Code in an information repository reasonably calculated to
|
||||
permit inexpensive and convenient access by You for as long as Licensor
|
||||
continues to distribute the Original Work.
|
||||
|
||||
4) Exclusions From License Grant. Neither the names of Licensor, nor the names
|
||||
of any contributors to the Original Work, nor any of their trademarks or
|
||||
service marks, may be used to endorse or promote products derived from this
|
||||
Original Work without express prior permission of the Licensor. Except as
|
||||
expressly stated herein, nothing in this License grants any license to
|
||||
Licensor's trademarks, copyrights, patents, trade secrets or any other
|
||||
intellectual property. No patent license is granted to make, use, sell, offer
|
||||
for sale, have made, or import embodiments of any patent claims other than the
|
||||
licensed claims defined in Section 2. No license is granted to the trademarks
|
||||
of Licensor even if such marks are included in the Original Work. Nothing in
|
||||
this License shall be interpreted to prohibit Licensor from licensing under
|
||||
terms different from this License any Original Work that Licensor otherwise
|
||||
would have a right to license.
|
||||
|
||||
5) External Deployment. The term "External Deployment" means the use,
|
||||
distribution, or communication of the Original Work or Derivative Works in any
|
||||
way such that the Original Work or Derivative Works may be used by anyone
|
||||
other than You, whether those works are distributed or communicated to those
|
||||
persons or made available as an application intended for use over a network.
|
||||
As an express condition for the grants of license hereunder, You must treat
|
||||
any External Deployment by You of the Original Work or a Derivative Work as a
|
||||
distribution under section 1(c).
|
||||
|
||||
6) Attribution Rights. You must retain, in the Source Code of any Derivative
|
||||
Works that You create, all copyright, patent, or trademark notices from the
|
||||
Source Code of the Original Work, as well as any notices of licensing and any
|
||||
descriptive text identified therein as an "Attribution Notice." You must cause
|
||||
the Source Code for any Derivative Works that You create to carry a prominent
|
||||
Attribution Notice reasonably calculated to inform recipients that You have
|
||||
modified the Original Work.
|
||||
|
||||
7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that
|
||||
the copyright in and to the Original Work and the patent rights granted herein
|
||||
by Licensor are owned by the Licensor or are sublicensed to You under the
|
||||
terms of this License with the permission of the contributor(s) of those
|
||||
copyrights and patent rights. Except as expressly stated in the immediately
|
||||
preceding sentence, the Original Work is provided under this License on an "AS
|
||||
IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without
|
||||
limitation, the warranties of non-infringement, merchantability or fitness for
|
||||
a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK
|
||||
IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this
|
||||
License. No license to the Original Work is granted by this License except
|
||||
under this disclaimer.
|
||||
|
||||
8) Limitation of Liability. Under no circumstances and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise, shall the
|
||||
Licensor be liable to anyone for any indirect, special, incidental, or
|
||||
consequential damages of any character arising as a result of this License or
|
||||
the use of the Original Work including, without limitation, damages for loss
|
||||
of goodwill, work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses. This limitation of liability shall not
|
||||
apply to the extent applicable law prohibits such limitation.
|
||||
|
||||
9) Acceptance and Termination. If, at any time, You expressly assented to this
|
||||
License, that assent indicates your clear and irrevocable acceptance of this
|
||||
License and all of its terms and conditions. If You distribute or communicate
|
||||
copies of the Original Work or a Derivative Work, You must make a reasonable
|
||||
effort under the circumstances to obtain the express assent of recipients to
|
||||
the terms of this License. This License conditions your rights to undertake
|
||||
the activities listed in Section 1, including your right to create Derivative
|
||||
Works based upon the Original Work, and doing so without honoring these terms
|
||||
and conditions is prohibited by copyright law and international treaty.
|
||||
Nothing in this License is intended to affect copyright exceptions and
|
||||
limitations (including "fair use" or "fair dealing"). This License shall
|
||||
terminate immediately and You may no longer exercise any of the rights granted
|
||||
to You by this License upon your failure to honor the conditions in Section
|
||||
1(c).
|
||||
|
||||
10) Termination for Patent Action. This License shall terminate automatically
|
||||
and You may no longer exercise any of the rights granted to You by this
|
||||
License as of the date You commence an action, including a cross-claim or
|
||||
counterclaim, against Licensor or any licensee alleging that the Original Work
|
||||
infringes a patent. This termination provision shall not apply for an action
|
||||
alleging patent infringement by combinations of the Original Work with other
|
||||
software or hardware.
|
||||
|
||||
11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this
|
||||
License may be brought only in the courts of a jurisdiction wherein the
|
||||
Licensor resides or in which Licensor conducts its primary business, and under
|
||||
the laws of that jurisdiction excluding its conflict-of-law provisions. The
|
||||
application of the United Nations Convention on Contracts for the
|
||||
International Sale of Goods is expressly excluded. Any use of the Original
|
||||
Work outside the scope of this License or after its termination shall be
|
||||
subject to the requirements and penalties of copyright or patent law in the
|
||||
appropriate jurisdiction. This section shall survive the termination of this
|
||||
License.
|
||||
|
||||
12) Attorneys' Fees. In any action to enforce the terms of this License or
|
||||
seeking damages relating thereto, the prevailing party shall be entitled to
|
||||
recover its costs and expenses, including, without limitation, reasonable
|
||||
attorneys' fees and costs incurred in connection with such action, including
|
||||
any appeal of such action. This section shall survive the termination of this
|
||||
License.
|
||||
|
||||
13) Miscellaneous. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent necessary
|
||||
to make it enforceable.
|
||||
|
||||
14) Definition of "You" in This License. "You" throughout this License,
|
||||
whether in upper or lower case, means an individual or a legal entity
|
||||
exercising rights under, and complying with all of the terms of, this License.
|
||||
For legal entities, "You" includes any entity that controls, is controlled by,
|
||||
or is under common control with you. For purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the direction or
|
||||
management of such entity, whether by contract or otherwise, or (ii) ownership
|
||||
of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial
|
||||
ownership of such entity.
|
||||
|
||||
15) Right to Use. You may use the Original Work in all ways not otherwise
|
||||
restricted or conditioned by this License or by law, and Licensor promises not
|
||||
to interfere with or be responsible for such uses by You.
|
||||
|
||||
16) Modification of This License. This License is Copyright © 2005 Lawrence
|
||||
Rosen. Permission is granted to copy, distribute, or communicate this License
|
||||
without modification. Nothing in this License permits You to modify this
|
||||
License as applied to the Original Work or to Derivative Works. However, You
|
||||
may modify the text of this License and copy, distribute or communicate your
|
||||
modified version (the "Modified License") and apply it to other original works
|
||||
of authorship subject to the following conditions: (i) You may not indicate in
|
||||
any way that your Modified License is the "Open Software License" or "OSL" and
|
||||
you may not use those names in the name of your Modified License; (ii) You
|
||||
must replace the notice specified in the first paragraph above with the notice
|
||||
"Licensed under <insert your license name here>" or with a notice of your own
|
||||
that is not confusingly similar to the notice in this License; and (iii) You
|
||||
may not claim that your original works are open source software unless your
|
||||
Modified License has been approved by Open Source Initiative (OSI) and You
|
||||
comply with its license review and certification process.
|
||||
|
|
@ -1,15 +1,17 @@
|
|||
rootProject.name = "Awoocord"
|
||||
|
||||
// This file sets what projects are included. Every time you add a new project, you must add it
|
||||
// to the includes below.
|
||||
val canaryPlugins = arrayOf("ComponentsV2")
|
||||
|
||||
// Plugins are included like this
|
||||
include(
|
||||
"AlignThreads",
|
||||
"Scout"
|
||||
"Scout",
|
||||
*canaryPlugins,
|
||||
)
|
||||
|
||||
rootProject.children.forEach {
|
||||
// Change kotlin to java if you'd rather use java
|
||||
it.projectDir = file("plugins/${it.name}")
|
||||
val isCanary = it.name in canaryPlugins
|
||||
val dir = if (isCanary) "canary" else "plugins"
|
||||
val name = it.name
|
||||
if (isCanary) it.name += "-Beta"
|
||||
it.projectDir = file("${dir}/${name}")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue