feat(Zinnia): add configurable thresholds and previews for each size

Currently thresholds are unused, one day they should be hooked up to
some formula based on real device pixels
This commit is contained in:
Cilly Leang 2026-02-17 17:12:36 +11:00
parent e0b86e0fb4
commit 4fb5486a39
Signed by: cilly
GPG key ID: 6500251E087653C9
3 changed files with 89 additions and 83 deletions

View file

@ -8,19 +8,25 @@ 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?) {
internal fun configureOn(view: TextView, colour: Int?, threshold: Threshold) {
when (settings.mode) {
Mode.Block -> configureBlock(view, colour ?: Color.BLACK)
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) {
private fun configureBlock(view: TextView, colourP: Int, threshold: Threshold) {
val isLight = StoreStream.getUserSettingsSystem().theme == "light"
var colour = colourP
val bcol = GradientDrawable()
@ -68,10 +74,10 @@ internal object APCAUtil {
}
val usePreferred = when (settings.blockMode) {
BlockMode.ApcaOnly -> isApca(colours)
BlockMode.ApcaOnly -> isApca(colours, threshold)
BlockMode.WcagOnly -> isWcag(colours)
BlockMode.ApcaLightWcagDark -> if (isLight) isApca(colours) else isWcag(colours)
BlockMode.WcagLightApcaDark -> if (isLight) isWcag(colours) else isApca(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,
@ -90,10 +96,15 @@ internal object APCAUtil {
}
}
private fun isApca(c: Colours): Boolean {
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))
return cPref > settings.blockApcaThreshold || cPref > cOth
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 {
@ -101,5 +112,4 @@ internal object APCAUtil {
val cOth = ColorUtils.calculateContrast(c.fgO, c.bgO)
return cPref > settings.blockWcagThreshold || cPref > cOth
}
}

View file

@ -62,7 +62,7 @@ class Zinnia : Plugin() {
}
}
APCAUtil.configureOn(usernameTextView, member.color)
APCAUtil.configureOn(usernameTextView, member.color, Threshold.Medium)
}
}
@ -75,7 +75,7 @@ class Zinnia : Plugin() {
) { (_, _: Int, entry: MessageEntry) ->
val username = itemView.findViewById<TextView?>("chat_list_adapter_item_text_name")
?: return@after
APCAUtil.configureOn(username, entry.author?.color)
APCAUtil.configureOn(username, entry.author?.color, Threshold.Large)
}
// Configures for reply preview username
@ -86,7 +86,7 @@ class Zinnia : Plugin() {
val referencedAuthor = entry.replyData?.messageEntry?.author
val replyUsername = itemView.findViewById<TextView?>("chat_list_adapter_item_text_decorator_reply_name")
?: return@after
APCAUtil.configureOn(replyUsername, referencedAuthor?.color)
APCAUtil.configureOn(replyUsername, referencedAuthor?.color, Threshold.Small)
}
}
}

View file

@ -1,6 +1,7 @@
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
@ -18,7 +19,6 @@ import com.aliucord.wrappers.users.globalName
import com.discord.stores.StoreStream
import com.discord.utilities.color.ColorCompat
import com.discord.views.CheckedSetting
import com.discord.views.RadioManager
import com.lytefast.flexinput.R
import kotlin.math.roundToInt
import kotlin.properties.ReadWriteProperty
@ -94,22 +94,18 @@ object ZinniaSettings {
var mode by reactive { api.delegateEnum(Mode.Block) }
var dotKeepNameColour by reactive { api.delegate(false) }
var blockAlsoDefault by reactive { api.delegate(true) }
var blockInverted by reactive { api.delegate(false) }
var blockMode by reactive { api.delegateEnum(BlockMode.ApcaLightWcagDark) }
var blockApcaThreshold by reactive { api.delegate(45.0f) }
var blockApcaThresholdLarge by reactive { api.delegate(45.0f) }
var blockApcaThresholdMedium by reactive { api.delegate(45.0f) }
var blockApcaThresholdSmall by reactive { api.delegate(45.0f) }
var blockWcagThreshold by reactive { api.delegate(4.5f) }
private val _alpha = reactive { api.delegate("alpha", 255) }
var alpha by _alpha
class Page : SettingsPage() {
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) }
@ -119,7 +115,7 @@ object ZinniaSettings {
private val _previewV = reactive { basicDelegate(100) }
private var previewV by _previewV
private fun createRadio(newMode: BlockMode, text: String, subtext: String? = null): CheckedSetting {
private fun addRadio(newMode: BlockMode, text: String, subtext: String? = null): CheckedSetting {
return Utils.createCheckedSetting(requireContext(), CheckedSetting.ViewType.RADIO, text, subtext).addTo(linearLayout) {
isChecked = blockMode == newMode
setOnCheckedListener {
@ -131,7 +127,18 @@ object ZinniaSettings {
}
}
private fun createSlider(
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,
@ -140,14 +147,7 @@ object ZinniaSettings {
var pendingValue = initial
return LinearLayout(requireContext(), null, 0, R.i.UiKit_Settings_Item).addTo(linearLayout) {
orientation = LinearLayout.VERTICAL
val display = TextView(context, null, 0, R.i.UiKit_TextView).addTo(this) {
textSize = 16.0f
typeface = ResourcesCompat.getFont(context, Constants.Fonts.whitney_medium)
text = onChange(initial, false)
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
bottomMargin = 4.dp
}
}
val display = createLabel(onChange(initial, false)).addTo(this)
SeekBar(context, null, 0, R.i.UiKit_SeekBar).addTo(this) {
this.max = max - min
progress = initial
@ -171,15 +171,39 @@ object ZinniaSettings {
}
}
private fun createSlider(binding: Delegate<Int>, min: Int, max: Int, immediate: Boolean = false, label: (Int) -> String): LinearLayout {
private fun addSlider(binding: Delegate<Int>, min: Int, max: Int, immediate: Boolean = false, label: (Int) -> String): LinearLayout {
var value by binding
return createSlider(min, max, value) { newValue, commit ->
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()
@ -196,42 +220,12 @@ object ZinniaSettings {
val roleDotSettings = mutableListOf<CheckedSetting>()
addHeader(ctx, "Text colour")
createRadio(BlockMode.ApcaLightWcagDark, "Automatic", "Adjusts text colour based on optimal contrast with role colour")
createRadio(BlockMode.ThemeOnly, "By theme", "Adjusts text colour based on system theme (dark/light)")
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")
createRadio(BlockMode.Unchanged, "Unchanged", "Keep text colour; ideal for using with a translucent block")
/*
addHeader(ctx, "Mode")
mBlock = Utils.createCheckedSetting(
ctx,
CheckedSetting.ViewType.RADIO,
"Block mode",
"Wraps the username in a coloured block",
).addTo(this) {
isChecked = mode == Mode.Block
setOnCheckedListener {
mode = Mode.Block
mRoleDot.isChecked = false
}
}
mRoleDot = Utils.createCheckedSetting(
ctx,
CheckedSetting.ViewType.RADIO,
"Role dot mode",
"Adds a coloured role dot next to the username, similar to how Discord does it in their new accessibility settings",
).addTo(this) {
isChecked = mode == Mode.RoleDot
setOnCheckedListener {
mode = Mode.RoleDot
mBlock.isChecked = false
}
}
*/
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")
addHeader(ctx, "Block Settings")
@ -248,7 +242,7 @@ object ZinniaSettings {
blockSettings.add(this)
}
createSlider(_alpha, 0, 255, true) { "Alpha: ${(it / 2.55f).roundToInt()}%" }
addSlider(_alpha, 0, 255, true) { "Alpha: ${(it / 2.55f).roundToInt()}%" }
// createSlider(0, 255, blockApcaThreshold.roundToInt()) { value, commit ->
// blockApcaThreshold = value.toFloat()
@ -256,26 +250,27 @@ object ZinniaSettings {
// }
addHeader(ctx, "Preview")
val preview = TextView(ctx, null, 0, R.i.UiKit_TextView_Large_SingleLine).addTo(this) {
val me = StoreStream.getUsers().me
text = me.globalName ?: me.username
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
marginStart = 16.dp
marginEnd = 16.dp
}
}
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()
createSlider(_previewH, 0, 360, true) { "Hue: $it" }
createSlider(_previewS, 0, 100, true) { "Saturation: $it%" }
createSlider(_previewV, 0, 100, true) { "Value: $it%" }
addSlider(_previewH, 0, 360, true) { "Hue: $it" }
addSlider(_previewS, 0, 100, true) { "Saturation: $it%" }
addSlider(_previewV, 0, 100, true) { "Value: $it%" }
onStateUpdate = {
updatePreview(preview)
previews.forEach { updatePreview(it) }
if (blockMode != BlockMode.Unchanged) {
invertSwitch.l.b().isClickable = true
invertSwitch.alpha = 1f
@ -288,9 +283,10 @@ object ZinniaSettings {
}
}
fun updatePreview(preview: TextView) {
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)
APCAUtil.configureOn(preview, colour, threshold)
}
}
}