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. -->
+
+