From 4fb5486a395fe87be788b8a88b806d45d875607b Mon Sep 17 00:00:00 2001 From: Cilly Leang Date: Tue, 17 Feb 2026 17:12:36 +1100 Subject: [PATCH] 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 --- .../moe/lava/awoocord/zinnia/APCAUtil.kt | 28 ++-- .../kotlin/moe/lava/awoocord/zinnia/Zinnia.kt | 6 +- .../lava/awoocord/zinnia/ZinniaSettings.kt | 138 +++++++++--------- 3 files changed, 89 insertions(+), 83 deletions(-) diff --git a/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/APCAUtil.kt b/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/APCAUtil.kt index 7b1d785..e96f9d3 100644 --- a/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/APCAUtil.kt +++ b/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/APCAUtil.kt @@ -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 } - } 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 index f837bf2..094f894 100644 --- a/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/Zinnia.kt +++ b/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/Zinnia.kt @@ -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("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("chat_list_adapter_item_text_decorator_reply_name") ?: return@after - APCAUtil.configureOn(replyUsername, referencedAuthor?.color) + APCAUtil.configureOn(replyUsername, referencedAuthor?.color, Threshold.Small) } } } 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 index 461878e..72ca84c 100644 --- a/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/ZinniaSettings.kt +++ b/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/ZinniaSettings.kt @@ -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() 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, min: Int, max: Int, immediate: Boolean = false, label: (Int) -> String): LinearLayout { + private fun addSlider(binding: Delegate, 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() 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) { + val (threshold, preview) = pair val colour = Color.HSVToColor(floatArrayOf(previewH.toFloat(), previewS / 100f, previewV / 100f)) - APCAUtil.configureOn(preview, colour) + APCAUtil.configureOn(preview, colour, threshold) } } }