Compare commits
30 commits
feat/croco
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ea6536414 | |||
| a419914b6b | |||
| 2f2a929c97 | |||
| 3a460de488 | |||
| f7c91222fc | |||
| e415b22dbd | |||
| 5fb72078f6 | |||
| 4904866f51 | |||
| e50bb66c40 | |||
| e4ab9f936d | |||
| 302ea0094a | |||
| 0116199838 | |||
| 0741b7951d | |||
| ca3960e279 | |||
| fb4926cb04 | |||
| 8deb4d95da | |||
| 69580d72b0 | |||
| 4fb5486a39 | |||
| e0b86e0fb4 | |||
| bc4aa09fff | |||
| 5bee566297 | |||
| cf89612cb2 | |||
| 782538bb10 | |||
| e0a2d0e030 | |||
| 6af545070e | |||
| 53248f08fe | |||
| 62fe1cfb47 | |||
| be88f41d31 | |||
| e051d37483 | |||
| 1bc5341a31 |
30 changed files with 1677 additions and 262 deletions
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
|
|
@ -33,10 +33,10 @@ jobs:
|
||||||
repository: "Aliucord/Aliucord"
|
repository: "Aliucord/Aliucord"
|
||||||
path: "repo"
|
path: "repo"
|
||||||
|
|
||||||
- name: Setup JDK 11
|
- name: Setup JDK 21
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
java-version: 11
|
java-version: 21
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v4
|
||||||
|
|
@ -46,8 +46,8 @@ jobs:
|
||||||
cd $GITHUB_WORKSPACE/src
|
cd $GITHUB_WORKSPACE/src
|
||||||
chmod +x gradlew
|
chmod +x gradlew
|
||||||
./gradlew make generateUpdaterJson
|
./gradlew make generateUpdaterJson
|
||||||
cp {canary,plugins}/*/build/*.zip $GITHUB_WORKSPACE/builds
|
cp {canary,plugins}/*/build/outputs/*.zip $GITHUB_WORKSPACE/builds
|
||||||
cp build/updater.json $GITHUB_WORKSPACE/builds
|
cp build/outputs/updater.json $GITHUB_WORKSPACE/builds
|
||||||
|
|
||||||
- name: Push builds
|
- name: Push builds
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
39
README.md
39
README.md
|
|
@ -1,22 +1,43 @@
|
||||||
# Awoocord Plugins
|
# 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
|
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:
|
Vastly improves the search experience on Aliucord.
|
||||||
- Sorting by oldest first
|
|
||||||
- Filter by date
|
Features:
|
||||||
- Search from user ID
|
- 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
|
# 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.
|
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.
|
Fix missing/empty bot messages using the new embed system. Such messages will be marked "CV2" as part of its tag.
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,8 @@ subprojects {
|
||||||
}
|
}
|
||||||
|
|
||||||
configure<AliucordExtension> {
|
configure<AliucordExtension> {
|
||||||
author("Lava", 368398754077868032L, hyperlink = true)
|
author("cilly", 368398754077868032L, hyperlink = false)
|
||||||
github("https://github.com/LavaDesu/Awoocord")
|
github("https://github.com/cillynder/Awoocord")
|
||||||
}
|
}
|
||||||
|
|
||||||
configure<KtlintExtension> {
|
configure<KtlintExtension> {
|
||||||
|
|
|
||||||
25
plugins/Bocchi/build.gradle.kts
Normal file
25
plugins/Bocchi/build.gradle.kts
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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<SimpleDraweeSpanTextView>()
|
||||||
|
|
||||||
|
@AliucordPlugin(requiresRestart = true)
|
||||||
|
@Suppress("unused")
|
||||||
|
class Bocchi : Plugin() {
|
||||||
|
override fun start(context: Context) {
|
||||||
|
patcher.after<WidgetChatListAdapterItemMessage>(
|
||||||
|
"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<WidgetChatListModelMessages.Companion>(
|
||||||
|
"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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
plugins/Crocosmia/build.gradle.kts
Normal file
12
plugins/Crocosmia/build.gradle.kts
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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<WidgetChatListAdapterItemBotComponentRowBinding>()
|
||||||
|
private val WidgetChatListAdapterItemAttachment.binding get() = WidgetChatListAdapterItemAttachment.`access$getBinding$p`(this)
|
||||||
|
private val WidgetChatListAdapterItemEmbed.binding by accessField<WidgetChatListAdapterItemEmbedBinding>()
|
||||||
|
private val WidgetChatListAdapterItemSticker.binding get() = WidgetChatListAdapterItemSticker.`access$getBinding$p`(this)
|
||||||
|
|
||||||
|
private var MessageEntry.keyField by accessField<String>()
|
||||||
|
|
||||||
|
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<MaterialCardView>(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<View>("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<WidgetChatListAdapter>(
|
||||||
|
"setData",
|
||||||
|
WidgetChatListAdapter.Data::class.java,
|
||||||
|
) {
|
||||||
|
notifyItemChanged(1, Unit.a)
|
||||||
|
}
|
||||||
|
|
||||||
|
patcher.after<WidgetChatListAdapterItemEmbed>(
|
||||||
|
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<EmbedResourceUtils>(
|
||||||
|
"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<WidgetChatListAdapterItemAttachment>(
|
||||||
|
"onConfigure",
|
||||||
|
Int::class.javaPrimitiveType!!,
|
||||||
|
ChatListEntry::class.java,
|
||||||
|
) { (_, _: Int, entry: AttachmentEntry) ->
|
||||||
|
configBubble(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun patchAttachmentInit() {
|
||||||
|
patcher.after<WidgetChatListAdapterItemAttachment>(
|
||||||
|
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<LinearLayout, Unit>()
|
||||||
|
private fun patchComponentsConfig() {
|
||||||
|
patcher.after<WidgetChatListAdapterItemBotComponentRow>(
|
||||||
|
"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<WidgetChatListAdapterItemEmbed>(
|
||||||
|
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<WidgetChatListAdapterItemEmbed>(
|
||||||
|
"onConfigure",
|
||||||
|
Int::class.javaPrimitiveType!!,
|
||||||
|
ChatListEntry::class.java,
|
||||||
|
) { (_, _: Int, entry: EmbedEntry) ->
|
||||||
|
if (EmbedResourceUtils.INSTANCE.isInlineEmbed(entry.embed)) {
|
||||||
|
itemView.findViewById<View>(bubbleId).visibility = View.VISIBLE
|
||||||
|
configBubble(entry)
|
||||||
|
} else {
|
||||||
|
itemView.findViewById<View>(bubbleId).visibility = View.GONE
|
||||||
|
configBubble(binding.f, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun patchMessageInit() {
|
||||||
|
patcher.after<WidgetChatListAdapterItemMessage>(
|
||||||
|
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<View?>("chat_list_adapter_item_text_header")?.apply {
|
||||||
|
layoutParams = (layoutParams as ConstraintLayout.LayoutParams).apply {
|
||||||
|
setPadding(
|
||||||
|
paddingLeft + padding,
|
||||||
|
paddingTop + topPad,
|
||||||
|
paddingRight + padding,
|
||||||
|
paddingBottom
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemView.findViewById<View>("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<WidgetChatListAdapterItemMessage>(
|
||||||
|
"onConfigure",
|
||||||
|
Int::class.javaPrimitiveType!!,
|
||||||
|
ChatListEntry::class.java,
|
||||||
|
) { (_, _: Int, entry: MessageEntry) ->
|
||||||
|
if (entry.message.content.isNullOrEmpty()) {
|
||||||
|
itemView.findViewById<View>("chat_list_adapter_item_text").visibility = View.GONE
|
||||||
|
}
|
||||||
|
configBubble(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun patchStickerInit() {
|
||||||
|
patcher.after<WidgetChatListAdapterItemSticker>(
|
||||||
|
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<WidgetChatListAdapterItemSticker>(
|
||||||
|
"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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
plugins/Myosotis/build.gradle.kts
Normal file
12
plugins/Myosotis/build.gradle.kts
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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<WidgetChannelsListItemChannelPrivateBinding>()
|
||||||
|
|
||||||
|
private val responseType = TypeToken.getParameterized(List::class.java, Message::class.java).type
|
||||||
|
|
||||||
|
data class ChannelIdsPayload(
|
||||||
|
@SerializedName("channel_ids") val channelIds: List<Long>,
|
||||||
|
)
|
||||||
|
|
||||||
|
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<Long, String>) {
|
||||||
|
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<Long, MessageItem>()
|
||||||
|
var adapterRef: WeakReference<WidgetChannelsListAdapter>? = null
|
||||||
|
|
||||||
|
override fun stop(context: Context) { patcher.unpatchAll() }
|
||||||
|
|
||||||
|
override fun start(context: Context) {
|
||||||
|
GatewayAPI.onEvent<Any>("READY") { refreshAll() }
|
||||||
|
GatewayAPI.onEvent<Any>("RESUMED") { refreshAll() }
|
||||||
|
|
||||||
|
patcher.after<WidgetChannelsListAdapter.ItemChannelPrivate>(
|
||||||
|
"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<WidgetChannelsListAdapter>(
|
||||||
|
RecyclerView::class.java,
|
||||||
|
FragmentManager::class.java,
|
||||||
|
) {
|
||||||
|
adapterRef = WeakReference(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
patcher.before<StoreStream>(
|
||||||
|
"handleMessageCreate",
|
||||||
|
Message::class.java
|
||||||
|
) { (_, msg: Message) ->
|
||||||
|
handleMessageUpdate(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
patcher.before<StoreStream>(
|
||||||
|
"handleMessageUpdate",
|
||||||
|
Message::class.java
|
||||||
|
) { (_, msg: Message) ->
|
||||||
|
handleMessageUpdate(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
patcher.before<StoreStream>(
|
||||||
|
"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<List<Message>>(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
version = "1.3.0"
|
version = "1.4.0"
|
||||||
description = "Backported and improved search functionality"
|
description = "Backported and improved search functionality"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
@ -14,6 +14,12 @@ aliucord {
|
||||||
|
|
||||||
Changelog {added marginTop}
|
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
|
# 1.3.0
|
||||||
* Removes empty discriminator when searching with users
|
* Removes empty discriminator when searching with users
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,15 @@ package moe.lava.awoocord.scout
|
||||||
import com.discord.utilities.search.query.FilterType
|
import com.discord.utilities.search.query.FilterType
|
||||||
|
|
||||||
object FilterTypeExtension {
|
object FilterTypeExtension {
|
||||||
|
lateinit var EXPAND: FilterType
|
||||||
|
lateinit var SORT: FilterType
|
||||||
lateinit var BEFORE: FilterType
|
lateinit var BEFORE: FilterType
|
||||||
lateinit var DURING: FilterType
|
lateinit var DURING: FilterType
|
||||||
lateinit var AFTER: FilterType
|
lateinit var AFTER: FilterType
|
||||||
lateinit var SORT: FilterType
|
|
||||||
lateinit var EXCLUDE: FilterType
|
lateinit var EXCLUDE: FilterType
|
||||||
|
lateinit var AUTHOR_TYPE: FilterType
|
||||||
lateinit var dates: Array<FilterType>
|
lateinit var dates: Array<FilterType>
|
||||||
|
lateinit var filters: Array<FilterType>
|
||||||
lateinit var values: Array<FilterType>
|
lateinit var values: Array<FilterType>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
package moe.lava.awoocord.scout
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import com.aliucord.Utils
|
import com.aliucord.Utils
|
||||||
import com.aliucord.annotations.AliucordPlugin
|
import com.aliucord.annotations.AliucordPlugin
|
||||||
import com.aliucord.entities.Plugin
|
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.DimenUtils.dp
|
||||||
|
import com.aliucord.utils.RxUtils.subscribe
|
||||||
import com.aliucord.utils.ViewUtils.findViewById
|
import com.aliucord.utils.ViewUtils.findViewById
|
||||||
|
import com.aliucord.utils.accessField
|
||||||
import com.aliucord.wrappers.ChannelWrapper.Companion.id
|
import com.aliucord.wrappers.ChannelWrapper.Companion.id
|
||||||
import com.discord.BuildConfig
|
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.api.permission.Permission
|
||||||
|
import com.discord.databinding.WidgetSearchSuggestionItemHeaderBinding
|
||||||
import com.discord.databinding.WidgetSearchSuggestionsItemHasBinding
|
import com.discord.databinding.WidgetSearchSuggestionsItemHasBinding
|
||||||
|
import com.discord.databinding.WidgetSearchSuggestionsItemSuggestionBinding
|
||||||
import com.discord.models.member.GuildMember
|
import com.discord.models.member.GuildMember
|
||||||
import com.discord.models.user.User
|
import com.discord.models.user.User
|
||||||
import com.discord.restapi.RequiredHeadersInterceptor
|
import com.discord.restapi.RequiredHeadersInterceptor
|
||||||
import com.discord.restapi.RestAPIBuilder
|
import com.discord.restapi.RestAPIBuilder
|
||||||
import com.discord.simpleast.core.parser.*
|
import com.discord.simpleast.core.parser.ParseSpec
|
||||||
import com.discord.stores.*
|
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.MGRecyclerDataPayload
|
||||||
import com.discord.utilities.mg_recycler.SingleTypePayload
|
import com.discord.utilities.mg_recycler.SingleTypePayload
|
||||||
import com.discord.utilities.rest.RestAPI.AppHeadersProvider
|
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.network.SearchQuery
|
||||||
import com.discord.utilities.search.query.FilterType
|
import com.discord.utilities.search.query.FilterType
|
||||||
import com.discord.utilities.search.query.node.QueryNode
|
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.content.ContentNode
|
||||||
import com.discord.utilities.search.query.node.filter.FilterNode
|
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
|
||||||
import com.discord.utilities.search.query.parsing.`QueryParser$Companion$getInAnswerRule$1`
|
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.strings.SearchStringProvider
|
||||||
import com.discord.utilities.search.suggestion.SearchSuggestionEngine
|
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.utilities.search.validation.SearchData
|
||||||
import com.discord.widgets.search.results.WidgetSearchResults
|
import com.discord.widgets.search.results.WidgetSearchResults
|
||||||
import com.discord.widgets.search.suggestions.WidgetSearchSuggestions
|
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.discord.widgets.search.suggestions.WidgetSearchSuggestionsAdapter
|
||||||
import com.franmontiel.persistentcookiejar.PersistentCookieJar
|
import com.franmontiel.persistentcookiejar.PersistentCookieJar
|
||||||
import com.franmontiel.persistentcookiejar.cache.SetCookieCache
|
import com.franmontiel.persistentcookiejar.cache.SetCookieCache
|
||||||
import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor
|
import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor
|
||||||
import com.lytefast.flexinput.R
|
import com.lytefast.flexinput.R
|
||||||
import moe.lava.awoocord.scout.api.SearchAPIInterface
|
import moe.lava.awoocord.scout.api.SearchAPIInterface
|
||||||
import moe.lava.awoocord.scout.parsing.*
|
import moe.lava.awoocord.scout.entries.AuthorTypeSuggestion
|
||||||
import moe.lava.awoocord.scout.ui.*
|
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 java.util.regex.Pattern
|
||||||
import b.a.k.b as FormatUtils
|
import b.a.k.b as FormatUtils
|
||||||
|
|
||||||
@AliucordPlugin()
|
private val WidgetSearchSuggestionsAdapter.FilterViewHolder.binding
|
||||||
|
by accessField<WidgetSearchSuggestionsItemSuggestionBinding>()
|
||||||
|
|
||||||
|
private val WidgetSearchSuggestionsAdapter.HeaderViewHolder.binding
|
||||||
|
by accessField<WidgetSearchSuggestionItemHeaderBinding>()
|
||||||
|
|
||||||
|
@AliucordPlugin
|
||||||
@Suppress("unused", "unchecked_cast")
|
@Suppress("unused", "unchecked_cast")
|
||||||
class Scout : Plugin() {
|
class Scout : Plugin() {
|
||||||
lateinit var scoutRes: ScoutResource
|
lateinit var scoutRes: ScoutResource
|
||||||
lateinit var ssProvider: ScoutSearchStringProvider
|
lateinit var ssProvider: ScoutSearchStringProvider
|
||||||
lateinit var searchApi: SearchAPIInterface
|
lateinit var searchApi: SearchAPIInterface
|
||||||
|
|
||||||
|
var optionsExpanded = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
needsResources = true
|
needsResources = true
|
||||||
|
|
@ -72,20 +124,24 @@ class Scout : Plugin() {
|
||||||
override fun start(context: Context) {
|
override fun start(context: Context) {
|
||||||
extendFilterType()
|
extendFilterType()
|
||||||
extendHasAnswerOption()
|
extendHasAnswerOption()
|
||||||
|
extendSuggestionCategory()
|
||||||
|
fixFiltersKeying()
|
||||||
|
fixHasFilterSuggestion()
|
||||||
|
fixSearchPadding()
|
||||||
patchHasAnswerOption()
|
patchHasAnswerOption()
|
||||||
patchHasNode()
|
patchHasNode()
|
||||||
patchQuery()
|
patchQuery()
|
||||||
patchQueryParser()
|
patchQueryParser()
|
||||||
patchSearchUI(context)
|
patchSearchUI(context)
|
||||||
patchSearchPadding()
|
|
||||||
patchThreadSupport()
|
patchThreadSupport()
|
||||||
patchUsernameDiscriminator()
|
patchUsernameDiscriminator()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop(context: Context) {
|
override fun stop(context: Context) {
|
||||||
|
patcher.unpatchAll()
|
||||||
resetFilterType()
|
resetFilterType()
|
||||||
resetHasAnswerOption()
|
resetHasAnswerOption()
|
||||||
patcher.unpatchAll()
|
resetSuggestionCategory()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new custom search API implementation, for the extra `min_id` param in search queries
|
// 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<FilterType>? = null
|
private var origFilterTypes: Array<FilterType>? = null
|
||||||
// Creates new pseudo-values of the `FilterType` enum for date filters
|
// Creates new pseudo-values of the `FilterType` enum for date filters
|
||||||
@Suppress("LocalVariableName")
|
@Suppress("LocalVariableName", "AssignedValueIsNeverRead")
|
||||||
private fun extendFilterType() {
|
private fun extendFilterType() {
|
||||||
val cls = FilterType::class.java
|
val cls = FilterType::class.java
|
||||||
val constructor = cls.declaredConstructors[0]
|
val constructor = cls.declaredConstructors[0]
|
||||||
|
|
@ -123,18 +179,23 @@ class Scout : Plugin() {
|
||||||
origFilterTypes = origFilterTypes ?: values
|
origFilterTypes = origFilterTypes ?: values
|
||||||
var nextIdx = values.size
|
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 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 BEFORE = constructor.newInstance("BEFORE", nextIdx++) as FilterType
|
||||||
val DURING = constructor.newInstance("DURING", nextIdx++) as FilterType
|
val DURING = constructor.newInstance("DURING", nextIdx++) as FilterType
|
||||||
val AFTER = constructor.newInstance("AFTER", 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.EXCLUDE = EXCLUDE
|
||||||
|
FilterTypeExtension.AUTHOR_TYPE = AUTHOR_TYPE
|
||||||
FilterTypeExtension.BEFORE = BEFORE
|
FilterTypeExtension.BEFORE = BEFORE
|
||||||
FilterTypeExtension.DURING = DURING
|
FilterTypeExtension.DURING = DURING
|
||||||
FilterTypeExtension.AFTER = AFTER
|
FilterTypeExtension.AFTER = AFTER
|
||||||
FilterTypeExtension.SORT = SORT
|
|
||||||
FilterTypeExtension.dates = arrayOf(BEFORE, DURING, AFTER)
|
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()
|
val newValues = values.toMutableList()
|
||||||
newValues.addAll(FilterTypeExtension.values)
|
newValues.addAll(FilterTypeExtension.values)
|
||||||
|
|
@ -154,7 +215,7 @@ class Scout : Plugin() {
|
||||||
|
|
||||||
private var origHasAnswerOptions: Array<HasAnswerOption>? = null
|
private var origHasAnswerOptions: Array<HasAnswerOption>? = null
|
||||||
// Creates new pseudo-values of the `HasAnswerOption` enum for poll and forwarded filters
|
// Creates new pseudo-values of the `HasAnswerOption` enum for poll and forwarded filters
|
||||||
@Suppress("LocalVariableName")
|
@Suppress("LocalVariableName", "AssignedValueIsNeverRead")
|
||||||
private fun extendHasAnswerOption() {
|
private fun extendHasAnswerOption() {
|
||||||
val cls = HasAnswerOption::class.java
|
val cls = HasAnswerOption::class.java
|
||||||
val constructor = cls.declaredConstructors[0]
|
val constructor = cls.declaredConstructors[0]
|
||||||
|
|
@ -167,7 +228,7 @@ class Scout : Plugin() {
|
||||||
var nextIdx = values.size
|
var nextIdx = values.size
|
||||||
|
|
||||||
val POLL = constructor.newInstance("POLL", nextIdx++, "poll") as HasAnswerOption
|
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.POLL = POLL
|
||||||
HasAnswerOptionExtension.SNAPSHOT = SNAPSHOT
|
HasAnswerOptionExtension.SNAPSHOT = SNAPSHOT
|
||||||
HasAnswerOptionExtension.values = arrayOf(POLL, SNAPSHOT)
|
HasAnswerOptionExtension.values = arrayOf(POLL, SNAPSHOT)
|
||||||
|
|
@ -188,6 +249,83 @@ class Scout : Plugin() {
|
||||||
origHasAnswerOptions = null
|
origHasAnswerOptions = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var origSuggestionCategories: Array<SearchSuggestion.Category>? = 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<SearchSuggestion.Category>
|
||||||
|
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<WidgetSearchSuggestionsAdapter.Companion>(
|
||||||
|
"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<FilterSuggestion.Companion>(
|
||||||
|
"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<WidgetSearchResults>("onViewBound", View::class.java) {
|
||||||
|
view?.run {
|
||||||
|
fitsSystemWindows = false
|
||||||
|
setPadding(paddingLeft, 16.dp, paddingRight, paddingBottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
patcher.after<WidgetSearchSuggestions>("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
|
// Patches various methods that use HasAnswerOption to include our new options
|
||||||
private fun patchHasAnswerOption() {
|
private fun patchHasAnswerOption() {
|
||||||
patcher.before<HasAnswerOption.Companion>(
|
patcher.before<HasAnswerOption.Companion>(
|
||||||
|
|
@ -258,24 +396,22 @@ class Scout : Plugin() {
|
||||||
CharSequence::class.java,
|
CharSequence::class.java,
|
||||||
FilterType::class.java,
|
FilterType::class.java,
|
||||||
SearchStringProvider::class.java,
|
SearchStringProvider::class.java,
|
||||||
) { param ->
|
) { (_, query: CharSequence, type: FilterType, provider: SearchStringProvider) ->
|
||||||
val query = param.args[0] as CharSequence
|
// Generate entries for author type
|
||||||
val filterType = param.args[1] as FilterType
|
if (type == FilterTypeExtension.AUTHOR_TYPE) {
|
||||||
val ossProvider = param.args[2] as SearchStringProvider
|
return@instead AuthorType.values()
|
||||||
|
.filter { it.value.contains(query) }
|
||||||
if (filterType != FilterType.HAS && filterType != FilterTypeExtension.EXCLUDE)
|
.map { AuthorTypeSuggestion(it) }
|
||||||
return@instead listOf<Any>()
|
|
||||||
|
|
||||||
val res = mutableListOf<HasSuggestion>()
|
|
||||||
for (opt in HasAnswerOption.values()) {
|
|
||||||
val filterText = opt.getLocalizedInputText(ossProvider)
|
|
||||||
|
|
||||||
if (filterText.contains(query))
|
|
||||||
res.add(HasSuggestion(opt))
|
|
||||||
}
|
}
|
||||||
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<Any>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patching HasNode related methods for our exclude: filter type
|
// Patching HasNode related methods for our exclude: filter type
|
||||||
|
|
@ -301,9 +437,9 @@ class Scout : Plugin() {
|
||||||
val opt = field.get(this) as HasAnswerOption
|
val opt = field.get(this) as HasAnswerOption
|
||||||
|
|
||||||
if (filterType == FilterType.HAS)
|
if (filterType == FilterType.HAS)
|
||||||
builder.appendParam("has", opt.restParamValue);
|
builder.appendParam("has", opt.restParamValue)
|
||||||
else if (filterType == FilterTypeExtension.EXCLUDE)
|
else if (filterType == FilterTypeExtension.EXCLUDE)
|
||||||
builder.appendParam("has", "-" + opt.restParamValue);
|
builder.appendParam("has", "-" + opt.restParamValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patching the behaviour when the has suggestion is clicked
|
// Patching the behaviour when the has suggestion is clicked
|
||||||
|
|
@ -333,8 +469,6 @@ class Scout : Plugin() {
|
||||||
)
|
)
|
||||||
getAnswerReplacementStart.isAccessible = true
|
getAnswerReplacementStart.isAccessible = true
|
||||||
|
|
||||||
logger.info(query.joinToString("|") { it.text })
|
|
||||||
|
|
||||||
val replacementIdx = getAnswerReplacementStart.invoke(this, query) as Int
|
val replacementIdx = getAnswerReplacementStart.invoke(this, query) as Int
|
||||||
val previousFilterText = query[replacementIdx]
|
val previousFilterText = query[replacementIdx]
|
||||||
val filterNode = if (previousFilterText.text == ssProvider.excludeFilterString)
|
val filterNode = if (previousFilterText.text == ssProvider.excludeFilterString)
|
||||||
|
|
@ -358,6 +492,7 @@ class Scout : Plugin() {
|
||||||
var minID = params["min_id"]
|
var minID = params["min_id"]
|
||||||
var maxID = params["max_id"]
|
var maxID = params["max_id"]
|
||||||
val sortOrder = params["sort_order"]
|
val sortOrder = params["sort_order"]
|
||||||
|
val authorType = params["author_type"]
|
||||||
self.`$oldestMessageId`?.let {
|
self.`$oldestMessageId`?.let {
|
||||||
if (sortOrder?.getOrNull(0) == "asc")
|
if (sortOrder?.getOrNull(0) == "asc")
|
||||||
minID = listOf(it.toString())
|
minID = listOf(it.toString())
|
||||||
|
|
@ -378,7 +513,8 @@ class Scout : Plugin() {
|
||||||
retryAttempts,
|
retryAttempts,
|
||||||
self.`$searchQuery`.includeNsfw,
|
self.`$searchQuery`.includeNsfw,
|
||||||
listOf("timestamp"),
|
listOf("timestamp"),
|
||||||
sortOrder
|
sortOrder,
|
||||||
|
authorType,
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
searchApi.searchChannelMessages(
|
searchApi.searchChannelMessages(
|
||||||
|
|
@ -392,7 +528,8 @@ class Scout : Plugin() {
|
||||||
retryAttempts,
|
retryAttempts,
|
||||||
self.`$searchQuery`.includeNsfw,
|
self.`$searchQuery`.includeNsfw,
|
||||||
listOf("timestamp"),
|
listOf("timestamp"),
|
||||||
sortOrder
|
sortOrder,
|
||||||
|
authorType,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -412,6 +549,8 @@ class Scout : Plugin() {
|
||||||
DateNode.getDateRule(),
|
DateNode.getDateRule(),
|
||||||
SortNode.getFilterRule(ssProvider.sortFilterString),
|
SortNode.getFilterRule(ssProvider.sortFilterString),
|
||||||
SortNode.getSortRule(ssProvider),
|
SortNode.getSortRule(ssProvider),
|
||||||
|
AuthorTypeNode.getFilterRule(ssProvider.authorTypeFilter),
|
||||||
|
AuthorTypeNode.getAuthorTypesRule(),
|
||||||
SimpleParserRule(Pattern.compile("^\\s*?${ssProvider.excludeFilterString}:", 64)) { _, _, obj ->
|
SimpleParserRule(Pattern.compile("^\\s*?${ssProvider.excludeFilterString}:", 64)) { _, _, obj ->
|
||||||
ParseSpec(FilterNode(FilterTypeExtension.EXCLUDE, ssProvider.excludeFilterString), obj)
|
ParseSpec(FilterNode(FilterTypeExtension.EXCLUDE, ssProvider.excludeFilterString), obj)
|
||||||
}
|
}
|
||||||
|
|
@ -420,6 +559,7 @@ class Scout : Plugin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is probably the worst bit of this plugin
|
// This is probably the worst bit of this plugin
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
private fun patchSearchUI(context: Context) {
|
private fun patchSearchUI(context: Context) {
|
||||||
// Run when a filter suggestion is clicked
|
// Run when a filter suggestion is clicked
|
||||||
// Most of the code is copied from its implementation
|
// Most of the code is copied from its implementation
|
||||||
|
|
@ -432,7 +572,7 @@ class Scout : Plugin() {
|
||||||
) { param ->
|
) { param ->
|
||||||
val filter = param.args[0] as FilterType
|
val filter = param.args[0] as FilterType
|
||||||
if (filter !in FilterTypeExtension.values)
|
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(
|
val replaceAndPublish = StoreSearchInput::class.java.getDeclaredMethod(
|
||||||
"replaceAndPublish",
|
"replaceAndPublish",
|
||||||
|
|
@ -466,7 +606,7 @@ class Scout : Plugin() {
|
||||||
getAnswerReplacementStart.invoke(this, list),
|
getAnswerReplacementStart.invoke(this, list),
|
||||||
listOf(filterNode, DateNode(it)),
|
listOf(filterNode, DateNode(it)),
|
||||||
list
|
list
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -475,14 +615,21 @@ class Scout : Plugin() {
|
||||||
lastIndex,
|
lastIndex,
|
||||||
listOf(filterNode, SortNode(ssProvider.sortOldString)),
|
listOf(filterNode, SortNode(ssProvider.sortOldString)),
|
||||||
list
|
list
|
||||||
);
|
)
|
||||||
|
|
||||||
if (filter == FilterTypeExtension.EXCLUDE)
|
if (filter == FilterTypeExtension.EXCLUDE)
|
||||||
replaceAndPublish.invoke(this,
|
replaceAndPublish.invoke(this,
|
||||||
lastIndex,
|
lastIndex,
|
||||||
listOf(filterNode),
|
listOf(filterNode),
|
||||||
list
|
list
|
||||||
);
|
)
|
||||||
|
|
||||||
|
if (filter == FilterTypeExtension.AUTHOR_TYPE)
|
||||||
|
replaceAndPublish.invoke(this,
|
||||||
|
lastIndex,
|
||||||
|
listOf(filterNode),
|
||||||
|
list
|
||||||
|
)
|
||||||
|
|
||||||
param.result = null
|
param.result = null
|
||||||
}
|
}
|
||||||
|
|
@ -501,6 +648,7 @@ class Scout : Plugin() {
|
||||||
FilterTypeExtension.AFTER -> false to scoutRes.getDrawableId("baseline_update_24")
|
FilterTypeExtension.AFTER -> false to scoutRes.getDrawableId("baseline_update_24")
|
||||||
FilterTypeExtension.SORT -> true to R.e.ic_sort_white_24dp
|
FilterTypeExtension.SORT -> true to R.e.ic_sort_white_24dp
|
||||||
FilterTypeExtension.EXCLUDE -> false to scoutRes.getDrawableId("baseline_do_disturb_on_24")
|
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
|
else -> false to null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -522,6 +670,8 @@ class Scout : Plugin() {
|
||||||
param.result = ScoutResource.SORT_ANSWER
|
param.result = ScoutResource.SORT_ANSWER
|
||||||
if (type == FilterTypeExtension.EXCLUDE)
|
if (type == FilterTypeExtension.EXCLUDE)
|
||||||
param.result = ssProvider.getIdentifier("search_answer_has")
|
param.result = ssProvider.getIdentifier("search_answer_has")
|
||||||
|
if (type == FilterTypeExtension.AUTHOR_TYPE)
|
||||||
|
param.result = ScoutResource.AUTHOR_TYPE_ANSWER
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch for retrieving filter name
|
// Patch for retrieving filter name
|
||||||
|
|
@ -536,6 +686,7 @@ class Scout : Plugin() {
|
||||||
FilterTypeExtension.DURING -> ssProvider.getIdentifier("search_filter_during")
|
FilterTypeExtension.DURING -> ssProvider.getIdentifier("search_filter_during")
|
||||||
FilterTypeExtension.AFTER -> ssProvider.getIdentifier("search_filter_after")
|
FilterTypeExtension.AFTER -> ssProvider.getIdentifier("search_filter_after")
|
||||||
FilterTypeExtension.SORT -> ScoutResource.SORT_FILTER
|
FilterTypeExtension.SORT -> ScoutResource.SORT_FILTER
|
||||||
|
FilterTypeExtension.AUTHOR_TYPE -> ScoutResource.AUTHOR_TYPE_FILTER
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
res?.let { param.result = it }
|
res?.let { param.result = it }
|
||||||
|
|
@ -544,7 +695,8 @@ class Scout : Plugin() {
|
||||||
// Patch formatting utils to use our custom lowercase strings
|
// Patch formatting utils to use our custom lowercase strings
|
||||||
// This is called by FilterViewHolder.onConfigure, using the results from getAnswerText and getFilterText
|
// This is called by FilterViewHolder.onConfigure, using the results from getAnswerText and getFilterText
|
||||||
patcher.patch(
|
patcher.patch(
|
||||||
FormatUtils::class.java.getDeclaredMethod("c",
|
FormatUtils::class.java.getDeclaredMethod(
|
||||||
|
"c",
|
||||||
Resources::class.java,
|
Resources::class.java,
|
||||||
Int::class.javaPrimitiveType!!,
|
Int::class.javaPrimitiveType!!,
|
||||||
Array::class.java,
|
Array::class.java,
|
||||||
|
|
@ -557,6 +709,8 @@ class Scout : Plugin() {
|
||||||
ScoutResource.SORT_FILTER -> ssProvider.sortFilterString
|
ScoutResource.SORT_FILTER -> ssProvider.sortFilterString
|
||||||
ScoutResource.SORT_ANSWER -> ssProvider.sortOldString
|
ScoutResource.SORT_ANSWER -> ssProvider.sortOldString
|
||||||
ScoutResource.EXCLUDE_FILTER -> ssProvider.excludeFilterString
|
ScoutResource.EXCLUDE_FILTER -> ssProvider.excludeFilterString
|
||||||
|
ScoutResource.AUTHOR_TYPE_FILTER -> ssProvider.authorTypeFilter
|
||||||
|
ScoutResource.AUTHOR_TYPE_ANSWER -> ssProvider.authorTypeAnswer
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
override?.let {
|
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<WidgetSearchSuggestionsAdapter.FilterViewHolder>(
|
||||||
|
"onConfigure",
|
||||||
|
Int::class.javaPrimitiveType!!,
|
||||||
|
MGRecyclerDataPayload::class.java,
|
||||||
|
) { (param, _: Int, payload: SingleTypePayload<FilterSuggestion>) ->
|
||||||
|
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
|
// Patch to add our new filters into the initial suggestions
|
||||||
patcher.after<SearchSuggestionEngine>(
|
patcher.after<SearchSuggestionEngine>(
|
||||||
"getFilterSuggestions",
|
"getFilterSuggestions",
|
||||||
CharSequence::class.java,
|
CharSequence::class.java,
|
||||||
SearchStringProvider::class.java,
|
SearchStringProvider::class.java,
|
||||||
Boolean::class.javaPrimitiveType!!,
|
Boolean::class.javaPrimitiveType!!,
|
||||||
) { param ->
|
) { (param, query: CharSequence) ->
|
||||||
val query = param.args[0] as CharSequence
|
|
||||||
val res = (param.result as List<SearchSuggestion>).toMutableList()
|
val res = (param.result as List<SearchSuggestion>).toMutableList()
|
||||||
for (type in FilterTypeExtension.values) {
|
|
||||||
val st = ssProvider.stringFor(type) + ":"
|
|
||||||
|
|
||||||
if (st.contains(query))
|
if (optionsExpanded || query != "") {
|
||||||
res.add(FilterSuggestion(type))
|
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()
|
param.result = res.toList()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Patch out the gigantic padding in search results
|
// Patch to add header for new categories
|
||||||
private fun patchSearchPadding() {
|
patcher.before<WidgetSearchSuggestionsAdapter.HeaderViewHolder>(
|
||||||
patcher.after<WidgetSearchResults>("onViewBound", View::class.java) {
|
"onConfigure",
|
||||||
view?.run {
|
Int::class.javaPrimitiveType!!,
|
||||||
fitsSystemWindows = false
|
MGRecyclerDataPayload::class.java,
|
||||||
setPadding(paddingLeft, 16.dp, paddingRight, paddingBottom)
|
) { (param, _: Int, payload: SingleTypePayload<SearchSuggestion.Category>) ->
|
||||||
|
val category = payload.data
|
||||||
|
if (category == SuggestionCategoryExtension.AUTHOR_TYPE) {
|
||||||
|
binding.b.text = "Author Type"
|
||||||
|
param.result = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
patcher.after<WidgetSearchSuggestions>("onViewBound", View::class.java) {
|
// Patch to add entries depending on category
|
||||||
view?.run {
|
patcher.after<WidgetSearchSuggestions.Model>(
|
||||||
fitsSystemWindows = false
|
List::class.java,
|
||||||
setPadding(paddingLeft, 16.dp, paddingRight, paddingBottom)
|
List::class.java,
|
||||||
|
) { (_, _: List<QueryNode>, suggestions: List<SearchSuggestion>) ->
|
||||||
|
var lastCategory: SearchSuggestion.Category? = null
|
||||||
|
val newItems = mutableListOf<MGRecyclerDataPayload>()
|
||||||
|
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<WidgetSearchSuggestionsAdapter>(
|
||||||
|
"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
|
// 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
|
// This enables searching for threads which can have spaces in their names
|
||||||
patcher.instead<QueryParser.Companion>("getInAnswerRule") {
|
patcher.instead<QueryParser.Companion>("getInAnswerRule") {
|
||||||
val compile = Pattern.compile("^\\s*#(\".*?\"|[^ ]+)", 64);
|
val compile = Pattern.compile("^\\s*#(\".*?\"|[^ ]+)", 64)
|
||||||
`QueryParser$Companion$getInAnswerRule$1`(compile, compile)
|
`QueryParser$Companion$getInAnswerRule$1`(compile, compile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -701,7 +922,7 @@ class Scout : Plugin() {
|
||||||
// Now it matches something like @<username>[#<discrim>] (bots still have discriminators)
|
// Now it matches something like @<username>[#<discrim>] (bots still have discriminators)
|
||||||
// The @ is required unfortunately, to distinguish it from literally any other word
|
// The @ is required unfortunately, to distinguish it from literally any other word
|
||||||
patcher.instead<QueryParser.Companion>("getUserRule") {
|
patcher.instead<QueryParser.Companion>("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)
|
// Returns a new rule to support our optional second group (discriminator)
|
||||||
return@instead SimpleParserRule(regex) { matcher, _, obj ->
|
return@instead SimpleParserRule(regex) { matcher, _, obj ->
|
||||||
|
|
|
||||||
|
|
@ -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<SearchSuggestion.Category>
|
||||||
|
|
||||||
|
object AdapterType {
|
||||||
|
const val AUTHOR_TYPE = 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,7 @@ interface SearchAPIInterface {
|
||||||
@t("include_nsfw") includeNsfw: Boolean?,
|
@t("include_nsfw") includeNsfw: Boolean?,
|
||||||
@t("sort_by") sortBy: List<String>?, // "timestamp" is one, not sure about any other sort types
|
@t("sort_by") sortBy: List<String>?, // "timestamp" is one, not sure about any other sort types
|
||||||
@t("sort_order") sortOrder: List<String>?, // "asc" or "desc"
|
@t("sort_order") sortOrder: List<String>?, // "asc" or "desc"
|
||||||
|
@t("author_type") authorType: List<String>?,
|
||||||
): Observable<ModelSearchResponse?>
|
): Observable<ModelSearchResponse?>
|
||||||
|
|
||||||
@f("guilds/{guildId}/messages/search")
|
@f("guilds/{guildId}/messages/search")
|
||||||
|
|
@ -40,5 +41,6 @@ interface SearchAPIInterface {
|
||||||
@t("include_nsfw") includeNsfw: Boolean?,
|
@t("include_nsfw") includeNsfw: Boolean?,
|
||||||
@t("sort_by") sortBy: List<String>?,
|
@t("sort_by") sortBy: List<String>?,
|
||||||
@t("sort_order") sortOrder: List<String>?,
|
@t("sort_order") sortOrder: List<String>?,
|
||||||
|
@t("author_type") authorType: List<String>?,
|
||||||
): Observable<ModelSearchResponse?>
|
): Observable<ModelSearchResponse?>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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<WidgetSearchSuggestionsAdapter, MGRecyclerDataPayload>(
|
||||||
|
Utils.getResId("widget_search_suggestions_item_has", "layout"),
|
||||||
|
adapter,
|
||||||
|
) {
|
||||||
|
private val imageView = itemView.findViewById<ImageView>("search_suggestions_item_has_icon")
|
||||||
|
private val textView = itemView.findViewById<TextView>("search_suggestions_item_has_text")
|
||||||
|
|
||||||
|
override fun onConfigure(i: Int, oPayload: MGRecyclerDataPayload) {
|
||||||
|
super.onConfigure(i, oPayload)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val payload = oPayload as SingleTypePayload<AuthorTypeSuggestion>
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Context, QueryNode, Any> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,9 +20,7 @@ class DateNode(private val date: Long?, private val unparsed: String) : AnswerNo
|
||||||
val fmt = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
val fmt = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||||
val regex: Pattern = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}", Pattern.UNICODE_CASE)
|
val regex: Pattern = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}", Pattern.UNICODE_CASE)
|
||||||
fun getDateRule(): ParserRule {
|
fun getDateRule(): ParserRule {
|
||||||
return SimpleParserRule(regex) { matcher, parser, obj ->
|
return SimpleParserRule(regex) { matcher, _, obj ->
|
||||||
checkNotNull(matcher) { "matcher" }
|
|
||||||
checkNotNull(parser) { "parser" }
|
|
||||||
val match = matcher.group()
|
val match = matcher.group()
|
||||||
val date = fmt.parse(match)
|
val date = fmt.parse(match)
|
||||||
val node = DateNode(date?.time, 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 {
|
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 ->
|
return SimpleParserRule(regex) { _, _, obj ->
|
||||||
ParseSpec(FilterNode(type, str), 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<FilterType> = FilterTypeExtension.dates.toSet()
|
override fun getValidFilters(): Set<FilterType> = FilterTypeExtension.dates.toSet()
|
||||||
override fun isValid(searchData: SearchData?): Boolean = date != null
|
override fun isValid(searchData: SearchData?): Boolean = date != null
|
||||||
override fun getText(): CharSequence? = unparsed
|
override fun getText(): CharSequence = unparsed
|
||||||
|
|
||||||
private val snowflake: String?
|
private val snowflake: String?
|
||||||
get() = date?.let { SnowflakeUtils.fromTimestamp(date).toString() }
|
get() = date?.let { SnowflakeUtils.fromTimestamp(date).toString() }
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package moe.lava.awoocord.scout.parsing
|
package moe.lava.awoocord.scout.parsing
|
||||||
|
|
||||||
import android.content.Context
|
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 com.discord.utilities.search.query.node.QueryNode
|
||||||
import java.util.regex.Matcher
|
import java.util.regex.Matcher
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
@ -16,12 +18,10 @@ internal class SimpleParserRule(
|
||||||
) -> ParseSpec<Context, Any?>
|
) -> ParseSpec<Context, Any?>
|
||||||
) : ParserRule(regex) {
|
) : ParserRule(regex) {
|
||||||
override fun parse(
|
override fun parse(
|
||||||
matcher: Matcher?,
|
matcher: Matcher,
|
||||||
parser: Parser<Context, in QueryNode, in Any?>,
|
parser: Parser<Context, in QueryNode, in Any?>,
|
||||||
obj: Any?
|
obj: Any?
|
||||||
): ParseSpec<Context, in Any?> {
|
): ParseSpec<Context, in Any?> {
|
||||||
checkNotNull(matcher) { "matcher" }
|
|
||||||
checkNotNull(parser) { "parser" }
|
|
||||||
return parseMethod(matcher, parser, obj)
|
return parseMethod(matcher, parser, obj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ class SortNode(private val text: String): AnswerNode() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFilterRule(str: String): ParserRule {
|
fun getFilterRule(str: String): ParserRule {
|
||||||
val regex = Pattern.compile("^\\s*?(${str}):", 64);
|
val regex = Pattern.compile("^\\s*?(${str}):", 64)
|
||||||
return SimpleParserRule(regex) { _, _, obj ->
|
return SimpleParserRule(regex) { _, _, obj ->
|
||||||
ParseSpec(FilterNode(FilterTypeExtension.SORT, str), 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 getText() = this.text
|
||||||
|
|
||||||
override fun updateQuery(
|
override fun updateQuery(
|
||||||
builder: SearchQuery.Builder?,
|
builder: SearchQuery.Builder,
|
||||||
searchData: SearchData?,
|
searchData: SearchData?,
|
||||||
filterType: FilterType?
|
filterType: FilterType?
|
||||||
) {
|
) {
|
||||||
checkNotNull(builder) { "queryBuilder" }
|
|
||||||
builder.appendParam("sort_order", "asc")
|
builder.appendParam("sort_order", "asc")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ class UserIdNode(private val userID: String) : AnswerNode() {
|
||||||
|
|
||||||
override fun getValidFilters() = setOf(FilterType.FROM, FilterType.MENTIONS)
|
override fun getValidFilters() = setOf(FilterType.FROM, FilterType.MENTIONS)
|
||||||
override fun isValid(searchData: SearchData?) = true
|
override fun isValid(searchData: SearchData?) = true
|
||||||
override fun getText() = userID.toString()
|
override fun getText() = userID
|
||||||
|
|
||||||
override fun updateQuery(
|
override fun updateQuery(
|
||||||
builder: SearchQuery.Builder?,
|
builder: SearchQuery.Builder?,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ class ScoutResource(private val resources: Resources) {
|
||||||
val SORT_FILTER = View.generateViewId()
|
val SORT_FILTER = View.generateViewId()
|
||||||
val SORT_ANSWER = View.generateViewId()
|
val SORT_ANSWER = View.generateViewId()
|
||||||
val EXCLUDE_FILTER = 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) =
|
fun getId(name: String, type: String) =
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ class ScoutSearchStringProvider(private val context: Context) {
|
||||||
FilterTypeExtension.DURING -> duringFilterString
|
FilterTypeExtension.DURING -> duringFilterString
|
||||||
FilterTypeExtension.AFTER -> afterFilterString
|
FilterTypeExtension.AFTER -> afterFilterString
|
||||||
FilterTypeExtension.SORT -> sortFilterString
|
FilterTypeExtension.SORT -> sortFilterString
|
||||||
|
FilterTypeExtension.AUTHOR_TYPE -> authorTypeFilter
|
||||||
else -> throw IllegalArgumentException("invalid extended filter type")
|
else -> throw IllegalArgumentException("invalid extended filter type")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,6 +42,8 @@ class ScoutSearchStringProvider(private val context: Context) {
|
||||||
get() = getString("sort").decapitalise(context)
|
get() = getString("sort").decapitalise(context)
|
||||||
val sortOldString: String
|
val sortOldString: String
|
||||||
get() = getString("search_oldest_short").decapitalise(context)
|
get() = getString("search_oldest_short").decapitalise(context)
|
||||||
|
val expandFilterString: String
|
||||||
|
get() = getString("friends_pending_request_expand")
|
||||||
|
|
||||||
// Not localised
|
// Not localised
|
||||||
val hasPollString: String
|
val hasPollString: String
|
||||||
|
|
@ -49,4 +52,9 @@ class ScoutSearchStringProvider(private val context: Context) {
|
||||||
get() = "forward"
|
get() = "forward"
|
||||||
val excludeFilterString: String
|
val excludeFilterString: String
|
||||||
get() = "exclude"
|
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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
plugins/Scout/src/main/res/drawable/smart_toy_24px.xml
Normal file
10
plugins/Scout/src/main/res/drawable/smart_toy_24px.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M160,600Q110,600 75,565Q40,530 40,480Q40,430 75,395Q110,360 160,360L160,280Q160,247 183.5,223.5Q207,200 240,200L360,200Q360,150 395,115Q430,80 480,80Q530,80 565,115Q600,150 600,200L720,200Q753,200 776.5,223.5Q800,247 800,280L800,360Q850,360 885,395Q920,430 920,480Q920,530 885,565Q850,600 800,600L800,760Q800,793 776.5,816.5Q753,840 720,840L240,840Q207,840 183.5,816.5Q160,793 160,760L160,600ZM402.5,502.5Q420,485 420,460Q420,435 402.5,417.5Q385,400 360,400Q335,400 317.5,417.5Q300,435 300,460Q300,485 317.5,502.5Q335,520 360,520Q385,520 402.5,502.5ZM642.5,502.5Q660,485 660,460Q660,435 642.5,417.5Q625,400 600,400Q575,400 557.5,417.5Q540,435 540,460Q540,485 557.5,502.5Q575,520 600,520Q625,520 642.5,502.5ZM320,680L640,680L640,600L320,600L320,680ZM240,760L720,760Q720,760 720,760Q720,760 720,760L720,280Q720,280 720,280Q720,280 720,280L240,280Q240,280 240,280Q240,280 240,280L240,760Q240,760 240,760Q240,760 240,760ZM480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520Z"/>
|
||||||
|
</vector>
|
||||||
10
plugins/Scout/src/main/res/drawable/webhook_24px.xml
Normal file
10
plugins/Scout/src/main/res/drawable/webhook_24px.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M280,840Q197,840 138.5,781.5Q80,723 80,640Q80,567 125.5,512.5Q171,458 240,444L240,527Q205,539 182.5,570Q160,601 160,640Q160,690 195,725Q230,760 280,760Q330,760 365,725Q400,690 400,640L400,600L635,600Q643,591 654.5,585.5Q666,580 680,580Q705,580 722.5,597.5Q740,615 740,640Q740,665 722.5,682.5Q705,700 680,700Q666,700 654.5,694.5Q643,689 635,680L476,680Q462,749 407.5,794.5Q353,840 280,840ZM680,840Q624,840 578.5,812.5Q533,785 507,740L614,740Q628,750 645,755Q662,760 680,760Q730,760 765,725Q800,690 800,640Q800,590 765,555Q730,520 680,520Q660,520 643,525.5Q626,531 611,542L489,339Q468,335 454,319Q440,303 440,280Q440,255 457.5,237.5Q475,220 500,220Q525,220 542.5,237.5Q560,255 560,280Q560,285 560,288.5Q560,292 558,297L645,443Q653,441 662,440.5Q671,440 680,440Q763,440 821.5,498.5Q880,557 880,640Q880,723 821.5,781.5Q763,840 680,840ZM280,700Q255,700 237.5,682.5Q220,665 220,640Q220,618 234,602Q248,586 268,581L362,425Q333,398 316.5,360.5Q300,323 300,280Q300,197 358.5,138.5Q417,80 500,80Q583,80 641.5,138.5Q700,197 700,280L620,280Q620,230 585,195Q550,160 500,160Q450,160 415,195Q380,230 380,280Q380,323 406,355.5Q432,388 472,397L337,622Q339,627 339.5,631Q340,635 340,640Q340,665 322.5,682.5Q305,700 280,700Z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -1,9 +1,19 @@
|
||||||
version = "1.1.1"
|
version = "1.2.1"
|
||||||
description = "Coloured usernames to be a bit more pleasing on the eyes"
|
description = "Coloured usernames to be a bit more pleasing on the eyes"
|
||||||
|
|
||||||
aliucord {
|
aliucord {
|
||||||
// Changelog of your plugin
|
// Changelog of your plugin
|
||||||
changelog.set("""
|
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
|
# 1.1.1
|
||||||
* Revert incorrect spacing fix, since it just breaks replies. Proper fix soon
|
* Revert incorrect spacing fix, since it just breaks replies. Proper fix soon
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,28 +2,32 @@ package moe.lava.awoocord.zinnia
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.GradientDrawable
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.graphics.ColorUtils
|
|
||||||
import com.aliucord.annotations.AliucordPlugin
|
import com.aliucord.annotations.AliucordPlugin
|
||||||
import com.aliucord.entities.Plugin
|
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.DimenUtils.dp
|
||||||
import com.aliucord.utils.ViewUtils.findViewById
|
|
||||||
import com.aliucord.utils.accessField
|
import com.aliucord.utils.accessField
|
||||||
import com.discord.databinding.WidgetChannelMembersListItemUserBinding
|
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.ChannelMembersListAdapter
|
||||||
import com.discord.widgets.channels.memberlist.adapter.ChannelMembersListViewHolderMember
|
import com.discord.widgets.channels.memberlist.adapter.ChannelMembersListViewHolderMember
|
||||||
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemMessage
|
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemMessage
|
||||||
import com.discord.widgets.chat.list.entries.ChatListEntry
|
import com.discord.widgets.chat.list.entries.ChatListEntry
|
||||||
import com.discord.widgets.chat.list.entries.MessageEntry
|
import com.discord.widgets.chat.list.entries.MessageEntry
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
private val ChannelMembersListViewHolderMember.binding
|
private val ChannelMembersListViewHolderMember.binding
|
||||||
by accessField<WidgetChannelMembersListItemUserBinding>()
|
by accessField<WidgetChannelMembersListItemUserBinding>()
|
||||||
|
private val WidgetChatListAdapterItemMessage.itemName
|
||||||
|
by accessField<TextView?>()
|
||||||
|
private val WidgetChatListAdapterItemMessage.replyName
|
||||||
|
by accessField<TextView?>()
|
||||||
|
|
||||||
data class Colours(
|
data class Colours(
|
||||||
val fgP: Int,
|
val fgP: Int,
|
||||||
|
|
@ -36,8 +40,6 @@ data class Colours(
|
||||||
class Zinnia : Plugin() {
|
class Zinnia : Plugin() {
|
||||||
companion object { const val NAME = "RoleBlocks" }
|
companion object { const val NAME = "RoleBlocks" }
|
||||||
|
|
||||||
private val localSettings = ZinniaSettings
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
settingsTab = SettingsTab(ZinniaSettings.Page::class.java, SettingsTab.Type.PAGE)
|
settingsTab = SettingsTab(ZinniaSettings.Page::class.java, SettingsTab.Type.PAGE)
|
||||||
}
|
}
|
||||||
|
|
@ -49,93 +51,6 @@ class Zinnia : Plugin() {
|
||||||
|
|
||||||
override fun stop(context: Context) { patcher.unpatchAll() }
|
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() {
|
private fun patchMemberList() {
|
||||||
// Patches the method that configures the username in members list
|
// Patches the method that configures the username in members list
|
||||||
patcher.after<ChannelMembersListViewHolderMember>(
|
patcher.after<ChannelMembersListViewHolderMember>(
|
||||||
|
|
@ -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!!,
|
Int::class.javaPrimitiveType!!,
|
||||||
ChatListEntry::class.java,
|
ChatListEntry::class.java,
|
||||||
) { (_, _: Int, entry: MessageEntry) ->
|
) { (_, _: Int, entry: MessageEntry) ->
|
||||||
val username = itemView.findViewById<TextView?>("chat_list_adapter_item_text_name")
|
itemName?.let {
|
||||||
?: return@after
|
APCAUtil.configureOn(it, entry.author?.color, Threshold.Large)
|
||||||
configureOn(username, entry.author?.color)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
patcher.instead<WidgetChatListAdapterItemMessage>(
|
||||||
|
"getAuthorTextColor",
|
||||||
|
GuildMember::class.java,
|
||||||
|
) { (_, member: GuildMember?) ->
|
||||||
|
member?.color ?: Color.BLACK
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configures for reply preview username
|
// Configures for reply preview username
|
||||||
patcher.after<WidgetChatListAdapterItemMessage>(
|
patcher.after<WidgetChatListAdapterItemMessage>(
|
||||||
"configureReplyPreview",
|
"configureReplyName",
|
||||||
MessageEntry::class.java,
|
String::class.java,
|
||||||
) { (_, entry: MessageEntry) ->
|
Int::class.javaPrimitiveType!!,
|
||||||
val referencedAuthor = entry.replyData?.messageEntry?.author
|
Boolean::class.javaPrimitiveType!!,
|
||||||
val replyUsername = itemView.findViewById<TextView?>("chat_list_adapter_item_text_decorator_reply_name")
|
) { (_, _: String, colour: Int) ->
|
||||||
?: return@after
|
replyName?.let {
|
||||||
configureOn(replyUsername, referencedAuthor?.color)
|
APCAUtil.configureOn(it, colour, Threshold.Small)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,26 @@
|
||||||
package moe.lava.awoocord.zinnia
|
package moe.lava.awoocord.zinnia
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
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.Utils
|
||||||
import com.aliucord.api.SettingsAPI
|
import com.aliucord.api.SettingsAPI
|
||||||
import com.aliucord.fragments.SettingsPage
|
import com.aliucord.fragments.SettingsPage
|
||||||
import com.aliucord.settings.delegate
|
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.CheckedSetting
|
||||||
import com.discord.views.RadioManager
|
import com.lytefast.flexinput.R
|
||||||
|
import kotlin.math.roundToInt
|
||||||
import kotlin.properties.ReadWriteProperty
|
import kotlin.properties.ReadWriteProperty
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
|
@ -25,6 +38,7 @@ enum class BlockMode {
|
||||||
InvertedThemeOnly,
|
InvertedThemeOnly,
|
||||||
WhiteOnly,
|
WhiteOnly,
|
||||||
BlackOnly,
|
BlackOnly,
|
||||||
|
Unchanged,
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsDelegateEnum<T : Enum<T>>(
|
class SettingsDelegateEnum<T : Enum<T>>(
|
||||||
|
|
@ -49,27 +63,59 @@ private inline fun <T : View> T.addTo(parent: ViewGroup, block: T.() -> Unit = {
|
||||||
parent.addView(this)
|
parent.addView(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private typealias Delegate<Type> = ReadWriteProperty<Any, Type>
|
||||||
|
|
||||||
|
fun <T> basicDelegate(initial: T) = object : Delegate<T> {
|
||||||
|
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<T>(
|
||||||
|
private val inner: Delegate<T>,
|
||||||
|
private val update: (T) -> Unit,
|
||||||
|
) : Delegate<T> {
|
||||||
|
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 {
|
object ZinniaSettings {
|
||||||
private val api = SettingsAPI(Zinnia.NAME)
|
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 <T> reactive(backing: () -> Delegate<T>): StateDelegate<T> {
|
||||||
|
return StateDelegate(backing()) { onStateUpdate() }
|
||||||
|
}
|
||||||
|
|
||||||
var blockAlsoDefault by api.delegate(true)
|
var mode by reactive { api.delegateEnum(Mode.Block) }
|
||||||
var blockInverted by api.delegate(false)
|
|
||||||
var blockMode by api.delegateEnum(BlockMode.ApcaLightWcagDark)
|
var blockAlsoDefault by reactive { api.delegate(true) }
|
||||||
var blockApcaThreshold by api.delegate(75.0)
|
var blockInverted by reactive { api.delegate(false) }
|
||||||
var blockWcagThreshold by api.delegate(4.5)
|
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() {
|
class Page : SettingsPage() {
|
||||||
private lateinit var manager: RadioManager
|
|
||||||
private lateinit var mRoleDot: CheckedSetting
|
|
||||||
private lateinit var mBlock: CheckedSetting
|
|
||||||
|
|
||||||
private val checks = mutableListOf<CheckedSetting>()
|
private val checks = mutableListOf<CheckedSetting>()
|
||||||
|
|
||||||
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) {
|
return Utils.createCheckedSetting(requireContext(), CheckedSetting.ViewType.RADIO, text, subtext).addTo(linearLayout) {
|
||||||
isChecked = blockMode == newMode
|
isChecked = blockMode == newMode
|
||||||
setOnCheckedListener {
|
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<Int>, 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) {
|
override fun onViewBound(view: View) {
|
||||||
super.onViewBound(view)
|
super.onViewBound(view)
|
||||||
setActionBarTitle(Zinnia.NAME)
|
setActionBarTitle(Zinnia.NAME)
|
||||||
|
|
@ -92,61 +220,20 @@ object ZinniaSettings {
|
||||||
val roleDotSettings = mutableListOf<CheckedSetting>()
|
val roleDotSettings = mutableListOf<CheckedSetting>()
|
||||||
|
|
||||||
addHeader(ctx, "Text colour")
|
addHeader(ctx, "Text colour")
|
||||||
createRadio(BlockMode.ApcaLightWcagDark, "Automatic", "Adjusts text colour based on role colour")
|
addRadio(BlockMode.ApcaLightWcagDark, "Automatic", "Adjusts text colour based on optimal contrast with role colour")
|
||||||
createRadio(BlockMode.ThemeOnly, "By theme", "Adjusts text colour based on theme")
|
addRadio(BlockMode.ThemeOnly, "By theme", "Adjusts text colour based on system theme (dark/light)")
|
||||||
createRadio(BlockMode.InvertedThemeOnly, "By theme (inverted)", "Same as above, but inverted")
|
addRadio(BlockMode.InvertedThemeOnly, "By theme (inverted)", "Same as above, but inverted")
|
||||||
createRadio(BlockMode.WhiteOnly, "White", "Force text colour to be white")
|
addRadio(BlockMode.WhiteOnly, "White", "Force text colour to be white")
|
||||||
createRadio(BlockMode.BlackOnly, "Black", "Force text colour to be black")
|
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, "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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
addHeader(ctx, "Block Settings")
|
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,
|
ctx,
|
||||||
CheckedSetting.ViewType.SWITCH,
|
CheckedSetting.ViewType.SWITCH,
|
||||||
"Invert block colours",
|
"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) {
|
).addTo(this) {
|
||||||
isChecked = blockInverted
|
isChecked = blockInverted
|
||||||
setOnCheckedListener {
|
setOnCheckedListener {
|
||||||
|
|
@ -154,7 +241,52 @@ object ZinniaSettings {
|
||||||
}
|
}
|
||||||
blockSettings.add(this)
|
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<Threshold, TextView>) {
|
||||||
|
val (threshold, preview) = pair
|
||||||
|
val colour = Color.HSVToColor(floatArrayOf(previewH.toFloat(), previewS / 100f, previewV / 100f))
|
||||||
|
APCAUtil.configureOn(preview, colour, threshold)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,11 @@ rootProject.name = "Awoocord"
|
||||||
val plugins = mapOf(
|
val plugins = mapOf(
|
||||||
"ComponentsV2Beta" to "canary/ComponentsV2",
|
"ComponentsV2Beta" to "canary/ComponentsV2",
|
||||||
"SlashCommandsFixBeta" to "canary/SlashCommandsFix",
|
"SlashCommandsFixBeta" to "canary/SlashCommandsFix",
|
||||||
|
"Bubbles" to "plugins/Crocosmia",
|
||||||
|
"Clump" to "plugins/Bocchi",
|
||||||
"Scout" to "plugins/Scout",
|
"Scout" to "plugins/Scout",
|
||||||
"RoleBlocks" to "plugins/Zinnia",
|
"RoleBlocks" to "plugins/Zinnia",
|
||||||
|
"Glance" to "plugins/Myosotis",
|
||||||
)
|
)
|
||||||
|
|
||||||
include(*plugins.keys.toTypedArray())
|
include(*plugins.keys.toTypedArray())
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue