From 8efcd3910deb5e71e0d3a4258a496e695fbb611b Mon Sep 17 00:00:00 2001 From: hawkff <109485367+hawkff@users.noreply.github.com> Date: Fri, 19 Jun 2026 23:55:05 -0400 Subject: [PATCH 1/7] Color protocol labels green in Dracula theme Add a protocolColor theme attr for the profile-row protocol/type label. The base themes default it to ?attr/accentOrTextSecondary (preserving the current accent-colored behavior for all themes), and the Dracula day/night styles override it with Dracula green (#50fa7b). getProtocolColor() now reads R.attr.protocolColor instead of accentOrTextSecondary directly. Verified on device: Dracula, Black (deepest attr chain) and Pink_SSR all start with no crash and no attribute-resolution failures. --- app/src/main/java/moe/matsuri/nb4a/Protocols.kt | 2 +- app/src/main/res/values-v26/themes.xml | 2 ++ app/src/main/res/values/attrs.xml | 3 +++ app/src/main/res/values/colors.xml | 2 ++ app/src/main/res/values/themes.xml | 5 +++++ 5 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/moe/matsuri/nb4a/Protocols.kt b/app/src/main/java/moe/matsuri/nb4a/Protocols.kt index 28f826800..12d1dbdee 100644 --- a/app/src/main/java/moe/matsuri/nb4a/Protocols.kt +++ b/app/src/main/java/moe/matsuri/nb4a/Protocols.kt @@ -44,7 +44,7 @@ object Protocols { fun Context.getProtocolColor(type: Int): Int { return when (type) { TYPE_NEKO -> getColorAttr(android.R.attr.textColorPrimary) - else -> getColorAttr(R.attr.accentOrTextSecondary) + else -> getColorAttr(R.attr.protocolColor) } } diff --git a/app/src/main/res/values-v26/themes.xml b/app/src/main/res/values-v26/themes.xml index 6bce0da1c..96b4733f6 100644 --- a/app/src/main/res/values-v26/themes.xml +++ b/app/src/main/res/values-v26/themes.xml @@ -5,6 +5,8 @@ ?colorPrimary + + ?attr/accentOrTextSecondary @drawable/ic_navigation_close ?colorAccent @android:color/transparent diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 20cef29eb..073a0c59b 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -10,6 +10,9 @@ + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 1fc3610ac..c5370adae 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -300,6 +300,8 @@ #FF79C6 #E9DDFB #9A7BD0 + + #50FA7B #9E9E9E #2B2B2B diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index e06ebb322..ab90f9704 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -2,6 +2,8 @@ From ae4854d0c73db76cd18d18849f5e066e994db767 Mon Sep 17 00:00:00 2001 From: hawkff <109485367+hawkff@users.noreply.github.com> Date: Sat, 20 Jun 2026 01:15:02 -0400 Subject: [PATCH 3/7] Add Dracula semantic colors: yellow names, red fails, state-colored connect Extend the Dracula theme's semantic palette via theme attrs (default to the prior colors for all other themes, so they are unchanged): - profileNameColor: profile name label -> Dracula yellow (#f1fa8c) - testFailColor: connection-test failures -> Dracula red (#ff5555) - statusConnectedColor / statusStoppedColor: the bottom StatsBar status text and the connect-button (airplane) icon are tinted green when Connected and red when Stopped; transient Connecting/Stopping states stay neutral. All overrides are duplicated into values-night (Dracula's active path) as well as day. Verified in the compiled APK via aapt2 that the night Dracula style resolves profileNameColor=yellow, statusConnected=green, statusStopped/testFail=red. --- .../sagernet/ui/ConfigurationFragment.kt | 7 ++++--- .../nekohasekai/sagernet/widget/ServiceButton.kt | 15 +++++++++++++++ .../io/nekohasekai/sagernet/widget/StatsBar.kt | 13 +++++++++++++ app/src/main/res/values-night/themes.xml | 6 +++++- app/src/main/res/values-v26/themes.xml | 4 ++++ app/src/main/res/values/attrs.xml | 8 ++++++++ app/src/main/res/values/colors.xml | 4 ++++ app/src/main/res/values/themes.xml | 14 ++++++++++++++ 8 files changed, 67 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt index 332aff75e..1bff1dcc2 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt @@ -762,14 +762,14 @@ class ConfigurationFragment @JvmOverloads constructor( 2 -> { profileStatusText = profile.error - profileStatusColor = context.getColour(R.color.material_red_500) + profileStatusColor = context.getColorAttr(R.attr.testFailColor) } 3 -> { val err = profile.error ?: "" val msg = Protocols.genFriendlyMsg(err) profileStatusText = if (msg != err) msg else getString(R.string.unavailable) - profileStatusColor = context.getColour(R.color.material_red_500) + profileStatusColor = context.getColorAttr(R.attr.testFailColor) } } @@ -1803,6 +1803,7 @@ class ConfigurationFragment @JvmOverloads constructor( } profileName.text = proxyEntity.displayName() + profileName.setTextColor(requireContext().getColorAttr(R.attr.profileNameColor)) profileType.text = proxyEntity.displayType() profileType.setTextColor(requireContext().getProtocolColor(proxyEntity.type)) @@ -1854,7 +1855,7 @@ class ConfigurationFragment @JvmOverloads constructor( profileStatus.text = getString(R.string.available, proxyEntity.ping) profileStatus.setTextColor(requireContext().getColour(R.color.material_green_500)) } else { - profileStatus.setTextColor(requireContext().getColour(R.color.material_red_500)) + profileStatus.setTextColor(requireContext().getColorAttr(R.attr.testFailColor)) if (proxyEntity.status == 2) { profileStatus.text = proxyEntity.error } diff --git a/app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt b/app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt index b86659e60..a31f7bbe7 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt @@ -1,6 +1,7 @@ package io.nekohasekai.sagernet.widget import android.content.Context +import android.content.res.ColorStateList import android.graphics.drawable.Drawable import android.os.Build import android.util.AttributeSet @@ -17,6 +18,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.progressindicator.BaseProgressIndicator import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.bg.BaseService +import io.nekohasekai.sagernet.ktx.getColorAttr import kotlinx.coroutines.Job import kotlinx.coroutines.delay import java.util.* @@ -118,6 +120,7 @@ class ServiceButton @JvmOverloads constructor( } checked = state == BaseService.State.Connected refreshDrawableState() + applyStateTint(state) val description = context.getText(if (state.canStop) R.string.stop else R.string.connect) contentDescription = description TooltipCompat.setTooltipText(this, description) @@ -129,6 +132,18 @@ class ServiceButton @JvmOverloads constructor( ) } + private fun applyStateTint(state: BaseService.State) { + // Tint the connect FAB icon by state: connected=green, stopped=red, + // transient states neutral (cleared). Non-Dracula themes default these + // attrs to the primary text color, so there is no visible change. + val attr = when (state) { + BaseService.State.Connected -> R.attr.statusConnectedColor + BaseService.State.Stopped -> R.attr.statusStoppedColor + else -> null + } + imageTintList = attr?.let { ColorStateList.valueOf(context.getColorAttr(it)) } + } + private fun changeState(icon: AnimatedState, animate: Boolean) { fun counters(a: AnimatedState, b: AnimatedState): Boolean = a == iconStopped && b == iconConnecting || diff --git a/app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt b/app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt index 2116c237e..9853a57be 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt @@ -80,8 +80,21 @@ class StatsBar @JvmOverloads constructor( TooltipCompat.setTooltipText(this, text) } + private fun setStatusColorByState(state: BaseService.State) { + // Connected = statusConnectedColor, Stopped = statusStoppedColor, transient + // states stay neutral. For non-Dracula themes these attrs default to the + // primary text color, so there is no visible change. + val attr = when (state) { + BaseService.State.Connected -> R.attr.statusConnectedColor + BaseService.State.Stopped -> R.attr.statusStoppedColor + else -> android.R.attr.textColorPrimary + } + statusText.setTextColor(context.getColorAttr(attr)) + } + fun changeState(state: BaseService.State) { val activity = context as MainActivity + setStatusColorByState(state) fun postWhenStarted(what: () -> Unit) = activity.lifecycleScope.launch(Dispatchers.Main) { delay(100L) activity.whenStarted { what() } diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index db8d04655..a4805bd5c 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -27,8 +27,12 @@ @color/dracula_on_surface @color/dracula_background + semantic color overrides must live here too, not only day. --> @color/color_dracula_green + @color/color_dracula_yellow + @color/color_dracula_green + @color/color_dracula_red + @color/color_dracula_red diff --git a/app/src/main/res/values-v26/themes.xml b/app/src/main/res/values-v26/themes.xml index 4eae37e9f..4e66e7dad 100644 --- a/app/src/main/res/values-v26/themes.xml +++ b/app/src/main/res/values-v26/themes.xml @@ -8,8 +8,8 @@ ?attr/accentOrTextSecondary ?android:attr/textColorPrimary - ?android:attr/textColorPrimary - ?android:attr/textColorPrimary + ?attr/colorOnPrimary + ?attr/colorOnPrimary @color/material_red_500 @drawable/ic_navigation_close ?colorAccent diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 03a78d95b..05d27f806 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -7,8 +7,8 @@ ?android:attr/textColorPrimary - ?android:attr/textColorPrimary - ?android:attr/textColorPrimary + ?attr/colorOnPrimary + ?attr/colorOnPrimary @color/material_red_500 + + + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 67fb6f00b..cad484329 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -306,6 +306,8 @@ #FF5555 #F1FA8C + + #8BE9FD #9E9E9E #2B2B2B diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 05d27f806..8c01ecf00 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -10,6 +10,11 @@ ?attr/colorOnPrimary ?attr/colorOnPrimary @color/material_red_500 + ?attr/colorOnPrimary + ?attr/colorOnPrimary + ?attr/colorOnPrimary + @color/color_route_proxy + ?attr/colorPrimary + Default on-primary; Dracula = cyan. --> + +