feat(Zinnia): init
This commit is contained in:
parent
fea05eff78
commit
59d18d76c0
6 changed files with 409 additions and 11 deletions
12
plugins/Zinnia/build.gradle.kts
Normal file
12
plugins/Zinnia/build.gradle.kts
Normal 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)
|
||||
}
|
||||
2
plugins/Zinnia/src/main/AndroidManifest.xml
Normal file
2
plugins/Zinnia/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="moe.lava.awoocord.zinnia" />
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue