diff --git a/plugins/Zinnia/build.gradle.kts b/plugins/Zinnia/build.gradle.kts
new file mode 100644
index 0000000..5ee7f56
--- /dev/null
+++ b/plugins/Zinnia/build.gradle.kts
@@ -0,0 +1,12 @@
+version = "1.0.0"
+description = "Coloured usernames to be a bit more pleasing on the eyes"
+
+aliucord {
+ // Changelog of your plugin
+ changelog.set("""
+ # 1.0.0
+ * Initial release >w<
+ """.trimIndent())
+
+ excludeFromUpdaterJson.set(false)
+}
diff --git a/plugins/Zinnia/src/main/AndroidManifest.xml b/plugins/Zinnia/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ce4638a
--- /dev/null
+++ b/plugins/Zinnia/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/APCA.kt b/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/APCA.kt
new file mode 100644
index 0000000..e098626
--- /dev/null
+++ b/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/APCA.kt
@@ -0,0 +1,77 @@
+package moe.lava.awoocord.zinnia
+
+import kotlin.math.abs
+import kotlin.math.pow
+
+// https://github.com/Myndex/apca-w3/blob/c012257167d822f91bc417120bdb82e1b854b4a4/src/apca-w3.js
+object APCA {
+ @Suppress("ConstPropertyName")
+ private object SA98G {
+ const val mainTRC = 2.4
+
+ const val sRco = 0.2126729
+ const val sGco = 0.7151522
+ const val sBco = 0.0721750
+
+ const val normBG = 0.56
+ const val normTXT = 0.57
+ const val revTXT = 0.62
+ const val revBG = 0.65
+
+ const val blkThrs = 0.022
+ const val blkClmp = 1.414
+ const val scaleBoW = 1.14
+ const val scaleWoB = 1.14
+ const val loBoWoffset = 0.027
+ const val loWoBoffset = 0.027
+ const val deltaYmin = 0.0005
+ const val loClip = 0.1
+ }
+
+ private fun exp(c: Int) =
+ (c.toDouble() / 255.0).pow(SA98G.mainTRC)
+
+ private fun argbToY(color: Int): Double {
+ val r = (color shr 16) and 0xff
+ val g = (color shr 8) and 0xff
+ val b = color and 0xff
+
+ return SA98G.run {
+ sRco * exp(r) + sGco * exp(g) + sBco * exp(b)
+ }
+ }
+
+ fun contrast(fgC: Int, bgC: Int): Double {
+ var fg = argbToY(fgC)
+ var bg = argbToY(bgC)
+
+ if (fg.coerceAtMost(bg) < 0 || fg.coerceAtLeast(bg) > 1.1)
+ return 0.0
+
+ if (fg <= SA98G.blkThrs)
+ fg += (SA98G.blkThrs - fg).pow(SA98G.blkClmp)
+ if (bg <= SA98G.blkThrs)
+ bg += (SA98G.blkThrs - bg).pow(SA98G.blkClmp)
+
+ if (abs(bg - fg) < SA98G.deltaYmin)
+ return 0.0
+
+ val outputContrast = if (bg > fg) {
+ val sapc = (bg.pow(SA98G.normBG) - fg.pow(SA98G.normTXT)) * SA98G.scaleBoW
+
+ if (sapc < SA98G.loClip)
+ 0.0
+ else
+ sapc - SA98G.loBoWoffset
+ } else {
+ val sapc = (bg.pow(SA98G.revBG) - fg.pow(SA98G.revTXT)) * SA98G.scaleWoB
+
+ if (sapc > -SA98G.loClip)
+ 0.0
+ else
+ sapc + SA98G.loWoBoffset
+ }
+
+ return outputContrast * 100
+ }
+}
diff --git a/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/Zinnia.kt b/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/Zinnia.kt
new file mode 100644
index 0000000..6b0f115
--- /dev/null
+++ b/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/Zinnia.kt
@@ -0,0 +1,173 @@
+package moe.lava.awoocord.zinnia
+
+import android.content.Context
+import android.graphics.Color
+import android.graphics.drawable.GradientDrawable
+import android.view.View
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.graphics.ColorUtils
+import com.aliucord.annotations.AliucordPlugin
+import com.aliucord.entities.Plugin
+import com.aliucord.patcher.*
+import com.aliucord.utils.DimenUtils.dp
+import com.aliucord.utils.ViewUtils.findViewById
+import com.aliucord.utils.accessField
+import com.discord.databinding.WidgetChannelMembersListItemUserBinding
+import com.discord.stores.StoreStream
+import com.discord.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()
+
+data class Colours(
+ val fgP: Int,
+ val bgP: Int,
+ val fgO: Int,
+ val bgO: Int,
+)
+
+@AliucordPlugin
+class Zinnia : Plugin() {
+ companion object { const val NAME = "RoleBlocks" }
+
+ private val localSettings = ZinniaSettings
+
+ init {
+ settingsTab = SettingsTab(ZinniaSettings.Page::class.java, SettingsTab.Type.PAGE)
+ }
+
+ override fun start(context: Context) {
+ patchMemberList()
+ patchMessageAuthor()
+ }
+
+ 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)
+
+ val (preferred, other) = if (isLight) {
+ Color.WHITE to Color.BLACK
+ } else {
+ Color.BLACK to Color.WHITE
+ }
+
+ 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)
+ }
+
+ 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(
+ "bind",
+ ChannelMembersListAdapter.Item.Member::class.java,
+ Function0::class.java,
+ ) { (_, member: ChannelMembersListAdapter.Item.Member) ->
+ val presenceTextView = binding.d
+ val usernameView = binding.f
+ val usernameTextView = usernameView.j.c
+
+ if (presenceTextView.visibility == View.VISIBLE) {
+ usernameView.layoutParams = (usernameView.layoutParams as ConstraintLayout.LayoutParams).apply {
+ bottomMargin = 2.dp
+ }
+ }
+
+ configureOn(usernameTextView, member.color)
+ }
+ }
+
+ private fun patchMessageAuthor() {
+ // Configures for message author username
+ patcher.after(
+ "onConfigure",
+ Int::class.javaPrimitiveType!!,
+ ChatListEntry::class.java,
+ ) { (_, _: Int, entry: MessageEntry) ->
+ val username = itemView.findViewById("chat_list_adapter_item_text_name")
+ ?: return@after
+ configureOn(username, entry.author?.color)
+ }
+
+ // Configures for reply preview username
+ patcher.after(
+ "configureReplyPreview",
+ MessageEntry::class.java,
+ ) { (_, entry: MessageEntry) ->
+ val referencedAuthor = entry.replyData?.messageEntry?.author
+ val replyUsername = itemView.findViewById("chat_list_adapter_item_text_decorator_reply_name")
+ ?: return@after
+ configureOn(replyUsername, referencedAuthor?.color)
+ }
+ }
+}
diff --git a/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/ZinniaSettings.kt b/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/ZinniaSettings.kt
new file mode 100644
index 0000000..3d44b19
--- /dev/null
+++ b/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/ZinniaSettings.kt
@@ -0,0 +1,134 @@
+package moe.lava.awoocord.zinnia
+
+import android.view.View
+import android.view.ViewGroup
+import com.aliucord.Utils
+import com.aliucord.api.SettingsAPI
+import com.aliucord.fragments.SettingsPage
+import com.aliucord.settings.delegate
+import com.discord.views.CheckedSetting
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+
+enum class Mode {
+ RoleDot,
+ Block,
+}
+
+enum class BlockMode {
+ ApcaLightWcagDark,
+ WcagLightApcaDark,
+ ApcaOnly,
+ WcagOnly,
+}
+
+class SettingsDelegateEnum>(
+ private val defaultValue: T,
+ private val settings: SettingsAPI,
+ private val deserialiser: (String) -> T,
+) : ReadWriteProperty {
+ override fun getValue(thisRef: Any, property: KProperty<*>): T =
+ deserialiser(settings.getString(property.name, defaultValue.name))
+
+ override fun setValue(thisRef: Any, property: KProperty<*>, value: T) =
+ settings.setString(property.name, value.name)
+}
+
+inline fun > SettingsAPI.delegateEnum(
+ defaultValue: T
+) = SettingsDelegateEnum(defaultValue, this) { enumValueOf(it) }
+
+private inline fun T.addTo(parent: ViewGroup, block: T.() -> Unit = {}) =
+ apply {
+ block()
+ parent.addView(this)
+ }
+
+object ZinniaSettings {
+ private val api = SettingsAPI(Zinnia.NAME)
+
+ var mode by api.delegateEnum(Mode.Block)
+
+ var dotKeepNameColour by api.delegate(false)
+
+ 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)
+
+ @Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS")
+ class Page : SettingsPage() {
+ private lateinit var mRoleDot: CheckedSetting
+ private lateinit var mBlock: CheckedSetting
+
+ override fun onViewBound(view: View) {
+ super.onViewBound(view)
+ setActionBarTitle(Zinnia.NAME)
+ setPadding(0)
+
+ val ctx = requireContext()
+ linearLayout.run {
+ val blockSettings = mutableListOf()
+ val roleDotSettings = mutableListOf()
+
+ /*
+ 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)
+ }
+
+ Utils.createCheckedSetting(
+ ctx,
+ CheckedSetting.ViewType.SWITCH,
+ "Invert block colours",
+ "By default, the role colour is applied as the block background. Turning this setting on instead makes the block black or white, and the text stays coloured.",
+ ).addTo(this) {
+ isChecked = blockInverted
+ setOnCheckedListener {
+ blockInverted = !blockInverted
+ }
+ blockSettings.add(this)
+ }
+ }
+ }
+ }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 739eadd..cbcb171 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,16 +1,16 @@
rootProject.name = "Awoocord"
-val canaryPlugins = arrayOf("ComponentsV2", "SlashCommandsFix")
-
-include(
- "Scout",
- *canaryPlugins,
+val plugins = mapOf(
+ "ComponentsV2Beta" to "canary/ComponentsV2",
+ "SlashCommandsFixBeta" to "canary/SlashCommandsFix",
+ "Scout" to "plugins/Scout",
+ "RoleBlocks" to "plugins/Zinnia",
)
-rootProject.children.forEach {
- val isCanary = it.name in canaryPlugins
- val dir = if (isCanary) "canary" else "plugins"
- val name = it.name
- if (isCanary) it.name += "Beta"
- it.projectDir = file("${dir}/${name}")
+include(*plugins.keys.toTypedArray())
+
+rootProject.children.forEach { project ->
+ plugins[project.name]?.let {
+ project.projectDir = file(it)
+ }
}