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/ui/RouteFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/RouteFragment.kt
index cf4e7aa6c..25720e9f1 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/RouteFragment.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/RouteFragment.kt
@@ -7,6 +7,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
+import io.nekohasekai.sagernet.ktx.getColorAttr
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
@@ -287,13 +288,14 @@ class RouteFragment : ToolbarFragment(R.layout.layout_route), Toolbar.OnMenuItem
routeOutbound.text = rule.displayOutbound()
// set text color based on route type
- val colorRes = when (rule.outbound) {
- -2L -> R.color.color_route_block // block: red
- -1L -> R.color.color_route_direct // direct: green
- 0L -> R.color.color_route_proxy // proxy: blue
- else -> R.color.color_route_config // config: purple
+ val ctx = itemView.context
+ val outboundColor = when (rule.outbound) {
+ -2L -> ContextCompat.getColor(ctx, R.color.color_route_block) // block: red
+ -1L -> ContextCompat.getColor(ctx, R.color.color_route_direct) // direct: green
+ 0L -> ctx.getColorAttr(R.attr.routeProxyColor) // proxy: blue/cyan
+ else -> ContextCompat.getColor(ctx, R.color.color_route_config) // config: purple
}
- routeOutbound.setTextColor(ContextCompat.getColor(itemView.context, colorRes))
+ routeOutbound.setTextColor(outboundColor)
itemView.setOnClickListener {
enableSwitch.performClick()
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..e8e0183f4 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. For
+ // transient states (and on non-Dracula themes) fall back to colorOnPrimary,
+ // which is the FAB's normal icon color, so those themes look unchanged.
+ val attr = when (state) {
+ BaseService.State.Connected -> R.attr.statusConnectedColor
+ BaseService.State.Stopped -> R.attr.fabStoppedColor
+ else -> com.google.android.material.R.attr.colorOnPrimary
+ }
+ imageTintList = ColorStateList.valueOf(context.getColorAttr(attr))
+ }
+
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..821670db2 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt
@@ -2,7 +2,9 @@ package io.nekohasekai.sagernet.widget
import android.annotation.SuppressLint
import android.content.Context
+import android.text.SpannableStringBuilder
import android.text.format.Formatter
+import android.text.style.ForegroundColorSpan
import android.util.AttributeSet
import android.view.View
import android.widget.TextView
@@ -80,6 +82,43 @@ class StatsBar @JvmOverloads constructor(
TooltipCompat.setTooltipText(this, text)
}
+ // Two-tone status: color the lead segment (split at [sep], kept with the lead)
+ // and the remainder separately. Used for "Connected, …" and "Success: …".
+ private fun setStatusTwoTone(
+ full: CharSequence, sep: Char, leadAttr: Int, restAttr: Int
+ ) {
+ val s = full.toString()
+ val idx = s.indexOf(sep)
+ if (idx < 0) {
+ statusText.setTextColor(context.getColorAttr(leadAttr))
+ setStatus(full)
+ return
+ }
+ val cut = idx + 1 // keep the separator char with the lead segment
+ val span = SpannableStringBuilder(s)
+ span.setSpan(
+ ForegroundColorSpan(context.getColorAttr(leadAttr)),
+ 0, cut, SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ span.setSpan(
+ ForegroundColorSpan(context.getColorAttr(restAttr)),
+ cut, s.length, SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ setStatus(span)
+ }
+
+ private fun setStatusColorByState(state: BaseService.State) {
+ // Connected = statusConnectedColor (green), Stopped/Stopping = statusStoppedColor
+ // (red — "Shutting down…" reads as red), Connecting/other = colorOnPrimary.
+ // Non-Dracula themes default these attrs to colorOnPrimary, so no change.
+ val attr = when (state) {
+ BaseService.State.Connected -> R.attr.statusConnectedColor
+ BaseService.State.Stopped, BaseService.State.Stopping -> R.attr.statusStoppedColor
+ else -> com.google.android.material.R.attr.colorOnPrimary
+ }
+ statusText.setTextColor(context.getColorAttr(attr))
+ }
+
fun changeState(state: BaseService.State) {
val activity = context as MainActivity
fun postWhenStarted(what: () -> Unit) = activity.lifecycleScope.launch(Dispatchers.Main) {
@@ -89,13 +128,18 @@ class StatsBar @JvmOverloads constructor(
if ((state == BaseService.State.Connected).also { hideOnScroll = it }) {
postWhenStarted {
if (allowShow) performShow()
- setStatus(app.getText(R.string.vpn_connected))
+ // "Connected," in green; the "tap to check connection" hint in detail color.
+ setStatusTwoTone(
+ app.getText(R.string.vpn_connected), ',',
+ R.attr.statusConnectedColor, R.attr.statusDetailColor
+ )
}
} else {
postWhenStarted {
performHide()
}
updateSpeed(0, 0)
+ setStatusColorByState(state)
setStatus(
context.getText(
when (state) {
@@ -110,6 +154,9 @@ class StatsBar @JvmOverloads constructor(
@SuppressLint("SetTextI18n")
fun updateSpeed(txRate: Long, rxRate: Long) {
+ val speedColor = context.getColorAttr(R.attr.speedTextColor)
+ txText.setTextColor(speedColor)
+ rxText.setTextColor(speedColor)
txText.text = "▲ ${
context.getString(
R.string.speed, Formatter.formatFileSize(context, txRate)
@@ -125,20 +172,24 @@ class StatsBar @JvmOverloads constructor(
fun testConnection() {
val activity = context as MainActivity
isEnabled = false
+ // "Testing…" in the testing color.
+ statusText.setTextColor(context.getColorAttr(R.attr.statusTestingColor))
setStatus(app.getText(R.string.connection_test_testing))
runOnDefaultDispatcher {
try {
val elapsed = activity.urlTest()
onMainDispatcher {
isEnabled = true
- setStatus(
+ // "Success:" in green; the handshake detail in detail color.
+ setStatusTwoTone(
app.getString(
if (DataStore.connectionTestURL.startsWith("https://")) {
R.string.connection_test_available
} else {
R.string.connection_test_available_http
}, elapsed
- )
+ ), ':',
+ R.attr.statusConnectedColor, R.attr.statusDetailColor
)
}
@@ -146,6 +197,7 @@ class StatsBar @JvmOverloads constructor(
Logs.w(e.toString())
onMainDispatcher {
isEnabled = true
+ statusText.setTextColor(context.getColorAttr(R.attr.statusTestingColor))
setStatus(app.getText(R.string.connection_test_testing))
activity.snackbar(
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/color/nav_item_fill.xml b/app/src/main/res/color/nav_item_fill.xml
index f23d44ec7..d3cdd7475 100644
--- a/app/src/main/res/color/nav_item_fill.xml
+++ b/app/src/main/res/color/nav_item_fill.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/app/src/main/res/color/navigation_icon.xml b/app/src/main/res/color/navigation_icon.xml
index 99c3f94f7..eff42755a 100644
--- a/app/src/main/res/color/navigation_icon.xml
+++ b/app/src/main/res/color/navigation_icon.xml
@@ -1,5 +1,5 @@
-
+
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index 9b3c36627..ff5dc33b4 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -26,6 +26,19 @@
- @color/dracula_background
- @color/dracula_on_surface
- @color/dracula_background
+
+ - @color/color_dracula_green
+ - @color/color_dracula_yellow
+ - @color/color_dracula_green
+ - @color/color_dracula_red
+ - @color/color_dracula_red
+ - @color/color_dracula_cyan
+ - @color/color_dracula_cyan
+ - @color/color_dracula_cyan
+ - @color/color_dracula_yellow
+ - @color/color_dracula_cyan
+ - ?attr/colorAccent
diff --git a/app/src/main/res/values-v26/themes.xml b/app/src/main/res/values-v26/themes.xml
index 6bce0da1c..6b1efb0c9 100644
--- a/app/src/main/res/values-v26/themes.xml
+++ b/app/src/main/res/values-v26/themes.xml
@@ -5,6 +5,18 @@
- ?colorPrimary
+
+ - ?attr/accentOrTextSecondary
+ - ?android:attr/textColorPrimary
+ - ?attr/colorOnPrimary
+ - ?attr/colorOnPrimary
+ - @color/material_red_500
+ - ?attr/colorOnPrimary
+ - ?attr/colorOnPrimary
+ - ?attr/colorOnPrimary
+ - ?attr/colorOnPrimary
+ - @color/color_route_proxy
+ - ?attr/colorPrimary
- @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..bda8ff5b5 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -10,6 +10,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 1fc3610ac..cad484329 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -300,6 +300,14 @@
#FF79C6
#E9DDFB
#9A7BD0
+
+ #50FA7B
+
+ #FF5555
+
+ #F1FA8C
+
+ #8BE9FD
#9E9E9E
#2B2B2B
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index e06ebb322..021a3934a 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -2,6 +2,20 @@