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 new file mode 100644 index 0000000..7b1d785 --- /dev/null +++ b/plugins/Zinnia/src/main/kotlin/moe/lava/awoocord/zinnia/APCAUtil.kt @@ -0,0 +1,105 @@ +package moe.lava.awoocord.zinnia + +import android.graphics.Color +import android.graphics.drawable.GradientDrawable +import android.widget.TextView +import androidx.core.graphics.ColorUtils +import com.aliucord.utils.DimenUtils.dp +import com.discord.stores.StoreStream +import kotlin.math.abs + +internal object APCAUtil { + private val settings = ZinniaSettings + + internal fun configureOn(view: TextView, colour: Int?) { + when (settings.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 (settings.blockAlsoDefault) { + colour = if (isLight && !settings.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) + + var (preferred, other) = if (isLight) { + Color.WHITE to Color.BLACK + } else { + Color.BLACK to Color.WHITE + } + when (settings.blockMode) { + BlockMode.InvertedThemeOnly -> preferred = other + BlockMode.WhiteOnly -> preferred = Color.WHITE + BlockMode.BlackOnly -> preferred = Color.BLACK + BlockMode.Unchanged -> preferred = colourP + else -> {} + } + + val colours = if (!settings.blockInverted) { + Colours( + fgP = preferred, + fgO = other, + bgP = colour, + bgO = colour, + ) + } else { + Colours( + fgP = colour, + fgO = colour, + bgP = preferred, + bgO = other, + ) + } + + val usePreferred = when (settings.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) + BlockMode.ThemeOnly, + BlockMode.InvertedThemeOnly, + BlockMode.WhiteOnly, + BlockMode.BlackOnly, + BlockMode.Unchanged -> true + } + + if (usePreferred) { + view.setTextColor(colours.fgP) + bcol.setColor(ColorUtils.setAlphaComponent(colours.bgP, settings.alpha)) + bcol.alpha = settings.alpha + } else { + view.setTextColor(colours.fgO) + bcol.setColor(ColorUtils.setAlphaComponent(colours.bgO, settings.alpha)) + bcol.alpha = settings.alpha + } + } + + 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 > settings.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 > 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 8aefe72..f837bf2 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 @@ -1,26 +1,24 @@ 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.patcher.after +import com.aliucord.patcher.component1 +import com.aliucord.patcher.component2 +import com.aliucord.patcher.component3 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() @@ -36,8 +34,6 @@ data class Colours( class Zinnia : Plugin() { companion object { const val NAME = "RoleBlocks" } - private val localSettings = ZinniaSettings - init { settingsTab = SettingsTab(ZinniaSettings.Page::class.java, SettingsTab.Type.PAGE) } @@ -49,93 +45,6 @@ class Zinnia : Plugin() { 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) - - var (preferred, other) = if (isLight) { - Color.WHITE to Color.BLACK - } else { - Color.BLACK to Color.WHITE - } - when (localSettings.blockMode) { - BlockMode.InvertedThemeOnly -> preferred = other - BlockMode.WhiteOnly -> preferred = Color.WHITE - BlockMode.BlackOnly -> preferred = Color.BLACK - else -> {} - } - - 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) - BlockMode.ThemeOnly, - BlockMode.InvertedThemeOnly, - BlockMode.WhiteOnly, - BlockMode.BlackOnly -> true - } - - 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( @@ -153,7 +62,7 @@ class Zinnia : Plugin() { } } - configureOn(usernameTextView, member.color) + APCAUtil.configureOn(usernameTextView, member.color) } } @@ -166,7 +75,7 @@ class Zinnia : Plugin() { ) { (_, _: Int, entry: MessageEntry) -> val username = itemView.findViewById("chat_list_adapter_item_text_name") ?: return@after - configureOn(username, entry.author?.color) + APCAUtil.configureOn(username, entry.author?.color) } // Configures for reply preview username @@ -177,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 - configureOn(replyUsername, referencedAuthor?.color) + APCAUtil.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 index 9d9055d..461878e 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,13 +1,26 @@ package moe.lava.awoocord.zinnia +import android.graphics.Color import android.view.View import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.LinearLayout +import android.widget.SeekBar +import android.widget.TextView +import androidx.core.content.res.ResourcesCompat +import com.aliucord.Constants import com.aliucord.Utils import com.aliucord.api.SettingsAPI import com.aliucord.fragments.SettingsPage import com.aliucord.settings.delegate +import com.aliucord.utils.DimenUtils.dp +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 import kotlin.reflect.KProperty @@ -25,6 +38,7 @@ enum class BlockMode { InvertedThemeOnly, WhiteOnly, BlackOnly, + Unchanged, } class SettingsDelegateEnum>( @@ -49,18 +63,47 @@ private inline fun T.addTo(parent: ViewGroup, block: T.() -> Unit = { parent.addView(this) } +private typealias Delegate = ReadWriteProperty + +fun basicDelegate(initial: T) = object : Delegate { + private var current = initial + override fun getValue(self: Any?, prop: KProperty<*>): T = current + override fun setValue(self: Any?, prop: KProperty<*>, value: T) { current = value } +} + +private class StateDelegate( + private val inner: Delegate, + private val update: (T) -> Unit, +) : Delegate { + override fun getValue(self: Any?, prop: KProperty<*>): T = inner.getValue(self, prop) + + override fun setValue(self: Any?, prop: KProperty<*>, value: T) { + inner.setValue(self, prop, value) + update(value) + } +} + object ZinniaSettings { private val api = SettingsAPI(Zinnia.NAME) - var mode by api.delegateEnum(Mode.Block) + private var onStateUpdate = {} - var dotKeepNameColour by api.delegate(false) + private inline fun reactive(backing: () -> Delegate): StateDelegate { + return StateDelegate(backing()) { onStateUpdate() } + } - 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) + 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 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 @@ -69,6 +112,13 @@ object ZinniaSettings { private val checks = mutableListOf() + private val _previewH = reactive { basicDelegate(0) } + private var previewH by _previewH + private val _previewS = reactive { basicDelegate(100) } + private var previewS by _previewS + private val _previewV = reactive { basicDelegate(100) } + private var previewV by _previewV + private fun createRadio(newMode: BlockMode, text: String, subtext: String? = null): CheckedSetting { return Utils.createCheckedSetting(requireContext(), CheckedSetting.ViewType.RADIO, text, subtext).addTo(linearLayout) { isChecked = blockMode == newMode @@ -81,6 +131,60 @@ object ZinniaSettings { } } + private fun createSlider( + min: Int, + max: Int, + initial: Int = min, + onChange: (value: Int, commit: Boolean) -> String + ): LinearLayout { + 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 + } + } + SeekBar(context, null, 0, R.i.UiKit_SeekBar).addTo(this) { + this.max = max - min + progress = initial + setPadding(12.dp, 0, 12.dp, 0) + setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged( + seekBar: SeekBar, + progress: Int, + fromUser: Boolean, + ) { + pendingValue = min + progress + display.text = onChange(pendingValue, false) + } + + override fun onStartTrackingTouch(seekBar: SeekBar) {} + override fun onStopTrackingTouch(seekBar: SeekBar) { + onChange(pendingValue, true) + } + }) + } + } + } + + private fun createSlider(binding: Delegate, min: Int, max: Int, immediate: Boolean = false, label: (Int) -> String): LinearLayout { + var value by binding + return createSlider(min, max, value) { newValue, commit -> + @Suppress("AssignedValueIsNeverRead") // kt so dumb + if (immediate || commit) value = newValue + label(newValue) + } + } + + override fun onDestroyView() { + onStateUpdate = {} + super.onDestroyView() + } + override fun onViewBound(view: View) { super.onViewBound(view) setActionBarTitle(Zinnia.NAME) @@ -92,11 +196,12 @@ object ZinniaSettings { val roleDotSettings = mutableListOf() addHeader(ctx, "Text colour") - createRadio(BlockMode.ApcaLightWcagDark, "Automatic", "Adjusts text colour based on role colour") - createRadio(BlockMode.ThemeOnly, "By theme", "Adjusts text colour based on theme") + 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") @@ -129,24 +234,12 @@ object ZinniaSettings { */ 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( + val invertSwitch = 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.", + "By default, the role colour is applied as the block background. Turning this setting on inverts this.\nHas no effect with \"Unchanged\" colour option", ).addTo(this) { isChecked = blockInverted setOnCheckedListener { @@ -154,7 +247,50 @@ object ZinniaSettings { } blockSettings.add(this) } + + createSlider(_alpha, 0, 255, true) { "Alpha: ${(it / 2.55f).roundToInt()}%" } + +// createSlider(0, 255, blockApcaThreshold.roundToInt()) { value, commit -> +// blockApcaThreshold = value.toFloat() +// "Apca Threshold: $value" +// } + + 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 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%" } + + onStateUpdate = { + updatePreview(preview) + if (blockMode != BlockMode.Unchanged) { + invertSwitch.l.b().isClickable = true + invertSwitch.alpha = 1f + } else { + invertSwitch.l.b().isClickable = false + invertSwitch.alpha = 0.3f + } + } + onStateUpdate() } } + + fun updatePreview(preview: TextView) { + val colour = Color.HSVToColor(floatArrayOf(previewH.toFloat(), previewS / 100f, previewV / 100f)) + APCAUtil.configureOn(preview, colour) + } } }