Compare commits

..

1 commit

Author SHA1 Message Date
b7942baf3c
feat(Crocosmia): init 2025-12-06 18:02:32 +11:00
29 changed files with 263 additions and 1186 deletions

View file

@ -33,10 +33,10 @@ jobs:
repository: "Aliucord/Aliucord"
path: "repo"
- name: Setup JDK 21
- name: Setup JDK 11
uses: actions/setup-java@v1
with:
java-version: 21
java-version: 11
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
@ -46,8 +46,8 @@ jobs:
cd $GITHUB_WORKSPACE/src
chmod +x gradlew
./gradlew make generateUpdaterJson
cp {canary,plugins}/*/build/outputs/*.zip $GITHUB_WORKSPACE/builds
cp build/outputs/updater.json $GITHUB_WORKSPACE/builds
cp {canary,plugins}/*/build/*.zip $GITHUB_WORKSPACE/builds
cp build/updater.json $GITHUB_WORKSPACE/builds
- name: Push builds
run: |

View file

@ -1,43 +1,26 @@
# Awoocord Plugins
## [Bubbles](plugins/Crocosmia) | [Download](https://github.com/cillynder/Awoocord/raw/builds/Bubbles.zip)
## [Bubbles](plugins/Zinnia) | [Download](https://github.com/LavaDesu/Awoocord/raw/builds/RoleBlocks.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)
## [RoleBlocks](plugins/Zinnia) | [Download](https://github.com/LavaDesu/Awoocord/raw/builds/RoleBlocks.zip)
Apply the role colour as a background of usernames, improving contrast with some role colours
## [Scout](plugins/Scout) | [Download](https://github.com/cillynder/Awoocord/raw/builds/Scout.zip)
## [Scout](plugins/Scout) | [Download](https://github.com/LavaDesu/Awoocord/raw/builds/Scout.zip)
Vastly improves the search experience on Aliucord.
Features:
- Sort by oldest messages first
- Sort by oldest first
- Filter by date (before, during, after)
- Exclude certain messages (opposite of `in:`) (not even desktop has this!)
- Search by user ID
- Search in threads
Fixes:
- Removes the large padding from the top, most noticable if your device has a large status bar
- Removes the unnecessary #0000 discriminator
Reimplemented features from search of other clients:
- Sorting by oldest first
- Filter by date
- Search from user ID
# WIP Backports
## [SlashCommandsFix](canary/SlashCommandsFix) | [Download](https://github.com/cillynder/Awoocord/raw/builds/SlashCommandsFixBeta.zip)
## [SlashCommandsFix](canary/SlashCommandsFix) | [Download](https://github.com/LavaDesu/Awoocord/raw/builds/SlashCommandsFixBeta.zip)
Fixes slash commands not showing up.
## [ComponentsV2](canary/ComponentsV2) | [Download](https://github.com/cillynder/Awoocord/raw/builds/ComponentsV2Beta.zip)
## [ComponentsV2](canary/ComponentsV2) | [Download](https://github.com/LavaDesu/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.

View file

@ -46,8 +46,8 @@ subprojects {
}
configure<AliucordExtension> {
author("cilly", 368398754077868032L, hyperlink = false)
github("https://github.com/cillynder/Awoocord")
author("Lava", 368398754077868032L, hyperlink = true)
github("https://github.com/LavaDesu/Awoocord")
}
configure<KtlintExtension> {

View file

@ -1,25 +0,0 @@
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)
}

View file

@ -1,59 +0,0 @@
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)
)
}
}
}

View file

@ -8,5 +8,5 @@ aliucord {
* Initial release >w<
""".trimIndent())
deploy.set(true)
deploy.set(false)
}

View file

@ -1,12 +0,0 @@
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)
}

View file

@ -1,205 +0,0 @@
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)
}
}
}
}

View file

@ -1,4 +1,4 @@
version = "1.4.0"
version = "1.3.0"
description = "Backported and improved search functionality"
android {
@ -14,12 +14,6 @@ aliucord {
Changelog {added marginTop}
======================
# 1.4.0 - Scout is searching for clues about the elusive MvM update
* Added the authorType filter option to search by user, bot, or webhook
* Moved sort filter to the top of the new ones
* Fixes a Discord bug where typing "mentions" would also suggest "has"
* Some people said the options were getting bloated, so they're all hidden behind a "Show all" button now. They'll still show up in auto suggestions.
# 1.3.0
* Removes empty discriminator when searching with users

View file

@ -3,15 +3,12 @@ package moe.lava.awoocord.scout
import com.discord.utilities.search.query.FilterType
object FilterTypeExtension {
lateinit var EXPAND: FilterType
lateinit var SORT: FilterType
lateinit var BEFORE: FilterType
lateinit var DURING: FilterType
lateinit var AFTER: FilterType
lateinit var SORT: FilterType
lateinit var EXCLUDE: FilterType
lateinit var AUTHOR_TYPE: FilterType
lateinit var dates: Array<FilterType>
lateinit var filters: Array<FilterType>
lateinit var values: Array<FilterType>
}

View file

@ -1,54 +1,27 @@
@file:Suppress("EnumValuesSoftDeprecate", "CanConvertToMultiDollarString")
/**
* Hi to anyone who might be reading this; I am sorry for the atrocious code in this plugin
* but I promise I'll be fixing it up soon :3
*/
package moe.lava.awoocord.scout
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Resources
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.core.content.res.ResourcesCompat
import com.aliucord.Utils
import com.aliucord.annotations.AliucordPlugin
import com.aliucord.entities.Plugin
import com.aliucord.patcher.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.patcher.*
import com.aliucord.utils.DimenUtils.dp
import com.aliucord.utils.RxUtils.subscribe
import com.aliucord.utils.ViewUtils.findViewById
import com.aliucord.utils.accessField
import com.aliucord.wrappers.ChannelWrapper.Companion.id
import com.discord.BuildConfig
import com.discord.api.channel.Channel
import com.discord.api.channel.ChannelUtils
import com.discord.api.channel.`ChannelUtils$getSortByNameAndType$1`
import com.discord.api.channel.*
import com.discord.api.permission.Permission
import com.discord.databinding.WidgetSearchSuggestionItemHeaderBinding
import com.discord.databinding.WidgetSearchSuggestionsItemHasBinding
import com.discord.databinding.WidgetSearchSuggestionsItemSuggestionBinding
import com.discord.models.member.GuildMember
import com.discord.models.user.User
import com.discord.restapi.RequiredHeadersInterceptor
import com.discord.restapi.RestAPIBuilder
import com.discord.simpleast.core.parser.ParseSpec
import com.discord.simpleast.core.parser.Parser
import com.discord.simpleast.core.parser.Rule
import com.discord.stores.StoreSearch
import com.discord.stores.StoreSearchInput
import com.discord.stores.StoreStream
import com.discord.simpleast.core.parser.*
import com.discord.stores.*
import com.discord.utilities.mg_recycler.MGRecyclerDataPayload
import com.discord.utilities.mg_recycler.SingleTypePayload
import com.discord.utilities.rest.RestAPI.AppHeadersProvider
@ -56,60 +29,35 @@ import com.discord.utilities.search.network.`SearchFetcher$getRestObservable$3`
import com.discord.utilities.search.network.SearchQuery
import com.discord.utilities.search.query.FilterType
import com.discord.utilities.search.query.node.QueryNode
import com.discord.utilities.search.query.node.answer.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.answer.*
import com.discord.utilities.search.query.node.content.ContentNode
import com.discord.utilities.search.query.node.filter.FilterNode
import com.discord.utilities.search.query.parsing.QueryParser
import com.discord.utilities.search.query.parsing.`QueryParser$Companion$getInAnswerRule$1`
import com.discord.utilities.search.strings.ContextSearchStringProvider
import com.discord.utilities.search.strings.SearchStringProvider
import com.discord.utilities.search.suggestion.SearchSuggestionEngine
import com.discord.utilities.search.suggestion.entries.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.suggestion.entries.*
import com.discord.utilities.search.validation.SearchData
import com.discord.widgets.search.results.WidgetSearchResults
import com.discord.widgets.search.suggestions.WidgetSearchSuggestions
import com.discord.widgets.search.suggestions.`WidgetSearchSuggestions$configureUI$1`
import com.discord.widgets.search.suggestions.WidgetSearchSuggestionsAdapter
import com.franmontiel.persistentcookiejar.PersistentCookieJar
import com.franmontiel.persistentcookiejar.cache.SetCookieCache
import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor
import com.lytefast.flexinput.R
import moe.lava.awoocord.scout.api.SearchAPIInterface
import moe.lava.awoocord.scout.entries.AuthorTypeSuggestion
import moe.lava.awoocord.scout.entries.AuthorTypeViewHolder
import moe.lava.awoocord.scout.parsing.AuthorType
import moe.lava.awoocord.scout.parsing.AuthorTypeNode
import moe.lava.awoocord.scout.parsing.DateNode
import moe.lava.awoocord.scout.parsing.SimpleParserRule
import moe.lava.awoocord.scout.parsing.SortNode
import moe.lava.awoocord.scout.parsing.UserIdNode
import moe.lava.awoocord.scout.ui.DatePickerFragment
import moe.lava.awoocord.scout.ui.ScoutResource
import moe.lava.awoocord.scout.ui.ScoutSearchStringProvider
import moe.lava.awoocord.scout.parsing.*
import moe.lava.awoocord.scout.ui.*
import java.util.regex.Pattern
import b.a.k.b as FormatUtils
private val WidgetSearchSuggestionsAdapter.FilterViewHolder.binding
by accessField<WidgetSearchSuggestionsItemSuggestionBinding>()
private val WidgetSearchSuggestionsAdapter.HeaderViewHolder.binding
by accessField<WidgetSearchSuggestionItemHeaderBinding>()
@AliucordPlugin
@AliucordPlugin()
@Suppress("unused", "unchecked_cast")
class Scout : Plugin() {
lateinit var scoutRes: ScoutResource
lateinit var ssProvider: ScoutSearchStringProvider
lateinit var searchApi: SearchAPIInterface
var optionsExpanded = false
init {
@Suppress("DEPRECATION")
needsResources = true
@ -124,24 +72,20 @@ class Scout : Plugin() {
override fun start(context: Context) {
extendFilterType()
extendHasAnswerOption()
extendSuggestionCategory()
fixFiltersKeying()
fixHasFilterSuggestion()
fixSearchPadding()
patchHasAnswerOption()
patchHasNode()
patchQuery()
patchQueryParser()
patchSearchUI(context)
patchSearchPadding()
patchThreadSupport()
patchUsernameDiscriminator()
}
override fun stop(context: Context) {
patcher.unpatchAll()
resetFilterType()
resetHasAnswerOption()
resetSuggestionCategory()
patcher.unpatchAll()
}
// Creates a new custom search API implementation, for the extra `min_id` param in search queries
@ -167,7 +111,7 @@ class Scout : Plugin() {
private var origFilterTypes: Array<FilterType>? = null
// Creates new pseudo-values of the `FilterType` enum for date filters
@Suppress("LocalVariableName", "AssignedValueIsNeverRead")
@Suppress("LocalVariableName")
private fun extendFilterType() {
val cls = FilterType::class.java
val constructor = cls.declaredConstructors[0]
@ -179,23 +123,18 @@ class Scout : Plugin() {
origFilterTypes = origFilterTypes ?: values
var nextIdx = values.size
val EXPAND = constructor.newInstance("EXPAND", nextIdx++) as FilterType
val SORT = constructor.newInstance("SORT", nextIdx++) as FilterType
val EXCLUDE = constructor.newInstance("EXCLUDE", nextIdx++) as FilterType
val AUTHOR_TYPE = constructor.newInstance("AUTHOR_TYPE", nextIdx++) as FilterType
val BEFORE = constructor.newInstance("BEFORE", nextIdx++) as FilterType
val DURING = constructor.newInstance("DURING", nextIdx++) as FilterType
val AFTER = constructor.newInstance("AFTER", nextIdx++) as FilterType
FilterTypeExtension.EXPAND = EXPAND
FilterTypeExtension.SORT = SORT
val SORT = constructor.newInstance("SORT", nextIdx) as FilterType
FilterTypeExtension.EXCLUDE = EXCLUDE
FilterTypeExtension.AUTHOR_TYPE = AUTHOR_TYPE
FilterTypeExtension.BEFORE = BEFORE
FilterTypeExtension.DURING = DURING
FilterTypeExtension.AFTER = AFTER
FilterTypeExtension.SORT = SORT
FilterTypeExtension.dates = arrayOf(BEFORE, DURING, AFTER)
FilterTypeExtension.filters = arrayOf(SORT, AUTHOR_TYPE, EXCLUDE) + FilterTypeExtension.dates
FilterTypeExtension.values = arrayOf(EXPAND) + FilterTypeExtension.filters
FilterTypeExtension.values = arrayOf(EXCLUDE, BEFORE, DURING, AFTER, SORT)
val newValues = values.toMutableList()
newValues.addAll(FilterTypeExtension.values)
@ -215,7 +154,7 @@ class Scout : Plugin() {
private var origHasAnswerOptions: Array<HasAnswerOption>? = null
// Creates new pseudo-values of the `HasAnswerOption` enum for poll and forwarded filters
@Suppress("LocalVariableName", "AssignedValueIsNeverRead")
@Suppress("LocalVariableName")
private fun extendHasAnswerOption() {
val cls = HasAnswerOption::class.java
val constructor = cls.declaredConstructors[0]
@ -228,7 +167,7 @@ class Scout : Plugin() {
var nextIdx = values.size
val POLL = constructor.newInstance("POLL", nextIdx++, "poll") as HasAnswerOption
val SNAPSHOT = constructor.newInstance("SNAPSHOT", nextIdx++, "snapshot") as HasAnswerOption
val SNAPSHOT = constructor.newInstance("SNAPSHOT", nextIdx, "snapshot") as HasAnswerOption
HasAnswerOptionExtension.POLL = POLL
HasAnswerOptionExtension.SNAPSHOT = SNAPSHOT
HasAnswerOptionExtension.values = arrayOf(POLL, SNAPSHOT)
@ -249,83 +188,6 @@ class Scout : Plugin() {
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
private fun patchHasAnswerOption() {
patcher.before<HasAnswerOption.Companion>(
@ -396,22 +258,24 @@ class Scout : Plugin() {
CharSequence::class.java,
FilterType::class.java,
SearchStringProvider::class.java,
) { (_, query: CharSequence, type: FilterType, provider: SearchStringProvider) ->
// Generate entries for author type
if (type == FilterTypeExtension.AUTHOR_TYPE) {
return@instead AuthorType.values()
.filter { it.value.contains(query) }
.map { AuthorTypeSuggestion(it) }
) { param ->
val query = param.args[0] as CharSequence
val filterType = param.args[1] as FilterType
val ossProvider = param.args[2] as SearchStringProvider
if (filterType != FilterType.HAS && filterType != FilterTypeExtension.EXCLUDE)
return@instead listOf<Any>()
val res = mutableListOf<HasSuggestion>()
for (opt in HasAnswerOption.values()) {
val filterText = opt.getLocalizedInputText(ossProvider)
if (filterText.contains(query))
res.add(HasSuggestion(opt))
}
// 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>()
res.toList()
}
}
// Patching HasNode related methods for our exclude: filter type
@ -437,9 +301,9 @@ class Scout : Plugin() {
val opt = field.get(this) as HasAnswerOption
if (filterType == FilterType.HAS)
builder.appendParam("has", opt.restParamValue)
builder.appendParam("has", opt.restParamValue);
else if (filterType == FilterTypeExtension.EXCLUDE)
builder.appendParam("has", "-" + opt.restParamValue)
builder.appendParam("has", "-" + opt.restParamValue);
}
// Patching the behaviour when the has suggestion is clicked
@ -469,6 +333,8 @@ class Scout : Plugin() {
)
getAnswerReplacementStart.isAccessible = true
logger.info(query.joinToString("|") { it.text })
val replacementIdx = getAnswerReplacementStart.invoke(this, query) as Int
val previousFilterText = query[replacementIdx]
val filterNode = if (previousFilterText.text == ssProvider.excludeFilterString)
@ -492,7 +358,6 @@ class Scout : Plugin() {
var minID = params["min_id"]
var maxID = params["max_id"]
val sortOrder = params["sort_order"]
val authorType = params["author_type"]
self.`$oldestMessageId`?.let {
if (sortOrder?.getOrNull(0) == "asc")
minID = listOf(it.toString())
@ -513,8 +378,7 @@ class Scout : Plugin() {
retryAttempts,
self.`$searchQuery`.includeNsfw,
listOf("timestamp"),
sortOrder,
authorType,
sortOrder
)
else
searchApi.searchChannelMessages(
@ -528,8 +392,7 @@ class Scout : Plugin() {
retryAttempts,
self.`$searchQuery`.includeNsfw,
listOf("timestamp"),
sortOrder,
authorType,
sortOrder
)
}
)
@ -549,8 +412,6 @@ class Scout : Plugin() {
DateNode.getDateRule(),
SortNode.getFilterRule(ssProvider.sortFilterString),
SortNode.getSortRule(ssProvider),
AuthorTypeNode.getFilterRule(ssProvider.authorTypeFilter),
AuthorTypeNode.getAuthorTypesRule(),
SimpleParserRule(Pattern.compile("^\\s*?${ssProvider.excludeFilterString}:", 64)) { _, _, obj ->
ParseSpec(FilterNode(FilterTypeExtension.EXCLUDE, ssProvider.excludeFilterString), obj)
}
@ -559,7 +420,6 @@ class Scout : Plugin() {
}
// This is probably the worst bit of this plugin
@SuppressLint("SetTextI18n")
private fun patchSearchUI(context: Context) {
// Run when a filter suggestion is clicked
// Most of the code is copied from its implementation
@ -572,7 +432,7 @@ class Scout : Plugin() {
) { param ->
val filter = param.args[0] as FilterType
if (filter !in FilterTypeExtension.values)
return@before // Exit if not an extended filter type
return@before; // Exit if not an extended filter type
val replaceAndPublish = StoreSearchInput::class.java.getDeclaredMethod(
"replaceAndPublish",
@ -606,7 +466,7 @@ class Scout : Plugin() {
getAnswerReplacementStart.invoke(this, list),
listOf(filterNode, DateNode(it)),
list
)
);
}
}
@ -615,21 +475,14 @@ class Scout : Plugin() {
lastIndex,
listOf(filterNode, SortNode(ssProvider.sortOldString)),
list
)
);
if (filter == FilterTypeExtension.EXCLUDE)
replaceAndPublish.invoke(this,
lastIndex,
listOf(filterNode),
list
)
if (filter == FilterTypeExtension.AUTHOR_TYPE)
replaceAndPublish.invoke(this,
lastIndex,
listOf(filterNode),
list
)
);
param.result = null
}
@ -648,7 +501,6 @@ class Scout : Plugin() {
FilterTypeExtension.AFTER -> false to scoutRes.getDrawableId("baseline_update_24")
FilterTypeExtension.SORT -> true to R.e.ic_sort_white_24dp
FilterTypeExtension.EXCLUDE -> false to scoutRes.getDrawableId("baseline_do_disturb_on_24")
FilterTypeExtension.AUTHOR_TYPE -> true to R.e.ic_members_24dp
else -> false to null
}
@ -670,8 +522,6 @@ class Scout : Plugin() {
param.result = ScoutResource.SORT_ANSWER
if (type == FilterTypeExtension.EXCLUDE)
param.result = ssProvider.getIdentifier("search_answer_has")
if (type == FilterTypeExtension.AUTHOR_TYPE)
param.result = ScoutResource.AUTHOR_TYPE_ANSWER
}
// Patch for retrieving filter name
@ -686,7 +536,6 @@ class Scout : Plugin() {
FilterTypeExtension.DURING -> ssProvider.getIdentifier("search_filter_during")
FilterTypeExtension.AFTER -> ssProvider.getIdentifier("search_filter_after")
FilterTypeExtension.SORT -> ScoutResource.SORT_FILTER
FilterTypeExtension.AUTHOR_TYPE -> ScoutResource.AUTHOR_TYPE_FILTER
else -> null
}
res?.let { param.result = it }
@ -695,8 +544,7 @@ class Scout : Plugin() {
// Patch formatting utils to use our custom lowercase strings
// This is called by FilterViewHolder.onConfigure, using the results from getAnswerText and getFilterText
patcher.patch(
FormatUtils::class.java.getDeclaredMethod(
"c",
FormatUtils::class.java.getDeclaredMethod("c",
Resources::class.java,
Int::class.javaPrimitiveType!!,
Array::class.java,
@ -709,8 +557,6 @@ class Scout : Plugin() {
ScoutResource.SORT_FILTER -> ssProvider.sortFilterString
ScoutResource.SORT_ANSWER -> ssProvider.sortOldString
ScoutResource.EXCLUDE_FILTER -> ssProvider.excludeFilterString
ScoutResource.AUTHOR_TYPE_FILTER -> ssProvider.authorTypeFilter
ScoutResource.AUTHOR_TYPE_ANSWER -> ssProvider.authorTypeAnswer
else -> null
}
override?.let {
@ -719,105 +565,38 @@ 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
patcher.after<SearchSuggestionEngine>(
"getFilterSuggestions",
CharSequence::class.java,
SearchStringProvider::class.java,
Boolean::class.javaPrimitiveType!!,
) { (param, query: CharSequence) ->
) { param ->
val query = param.args[0] as CharSequence
val res = (param.result as List<SearchSuggestion>).toMutableList()
for (type in FilterTypeExtension.values) {
val st = ssProvider.stringFor(type) + ":"
if (optionsExpanded || query != "") {
for (type in FilterTypeExtension.filters) {
val st = ssProvider.stringFor(type) + ":"
if (st.contains(query))
res.add(FilterSuggestion(type))
}
} else {
res.add(FilterSuggestion(FilterTypeExtension.EXPAND))
if (st.contains(query))
res.add(FilterSuggestion(type))
}
param.result = res.toList()
}
}
// Patch to add header for new categories
patcher.before<WidgetSearchSuggestionsAdapter.HeaderViewHolder>(
"onConfigure",
Int::class.javaPrimitiveType!!,
MGRecyclerDataPayload::class.java,
) { (param, _: Int, payload: SingleTypePayload<SearchSuggestion.Category>) ->
val category = payload.data
if (category == SuggestionCategoryExtension.AUTHOR_TYPE) {
binding.b.text = "Author Type"
param.result = null
// Patch out the gigantic padding in search results
private fun patchSearchPadding() {
patcher.after<WidgetSearchResults>("onViewBound", View::class.java) {
view?.run {
fitsSystemWindows = false
setPadding(paddingLeft, 16.dp, paddingRight, paddingBottom)
}
}
// Patch to add entries depending on category
patcher.after<WidgetSearchSuggestions.Model>(
List::class.java,
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)
}
patcher.after<WidgetSearchSuggestions>("onViewBound", View::class.java) {
view?.run {
fitsSystemWindows = false
setPadding(paddingLeft, 16.dp, paddingRight, paddingBottom)
}
}
}
@ -827,7 +606,7 @@ class Scout : Plugin() {
// Patch query parser for in: to support names with spaces, by wrapping them in quotes
// This enables searching for threads which can have spaces in their names
patcher.instead<QueryParser.Companion>("getInAnswerRule") {
val compile = Pattern.compile("^\\s*#(\".*?\"|[^ ]+)", 64)
val compile = Pattern.compile("^\\s*#(\".*?\"|[^ ]+)", 64);
`QueryParser$Companion$getInAnswerRule$1`(compile, compile)
}
@ -922,7 +701,7 @@ class Scout : Plugin() {
// Now it matches something like @<username>[#<discrim>] (bots still have discriminators)
// The @ is required unfortunately, to distinguish it from literally any other word
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)
return@instead SimpleParserRule(regex) { matcher, _, obj ->

View file

@ -1,12 +0,0 @@
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
}
}

View file

@ -24,7 +24,6 @@ interface SearchAPIInterface {
@t("include_nsfw") includeNsfw: Boolean?,
@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("author_type") authorType: List<String>?,
): Observable<ModelSearchResponse?>
@f("guilds/{guildId}/messages/search")
@ -41,6 +40,5 @@ interface SearchAPIInterface {
@t("include_nsfw") includeNsfw: Boolean?,
@t("sort_by") sortBy: List<String>?,
@t("sort_order") sortOrder: List<String>?,
@t("author_type") authorType: List<String>?,
): Observable<ModelSearchResponse?>
}

View file

@ -1,9 +0,0 @@
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
}

View file

@ -1,77 +0,0 @@
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,
)
}
}
}

View file

@ -1,64 +0,0 @@
@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)
}
}

View file

@ -20,7 +20,9 @@ class DateNode(private val date: Long?, private val unparsed: String) : AnswerNo
val fmt = SimpleDateFormat("yyyy-MM-dd", Locale.US)
val regex: Pattern = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}", Pattern.UNICODE_CASE)
fun getDateRule(): ParserRule {
return SimpleParserRule(regex) { matcher, _, obj ->
return SimpleParserRule(regex) { matcher, parser, obj ->
checkNotNull(matcher) { "matcher" }
checkNotNull(parser) { "parser" }
val match = matcher.group()
val date = fmt.parse(match)
val node = DateNode(date?.time, match)
@ -29,7 +31,7 @@ class DateNode(private val date: Long?, private val unparsed: String) : AnswerNo
}
private fun getFilterRule(str: String, type: FilterType): ParserRule {
val regex = Pattern.compile("^\\s*?(${str}):", 64)
val regex = Pattern.compile("^\\s*?(${str}):", 64);
return SimpleParserRule(regex) { _, _, obj ->
ParseSpec(FilterNode(type, str), obj)
}
@ -42,7 +44,7 @@ class DateNode(private val date: Long?, private val unparsed: String) : AnswerNo
override fun getValidFilters(): Set<FilterType> = FilterTypeExtension.dates.toSet()
override fun isValid(searchData: SearchData?): Boolean = date != null
override fun getText(): CharSequence = unparsed
override fun getText(): CharSequence? = unparsed
private val snowflake: String?
get() = date?.let { SnowflakeUtils.fromTimestamp(date).toString() }

View file

@ -1,9 +1,7 @@
package moe.lava.awoocord.scout.parsing
import android.content.Context
import com.discord.simpleast.core.parser.ParseSpec
import com.discord.simpleast.core.parser.Parser
import com.discord.simpleast.core.parser.Rule
import com.discord.simpleast.core.parser.*
import com.discord.utilities.search.query.node.QueryNode
import java.util.regex.Matcher
import java.util.regex.Pattern
@ -18,10 +16,12 @@ internal class SimpleParserRule(
) -> ParseSpec<Context, Any?>
) : ParserRule(regex) {
override fun parse(
matcher: Matcher,
matcher: Matcher?,
parser: Parser<Context, in QueryNode, in Any?>,
obj: Any?
): ParseSpec<Context, in Any?> {
checkNotNull(matcher) { "matcher" }
checkNotNull(parser) { "parser" }
return parseMethod(matcher, parser, obj)
}
}

View file

@ -24,7 +24,7 @@ class SortNode(private val text: String): AnswerNode() {
}
fun getFilterRule(str: String): ParserRule {
val regex = Pattern.compile("^\\s*?(${str}):", 64)
val regex = Pattern.compile("^\\s*?(${str}):", 64);
return SimpleParserRule(regex) { _, _, obj ->
ParseSpec(FilterNode(FilterTypeExtension.SORT, str), obj)
}
@ -36,10 +36,11 @@ class SortNode(private val text: String): AnswerNode() {
override fun getText() = this.text
override fun updateQuery(
builder: SearchQuery.Builder,
builder: SearchQuery.Builder?,
searchData: SearchData?,
filterType: FilterType?
) {
checkNotNull(builder) { "queryBuilder" }
builder.appendParam("sort_order", "asc")
}
}

View file

@ -22,7 +22,7 @@ class UserIdNode(private val userID: String) : AnswerNode() {
override fun getValidFilters() = setOf(FilterType.FROM, FilterType.MENTIONS)
override fun isValid(searchData: SearchData?) = true
override fun getText() = userID
override fun getText() = userID.toString()
override fun updateQuery(
builder: SearchQuery.Builder?,

View file

@ -10,8 +10,6 @@ class ScoutResource(private val resources: Resources) {
val SORT_FILTER = View.generateViewId()
val SORT_ANSWER = View.generateViewId()
val EXCLUDE_FILTER = View.generateViewId()
val AUTHOR_TYPE_FILTER = View.generateViewId()
val AUTHOR_TYPE_ANSWER = View.generateViewId()
}
fun getId(name: String, type: String) =

View file

@ -21,7 +21,6 @@ class ScoutSearchStringProvider(private val context: Context) {
FilterTypeExtension.DURING -> duringFilterString
FilterTypeExtension.AFTER -> afterFilterString
FilterTypeExtension.SORT -> sortFilterString
FilterTypeExtension.AUTHOR_TYPE -> authorTypeFilter
else -> throw IllegalArgumentException("invalid extended filter type")
}
@ -42,8 +41,6 @@ class ScoutSearchStringProvider(private val context: Context) {
get() = getString("sort").decapitalise(context)
val sortOldString: String
get() = getString("search_oldest_short").decapitalise(context)
val expandFilterString: String
get() = getString("friends_pending_request_expand")
// Not localised
val hasPollString: String
@ -52,9 +49,4 @@ class ScoutSearchStringProvider(private val context: Context) {
get() = "forward"
val excludeFilterString: String
get() = "exclude"
val authorTypeFilter: String
get() = "authorType"
val authorTypeAnswer: String
// TODO, could probably be localisable by joining each part together
get() = "user, bot or webhook"
}

View file

@ -1,10 +0,0 @@
<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>

View file

@ -1,10 +0,0 @@
<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>

View file

@ -1,19 +1,9 @@
version = "1.2.1"
version = "1.1.1"
description = "Coloured usernames to be a bit more pleasing on the eyes"
aliucord {
// Changelog of your plugin
changelog.set("""
# 1.2.1
* Use correct default block colour in replies
* Use correct default block colour in "unchanged" mode
# 1.2.0
* Finally fixes the annoying padding issue in replies
* Adds nice preview blocks in settings with configurable hsv bars for all your previewing needs
* Tweaked constrast ratio a bit which may improve some colours' legibility
* Added transparency option, alongside "unchanged" colour option which pairs nicely together for a translucent glass effect
# 1.1.1
* Revert incorrect spacing fix, since it just breaks replies. Proper fix soon

View file

@ -1,117 +0,0 @@
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
}
}

View file

@ -2,32 +2,28 @@ package moe.lava.awoocord.zinnia
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.view.View
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.graphics.ColorUtils
import com.aliucord.annotations.AliucordPlugin
import com.aliucord.entities.Plugin
import com.aliucord.patcher.after
import com.aliucord.patcher.component1
import com.aliucord.patcher.component2
import com.aliucord.patcher.component3
import com.aliucord.patcher.instead
import com.aliucord.patcher.*
import com.aliucord.utils.DimenUtils.dp
import com.aliucord.utils.ViewUtils.findViewById
import com.aliucord.utils.accessField
import com.discord.databinding.WidgetChannelMembersListItemUserBinding
import com.discord.models.member.GuildMember
import com.discord.stores.StoreStream
import com.discord.widgets.channels.memberlist.adapter.ChannelMembersListAdapter
import com.discord.widgets.channels.memberlist.adapter.ChannelMembersListViewHolderMember
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemMessage
import com.discord.widgets.chat.list.entries.ChatListEntry
import com.discord.widgets.chat.list.entries.MessageEntry
import kotlin.math.abs
private val ChannelMembersListViewHolderMember.binding
by accessField<WidgetChannelMembersListItemUserBinding>()
private val WidgetChatListAdapterItemMessage.itemName
by accessField<TextView?>()
private val WidgetChatListAdapterItemMessage.replyName
by accessField<TextView?>()
data class Colours(
val fgP: Int,
@ -40,6 +36,8 @@ data class Colours(
class Zinnia : Plugin() {
companion object { const val NAME = "RoleBlocks" }
private val localSettings = ZinniaSettings
init {
settingsTab = SettingsTab(ZinniaSettings.Page::class.java, SettingsTab.Type.PAGE)
}
@ -51,6 +49,93 @@ class Zinnia : Plugin() {
override fun stop(context: Context) { patcher.unpatchAll() }
private fun configureOn(view: TextView, colour: Int?) {
when (localSettings.mode) {
Mode.Block -> configureBlock(view, colour ?: Color.BLACK)
Mode.RoleDot -> configureRoleDot(view, colour ?: Color.BLACK)
}
}
private fun configureRoleDot(view: TextView, colour: Int) { }
private fun configureBlock(view: TextView, colourP: Int) {
val isLight = StoreStream.getUserSettingsSystem().theme == "light"
var colour = colourP
val bcol = GradientDrawable()
bcol.cornerRadius = 4.dp.toFloat()
view.background = bcol
if (colour == Color.BLACK) {
if (localSettings.blockAlsoDefault) {
colour = if (isLight && !localSettings.blockInverted) Color.WHITE else Color.BLACK
} else {
view.background = null
view.setPadding(0, 0, 0, 0)
return
}
}
view.setPadding(4.dp, 0, 4.dp, 0)
var (preferred, other) = if (isLight) {
Color.WHITE to Color.BLACK
} else {
Color.BLACK to Color.WHITE
}
when (localSettings.blockMode) {
BlockMode.InvertedThemeOnly -> preferred = other
BlockMode.WhiteOnly -> preferred = Color.WHITE
BlockMode.BlackOnly -> preferred = Color.BLACK
else -> {}
}
val colours = if (!localSettings.blockInverted) {
Colours(
fgP = preferred,
fgO = other,
bgP = colour,
bgO = colour,
)
} else {
Colours(
fgP = colour,
fgO = colour,
bgP = preferred,
bgO = other,
)
}
val usePreferred = when (localSettings.blockMode) {
BlockMode.ApcaOnly -> isApca(colours)
BlockMode.WcagOnly -> isWcag(colours)
BlockMode.ApcaLightWcagDark -> if (isLight) isApca(colours) else isWcag(colours)
BlockMode.WcagLightApcaDark -> if (isLight) isWcag(colours) else isApca(colours)
BlockMode.ThemeOnly,
BlockMode.InvertedThemeOnly,
BlockMode.WhiteOnly,
BlockMode.BlackOnly -> true
}
if (usePreferred) {
view.setTextColor(colours.fgP)
bcol.setColor(colours.bgP)
} else {
view.setTextColor(colours.fgO)
bcol.setColor(colours.bgO)
}
}
private fun isApca(c: Colours): Boolean {
val cPref = abs(APCA.contrast(c.fgP, c.bgP))
val cOth = abs(APCA.contrast(c.fgO, c.bgO))
return cPref > localSettings.blockApcaThreshold || cPref > cOth
}
private fun isWcag(c: Colours): Boolean {
val cPref = ColorUtils.calculateContrast(c.fgP, c.bgP)
val cOth = ColorUtils.calculateContrast(c.fgO, c.bgO)
return cPref > localSettings.blockWcagThreshold || cPref > cOth
}
private fun patchMemberList() {
// Patches the method that configures the username in members list
patcher.after<ChannelMembersListViewHolderMember>(
@ -68,7 +153,7 @@ class Zinnia : Plugin() {
}
}
APCAUtil.configureOn(usernameTextView, member.color, Threshold.Medium)
configureOn(usernameTextView, member.color)
}
}
@ -79,28 +164,20 @@ class Zinnia : Plugin() {
Int::class.javaPrimitiveType!!,
ChatListEntry::class.java,
) { (_, _: Int, entry: MessageEntry) ->
itemName?.let {
APCAUtil.configureOn(it, entry.author?.color, Threshold.Large)
}
}
patcher.instead<WidgetChatListAdapterItemMessage>(
"getAuthorTextColor",
GuildMember::class.java,
) { (_, member: GuildMember?) ->
member?.color ?: Color.BLACK
val username = itemView.findViewById<TextView?>("chat_list_adapter_item_text_name")
?: return@after
configureOn(username, entry.author?.color)
}
// Configures for reply preview username
patcher.after<WidgetChatListAdapterItemMessage>(
"configureReplyName",
String::class.java,
Int::class.javaPrimitiveType!!,
Boolean::class.javaPrimitiveType!!,
) { (_, _: String, colour: Int) ->
replyName?.let {
APCAUtil.configureOn(it, colour, Threshold.Small)
}
"configureReplyPreview",
MessageEntry::class.java,
) { (_, entry: MessageEntry) ->
val referencedAuthor = entry.replyData?.messageEntry?.author
val replyUsername = itemView.findViewById<TextView?>("chat_list_adapter_item_text_decorator_reply_name")
?: return@after
configureOn(replyUsername, referencedAuthor?.color)
}
}
}

View file

@ -1,26 +1,13 @@
package moe.lava.awoocord.zinnia
import android.graphics.Color
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.LinearLayout
import android.widget.SeekBar
import android.widget.TextView
import androidx.core.content.res.ResourcesCompat
import com.aliucord.Constants
import com.aliucord.Utils
import com.aliucord.api.SettingsAPI
import com.aliucord.fragments.SettingsPage
import com.aliucord.settings.delegate
import com.aliucord.utils.DimenUtils.dp
import com.aliucord.wrappers.users.globalName
import com.discord.stores.StoreStream
import com.discord.utilities.color.ColorCompat
import com.discord.views.CheckedSetting
import com.lytefast.flexinput.R
import kotlin.math.roundToInt
import com.discord.views.RadioManager
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
@ -38,7 +25,6 @@ enum class BlockMode {
InvertedThemeOnly,
WhiteOnly,
BlackOnly,
Unchanged,
}
class SettingsDelegateEnum<T : Enum<T>>(
@ -63,59 +49,27 @@ private inline fun <T : View> T.addTo(parent: ViewGroup, block: T.() -> Unit = {
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 {
private val api = SettingsAPI(Zinnia.NAME)
private var onStateUpdate = {}
var mode by api.delegateEnum(Mode.Block)
private inline fun <T> reactive(backing: () -> Delegate<T>): StateDelegate<T> {
return StateDelegate(backing()) { onStateUpdate() }
}
var dotKeepNameColour by api.delegate(false)
var mode by reactive { api.delegateEnum(Mode.Block) }
var blockAlsoDefault by reactive { api.delegate(true) }
var blockInverted by reactive { api.delegate(false) }
var blockMode by reactive { api.delegateEnum(BlockMode.ApcaLightWcagDark) }
var blockApcaThresholdLarge by reactive { api.delegate(45.0f) }
var blockApcaThresholdMedium by reactive { api.delegate(45.0f) }
var blockApcaThresholdSmall by reactive { api.delegate(45.0f) }
var blockWcagThreshold by reactive { api.delegate(4.5f) }
private val _alpha = reactive { api.delegate("alpha", 255) }
var alpha by _alpha
var blockAlsoDefault by api.delegate(true)
var blockInverted by api.delegate(false)
var blockMode by api.delegateEnum(BlockMode.ApcaLightWcagDark)
var blockApcaThreshold by api.delegate(75.0)
var blockWcagThreshold by api.delegate(4.5)
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 _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 {
private fun createRadio(newMode: BlockMode, text: String, subtext: String? = null): CheckedSetting {
return Utils.createCheckedSetting(requireContext(), CheckedSetting.ViewType.RADIO, text, subtext).addTo(linearLayout) {
isChecked = blockMode == newMode
setOnCheckedListener {
@ -127,88 +81,6 @@ 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) {
super.onViewBound(view)
setActionBarTitle(Zinnia.NAME)
@ -220,20 +92,61 @@ object ZinniaSettings {
val roleDotSettings = mutableListOf<CheckedSetting>()
addHeader(ctx, "Text colour")
addRadio(BlockMode.ApcaLightWcagDark, "Automatic", "Adjusts text colour based on optimal contrast with role colour")
addRadio(BlockMode.ThemeOnly, "By theme", "Adjusts text colour based on system theme (dark/light)")
addRadio(BlockMode.InvertedThemeOnly, "By theme (inverted)", "Same as above, but inverted")
addRadio(BlockMode.WhiteOnly, "White", "Force text colour to be white")
addRadio(BlockMode.BlackOnly, "Black", "Force text colour to be black")
addRadio(BlockMode.Unchanged, "Unchanged", "Keep text colour; ideal for using with a translucent block")
createRadio(BlockMode.ApcaLightWcagDark, "Automatic", "Adjusts text colour based on role colour")
createRadio(BlockMode.ThemeOnly, "By theme", "Adjusts text colour based on theme")
createRadio(BlockMode.InvertedThemeOnly, "By theme (inverted)", "Same as above, but inverted")
createRadio(BlockMode.WhiteOnly, "White", "Force text colour to be white")
createRadio(BlockMode.BlackOnly, "Black", "Force text colour to be black")
/*
addHeader(ctx, "Mode")
mBlock = Utils.createCheckedSetting(
ctx,
CheckedSetting.ViewType.RADIO,
"Block mode",
"Wraps the username in a coloured block",
).addTo(this) {
isChecked = mode == Mode.Block
setOnCheckedListener {
mode = Mode.Block
mRoleDot.isChecked = false
}
}
mRoleDot = Utils.createCheckedSetting(
ctx,
CheckedSetting.ViewType.RADIO,
"Role dot mode",
"Adds a coloured role dot next to the username, similar to how Discord does it in their new accessibility settings",
).addTo(this) {
isChecked = mode == Mode.RoleDot
setOnCheckedListener {
mode = Mode.RoleDot
mBlock.isChecked = false
}
}
*/
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)
}
val invertSwitch = Utils.createCheckedSetting(
Utils.createCheckedSetting(
ctx,
CheckedSetting.ViewType.SWITCH,
"Invert block colours",
"By default, the role colour is applied as the block background. Turning this setting on inverts this.\nHas no effect with \"Unchanged\" colour option",
"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.",
).addTo(this) {
isChecked = blockInverted
setOnCheckedListener {
@ -241,52 +154,7 @@ object ZinniaSettings {
}
blockSettings.add(this)
}
addSlider(_alpha, 0, 255, true) { "Alpha: ${(it / 2.55f).roundToInt()}%" }
// createSlider(0, 255, blockApcaThreshold.roundToInt()) { value, commit ->
// blockApcaThreshold = value.toFloat()
// "Apca Threshold: $value"
// }
addHeader(ctx, "Preview")
val previews = mutableListOf(
Threshold.Large to createPreview("Message header username", R.i.UiKit_TextView_Large_SingleLine),
Threshold.Medium to createPreview("Channels list", R.i.UiKit_TextView).apply {
setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.d.uikit_textsize_medium))
},
Threshold.Small to createPreview("Message reply username", R.i.UiKit_TextView).apply {
setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.d.uikit_textsize_small))
},
)
val hsv = floatArrayOf(0f, 0f, 0f)
Color.colorToHSV(ColorCompat.getThemedColor(this, R.b.color_brand), hsv)
previewH = hsv[0].roundToInt()
previewS = (hsv[1] * 100).roundToInt()
previewV = (hsv[2] * 100).roundToInt()
addSlider(_previewH, 0, 360, true) { "Hue: $it" }
addSlider(_previewS, 0, 100, true) { "Saturation: $it%" }
addSlider(_previewV, 0, 100, true) { "Value: $it%" }
onStateUpdate = {
previews.forEach { updatePreview(it) }
if (blockMode != BlockMode.Unchanged) {
invertSwitch.l.b().isClickable = true
invertSwitch.alpha = 1f
} else {
invertSwitch.l.b().isClickable = false
invertSwitch.alpha = 0.3f
}
}
onStateUpdate()
}
}
fun updatePreview(pair: Pair<Threshold, TextView>) {
val (threshold, preview) = pair
val colour = Color.HSVToColor(floatArrayOf(previewH.toFloat(), previewS / 100f, previewV / 100f))
APCAUtil.configureOn(preview, colour, threshold)
}
}
}

View file

@ -25,10 +25,8 @@ val plugins = mapOf(
"ComponentsV2Beta" to "canary/ComponentsV2",
"SlashCommandsFixBeta" to "canary/SlashCommandsFix",
"Bubbles" to "plugins/Crocosmia",
"Clump" to "plugins/Bocchi",
"Scout" to "plugins/Scout",
"RoleBlocks" to "plugins/Zinnia",
"Glance" to "plugins/Myosotis",
)
include(*plugins.keys.toTypedArray())