diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 186e979..6fe7679 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,10 +33,10 @@ jobs: repository: "Aliucord/Aliucord" path: "repo" - - name: Setup JDK 11 + - name: Setup JDK 21 uses: actions/setup-java@v1 with: - java-version: 11 + java-version: 21 - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 @@ -46,8 +46,8 @@ jobs: cd $GITHUB_WORKSPACE/src chmod +x gradlew ./gradlew make generateUpdaterJson - cp {canary,plugins}/*/build/*.zip $GITHUB_WORKSPACE/builds - cp build/updater.json $GITHUB_WORKSPACE/builds + cp {canary,plugins}/*/build/outputs/*.zip $GITHUB_WORKSPACE/builds + cp build/outputs/updater.json $GITHUB_WORKSPACE/builds - name: Push builds run: | diff --git a/README.md b/README.md index 1fefa06..63011cc 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,43 @@ # Awoocord Plugins -## [RoleBlocks](plugins/Zinnia) | [Download](https://github.com/LavaDesu/Awoocord/raw/builds/RoleBlocks.zip) +## [Bubbles](plugins/Crocosmia) | [Download](https://github.com/cillynder/Awoocord/raw/builds/Bubbles.zip) + +Wrap messages in bubbles inspired by Material 3 Expressive + +## [Clump](plugins/Bocchi) | [Download](https://github.com/cillynder/Awoocord/raw/builds/Clump.zip) + +Group messages more leniently (e.g. mentions, attachments, etc..), reducing clutter and wasted space. + +## [Glance](plugins/Myosotis) | [Download](https://github.com/cillynder/Awoocord/raw/builds/Glance.zip) + +Backports DM previews similar to latest RN. Shows you a line of the last message sent in a DM. + +## [RoleBlocks](plugins/Zinnia) | [Download](https://github.com/cillynder/Awoocord/raw/builds/RoleBlocks.zip) Apply the role colour as a background of usernames, improving contrast with some role colours -## [Scout](plugins/Scout) | [Download](https://github.com/LavaDesu/Awoocord/raw/builds/Scout.zip) +## [Scout](plugins/Scout) | [Download](https://github.com/cillynder/Awoocord/raw/builds/Scout.zip) -Reimplemented features from search of other clients: -- Sorting by oldest first -- Filter by date -- Search from user ID +Vastly improves the search experience on Aliucord. + +Features: +- Sort by oldest messages first +- Sort by oldest first +- Filter by date (before, during, after) +- Exclude certain messages (opposite of `in:`) (not even desktop has this!) +- Search by user ID +- Search in threads + +Fixes: +- Removes the large padding from the top, most noticable if your device has a large status bar +- Removes the unnecessary #0000 discriminator # WIP Backports -## [SlashCommandsFix](canary/SlashCommandsFix) | [Download](https://github.com/LavaDesu/Awoocord/raw/builds/SlashCommandsFixBeta.zip) +## [SlashCommandsFix](canary/SlashCommandsFix) | [Download](https://github.com/cillynder/Awoocord/raw/builds/SlashCommandsFixBeta.zip) Fixes slash commands not showing up. -## [ComponentsV2](canary/ComponentsV2) | [Download](https://github.com/LavaDesu/Awoocord/raw/builds/ComponentsV2Beta.zip) +## [ComponentsV2](canary/ComponentsV2) | [Download](https://github.com/cillynder/Awoocord/raw/builds/ComponentsV2Beta.zip) -Fix missing/empty bot messages using the new embed system. Such messages will be marked "CV2" as part of its tag. \ No newline at end of file +Fix missing/empty bot messages using the new embed system. Such messages will be marked "CV2" as part of its tag. diff --git a/build.gradle.kts b/build.gradle.kts index 4cc50c4..6cfa17b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -46,8 +46,8 @@ subprojects { } configure { - author("Lava", 368398754077868032L, hyperlink = true) - github("https://github.com/LavaDesu/Awoocord") + author("cilly", 368398754077868032L, hyperlink = false) + github("https://github.com/cillynder/Awoocord") } configure { diff --git a/plugins/Bocchi/build.gradle.kts b/plugins/Bocchi/build.gradle.kts new file mode 100644 index 0000000..1803a3c --- /dev/null +++ b/plugins/Bocchi/build.gradle.kts @@ -0,0 +1,25 @@ +version = "1.0.3" +description = "More lenient message grouping" + +android { + namespace = "moe.lava.awoocord.bocchi" +} + +aliucord { + // Changelog of your plugin + changelog.set(""" + # 1.0.3 + * Clump more than 6 messages together + + # 1.0.2 + * Fix (inverted) webhook clumping + + # 1.0.1 + * Hide blank space w.r.t attachments and embeds + + # 1.0.0 + * Initial release >w< + """.trimIndent()) + + deploy.set(true) +} diff --git a/plugins/Bocchi/src/main/kotlin/moe/lava/awoocord/bocchi/Bocchi.kt b/plugins/Bocchi/src/main/kotlin/moe/lava/awoocord/bocchi/Bocchi.kt new file mode 100644 index 0000000..d4d28da --- /dev/null +++ b/plugins/Bocchi/src/main/kotlin/moe/lava/awoocord/bocchi/Bocchi.kt @@ -0,0 +1,59 @@ +package moe.lava.awoocord.bocchi + +import android.content.Context +import android.view.View +import com.aliucord.annotations.AliucordPlugin +import com.aliucord.entities.Plugin +import com.aliucord.patcher.* +import com.aliucord.utils.accessField +import com.discord.api.message.MessageTypes +import com.discord.models.message.Message +import com.discord.utilities.view.text.SimpleDraweeSpanTextView +import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemMessage +import com.discord.widgets.chat.list.entries.ChatListEntry +import com.discord.widgets.chat.list.entries.MessageEntry +import com.discord.widgets.chat.list.model.WidgetChatListModelMessages + +private val WidgetChatListAdapterItemMessage.itemText by accessField() + +@AliucordPlugin(requiresRestart = true) +@Suppress("unused") +class Bocchi : Plugin() { + override fun start(context: Context) { + patcher.after( + "onConfigure", + Int::class.java, + ChatListEntry::class.java, + ) { (_, _: Int, entry: MessageEntry) -> + if (entry.type == ChatListEntry.MESSAGE_MINIMAL && entry.message.content.isNullOrEmpty()) { + itemText.visibility = View.GONE + } + } + patcher.instead( + "shouldConcatMessage", + WidgetChatListModelMessages.Items::class.java, + Message::class.java, + Message::class.java, + ) { (_, items: WidgetChatListModelMessages.Items, message: Message, message2: Message?) -> + val timeDiff = (message.timestamp?.g() ?: 0L) - (message2?.timestamp?.g() ?: 0L) + return@instead !( + message2 == null || + message2.isSystemMessage || + message.hasThread() || + message2.hasThread() || + message.type !in arrayOf(MessageTypes.DEFAULT, MessageTypes.LOCAL) || + message.author.id != message2.author.id || + timeDiff >= 420000 || // WidgetChatListModelMessages.MESSAGE_CONCAT_TIMESTAMP_DELTA_THRESHOLD +// items.listItemMostRecentlyAdded.type !in arrayOf(0, 1, 4, 21) || +// message2.hasAttachments() || +// message2.hasEmbeds() || +// message2.mentions?.isNotEmpty() == true || +// message.mentions?.isNotEmpty() == true || +// message.hasAttachments() || +// message.hasEmbeds() || +// items.concatCount >= 5 || + (message.isWebhook && message.author?.username != message2.author.username) + ) + } + } +} diff --git a/plugins/Crocosmia/build.gradle.kts b/plugins/Crocosmia/build.gradle.kts new file mode 100644 index 0000000..a3aaca9 --- /dev/null +++ b/plugins/Crocosmia/build.gradle.kts @@ -0,0 +1,12 @@ +version = "1.0.0" +description = "Bubbled messages" + +aliucord { + // Changelog of your plugin + changelog.set(""" + # 1.0.0 + * Initial release >w< + """.trimIndent()) + + deploy.set(true) +} diff --git a/plugins/Crocosmia/src/main/kotlin/moe/lava/awoocord/crocosmia/Crocosmia.kt b/plugins/Crocosmia/src/main/kotlin/moe/lava/awoocord/crocosmia/Crocosmia.kt new file mode 100644 index 0000000..0cd9eca --- /dev/null +++ b/plugins/Crocosmia/src/main/kotlin/moe/lava/awoocord/crocosmia/Crocosmia.kt @@ -0,0 +1,475 @@ +package moe.lava.awoocord.crocosmia + +import android.content.Context +import android.graphics.Color +import android.view.View +import android.view.View.GONE +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.FrameLayout +import android.widget.LinearLayout +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID +import com.aliucord.PluginManager +import com.aliucord.Utils +import com.aliucord.annotations.AliucordPlugin +import com.aliucord.api.SettingsAPI +import com.aliucord.entities.Plugin +import com.aliucord.patcher.* +import com.aliucord.utils.DimenUtils.dp +import com.aliucord.utils.ViewUtils.addTo +import com.aliucord.utils.ViewUtils.findViewById +import com.aliucord.utils.accessField +import com.discord.databinding.WidgetChatListAdapterItemBotComponentRowBinding +import com.discord.databinding.WidgetChatListAdapterItemEmbedBinding +import com.discord.utilities.color.ColorCompat +import com.discord.utilities.display.DisplayUtils +import com.discord.utilities.embed.EmbedResourceUtils +import com.discord.widgets.chat.list.adapter.* +import com.discord.widgets.chat.list.entries.* +import com.google.android.material.card.MaterialCardView +import com.google.android.material.shape.CornerFamily +import com.lytefast.flexinput.R +import de.robv.android.xposed.XC_MethodHook +import java.util.WeakHashMap +import kotlin.math.min + +private val padding get() = 12.dp +//private val topPad get() = 14.dp +private val topPad get() = 6.dp +private val bigCorner get() = 24.dp.toFloat() +private val smallCorner get() = 4.dp.toFloat() +private val ChatListEntry.connectBefore get() = this.type in arrayOf( + ChatListEntry.MESSAGE_MINIMAL, + ChatListEntry.MESSAGE_EMBED, + ChatListEntry.MESSAGE_ATTACHMENT, + ChatListEntry.STICKER, + ChatListEntry.BOT_UI_COMPONENT, + 101, +) +private val ChatListEntry.excepted get() = this.type in arrayOf( + ChatListEntry.REACTIONS, +) + +private val WidgetChatListAdapterItemBotComponentRow.binding by accessField() +private val WidgetChatListAdapterItemAttachment.binding get() = WidgetChatListAdapterItemAttachment.`access$getBinding$p`(this) +private val WidgetChatListAdapterItemEmbed.binding by accessField() +private val WidgetChatListAdapterItemSticker.binding get() = WidgetChatListAdapterItemSticker.`access$getBinding$p`(this) + +private var MessageEntry.keyField by accessField() + +private val fullId = Utils.getResId("widget_chat_list_adapter_item_text", "layout") +private val minimalId = Utils.getResId("widget_chat_list_adapter_item_minimal", "layout") +private val bubbleId = View.generateViewId() + +private const val messageLayoutTag = R.f.message // Just some random id + +@Suppress("UNUSED") +@AliucordPlugin +class Crocosmia : Plugin() { + private fun createBubble(context: Context, parentHandler: View? = null): MaterialCardView { + return MaterialCardView(context).apply { + id = bubbleId + setCardBackgroundColor( + ColorCompat.getThemedColor( + this, + R.b.colorBackgroundSecondary + ) + ) + parentHandler?.let { parent -> + setOnClickListener { parent.performClick() } + setOnLongClickListener { parent.performLongClick() } + } + elevation = 0f + } + } + + private fun WidgetChatListItem.configBubble(entry: ChatListEntry) { + itemView.findViewById(bubbleId)?.let { + configBubble(it, entry) + } + } + + private fun WidgetChatListItem.configBubble(view: MaterialCardView, entry: ChatListEntry) { + val idx = adapter.data.list.indexOf(entry) + val previousEntry = adapter.data.list.getOrNull(idx + 1) + val nextEntry = if (idx < 1) null else adapter.data.list[idx - 1] + view.shapeAppearanceModel = view.shapeAppearanceModel.toBuilder().run { + setAllCorners(CornerFamily.ROUNDED, bigCorner) + if (entry.connectBefore && previousEntry?.excepted != true) { + setTopLeftCornerSize(smallCorner) + setTopRightCornerSize(smallCorner) + } + if (nextEntry?.connectBefore == true) { + setBottomLeftCornerSize(smallCorner) + setBottomRightCornerSize(smallCorner) + } + build() + } + view.clipToOutline = true + } + + override fun load(context: Context) { + hasCompactMode = PluginManager.isPluginEnabled("CompactMode") + hasHighlightMessages = PluginManager.isPluginEnabled("HighlightOwnMessages") + if (hasCompactMode) { + logger.info("Enabling compatibility with CompactMode") + compactCompatOverride = SettingsAPI("CompactMode").getInt("contentMargin", 8) + } + } + + private fun compatHighlightMessages() { + val cls = try { + val cl = PluginManager.plugins["HighlightOwnMessages"]!!.javaClass + val loader = cl.classLoader!! + loader.loadClass( + $$$"cloudburst.plugins.highlightownmessages.HighlightOwnMessages$$ExternalSyntheticLambda0" + ) + } catch(e: Throwable) { + logger.warn("Tried to enable compatibility with HighlightOwnMessages, but no lambda class found", e) + return + } + logger.info("Enabling compatibility with HighlightOwnMessages") + val method = cls.getDeclaredMethod("call", Object::class.java) + patcher.patch(method) { mparam -> + val param = mparam.args[0] as XC_MethodHook.MethodHookParam + val self = param.thisObject as? WidgetChatListAdapterItemMessage + ?: return@patch logger.warn("Failed to cast thisObject (found: ${param.thisObject.javaClass.name})") + self.run { + val isFull = itemView.getTag(messageLayoutTag) as? Boolean + ?: return@patch + itemView.findViewById("chat_list_adapter_item_text").apply { + layoutParams = (layoutParams as ConstraintLayout.LayoutParams).apply { + if (isFull) { + setPadding(padding, 0, padding, padding) + } else { + setPadding(padding, padding + 2.dp, padding, padding) + } + } + } + } + } + } + + override fun stop(context: Context) { patcher.unpatchAll() } + + var hasCompactMode = false + var compactCompatOverride: Int? = null + var hasHighlightMessages = false + + override fun start(context: Context) { + patcher.after( + "setData", + WidgetChatListAdapter.Data::class.java, + ) { + notifyItemChanged(1, Unit.a) + } + + patcher.after( + WidgetChatListAdapter::class.java, + ) { + binding.a.layoutParams = binding.a.layoutParams.apply { + width = MATCH_PARENT + } + (binding.f.getChildAt(0) as? ConstraintLayout)?.run { + layoutParams = (layoutParams as FrameLayout.LayoutParams).apply { + width = WRAP_CONTENT + } + } + binding.f.setPadding(padding, padding, padding, padding) + binding.f.layoutParams = (binding.f.layoutParams as ConstraintLayout.LayoutParams).apply { + marginEnd = binding.f.resources.getDimension(R.d.chat_cell_horizontal_spacing_total).toInt() + } + } + + patcher.instead( + "computeMaximumImageWidthPx", + Context::class.java, + ) { (_, context: Context) -> + val res = context.resources + val screenWidth = DisplayUtils.getScreenSize(context).width() + val space = res.getDimensionPixelSize(R.d.uikit_guideline_chat) + res.getDimensionPixelSize(R.d.chat_cell_horizontal_spacing_total) + padding * 2 + return@instead min(1440, screenWidth - space); + } + + patchEmbed() + patchAttachmentInit() + patchAttachmentConfig() + patchComponentsConfig() + patchMessageInit() + patchMessageConfig() + patchStickerInit() + patchStickerConfig() + patchPollConfig() + + if (hasHighlightMessages) { + compatHighlightMessages() + } + } + + private fun patchAttachmentConfig() { + patcher.after( + "onConfigure", + Int::class.javaPrimitiveType!!, + ChatListEntry::class.java, + ) { (_, _: Int, entry: AttachmentEntry) -> + configBubble(entry) + } + } + + private fun patchAttachmentInit() { + patcher.after( + WidgetChatListAdapter::class.java, + ) { + val mediaView = binding.h + mediaView.layoutParams = + (mediaView.layoutParams as ConstraintLayout.LayoutParams).apply { + topMargin = padding + bottomMargin = padding + marginStart = padding + marginEnd = padding + } + itemView.layoutParams = (itemView.layoutParams as ViewGroup.MarginLayoutParams).apply { + bottomMargin = 2.dp + } + + binding.d.radius = 0f + binding.d.elevation = 0f + binding.d.strokeWidth = 0 + binding.d.setCardBackgroundColor(Color.TRANSPARENT) + + createBubble(itemView.context, binding.a).addTo(itemView as ConstraintLayout, 1) { + layoutParams = ConstraintLayout.LayoutParams(0, 0).apply { + startToStart = PARENT_ID + topToTop = PARENT_ID + bottomToBottom = PARENT_ID + endToEnd = PARENT_ID + marginStart = compactCompatOverride?.dp + ?: resources.getDimension(R.d.uikit_guideline_chat).toInt() + marginEnd = resources.getDimension(R.d.chat_cell_horizontal_spacing_total).toInt() + } + } + } + } + + private val marked = WeakHashMap() + private fun patchComponentsConfig() { + patcher.after( + "onConfigure", + Int::class.javaPrimitiveType!!, + ChatListEntry::class.java, + ) { (_, _: Int, entry: BotUiComponentEntry) -> + var i = 0 + val layout = binding.b + layout.layoutParams = (layout.layoutParams as ConstraintLayout.LayoutParams).apply { + marginEnd = layout.resources.getDimension(R.d.chat_cell_horizontal_spacing_total).toInt() + } + while (i < layout.childCount) { + val child = layout.getChildAt(i) + ?: break + val bubble: MaterialCardView + if (child.javaClass.simpleName == "ContainerComponentView") { + bubble = (child as? ConstraintLayout)?.getChildAt(0) as? MaterialCardView + ?: continue + if (i == (layout.childCount - 1)) { + ((bubble.getChildAt(0) as? ConstraintLayout)?.getChildAt(1) as? LinearLayout)?.run { + if (!marked.contains(this)) { + marked[this] = Unit.a + setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom + padding) + } + } + } + } else { + layout.removeViewAt(i) + bubble = createBubble(itemView.context).addTo(layout, i) { + layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT) + child.addTo(this) { + layoutParams = (layoutParams as LinearLayout.LayoutParams).apply { + topMargin += padding + bottomMargin += padding + rightMargin += padding + leftMargin += padding + } + } + } + bubble.setOnClickListener { + adapter.eventHandler.onMessageClicked(entry.message, false) + } + bubble.setOnLongClickListener { + adapter.eventHandler.onMessageLongClicked(entry.message, "", false) + true + } + } + + bubble.shapeAppearanceModel = bubble.shapeAppearanceModel.toBuilder().run { + setAllCorners(CornerFamily.ROUNDED, smallCorner) + if (i == (layout.childCount - 1)) { + setBottomLeftCornerSize(bigCorner) + setBottomRightCornerSize(bigCorner) + } + build() + } + bubble.clipToOutline = true + i++ + } + } + } + + private fun patchEmbed() { + patcher.after( + WidgetChatListAdapter::class.java, + ) { (_) -> + binding.t.layoutParams = + (binding.t.layoutParams as ConstraintLayout.LayoutParams).apply { + topMargin = padding + bottomMargin = padding + marginStart = padding + marginEnd = padding + } + createBubble(itemView.context, binding.a).addTo(itemView as ConstraintLayout, 1) { + visibility = GONE + layoutParams = ConstraintLayout.LayoutParams(0, 0).apply { + startToStart = PARENT_ID + topToTop = PARENT_ID + bottomToBottom = PARENT_ID + endToEnd = PARENT_ID + marginStart = compactCompatOverride?.dp + ?: resources.getDimension(R.d.uikit_guideline_chat).toInt() + marginEnd = resources.getDimension(R.d.chat_cell_horizontal_spacing_total).toInt() + } + } + } + patcher.after( + "onConfigure", + Int::class.javaPrimitiveType!!, + ChatListEntry::class.java, + ) { (_, _: Int, entry: EmbedEntry) -> + if (EmbedResourceUtils.INSTANCE.isInlineEmbed(entry.embed)) { + itemView.findViewById(bubbleId).visibility = View.VISIBLE + configBubble(entry) + } else { + itemView.findViewById(bubbleId).visibility = View.GONE + configBubble(binding.f, entry) + } + } + } + + private fun patchMessageInit() { + patcher.after( + Int::class.javaPrimitiveType!!, + WidgetChatListAdapter::class.java, + ) { (_, layoutId: Int) -> + val isFull = when (layoutId) { + fullId -> !hasCompactMode + minimalId -> false + else -> return@after + } + + itemView.layoutParams = (itemView.layoutParams as ViewGroup.MarginLayoutParams).apply { + bottomMargin = 2.dp + } + itemView.setTag(messageLayoutTag, isFull) + if (isFull) { + itemView.findViewById("chat_list_adapter_item_text_header")?.apply { + layoutParams = (layoutParams as ConstraintLayout.LayoutParams).apply { + setPadding( + paddingLeft + padding, + paddingTop + topPad, + paddingRight + padding, + paddingBottom + ) + } + } + } + itemView.findViewById("chat_list_adapter_item_text").apply { + layoutParams = (layoutParams as ConstraintLayout.LayoutParams).apply { + if (isFull) { + setPadding(padding, 0, padding, padding) + } else { + setPadding(padding, padding + 2.dp, padding, padding) + } + } + } + createBubble(itemView.context, itemView).addTo(itemView as ConstraintLayout, 2) { + layoutParams = ConstraintLayout.LayoutParams(0, 0).apply { + if (isFull) { + startToStart = Utils.getResId("uikit_chat_guideline", "id") + topToTop = Utils.getResId("chat_list_adapter_item_text_header", "id") + } else { + startToStart = PARENT_ID + topToTop = Utils.getResId("chat_list_adapter_item_text", "id") + marginStart = compactCompatOverride?.dp + ?: resources.getDimension(R.d.uikit_guideline_chat).toInt() + } + bottomToBottom = PARENT_ID + endToEnd = PARENT_ID + marginEnd = resources.getDimension(R.d.chat_cell_horizontal_spacing_total).toInt() + } + } + } + } + + private fun patchMessageConfig() { + patcher.after( + "onConfigure", + Int::class.javaPrimitiveType!!, + ChatListEntry::class.java, + ) { (_, _: Int, entry: MessageEntry) -> + if (entry.message.content.isNullOrEmpty()) { + itemView.findViewById("chat_list_adapter_item_text").visibility = View.GONE + } + configBubble(entry) + } + } + + private fun patchStickerInit() { + patcher.after( + WidgetChatListAdapter::class.java, + ) { + binding.b.layoutParams = (binding.b.layoutParams as FrameLayout.LayoutParams).apply { + topMargin = padding + bottomMargin = padding + marginStart = padding + marginEnd = padding + } + binding.a.layoutParams = binding.a.layoutParams.apply { + width = MATCH_PARENT + } + binding.a.removeView(binding.b) + createBubble(itemView.context, binding.b).addTo(binding.a, 0) { + layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) + binding.b.addTo(this) + } + } + } + private fun patchStickerConfig() { + patcher.after( + "onConfigure", + Int::class.javaPrimitiveType!!, + ChatListEntry::class.java, + ) { (_, _: Int, entry: StickerEntry) -> + configBubble(entry) + } + } + + private val pollClass = try { + Class.forName("com.aliucord.coreplugins.polls.chatview.WidgetChatListAdapterItemPoll") + } catch(_: Throwable) { + null + } + private val pollField = pollClass?.getDeclaredField("pollView")?.apply { isAccessible = true } + private fun patchPollConfig() { + if (pollClass == null) return + patcher.patch(pollClass.getDeclaredMethod( + "onConfigure", + Int::class.javaPrimitiveType!!, + ChatListEntry::class.java, + )) { (param, _: Int, entry: ChatListEntry) -> + val view = pollField?.get(param.thisObject) as? MaterialCardView + view?.let { + (param.thisObject as WidgetChatListItem).configBubble(it, entry) + } + } + } +} diff --git a/plugins/Myosotis/build.gradle.kts b/plugins/Myosotis/build.gradle.kts new file mode 100644 index 0000000..7bd110c --- /dev/null +++ b/plugins/Myosotis/build.gradle.kts @@ -0,0 +1,12 @@ +version = "1.0.0" +description = "Backports DM previews" + +aliucord { + // Changelog of your plugin + changelog.set(""" + # 1.0.0 + * Initial release >w< + """.trimIndent()) + + deploy.set(true) +} diff --git a/plugins/Myosotis/src/main/kotlin/moe/lava/awoocord/myosotis/Myosotis.kt b/plugins/Myosotis/src/main/kotlin/moe/lava/awoocord/myosotis/Myosotis.kt new file mode 100644 index 0000000..60ed811 --- /dev/null +++ b/plugins/Myosotis/src/main/kotlin/moe/lava/awoocord/myosotis/Myosotis.kt @@ -0,0 +1,205 @@ +package moe.lava.awoocord.myosotis + +import android.annotation.SuppressLint +import android.content.Context +import android.view.View +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.RecyclerView +import com.aliucord.Http +import com.aliucord.Utils +import com.aliucord.annotations.AliucordPlugin +import com.aliucord.api.GatewayAPI +import com.aliucord.entities.Plugin +import com.aliucord.patcher.after +import com.aliucord.patcher.before +import com.aliucord.patcher.component1 +import com.aliucord.patcher.component2 +import com.aliucord.patcher.component3 +import com.aliucord.utils.ChannelUtils +import com.aliucord.utils.GsonUtils +import com.aliucord.utils.SerializedName +import com.aliucord.utils.accessField +import com.aliucord.wrappers.ChannelWrapper.Companion.id +import com.aliucord.wrappers.users.globalName +import com.discord.api.message.Message +import com.discord.databinding.WidgetChannelsListItemChannelPrivateBinding +import com.discord.models.domain.ModelMessageDelete +import com.discord.stores.StoreStream +import com.discord.utilities.color.ColorCompat +import com.discord.utilities.textprocessing.DiscordParser +import com.discord.utilities.textprocessing.MessagePreprocessor +import com.discord.utilities.textprocessing.MessageRenderContext +import com.discord.utilities.view.text.SimpleDraweeSpanTextView +import com.discord.widgets.channels.list.WidgetChannelsListAdapter +import com.discord.widgets.channels.list.items.ChannelListItem +import com.discord.widgets.channels.list.items.ChannelListItemPrivate +import com.discord.widgets.chat.list.adapter.`WidgetChatListAdapterItemMessage$getMessageRenderContext$1` +import com.discord.widgets.chat.list.adapter.`WidgetChatListAdapterItemMessage$getMessageRenderContext$4` +import com.google.gson.reflect.TypeToken +import com.lytefast.flexinput.R +import java.lang.ref.WeakReference + +private val WidgetChannelsListAdapter.ItemChannelPrivate.binding + by accessField() + +private val responseType = TypeToken.getParameterized(List::class.java, Message::class.java).type + +data class ChannelIdsPayload( + @SerializedName("channel_ids") val channelIds: List, +) + +data class MessageItem( + val id: Long, + val content: String?, +) + +fun Message.wrap(): MessageItem { + val author = this.e() + val authorName = if (author.id == StoreStream.getUsers().me.id) { + "You" + } else { + author.globalName ?: author.username + } + val content = this.i() + .takeIf { it.isNotEmpty() } + ?.let { content -> "$authorName: ${content.takeWhile { it != '\n' }}" } + + return MessageItem( + id = this.o(), + content = content, + ) +} + +fun SimpleDraweeSpanTextView.renderText(content: String, other: Pair) { + val me = StoreStream.getUsers().me + val meId = me.id + val meName = me.globalName ?: me.username + val processor = MessagePreprocessor(meId, listOf(), null, false, 50) + val parseChannelMessage = DiscordParser.parseChannelMessage( + context, + content, + MessageRenderContext( + context, + meId, + false, + mapOf(meId to meName, other), + StoreStream.getChannels().channelNames, + mapOf(), + R.b.colorTextLink, + `WidgetChatListAdapterItemMessage$getMessageRenderContext$1`.INSTANCE, + { }, + ColorCompat.getThemedColor(context, R.b.theme_chat_spoiler_bg), + ColorCompat.getThemedColor(context, R.b.theme_chat_spoiler_bg_visible), + { }, + { }, + `WidgetChatListAdapterItemMessage$getMessageRenderContext$4`(context) + ), + processor, + DiscordParser.ParserOptions.DEFAULT, + false + ) + setDraweeSpanStringBuilder(parseChannelMessage); +} + +@AliucordPlugin +class Myosotis : Plugin() { + var cache = mutableMapOf() + var adapterRef: WeakReference? = null + + override fun stop(context: Context) { patcher.unpatchAll() } + + override fun start(context: Context) { + GatewayAPI.onEvent("READY") { refreshAll() } + GatewayAPI.onEvent("RESUMED") { refreshAll() } + + patcher.after( + "onConfigure", + Int::class.java, + ChannelListItem::class.java, + ) { (_, _: Int, item: ChannelListItemPrivate) -> + cache[item.channel.id]?.let { msg -> + val content = msg.content + ?: return@let + + val descView = binding.d + descView.visibility = View.VISIBLE + val user = ChannelUtils.getDMRecipient(item.channel) + descView.renderText(content, user.id to (user.globalName ?: user.username)) + } + } + patcher.after( + RecyclerView::class.java, + FragmentManager::class.java, + ) { + adapterRef = WeakReference(this) + } + + patcher.before( + "handleMessageCreate", + Message::class.java + ) { (_, msg: Message) -> + handleMessageUpdate(msg) + } + + patcher.before( + "handleMessageUpdate", + Message::class.java + ) { (_, msg: Message) -> + handleMessageUpdate(msg) + } + + patcher.before( + "handleMessageDelete", + ModelMessageDelete::class.java + ) { (_, deleteModel: ModelMessageDelete) -> + cache[deleteModel.channelId]?.let { msg -> + if (msg.id in deleteModel.messageIds) { + cache.remove(deleteModel.channelId) + rerender(deleteModel.channelId) + } + } + } + } + + private fun handleMessageUpdate(msg: Message) { + val gid = msg.m() + if (gid == null) { + val channelId = msg.g() + + val oldMsgId = cache[channelId]?.id ?: 0 + if (msg.o() > oldMsgId) { + cache[channelId] = msg.wrap() + rerender(channelId) + } + } + } + + @OptIn(ExperimentalStdlibApi::class) + private fun refreshAll() { + val channels = StoreStream.getChannels().getChannelsForGuild(0) + .filterValues { it.D() == 1 } // type == Type.DM + .keys.take(100) + Utils.threadPool.execute { + val res = Http.Request.newDiscordRNRequest("/channels/preload-messages", "POST") + .executeWithJson(ChannelIdsPayload(channels)) + .json>(GsonUtils.gsonRestApi, responseType) + cache = mutableMapOf(*res.map { it.g() to it.wrap() }.toTypedArray()) + + Utils.mainThread.post { + @SuppressLint("NotifyDataSetChanged") // I DONT CARE HAHAHAAHJAHAAJHDLAHD + adapterRef?.get()?.notifyDataSetChanged() + } + } + } + + private fun rerender(id: Long) { + val adapter = adapterRef?.get() ?: return + val idx = adapter.internalData.indexOfFirst { it.key == "3$id" } + logger.info("found $idx for $id") + if (idx != -1) { + Utils.mainThread.post { + adapter.notifyItemChanged(idx) + } + } + } +} diff --git a/plugins/Scout/build.gradle.kts b/plugins/Scout/build.gradle.kts index dc6cb7f..5f7859a 100644 --- a/plugins/Scout/build.gradle.kts +++ b/plugins/Scout/build.gradle.kts @@ -1,4 +1,4 @@ -version = "1.3.0" +version = "1.4.0" description = "Backported and improved search functionality" android { @@ -14,6 +14,12 @@ aliucord { Changelog {added marginTop} ====================== + # 1.4.0 - Scout is searching for clues about the elusive MvM update + * Added the authorType filter option to search by user, bot, or webhook + * Moved sort filter to the top of the new ones + * Fixes a Discord bug where typing "mentions" would also suggest "has" + * Some people said the options were getting bloated, so they're all hidden behind a "Show all" button now. They'll still show up in auto suggestions. + # 1.3.0 * Removes empty discriminator when searching with users diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/FilterTypeExtension.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/FilterTypeExtension.kt index 6c2ba23..58afef0 100644 --- a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/FilterTypeExtension.kt +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/FilterTypeExtension.kt @@ -3,12 +3,15 @@ package moe.lava.awoocord.scout import com.discord.utilities.search.query.FilterType object FilterTypeExtension { + lateinit var EXPAND: FilterType + lateinit var SORT: FilterType lateinit var BEFORE: FilterType lateinit var DURING: FilterType lateinit var AFTER: FilterType - lateinit var SORT: FilterType lateinit var EXCLUDE: FilterType + lateinit var AUTHOR_TYPE: FilterType lateinit var dates: Array + lateinit var filters: Array lateinit var values: Array } diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/Scout.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/Scout.kt index 10e3e6b..c32ddda 100644 --- a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/Scout.kt +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/Scout.kt @@ -1,27 +1,54 @@ +@file:Suppress("EnumValuesSoftDeprecate", "CanConvertToMultiDollarString") + +/** + * Hi to anyone who might be reading this; I am sorry for the atrocious code in this plugin + * but I promise I'll be fixing it up soon :3 + */ + package moe.lava.awoocord.scout +import android.annotation.SuppressLint import android.content.Context import android.content.res.Resources import android.view.View +import android.view.ViewGroup import android.widget.ImageView import androidx.core.content.res.ResourcesCompat import com.aliucord.Utils import com.aliucord.annotations.AliucordPlugin import com.aliucord.entities.Plugin -import com.aliucord.patcher.* +import com.aliucord.patcher.PreHook +import com.aliucord.patcher.after +import com.aliucord.patcher.before +import com.aliucord.patcher.component1 +import com.aliucord.patcher.component2 +import com.aliucord.patcher.component3 +import com.aliucord.patcher.component4 +import com.aliucord.patcher.component5 +import com.aliucord.patcher.instead import com.aliucord.utils.DimenUtils.dp +import com.aliucord.utils.RxUtils.subscribe import com.aliucord.utils.ViewUtils.findViewById +import com.aliucord.utils.accessField import com.aliucord.wrappers.ChannelWrapper.Companion.id import com.discord.BuildConfig -import com.discord.api.channel.* +import com.discord.api.channel.Channel +import com.discord.api.channel.ChannelUtils +import com.discord.api.channel.`ChannelUtils$getSortByNameAndType$1` import com.discord.api.permission.Permission +import com.discord.databinding.WidgetSearchSuggestionItemHeaderBinding import com.discord.databinding.WidgetSearchSuggestionsItemHasBinding +import com.discord.databinding.WidgetSearchSuggestionsItemSuggestionBinding import com.discord.models.member.GuildMember import com.discord.models.user.User import com.discord.restapi.RequiredHeadersInterceptor import com.discord.restapi.RestAPIBuilder -import com.discord.simpleast.core.parser.* -import com.discord.stores.* +import com.discord.simpleast.core.parser.ParseSpec +import com.discord.simpleast.core.parser.Parser +import com.discord.simpleast.core.parser.Rule +import com.discord.stores.StoreSearch +import com.discord.stores.StoreSearchInput +import com.discord.stores.StoreStream import com.discord.utilities.mg_recycler.MGRecyclerDataPayload import com.discord.utilities.mg_recycler.SingleTypePayload import com.discord.utilities.rest.RestAPI.AppHeadersProvider @@ -29,35 +56,60 @@ import com.discord.utilities.search.network.`SearchFetcher$getRestObservable$3` import com.discord.utilities.search.network.SearchQuery import com.discord.utilities.search.query.FilterType import com.discord.utilities.search.query.node.QueryNode -import com.discord.utilities.search.query.node.answer.* +import com.discord.utilities.search.query.node.answer.ChannelNode +import com.discord.utilities.search.query.node.answer.HasAnswerOption +import com.discord.utilities.search.query.node.answer.HasNode +import com.discord.utilities.search.query.node.answer.UserNode import com.discord.utilities.search.query.node.content.ContentNode import com.discord.utilities.search.query.node.filter.FilterNode import com.discord.utilities.search.query.parsing.QueryParser import com.discord.utilities.search.query.parsing.`QueryParser$Companion$getInAnswerRule$1` +import com.discord.utilities.search.strings.ContextSearchStringProvider import com.discord.utilities.search.strings.SearchStringProvider import com.discord.utilities.search.suggestion.SearchSuggestionEngine -import com.discord.utilities.search.suggestion.entries.* +import com.discord.utilities.search.suggestion.entries.ChannelSuggestion +import com.discord.utilities.search.suggestion.entries.FilterSuggestion +import com.discord.utilities.search.suggestion.entries.HasSuggestion +import com.discord.utilities.search.suggestion.entries.SearchSuggestion import com.discord.utilities.search.validation.SearchData import com.discord.widgets.search.results.WidgetSearchResults import com.discord.widgets.search.suggestions.WidgetSearchSuggestions +import com.discord.widgets.search.suggestions.`WidgetSearchSuggestions$configureUI$1` import com.discord.widgets.search.suggestions.WidgetSearchSuggestionsAdapter import com.franmontiel.persistentcookiejar.PersistentCookieJar import com.franmontiel.persistentcookiejar.cache.SetCookieCache import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor import com.lytefast.flexinput.R import moe.lava.awoocord.scout.api.SearchAPIInterface -import moe.lava.awoocord.scout.parsing.* -import moe.lava.awoocord.scout.ui.* +import moe.lava.awoocord.scout.entries.AuthorTypeSuggestion +import moe.lava.awoocord.scout.entries.AuthorTypeViewHolder +import moe.lava.awoocord.scout.parsing.AuthorType +import moe.lava.awoocord.scout.parsing.AuthorTypeNode +import moe.lava.awoocord.scout.parsing.DateNode +import moe.lava.awoocord.scout.parsing.SimpleParserRule +import moe.lava.awoocord.scout.parsing.SortNode +import moe.lava.awoocord.scout.parsing.UserIdNode +import moe.lava.awoocord.scout.ui.DatePickerFragment +import moe.lava.awoocord.scout.ui.ScoutResource +import moe.lava.awoocord.scout.ui.ScoutSearchStringProvider import java.util.regex.Pattern import b.a.k.b as FormatUtils -@AliucordPlugin() +private val WidgetSearchSuggestionsAdapter.FilterViewHolder.binding + by accessField() + +private val WidgetSearchSuggestionsAdapter.HeaderViewHolder.binding + by accessField() + +@AliucordPlugin @Suppress("unused", "unchecked_cast") class Scout : Plugin() { lateinit var scoutRes: ScoutResource lateinit var ssProvider: ScoutSearchStringProvider lateinit var searchApi: SearchAPIInterface + var optionsExpanded = false + init { @Suppress("DEPRECATION") needsResources = true @@ -72,20 +124,24 @@ class Scout : Plugin() { override fun start(context: Context) { extendFilterType() extendHasAnswerOption() + extendSuggestionCategory() + fixFiltersKeying() + fixHasFilterSuggestion() + fixSearchPadding() patchHasAnswerOption() patchHasNode() patchQuery() patchQueryParser() patchSearchUI(context) - patchSearchPadding() patchThreadSupport() patchUsernameDiscriminator() } override fun stop(context: Context) { + patcher.unpatchAll() resetFilterType() resetHasAnswerOption() - patcher.unpatchAll() + resetSuggestionCategory() } // Creates a new custom search API implementation, for the extra `min_id` param in search queries @@ -111,7 +167,7 @@ class Scout : Plugin() { private var origFilterTypes: Array? = null // Creates new pseudo-values of the `FilterType` enum for date filters - @Suppress("LocalVariableName") + @Suppress("LocalVariableName", "AssignedValueIsNeverRead") private fun extendFilterType() { val cls = FilterType::class.java val constructor = cls.declaredConstructors[0] @@ -123,18 +179,23 @@ class Scout : Plugin() { origFilterTypes = origFilterTypes ?: values var nextIdx = values.size + val EXPAND = constructor.newInstance("EXPAND", nextIdx++) as FilterType + val SORT = constructor.newInstance("SORT", nextIdx++) as FilterType val EXCLUDE = constructor.newInstance("EXCLUDE", nextIdx++) as FilterType + val AUTHOR_TYPE = constructor.newInstance("AUTHOR_TYPE", nextIdx++) as FilterType val BEFORE = constructor.newInstance("BEFORE", nextIdx++) as FilterType val DURING = constructor.newInstance("DURING", nextIdx++) as FilterType val AFTER = constructor.newInstance("AFTER", nextIdx++) as FilterType - val SORT = constructor.newInstance("SORT", nextIdx) as FilterType + FilterTypeExtension.EXPAND = EXPAND + FilterTypeExtension.SORT = SORT FilterTypeExtension.EXCLUDE = EXCLUDE + FilterTypeExtension.AUTHOR_TYPE = AUTHOR_TYPE FilterTypeExtension.BEFORE = BEFORE FilterTypeExtension.DURING = DURING FilterTypeExtension.AFTER = AFTER - FilterTypeExtension.SORT = SORT FilterTypeExtension.dates = arrayOf(BEFORE, DURING, AFTER) - FilterTypeExtension.values = arrayOf(EXCLUDE, BEFORE, DURING, AFTER, SORT) + FilterTypeExtension.filters = arrayOf(SORT, AUTHOR_TYPE, EXCLUDE) + FilterTypeExtension.dates + FilterTypeExtension.values = arrayOf(EXPAND) + FilterTypeExtension.filters val newValues = values.toMutableList() newValues.addAll(FilterTypeExtension.values) @@ -154,7 +215,7 @@ class Scout : Plugin() { private var origHasAnswerOptions: Array? = null // Creates new pseudo-values of the `HasAnswerOption` enum for poll and forwarded filters - @Suppress("LocalVariableName") + @Suppress("LocalVariableName", "AssignedValueIsNeverRead") private fun extendHasAnswerOption() { val cls = HasAnswerOption::class.java val constructor = cls.declaredConstructors[0] @@ -167,7 +228,7 @@ class Scout : Plugin() { var nextIdx = values.size val POLL = constructor.newInstance("POLL", nextIdx++, "poll") as HasAnswerOption - val SNAPSHOT = constructor.newInstance("SNAPSHOT", nextIdx, "snapshot") as HasAnswerOption + val SNAPSHOT = constructor.newInstance("SNAPSHOT", nextIdx++, "snapshot") as HasAnswerOption HasAnswerOptionExtension.POLL = POLL HasAnswerOptionExtension.SNAPSHOT = SNAPSHOT HasAnswerOptionExtension.values = arrayOf(POLL, SNAPSHOT) @@ -188,6 +249,83 @@ class Scout : Plugin() { origHasAnswerOptions = null } + private var origSuggestionCategories: Array? = null + // Creates new pseudo-values of the suggestion categories to add correct headers + @Suppress("LocalVariableName", "AssignedValueIsNeverRead") + private fun extendSuggestionCategory() { + val cls = SearchSuggestion.Category::class.java + val constructor = cls.declaredConstructors[0] + constructor.isAccessible = true + + val field = cls.getDeclaredField("\$VALUES") + field.isAccessible = true + val values = field.get(null) as Array + origSuggestionCategories = origSuggestionCategories ?: values + var nextIdx = values.size + + val AUTHOR_TYPE = constructor.newInstance("AUTHOR_TYPE", nextIdx++) as SearchSuggestion.Category + SuggestionCategoryExtension.AUTHOR_TYPE = AUTHOR_TYPE + SuggestionCategoryExtension.values = arrayOf(AUTHOR_TYPE) + + val newValues = values.toMutableList() + newValues.addAll(SuggestionCategoryExtension.values) + field.set(null, newValues.toTypedArray()) + } + + private fun resetSuggestionCategory() { + if (origSuggestionCategories == null) + return logger.error("No unpatched suggestion categories?", null) + + val cls = SearchSuggestion.Category::class.java + val field = cls.getDeclaredField("\$VALUES") + field.isAccessible = true + field.set(null, origSuggestionCategories) + origSuggestionCategories = null + } + + // Patch to key filters properly for smoother recycling + // Thank u discord for keying every filter type the same thing!! /s + private fun fixFiltersKeying() { + patcher.instead( + "getFilterItem", + FilterSuggestion::class.java, + ) { (_, suggestion: FilterSuggestion) -> + SingleTypePayload(suggestion, suggestion.filterType.name, 2) // 2 = WidgetSearchSuggestionsAdapter.TYPE_FILTER + } + } + + // YES DISCORD TYPO'ED THIS HAHAHAHAHAHAFAUHFAIUFHAIFBHUKFHYRISFSUOIRN + private fun fixHasFilterSuggestion() { + patcher.before( + "getStringRepresentation", + FilterType::class.java, + SearchStringProvider::class.java, + ) { (param, filter: FilterType, provider: SearchStringProvider) -> + if (filter == FilterType.HAS) { + param.result = provider.hasFilterString + ":" + } + } + } + + // Patch out the gigantic padding in search results + private fun fixSearchPadding() { + patcher.after("onViewBound", View::class.java) { + view?.run { + fitsSystemWindows = false + setPadding(paddingLeft, 16.dp, paddingRight, paddingBottom) + } + } + + patcher.after("onViewBound", View::class.java) { + // Being a bit sneaky and reset the expanded flag here + optionsExpanded = false + view?.run { + fitsSystemWindows = false + setPadding(paddingLeft, 16.dp, paddingRight, paddingBottom) + } + } + } + // Patches various methods that use HasAnswerOption to include our new options private fun patchHasAnswerOption() { patcher.before( @@ -258,24 +396,22 @@ class Scout : Plugin() { CharSequence::class.java, FilterType::class.java, SearchStringProvider::class.java, - ) { param -> - val query = param.args[0] as CharSequence - val filterType = param.args[1] as FilterType - val ossProvider = param.args[2] as SearchStringProvider - - if (filterType != FilterType.HAS && filterType != FilterTypeExtension.EXCLUDE) - return@instead listOf() - - val res = mutableListOf() - for (opt in HasAnswerOption.values()) { - val filterText = opt.getLocalizedInputText(ossProvider) - - if (filterText.contains(query)) - res.add(HasSuggestion(opt)) + ) { (_, query: CharSequence, type: FilterType, provider: SearchStringProvider) -> + // Generate entries for author type + if (type == FilterTypeExtension.AUTHOR_TYPE) { + return@instead AuthorType.values() + .filter { it.value.contains(query) } + .map { AuthorTypeSuggestion(it) } } - res.toList() - } + // Generate entries for has options, including new ones + if (type == FilterType.HAS || type == FilterTypeExtension.EXCLUDE) + return@instead HasAnswerOption.values() + .filter { it.getLocalizedInputText(provider).contains(query) } + .map { HasSuggestion(it) } + + listOf() + } } // Patching HasNode related methods for our exclude: filter type @@ -301,9 +437,9 @@ class Scout : Plugin() { val opt = field.get(this) as HasAnswerOption if (filterType == FilterType.HAS) - builder.appendParam("has", opt.restParamValue); + builder.appendParam("has", opt.restParamValue) else if (filterType == FilterTypeExtension.EXCLUDE) - builder.appendParam("has", "-" + opt.restParamValue); + builder.appendParam("has", "-" + opt.restParamValue) } // Patching the behaviour when the has suggestion is clicked @@ -333,8 +469,6 @@ class Scout : Plugin() { ) getAnswerReplacementStart.isAccessible = true - logger.info(query.joinToString("|") { it.text }) - val replacementIdx = getAnswerReplacementStart.invoke(this, query) as Int val previousFilterText = query[replacementIdx] val filterNode = if (previousFilterText.text == ssProvider.excludeFilterString) @@ -358,6 +492,7 @@ class Scout : Plugin() { var minID = params["min_id"] var maxID = params["max_id"] val sortOrder = params["sort_order"] + val authorType = params["author_type"] self.`$oldestMessageId`?.let { if (sortOrder?.getOrNull(0) == "asc") minID = listOf(it.toString()) @@ -378,7 +513,8 @@ class Scout : Plugin() { retryAttempts, self.`$searchQuery`.includeNsfw, listOf("timestamp"), - sortOrder + sortOrder, + authorType, ) else searchApi.searchChannelMessages( @@ -392,7 +528,8 @@ class Scout : Plugin() { retryAttempts, self.`$searchQuery`.includeNsfw, listOf("timestamp"), - sortOrder + sortOrder, + authorType, ) } ) @@ -412,6 +549,8 @@ class Scout : Plugin() { DateNode.getDateRule(), SortNode.getFilterRule(ssProvider.sortFilterString), SortNode.getSortRule(ssProvider), + AuthorTypeNode.getFilterRule(ssProvider.authorTypeFilter), + AuthorTypeNode.getAuthorTypesRule(), SimpleParserRule(Pattern.compile("^\\s*?${ssProvider.excludeFilterString}:", 64)) { _, _, obj -> ParseSpec(FilterNode(FilterTypeExtension.EXCLUDE, ssProvider.excludeFilterString), obj) } @@ -420,6 +559,7 @@ class Scout : Plugin() { } // This is probably the worst bit of this plugin + @SuppressLint("SetTextI18n") private fun patchSearchUI(context: Context) { // Run when a filter suggestion is clicked // Most of the code is copied from its implementation @@ -432,7 +572,7 @@ class Scout : Plugin() { ) { param -> val filter = param.args[0] as FilterType if (filter !in FilterTypeExtension.values) - return@before; // Exit if not an extended filter type + return@before // Exit if not an extended filter type val replaceAndPublish = StoreSearchInput::class.java.getDeclaredMethod( "replaceAndPublish", @@ -466,7 +606,7 @@ class Scout : Plugin() { getAnswerReplacementStart.invoke(this, list), listOf(filterNode, DateNode(it)), list - ); + ) } } @@ -475,14 +615,21 @@ class Scout : Plugin() { lastIndex, listOf(filterNode, SortNode(ssProvider.sortOldString)), list - ); + ) if (filter == FilterTypeExtension.EXCLUDE) replaceAndPublish.invoke(this, lastIndex, listOf(filterNode), list - ); + ) + + if (filter == FilterTypeExtension.AUTHOR_TYPE) + replaceAndPublish.invoke(this, + lastIndex, + listOf(filterNode), + list + ) param.result = null } @@ -501,6 +648,7 @@ class Scout : Plugin() { FilterTypeExtension.AFTER -> false to scoutRes.getDrawableId("baseline_update_24") FilterTypeExtension.SORT -> true to R.e.ic_sort_white_24dp FilterTypeExtension.EXCLUDE -> false to scoutRes.getDrawableId("baseline_do_disturb_on_24") + FilterTypeExtension.AUTHOR_TYPE -> true to R.e.ic_members_24dp else -> false to null } @@ -522,6 +670,8 @@ class Scout : Plugin() { param.result = ScoutResource.SORT_ANSWER if (type == FilterTypeExtension.EXCLUDE) param.result = ssProvider.getIdentifier("search_answer_has") + if (type == FilterTypeExtension.AUTHOR_TYPE) + param.result = ScoutResource.AUTHOR_TYPE_ANSWER } // Patch for retrieving filter name @@ -536,6 +686,7 @@ class Scout : Plugin() { FilterTypeExtension.DURING -> ssProvider.getIdentifier("search_filter_during") FilterTypeExtension.AFTER -> ssProvider.getIdentifier("search_filter_after") FilterTypeExtension.SORT -> ScoutResource.SORT_FILTER + FilterTypeExtension.AUTHOR_TYPE -> ScoutResource.AUTHOR_TYPE_FILTER else -> null } res?.let { param.result = it } @@ -544,7 +695,8 @@ class Scout : Plugin() { // Patch formatting utils to use our custom lowercase strings // This is called by FilterViewHolder.onConfigure, using the results from getAnswerText and getFilterText patcher.patch( - FormatUtils::class.java.getDeclaredMethod("c", + FormatUtils::class.java.getDeclaredMethod( + "c", Resources::class.java, Int::class.javaPrimitiveType!!, Array::class.java, @@ -557,6 +709,8 @@ class Scout : Plugin() { ScoutResource.SORT_FILTER -> ssProvider.sortFilterString ScoutResource.SORT_ANSWER -> ssProvider.sortOldString ScoutResource.EXCLUDE_FILTER -> ssProvider.excludeFilterString + ScoutResource.AUTHOR_TYPE_FILTER -> ssProvider.authorTypeFilter + ScoutResource.AUTHOR_TYPE_ANSWER -> ssProvider.authorTypeAnswer else -> null } override?.let { @@ -565,38 +719,105 @@ class Scout : Plugin() { } ) + // Patch to manually configure expander, need to do this to update the suggestions widget + patcher.before( + "onConfigure", + Int::class.javaPrimitiveType!!, + MGRecyclerDataPayload::class.java, + ) { (param, _: Int, payload: SingleTypePayload) -> + val suggestion = payload.data + if (suggestion.filterType != FilterTypeExtension.EXPAND) { + return@before + } + param.result = null + + val sampleText = binding.b + val layout = binding.c + val filterText = binding.d + val icon = binding.e + layout.setOnClickListener { + val onFilter = adapter.onFilterClicked as `WidgetSearchSuggestions$configureUI$1` + val widget = onFilter.`this$0` + optionsExpanded = true + WidgetSearchSuggestions.Model.Companion!!.get(ContextSearchStringProvider(context)).z().subscribe { + WidgetSearchSuggestions.`access$configureUI`(widget, this) + } + } + sampleText.text = null + filterText.text = ssProvider.expandFilterString + val drawable = R.e.ic_chevron_right_primary_300_12dp + icon.setImageDrawable(ResourcesCompat.getDrawable(context.resources, drawable, null)) + } + // Patch to add our new filters into the initial suggestions patcher.after( "getFilterSuggestions", CharSequence::class.java, SearchStringProvider::class.java, Boolean::class.javaPrimitiveType!!, - ) { param -> - val query = param.args[0] as CharSequence + ) { (param, query: CharSequence) -> val res = (param.result as List).toMutableList() - for (type in FilterTypeExtension.values) { - val st = ssProvider.stringFor(type) + ":" - if (st.contains(query)) - res.add(FilterSuggestion(type)) + if (optionsExpanded || query != "") { + for (type in FilterTypeExtension.filters) { + val st = ssProvider.stringFor(type) + ":" + + if (st.contains(query)) + res.add(FilterSuggestion(type)) + } + } else { + res.add(FilterSuggestion(FilterTypeExtension.EXPAND)) } param.result = res.toList() } - } - // Patch out the gigantic padding in search results - private fun patchSearchPadding() { - patcher.after("onViewBound", View::class.java) { - view?.run { - fitsSystemWindows = false - setPadding(paddingLeft, 16.dp, paddingRight, paddingBottom) + // Patch to add header for new categories + patcher.before( + "onConfigure", + Int::class.javaPrimitiveType!!, + MGRecyclerDataPayload::class.java, + ) { (param, _: Int, payload: SingleTypePayload) -> + val category = payload.data + if (category == SuggestionCategoryExtension.AUTHOR_TYPE) { + binding.b.text = "Author Type" + param.result = null } } - patcher.after("onViewBound", View::class.java) { - view?.run { - fitsSystemWindows = false - setPadding(paddingLeft, 16.dp, paddingRight, paddingBottom) + // Patch to add entries depending on category + patcher.after( + List::class.java, + List::class.java, + ) { (_, _: List, suggestions: List) -> + var lastCategory: SearchSuggestion.Category? = null + val newItems = mutableListOf() + suggestions.forEach { + if (it is AuthorTypeSuggestion) { + if (lastCategory != it.category) { + newItems.add( + SingleTypePayload(it.category, it.category.name, 0) + ) + lastCategory = it.category + } + newItems.add( + SingleTypePayload(it, it.type.value, SuggestionCategoryExtension.AdapterType.AUTHOR_TYPE) + ) + } + } + suggestionItems.removeAll { it in newItems } + suggestionItems.addAll(0, newItems) + } + + // Patch to add new types of suggestion entries + patcher.before( + "onCreateViewHolder", + ViewGroup::class.java, + Int::class.javaPrimitiveType!!, + ) { (param, _: ViewGroup, id: Int) -> + when (id) { + SuggestionCategoryExtension.AdapterType.AUTHOR_TYPE -> { + param.result = AuthorTypeViewHolder(this, scoutRes) + } } } } @@ -606,7 +827,7 @@ class Scout : Plugin() { // Patch query parser for in: to support names with spaces, by wrapping them in quotes // This enables searching for threads which can have spaces in their names patcher.instead("getInAnswerRule") { - val compile = Pattern.compile("^\\s*#(\".*?\"|[^ ]+)", 64); + val compile = Pattern.compile("^\\s*#(\".*?\"|[^ ]+)", 64) `QueryParser$Companion$getInAnswerRule$1`(compile, compile) } @@ -701,7 +922,7 @@ class Scout : Plugin() { // Now it matches something like @[#] (bots still have discriminators) // The @ is required unfortunately, to distinguish it from literally any other word patcher.instead("getUserRule") { - val regex = Pattern.compile("^\\s*@(?:([^@#:]+)#([0-9]{4})|([a-z0-9._]{2,32}))", 64); + val regex = Pattern.compile("^\\s*@(?:([^@#:]+)#([0-9]{4})|([a-z0-9._]{2,32}))", 64) // Returns a new rule to support our optional second group (discriminator) return@instead SimpleParserRule(regex) { matcher, _, obj -> diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/SuggestionCategoryExtension.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/SuggestionCategoryExtension.kt new file mode 100644 index 0000000..2fcc40b --- /dev/null +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/SuggestionCategoryExtension.kt @@ -0,0 +1,12 @@ +package moe.lava.awoocord.scout + +import com.discord.utilities.search.suggestion.entries.SearchSuggestion + +object SuggestionCategoryExtension { + lateinit var AUTHOR_TYPE: SearchSuggestion.Category + lateinit var values: Array + + object AdapterType { + const val AUTHOR_TYPE = 7 + } +} diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/api/SearchAPIInterface.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/api/SearchAPIInterface.kt index 6bbe273..c3b45e4 100644 --- a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/api/SearchAPIInterface.kt +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/api/SearchAPIInterface.kt @@ -24,6 +24,7 @@ interface SearchAPIInterface { @t("include_nsfw") includeNsfw: Boolean?, @t("sort_by") sortBy: List?, // "timestamp" is one, not sure about any other sort types @t("sort_order") sortOrder: List?, // "asc" or "desc" + @t("author_type") authorType: List?, ): Observable @f("guilds/{guildId}/messages/search") @@ -40,5 +41,6 @@ interface SearchAPIInterface { @t("include_nsfw") includeNsfw: Boolean?, @t("sort_by") sortBy: List?, @t("sort_order") sortOrder: List?, + @t("author_type") authorType: List?, ): Observable } diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/entries/AuthorTypeSuggestion.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/entries/AuthorTypeSuggestion.kt new file mode 100644 index 0000000..b989e03 --- /dev/null +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/entries/AuthorTypeSuggestion.kt @@ -0,0 +1,9 @@ +package moe.lava.awoocord.scout.entries + +import com.discord.utilities.search.suggestion.entries.SearchSuggestion +import moe.lava.awoocord.scout.SuggestionCategoryExtension +import moe.lava.awoocord.scout.parsing.AuthorType + +data class AuthorTypeSuggestion(val type: AuthorType) : SearchSuggestion { + override fun getCategory() = SuggestionCategoryExtension.AUTHOR_TYPE +} diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/entries/AuthorTypeViewHolder.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/entries/AuthorTypeViewHolder.kt new file mode 100644 index 0000000..afb1161 --- /dev/null +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/entries/AuthorTypeViewHolder.kt @@ -0,0 +1,77 @@ +package moe.lava.awoocord.scout.entries + +import android.widget.ImageView +import android.widget.TextView +import com.aliucord.Utils +import com.aliucord.utils.ViewUtils.findViewById +import com.discord.stores.StoreSearchInput +import com.discord.stores.StoreStream +import com.discord.utilities.mg_recycler.MGRecyclerDataPayload +import com.discord.utilities.mg_recycler.MGRecyclerViewHolder +import com.discord.utilities.mg_recycler.SingleTypePayload +import com.discord.utilities.search.query.node.filter.FilterNode +import com.discord.widgets.search.suggestions.`WidgetSearchSuggestions$configureUI$4` +import com.discord.widgets.search.suggestions.WidgetSearchSuggestionsAdapter +import com.lytefast.flexinput.R +import moe.lava.awoocord.scout.FilterTypeExtension +import moe.lava.awoocord.scout.parsing.AuthorType +import moe.lava.awoocord.scout.parsing.AuthorTypeNode +import moe.lava.awoocord.scout.ui.ScoutResource + +private val replaceAndPublish = StoreSearchInput::class.java.getDeclaredMethod( + "replaceAndPublish", + Int::class.javaPrimitiveType!!, + List::class.java, + List::class.java +).apply { isAccessible = true } + +private val getAnswerReplacementStart = StoreSearchInput::class.java.getDeclaredMethod( + "getAnswerReplacementStart", + List::class.java, +).apply { isAccessible = true } + +class AuthorTypeViewHolder( + adapter: WidgetSearchSuggestionsAdapter, + // This should be fine (?) + private val scoutRes: ScoutResource, +) : MGRecyclerViewHolder( + Utils.getResId("widget_search_suggestions_item_has", "layout"), + adapter, +) { + private val imageView = itemView.findViewById("search_suggestions_item_has_icon") + private val textView = itemView.findViewById("search_suggestions_item_has_text") + + override fun onConfigure(i: Int, oPayload: MGRecyclerDataPayload) { + super.onConfigure(i, oPayload) + + @Suppress("UNCHECKED_CAST") + val payload = oPayload as SingleTypePayload + val type = payload.data.type + textView.text = when (type) { + AuthorType.Bot -> "bot" + AuthorType.User -> "user" + AuthorType.Webhook -> "webhook" + } + when (type) { + AuthorType.Bot -> imageView.setImageDrawable(scoutRes.getDrawable("smart_toy_24px")) + AuthorType.User -> imageView.setImageResource(R.e.ic_members_24dp) + AuthorType.Webhook -> imageView.setImageDrawable(scoutRes.getDrawable("webhook_24px")) + } + + itemView.setOnClickListener { + val hasHandler = adapter.onHasClicked as `WidgetSearchSuggestions$configureUI$4` + val query = hasHandler.`$model`.query + + val storeInput = StoreStream.getSearch().storeSearchInput + replaceAndPublish.invoke( + storeInput, + getAnswerReplacementStart.invoke(storeInput, query) as Int, + listOf( + FilterNode(FilterTypeExtension.AUTHOR_TYPE, "authorType"), + AuthorTypeNode(type) + ), + query, + ) + } + } +} diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/AuthorTypeNode.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/AuthorTypeNode.kt new file mode 100644 index 0000000..ecee669 --- /dev/null +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/AuthorTypeNode.kt @@ -0,0 +1,64 @@ +@file:Suppress("EnumValuesSoftDeprecate") + +package moe.lava.awoocord.scout.parsing + +import android.content.Context +import com.discord.simpleast.core.parser.ParseSpec +import com.discord.simpleast.core.parser.Rule +import com.discord.utilities.search.network.SearchQuery +import com.discord.utilities.search.query.FilterType +import com.discord.utilities.search.query.node.QueryNode +import com.discord.utilities.search.query.node.answer.AnswerNode +import com.discord.utilities.search.query.node.filter.FilterNode +import com.discord.utilities.search.validation.SearchData +import moe.lava.awoocord.scout.FilterTypeExtension +import java.util.regex.Pattern + +// TODO: not localised, maybe one day +enum class AuthorType(val value: String) { + User("user"), + Bot("bot"), + Webhook("webhook"), + ; + + companion object { + fun from(value: String) = when (value) { + "user" -> User + "bot" -> Bot + "webhook" -> Webhook + else -> throw IllegalArgumentException("Unknown author type $value") + } + } +} + +class AuthorTypeNode(val type: AuthorType): AnswerNode() { + companion object { + fun getAuthorTypesRule(): Rule { + val joined = AuthorType.values().joinToString("|") { it.value } + val regexStr = "^\\s*(${joined})" + val regex = Pattern.compile(regexStr, Pattern.UNICODE_CASE) + return SimpleParserRule(regex) { matcher, _, obj -> + ParseSpec(AuthorTypeNode(AuthorType.from(matcher.group())), obj) + } + } + + fun getFilterRule(str: String): ParserRule { + val regex = Pattern.compile("^\\s*?(${str}):", 64) + return SimpleParserRule(regex) { _, _, obj -> + ParseSpec(FilterNode(FilterTypeExtension.AUTHOR_TYPE, str), obj) + } + } + } + + override fun getValidFilters() = setOf(FilterTypeExtension.AUTHOR_TYPE) + override fun isValid(searchData: SearchData?) = true + override fun getText() = type.value + + override fun updateQuery( + builder: SearchQuery.Builder, + searchData: SearchData?, + filterType: FilterType? + ) { + builder.appendParam("author_type", type.value) + } +} diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/DateNode.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/DateNode.kt index d0ffa02..f72084b 100644 --- a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/DateNode.kt +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/DateNode.kt @@ -20,9 +20,7 @@ class DateNode(private val date: Long?, private val unparsed: String) : AnswerNo val fmt = SimpleDateFormat("yyyy-MM-dd", Locale.US) val regex: Pattern = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}", Pattern.UNICODE_CASE) fun getDateRule(): ParserRule { - return SimpleParserRule(regex) { matcher, parser, obj -> - checkNotNull(matcher) { "matcher" } - checkNotNull(parser) { "parser" } + return SimpleParserRule(regex) { matcher, _, obj -> val match = matcher.group() val date = fmt.parse(match) val node = DateNode(date?.time, match) @@ -31,7 +29,7 @@ class DateNode(private val date: Long?, private val unparsed: String) : AnswerNo } private fun getFilterRule(str: String, type: FilterType): ParserRule { - val regex = Pattern.compile("^\\s*?(${str}):", 64); + val regex = Pattern.compile("^\\s*?(${str}):", 64) return SimpleParserRule(regex) { _, _, obj -> ParseSpec(FilterNode(type, str), obj) } @@ -44,7 +42,7 @@ class DateNode(private val date: Long?, private val unparsed: String) : AnswerNo override fun getValidFilters(): Set = FilterTypeExtension.dates.toSet() override fun isValid(searchData: SearchData?): Boolean = date != null - override fun getText(): CharSequence? = unparsed + override fun getText(): CharSequence = unparsed private val snowflake: String? get() = date?.let { SnowflakeUtils.fromTimestamp(date).toString() } diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/SimpleParserRule.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/SimpleParserRule.kt index 0a9a76c..c78c23b 100644 --- a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/SimpleParserRule.kt +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/SimpleParserRule.kt @@ -1,7 +1,9 @@ package moe.lava.awoocord.scout.parsing import android.content.Context -import com.discord.simpleast.core.parser.* +import com.discord.simpleast.core.parser.ParseSpec +import com.discord.simpleast.core.parser.Parser +import com.discord.simpleast.core.parser.Rule import com.discord.utilities.search.query.node.QueryNode import java.util.regex.Matcher import java.util.regex.Pattern @@ -16,12 +18,10 @@ internal class SimpleParserRule( ) -> ParseSpec ) : ParserRule(regex) { override fun parse( - matcher: Matcher?, + matcher: Matcher, parser: Parser, obj: Any? ): ParseSpec { - checkNotNull(matcher) { "matcher" } - checkNotNull(parser) { "parser" } return parseMethod(matcher, parser, obj) } } diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/SortNode.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/SortNode.kt index e74f2a9..e839712 100644 --- a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/SortNode.kt +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/SortNode.kt @@ -24,7 +24,7 @@ class SortNode(private val text: String): AnswerNode() { } fun getFilterRule(str: String): ParserRule { - val regex = Pattern.compile("^\\s*?(${str}):", 64); + val regex = Pattern.compile("^\\s*?(${str}):", 64) return SimpleParserRule(regex) { _, _, obj -> ParseSpec(FilterNode(FilterTypeExtension.SORT, str), obj) } @@ -36,11 +36,10 @@ class SortNode(private val text: String): AnswerNode() { override fun getText() = this.text override fun updateQuery( - builder: SearchQuery.Builder?, + builder: SearchQuery.Builder, searchData: SearchData?, filterType: FilterType? ) { - checkNotNull(builder) { "queryBuilder" } builder.appendParam("sort_order", "asc") } } diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/UserIdNode.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/UserIdNode.kt index a3c88b7..85ea6c1 100644 --- a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/UserIdNode.kt +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/UserIdNode.kt @@ -22,7 +22,7 @@ class UserIdNode(private val userID: String) : AnswerNode() { override fun getValidFilters() = setOf(FilterType.FROM, FilterType.MENTIONS) override fun isValid(searchData: SearchData?) = true - override fun getText() = userID.toString() + override fun getText() = userID override fun updateQuery( builder: SearchQuery.Builder?, diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutResource.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutResource.kt index 38c9e02..59b9ed7 100644 --- a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutResource.kt +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutResource.kt @@ -10,6 +10,8 @@ class ScoutResource(private val resources: Resources) { val SORT_FILTER = View.generateViewId() val SORT_ANSWER = View.generateViewId() val EXCLUDE_FILTER = View.generateViewId() + val AUTHOR_TYPE_FILTER = View.generateViewId() + val AUTHOR_TYPE_ANSWER = View.generateViewId() } fun getId(name: String, type: String) = diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutSearchStringProvider.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutSearchStringProvider.kt index 184ced8..d4accb2 100644 --- a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutSearchStringProvider.kt +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutSearchStringProvider.kt @@ -21,6 +21,7 @@ class ScoutSearchStringProvider(private val context: Context) { FilterTypeExtension.DURING -> duringFilterString FilterTypeExtension.AFTER -> afterFilterString FilterTypeExtension.SORT -> sortFilterString + FilterTypeExtension.AUTHOR_TYPE -> authorTypeFilter else -> throw IllegalArgumentException("invalid extended filter type") } @@ -41,6 +42,8 @@ class ScoutSearchStringProvider(private val context: Context) { get() = getString("sort").decapitalise(context) val sortOldString: String get() = getString("search_oldest_short").decapitalise(context) + val expandFilterString: String + get() = getString("friends_pending_request_expand") // Not localised val hasPollString: String @@ -49,4 +52,9 @@ class ScoutSearchStringProvider(private val context: Context) { get() = "forward" val excludeFilterString: String get() = "exclude" + val authorTypeFilter: String + get() = "authorType" + val authorTypeAnswer: String + // TODO, could probably be localisable by joining each part together + get() = "user, bot or webhook" } diff --git a/plugins/Scout/src/main/res/drawable/smart_toy_24px.xml b/plugins/Scout/src/main/res/drawable/smart_toy_24px.xml new file mode 100644 index 0000000..30dd4b2 --- /dev/null +++ b/plugins/Scout/src/main/res/drawable/smart_toy_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/plugins/Scout/src/main/res/drawable/webhook_24px.xml b/plugins/Scout/src/main/res/drawable/webhook_24px.xml new file mode 100644 index 0000000..74a3dc7 --- /dev/null +++ b/plugins/Scout/src/main/res/drawable/webhook_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/plugins/Zinnia/build.gradle.kts b/plugins/Zinnia/build.gradle.kts index 7d429a2..eec7d36 100644 --- a/plugins/Zinnia/build.gradle.kts +++ b/plugins/Zinnia/build.gradle.kts @@ -1,9 +1,19 @@ -version = "1.1.1" +version = "1.2.1" description = "Coloured usernames to be a bit more pleasing on the eyes" aliucord { // Changelog of your plugin changelog.set(""" + # 1.2.1 + * Use correct default block colour in replies + * Use correct default block colour in "unchanged" mode + + # 1.2.0 + * Finally fixes the annoying padding issue in replies + * Adds nice preview blocks in settings with configurable hsv bars for all your previewing needs + * Tweaked constrast ratio a bit which may improve some colours' legibility + * Added transparency option, alongside "unchanged" colour option which pairs nicely together for a translucent glass effect + # 1.1.1 * Revert incorrect spacing fix, since it just breaks replies. Proper fix soon diff --git a/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/APCAUtil.kt b/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/APCAUtil.kt new file mode 100644 index 0000000..fbf9466 --- /dev/null +++ b/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/APCAUtil.kt @@ -0,0 +1,117 @@ +package moe.lava.awoocord.zinnia + +import android.graphics.Color +import android.graphics.drawable.GradientDrawable +import android.widget.TextView +import androidx.core.graphics.ColorUtils +import com.aliucord.utils.DimenUtils.dp +import com.discord.stores.StoreStream +import kotlin.math.abs + +enum class Threshold { + Large, + Medium, + Small +} + +internal object APCAUtil { + private val settings = ZinniaSettings + + internal fun configureOn(view: TextView, colour: Int?, threshold: Threshold) { + when (settings.mode) { + Mode.Block -> configureBlock(view, colour ?: Color.BLACK, threshold) + Mode.RoleDot -> configureRoleDot(view, colour ?: Color.BLACK) + } + } + + private fun configureRoleDot(view: TextView, colour: Int) { } + + private fun configureBlock(view: TextView, colourP: Int, threshold: Threshold) { + val isLight = StoreStream.getUserSettingsSystem().theme == "light" + var colour = colourP + val bcol = GradientDrawable() + bcol.cornerRadius = 4.dp.toFloat() + view.background = bcol + view.setPadding(4.dp, 0, 4.dp, 0) + + if (colour == Color.BLACK) { + if (settings.blockAlsoDefault) { + colour = if (isLight && (settings.blockInverted || settings.blockMode == BlockMode.Unchanged)) { + Color.BLACK + } else { + Color.WHITE + } + } else { + view.background = null + view.setPadding(0, 0, 0, 0) + return + } + } + + var (preferred, other) = if (isLight) { + Color.WHITE to Color.BLACK + } else { + Color.BLACK to Color.WHITE + } + when (settings.blockMode) { + BlockMode.InvertedThemeOnly -> preferred = other + BlockMode.WhiteOnly -> preferred = Color.WHITE + BlockMode.BlackOnly -> preferred = Color.BLACK + BlockMode.Unchanged -> preferred = colour + else -> {} + } + + val colours = if (!settings.blockInverted) { + Colours( + fgP = preferred, + fgO = other, + bgP = colour, + bgO = colour, + ) + } else { + Colours( + fgP = colour, + fgO = colour, + bgP = preferred, + bgO = other, + ) + } + + val usePreferred = when (settings.blockMode) { + BlockMode.ApcaOnly -> isApca(colours, threshold) + BlockMode.WcagOnly -> isWcag(colours) + BlockMode.ApcaLightWcagDark -> if (isLight) isApca(colours, threshold) else isWcag(colours) + BlockMode.WcagLightApcaDark -> if (isLight) isWcag(colours) else isApca(colours, threshold) + BlockMode.ThemeOnly, + BlockMode.InvertedThemeOnly, + BlockMode.WhiteOnly, + BlockMode.BlackOnly, + BlockMode.Unchanged -> true + } + + if (usePreferred) { + view.setTextColor(colours.fgP) + bcol.setColor(ColorUtils.setAlphaComponent(colours.bgP, settings.alpha)) + } else { + view.setTextColor(colours.fgO) + bcol.setColor(ColorUtils.setAlphaComponent(colours.bgO, settings.alpha)) + } + } + + private fun isApca(c: Colours, threshold: Threshold): Boolean { + val cPref = abs(APCA.contrast(c.fgP, c.bgP)) + val cOth = abs(APCA.contrast(c.fgO, c.bgO)) + val thresholdValue = when (threshold) { + Threshold.Large -> settings.blockApcaThresholdLarge + Threshold.Medium -> settings.blockApcaThresholdMedium + Threshold.Small -> settings.blockApcaThresholdSmall + } + return cPref > thresholdValue || cPref > cOth + } + + private fun isWcag(c: Colours): Boolean { + val cPref = ColorUtils.calculateContrast(c.fgP, c.bgP) + val cOth = ColorUtils.calculateContrast(c.fgO, c.bgO) + return cPref > settings.blockWcagThreshold || cPref > cOth + } +} diff --git a/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/Zinnia.kt b/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/Zinnia.kt index 8aefe72..f70ebde 100644 --- a/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/Zinnia.kt +++ b/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/Zinnia.kt @@ -2,28 +2,32 @@ package moe.lava.awoocord.zinnia import android.content.Context import android.graphics.Color -import android.graphics.drawable.GradientDrawable import android.view.View import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.graphics.ColorUtils import com.aliucord.annotations.AliucordPlugin import com.aliucord.entities.Plugin -import com.aliucord.patcher.* +import com.aliucord.patcher.after +import com.aliucord.patcher.component1 +import com.aliucord.patcher.component2 +import com.aliucord.patcher.component3 +import com.aliucord.patcher.instead import com.aliucord.utils.DimenUtils.dp -import com.aliucord.utils.ViewUtils.findViewById import com.aliucord.utils.accessField import com.discord.databinding.WidgetChannelMembersListItemUserBinding -import com.discord.stores.StoreStream +import com.discord.models.member.GuildMember import com.discord.widgets.channels.memberlist.adapter.ChannelMembersListAdapter import com.discord.widgets.channels.memberlist.adapter.ChannelMembersListViewHolderMember import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemMessage import com.discord.widgets.chat.list.entries.ChatListEntry import com.discord.widgets.chat.list.entries.MessageEntry -import kotlin.math.abs private val ChannelMembersListViewHolderMember.binding by accessField() +private val WidgetChatListAdapterItemMessage.itemName + by accessField() +private val WidgetChatListAdapterItemMessage.replyName + by accessField() data class Colours( val fgP: Int, @@ -36,8 +40,6 @@ data class Colours( class Zinnia : Plugin() { companion object { const val NAME = "RoleBlocks" } - private val localSettings = ZinniaSettings - init { settingsTab = SettingsTab(ZinniaSettings.Page::class.java, SettingsTab.Type.PAGE) } @@ -49,93 +51,6 @@ class Zinnia : Plugin() { override fun stop(context: Context) { patcher.unpatchAll() } - private fun configureOn(view: TextView, colour: Int?) { - when (localSettings.mode) { - Mode.Block -> configureBlock(view, colour ?: Color.BLACK) - Mode.RoleDot -> configureRoleDot(view, colour ?: Color.BLACK) - } - } - - private fun configureRoleDot(view: TextView, colour: Int) { } - - private fun configureBlock(view: TextView, colourP: Int) { - val isLight = StoreStream.getUserSettingsSystem().theme == "light" - var colour = colourP - val bcol = GradientDrawable() - bcol.cornerRadius = 4.dp.toFloat() - view.background = bcol - - if (colour == Color.BLACK) { - if (localSettings.blockAlsoDefault) { - colour = if (isLight && !localSettings.blockInverted) Color.WHITE else Color.BLACK - } else { - view.background = null - view.setPadding(0, 0, 0, 0) - return - } - } - view.setPadding(4.dp, 0, 4.dp, 0) - - var (preferred, other) = if (isLight) { - Color.WHITE to Color.BLACK - } else { - Color.BLACK to Color.WHITE - } - when (localSettings.blockMode) { - BlockMode.InvertedThemeOnly -> preferred = other - BlockMode.WhiteOnly -> preferred = Color.WHITE - BlockMode.BlackOnly -> preferred = Color.BLACK - else -> {} - } - - val colours = if (!localSettings.blockInverted) { - Colours( - fgP = preferred, - fgO = other, - bgP = colour, - bgO = colour, - ) - } else { - Colours( - fgP = colour, - fgO = colour, - bgP = preferred, - bgO = other, - ) - } - - val usePreferred = when (localSettings.blockMode) { - BlockMode.ApcaOnly -> isApca(colours) - BlockMode.WcagOnly -> isWcag(colours) - BlockMode.ApcaLightWcagDark -> if (isLight) isApca(colours) else isWcag(colours) - BlockMode.WcagLightApcaDark -> if (isLight) isWcag(colours) else isApca(colours) - BlockMode.ThemeOnly, - BlockMode.InvertedThemeOnly, - BlockMode.WhiteOnly, - BlockMode.BlackOnly -> true - } - - if (usePreferred) { - view.setTextColor(colours.fgP) - bcol.setColor(colours.bgP) - } else { - view.setTextColor(colours.fgO) - bcol.setColor(colours.bgO) - } - } - - private fun isApca(c: Colours): Boolean { - val cPref = abs(APCA.contrast(c.fgP, c.bgP)) - val cOth = abs(APCA.contrast(c.fgO, c.bgO)) - return cPref > localSettings.blockApcaThreshold || cPref > cOth - } - - private fun isWcag(c: Colours): Boolean { - val cPref = ColorUtils.calculateContrast(c.fgP, c.bgP) - val cOth = ColorUtils.calculateContrast(c.fgO, c.bgO) - return cPref > localSettings.blockWcagThreshold || cPref > cOth - } - private fun patchMemberList() { // Patches the method that configures the username in members list patcher.after( @@ -153,7 +68,7 @@ class Zinnia : Plugin() { } } - configureOn(usernameTextView, member.color) + APCAUtil.configureOn(usernameTextView, member.color, Threshold.Medium) } } @@ -164,20 +79,28 @@ class Zinnia : Plugin() { Int::class.javaPrimitiveType!!, ChatListEntry::class.java, ) { (_, _: Int, entry: MessageEntry) -> - val username = itemView.findViewById("chat_list_adapter_item_text_name") - ?: return@after - configureOn(username, entry.author?.color) + itemName?.let { + APCAUtil.configureOn(it, entry.author?.color, Threshold.Large) + } + } + + patcher.instead( + "getAuthorTextColor", + GuildMember::class.java, + ) { (_, member: GuildMember?) -> + member?.color ?: Color.BLACK } // Configures for reply preview username patcher.after( - "configureReplyPreview", - MessageEntry::class.java, - ) { (_, entry: MessageEntry) -> - val referencedAuthor = entry.replyData?.messageEntry?.author - val replyUsername = itemView.findViewById("chat_list_adapter_item_text_decorator_reply_name") - ?: return@after - configureOn(replyUsername, referencedAuthor?.color) + "configureReplyName", + String::class.java, + Int::class.javaPrimitiveType!!, + Boolean::class.javaPrimitiveType!!, + ) { (_, _: String, colour: Int) -> + replyName?.let { + APCAUtil.configureOn(it, colour, Threshold.Small) + } } } } diff --git a/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/ZinniaSettings.kt b/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/ZinniaSettings.kt index 9d9055d..72ca84c 100644 --- a/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/ZinniaSettings.kt +++ b/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/ZinniaSettings.kt @@ -1,13 +1,26 @@ package moe.lava.awoocord.zinnia +import android.graphics.Color +import android.util.TypedValue import android.view.View import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.LinearLayout +import android.widget.SeekBar +import android.widget.TextView +import androidx.core.content.res.ResourcesCompat +import com.aliucord.Constants import com.aliucord.Utils import com.aliucord.api.SettingsAPI import com.aliucord.fragments.SettingsPage import com.aliucord.settings.delegate +import com.aliucord.utils.DimenUtils.dp +import com.aliucord.wrappers.users.globalName +import com.discord.stores.StoreStream +import com.discord.utilities.color.ColorCompat import com.discord.views.CheckedSetting -import com.discord.views.RadioManager +import com.lytefast.flexinput.R +import kotlin.math.roundToInt import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty @@ -25,6 +38,7 @@ enum class BlockMode { InvertedThemeOnly, WhiteOnly, BlackOnly, + Unchanged, } class SettingsDelegateEnum>( @@ -49,27 +63,59 @@ private inline fun T.addTo(parent: ViewGroup, block: T.() -> Unit = { parent.addView(this) } +private typealias Delegate = ReadWriteProperty + +fun basicDelegate(initial: T) = object : Delegate { + private var current = initial + override fun getValue(self: Any?, prop: KProperty<*>): T = current + override fun setValue(self: Any?, prop: KProperty<*>, value: T) { current = value } +} + +private class StateDelegate( + private val inner: Delegate, + private val update: (T) -> Unit, +) : Delegate { + override fun getValue(self: Any?, prop: KProperty<*>): T = inner.getValue(self, prop) + + override fun setValue(self: Any?, prop: KProperty<*>, value: T) { + inner.setValue(self, prop, value) + update(value) + } +} + object ZinniaSettings { private val api = SettingsAPI(Zinnia.NAME) - var mode by api.delegateEnum(Mode.Block) + private var onStateUpdate = {} - var dotKeepNameColour by api.delegate(false) + private inline fun reactive(backing: () -> Delegate): StateDelegate { + return StateDelegate(backing()) { onStateUpdate() } + } - var blockAlsoDefault by api.delegate(true) - var blockInverted by api.delegate(false) - var blockMode by api.delegateEnum(BlockMode.ApcaLightWcagDark) - var blockApcaThreshold by api.delegate(75.0) - var blockWcagThreshold by api.delegate(4.5) + var mode by reactive { api.delegateEnum(Mode.Block) } + + var blockAlsoDefault by reactive { api.delegate(true) } + var blockInverted by reactive { api.delegate(false) } + var blockMode by reactive { api.delegateEnum(BlockMode.ApcaLightWcagDark) } + var blockApcaThresholdLarge by reactive { api.delegate(45.0f) } + var blockApcaThresholdMedium by reactive { api.delegate(45.0f) } + var blockApcaThresholdSmall by reactive { api.delegate(45.0f) } + var blockWcagThreshold by reactive { api.delegate(4.5f) } + + private val _alpha = reactive { api.delegate("alpha", 255) } + var alpha by _alpha class Page : SettingsPage() { - private lateinit var manager: RadioManager - private lateinit var mRoleDot: CheckedSetting - private lateinit var mBlock: CheckedSetting - private val checks = mutableListOf() - private fun createRadio(newMode: BlockMode, text: String, subtext: String? = null): CheckedSetting { + private val _previewH = reactive { basicDelegate(0) } + private var previewH by _previewH + private val _previewS = reactive { basicDelegate(100) } + private var previewS by _previewS + private val _previewV = reactive { basicDelegate(100) } + private var previewV by _previewV + + private fun addRadio(newMode: BlockMode, text: String, subtext: String? = null): CheckedSetting { return Utils.createCheckedSetting(requireContext(), CheckedSetting.ViewType.RADIO, text, subtext).addTo(linearLayout) { isChecked = blockMode == newMode setOnCheckedListener { @@ -81,6 +127,88 @@ object ZinniaSettings { } } + private fun createLabel(text: String? = null): TextView { + return TextView(context, null, 0, R.i.UiKit_TextView).apply { + textSize = 16.0f + typeface = ResourcesCompat.getFont(context, Constants.Fonts.whitney_medium) + this.text = text + layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { + bottomMargin = 4.dp + } + } + } + + private fun addSlider( + min: Int, + max: Int, + initial: Int = min, + onChange: (value: Int, commit: Boolean) -> String + ): LinearLayout { + var pendingValue = initial + return LinearLayout(requireContext(), null, 0, R.i.UiKit_Settings_Item).addTo(linearLayout) { + orientation = LinearLayout.VERTICAL + val display = createLabel(onChange(initial, false)).addTo(this) + SeekBar(context, null, 0, R.i.UiKit_SeekBar).addTo(this) { + this.max = max - min + progress = initial + setPadding(12.dp, 0, 12.dp, 0) + setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged( + seekBar: SeekBar, + progress: Int, + fromUser: Boolean, + ) { + pendingValue = min + progress + display.text = onChange(pendingValue, false) + } + + override fun onStartTrackingTouch(seekBar: SeekBar) {} + override fun onStopTrackingTouch(seekBar: SeekBar) { + onChange(pendingValue, true) + } + }) + } + } + } + + private fun addSlider(binding: Delegate, min: Int, max: Int, immediate: Boolean = false, label: (Int) -> String): LinearLayout { + var value by binding + return addSlider(min, max, value) { newValue, commit -> + @Suppress("AssignedValueIsNeverRead") // kt so dumb + if (immediate || commit) value = newValue + label(newValue) + } + } + + private fun createPreview( + label: String, + styleRes: Int, + ): TextView { + val ctx = requireContext() + val view = TextView(ctx, null, 0, styleRes).apply { + val me = StoreStream.getUsers().me + text = me.globalName ?: me.username + layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { + marginStart = 16.dp + marginEnd = 16.dp + } + } + LinearLayout(ctx, null, 0, R.i.UiKit_Settings_Item).addTo(linearLayout) { + view.addTo(this) + createLabel(label).addTo(this) { + layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { + bottomMargin = 0 + } + } + } + return view + } + + override fun onDestroyView() { + onStateUpdate = {} + super.onDestroyView() + } + override fun onViewBound(view: View) { super.onViewBound(view) setActionBarTitle(Zinnia.NAME) @@ -92,61 +220,20 @@ object ZinniaSettings { val roleDotSettings = mutableListOf() addHeader(ctx, "Text colour") - createRadio(BlockMode.ApcaLightWcagDark, "Automatic", "Adjusts text colour based on role colour") - createRadio(BlockMode.ThemeOnly, "By theme", "Adjusts text colour based on theme") - createRadio(BlockMode.InvertedThemeOnly, "By theme (inverted)", "Same as above, but inverted") - createRadio(BlockMode.WhiteOnly, "White", "Force text colour to be white") - createRadio(BlockMode.BlackOnly, "Black", "Force text colour to be black") - - /* - addHeader(ctx, "Mode") - - mBlock = Utils.createCheckedSetting( - ctx, - CheckedSetting.ViewType.RADIO, - "Block mode", - "Wraps the username in a coloured block", - ).addTo(this) { - isChecked = mode == Mode.Block - setOnCheckedListener { - mode = Mode.Block - mRoleDot.isChecked = false - } - } - - mRoleDot = Utils.createCheckedSetting( - ctx, - CheckedSetting.ViewType.RADIO, - "Role dot mode", - "Adds a coloured role dot next to the username, similar to how Discord does it in their new accessibility settings", - ).addTo(this) { - isChecked = mode == Mode.RoleDot - setOnCheckedListener { - mode = Mode.RoleDot - mBlock.isChecked = false - } - } - */ + addRadio(BlockMode.ApcaLightWcagDark, "Automatic", "Adjusts text colour based on optimal contrast with role colour") + addRadio(BlockMode.ThemeOnly, "By theme", "Adjusts text colour based on system theme (dark/light)") + addRadio(BlockMode.InvertedThemeOnly, "By theme (inverted)", "Same as above, but inverted") + addRadio(BlockMode.WhiteOnly, "White", "Force text colour to be white") + addRadio(BlockMode.BlackOnly, "Black", "Force text colour to be black") + addRadio(BlockMode.Unchanged, "Unchanged", "Keep text colour; ideal for using with a translucent block") addHeader(ctx, "Block Settings") - Utils.createCheckedSetting( - ctx, - CheckedSetting.ViewType.SWITCH, - "Also block up default colours", - "Blocks up usernames that have no role colour", - ).addTo(this) { - isChecked = blockAlsoDefault - setOnCheckedListener { - blockAlsoDefault = !blockAlsoDefault - } - blockSettings.add(this) - } - Utils.createCheckedSetting( + val invertSwitch = Utils.createCheckedSetting( ctx, CheckedSetting.ViewType.SWITCH, "Invert block colours", - "By default, the role colour is applied as the block background. Turning this setting on instead makes the block black or white, and the text stays coloured.", + "By default, the role colour is applied as the block background. Turning this setting on inverts this.\nHas no effect with \"Unchanged\" colour option", ).addTo(this) { isChecked = blockInverted setOnCheckedListener { @@ -154,7 +241,52 @@ object ZinniaSettings { } blockSettings.add(this) } + + addSlider(_alpha, 0, 255, true) { "Alpha: ${(it / 2.55f).roundToInt()}%" } + +// createSlider(0, 255, blockApcaThreshold.roundToInt()) { value, commit -> +// blockApcaThreshold = value.toFloat() +// "Apca Threshold: $value" +// } + + addHeader(ctx, "Preview") + val previews = mutableListOf( + Threshold.Large to createPreview("Message header username", R.i.UiKit_TextView_Large_SingleLine), + Threshold.Medium to createPreview("Channels list", R.i.UiKit_TextView).apply { + setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.d.uikit_textsize_medium)) + }, + Threshold.Small to createPreview("Message reply username", R.i.UiKit_TextView).apply { + setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.d.uikit_textsize_small)) + }, + ) + + val hsv = floatArrayOf(0f, 0f, 0f) + Color.colorToHSV(ColorCompat.getThemedColor(this, R.b.color_brand), hsv) + previewH = hsv[0].roundToInt() + previewS = (hsv[1] * 100).roundToInt() + previewV = (hsv[2] * 100).roundToInt() + addSlider(_previewH, 0, 360, true) { "Hue: $it" } + addSlider(_previewS, 0, 100, true) { "Saturation: $it%" } + addSlider(_previewV, 0, 100, true) { "Value: $it%" } + + onStateUpdate = { + previews.forEach { updatePreview(it) } + if (blockMode != BlockMode.Unchanged) { + invertSwitch.l.b().isClickable = true + invertSwitch.alpha = 1f + } else { + invertSwitch.l.b().isClickable = false + invertSwitch.alpha = 0.3f + } + } + onStateUpdate() } } + + fun updatePreview(pair: Pair) { + val (threshold, preview) = pair + val colour = Color.HSVToColor(floatArrayOf(previewH.toFloat(), previewS / 100f, previewV / 100f)) + APCAUtil.configureOn(preview, colour, threshold) + } } } diff --git a/settings.gradle.kts b/settings.gradle.kts index bbe70b9..f6d7bdf 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,8 +24,11 @@ rootProject.name = "Awoocord" val plugins = mapOf( "ComponentsV2Beta" to "canary/ComponentsV2", "SlashCommandsFixBeta" to "canary/SlashCommandsFix", + "Bubbles" to "plugins/Crocosmia", + "Clump" to "plugins/Bocchi", "Scout" to "plugins/Scout", "RoleBlocks" to "plugins/Zinnia", + "Glance" to "plugins/Myosotis", ) include(*plugins.keys.toTypedArray())