feat(Zinnia): init

This commit is contained in:
Cilly Leang 2025-10-08 01:11:59 +11:00
parent fea05eff78
commit 59d18d76c0
Signed by: cilly
GPG key ID: 6500251E087653C9
6 changed files with 409 additions and 11 deletions

View file

@ -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)
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="moe.lava.awoocord.zinnia" />

View file

@ -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
}
}

View file

@ -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<WidgetChannelMembersListItemUserBinding>()
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<ChannelMembersListViewHolderMember>(
"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<WidgetChatListAdapterItemMessage>(
"onConfigure",
Int::class.javaPrimitiveType!!,
ChatListEntry::class.java,
) { (_, _: Int, entry: MessageEntry) ->
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>(
"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

@ -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<T : Enum<T>>(
private val defaultValue: T,
private val settings: SettingsAPI,
private val deserialiser: (String) -> T,
) : ReadWriteProperty<Any, T> {
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 <reified T : Enum<T>> SettingsAPI.delegateEnum(
defaultValue: T
) = SettingsDelegateEnum(defaultValue, this) { enumValueOf<T>(it) }
private inline fun <T : View> 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<CheckedSetting>()
val roleDotSettings = mutableListOf<CheckedSetting>()
/*
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)
}
}
}
}
}