Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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
}
Expand Down
14 changes: 8 additions & 6 deletions app/src/main/java/io/nekohasekai/sagernet/ui/RouteFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
15 changes: 15 additions & 0 deletions app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.*
Expand Down Expand Up @@ -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)
Expand All @@ -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))
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

private fun changeState(icon: AnimatedState, animate: Boolean) {
fun counters(a: AnimatedState, b: AnimatedState): Boolean =
a == iconStopped && b == iconConnecting ||
Expand Down
58 changes: 55 additions & 3 deletions app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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)
Expand All @@ -125,27 +172,32 @@ 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
)
}

} catch (e: Exception) {
Logs.w(e.toString())
onMainDispatcher {
isEnabled = true
statusText.setTextColor(context.getColorAttr(R.attr.statusTestingColor))
setStatus(app.getText(R.string.connection_test_testing))

activity.snackbar(
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/moe/matsuri/nb4a/Protocols.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/color/nav_item_fill.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorPrimary" android:alpha="0.16" android:state_checked="true" />
<item android:color="?attr/navSelectedColor" android:alpha="0.16" android:state_checked="true" />
<item android:color="@android:color/transparent" />
</selector>
4 changes: 2 additions & 2 deletions app/src/main/res/color/navigation_icon.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/primaryOrTextSecondary" android:state_checked="true" />
<item android:color="?attr/navSelectedColor" android:state_checked="true" />
<item android:color="?android:textColorPrimary" android:state_checked="false" />
</selector>
</selector>
23 changes: 23 additions & 0 deletions app/src/main/res/values-night/themes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@
<item name="android:colorBackground">@color/dracula_background</item>
<item name="colorOnBackground">@color/dracula_on_surface</item>
<item name="android:windowBackground">@color/dracula_background</item>
<!-- Night is Dracula's active code path (it forces night mode on), so the
semantic color overrides must live here too, not only day. -->
<item name="protocolColor">@color/color_dracula_green</item>
<item name="profileNameColor">@color/color_dracula_yellow</item>
<item name="statusConnectedColor">@color/color_dracula_green</item>
<item name="statusStoppedColor">@color/color_dracula_red</item>
<item name="testFailColor">@color/color_dracula_red</item>
<item name="speedTextColor">@color/color_dracula_cyan</item>
<item name="fabStoppedColor">@color/color_dracula_cyan</item>
<item name="statusDetailColor">@color/color_dracula_cyan</item>
<item name="statusTestingColor">@color/color_dracula_yellow</item>
<item name="routeProxyColor">@color/color_dracula_cyan</item>
<item name="navSelectedColor">?attr/colorAccent</item>
</style>

<style name="Theme.SagerNet.Dialog.Dracula" parent="Theme.SagerNet.Dialog">
Expand All @@ -40,5 +53,15 @@
<item name="colorOnSurface">@color/dracula_on_surface</item>
<item name="android:colorBackground">@color/dracula_background</item>
<item name="colorOnBackground">@color/dracula_on_surface</item>
<item name="protocolColor">@color/color_dracula_green</item>
<item name="profileNameColor">@color/color_dracula_yellow</item>
<item name="statusConnectedColor">@color/color_dracula_green</item>
<item name="statusStoppedColor">@color/color_dracula_red</item>
<item name="testFailColor">@color/color_dracula_red</item>
<item name="speedTextColor">@color/color_dracula_cyan</item>
<item name="fabStoppedColor">@color/color_dracula_cyan</item>
<item name="statusDetailColor">@color/color_dracula_cyan</item>
<item name="statusTestingColor">@color/color_dracula_yellow</item>
<item name="routeProxyColor">@color/color_dracula_cyan</item>
</style>
Comment thread
greptile-apps[bot] marked this conversation as resolved.
</resources>
12 changes: 12 additions & 0 deletions app/src/main/res/values-v26/themes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@
<!-- Default nav-drawer selected-item fill; satisfies the
?itemShapeFillColor self-reference in @color/navigation_item. -->
<item name="itemShapeFillColor">?colorPrimary</item>
<!-- Protocol/type label color; defaults to accent, Dracula overrides green. -->
<item name="protocolColor">?attr/accentOrTextSecondary</item>
<item name="profileNameColor">?android:attr/textColorPrimary</item>
<item name="statusConnectedColor">?attr/colorOnPrimary</item>
<item name="statusStoppedColor">?attr/colorOnPrimary</item>
<item name="testFailColor">@color/material_red_500</item>
<item name="speedTextColor">?attr/colorOnPrimary</item>
<item name="fabStoppedColor">?attr/colorOnPrimary</item>
<item name="statusDetailColor">?attr/colorOnPrimary</item>
<item name="statusTestingColor">?attr/colorOnPrimary</item>
<item name="routeProxyColor">@color/color_route_proxy</item>
<item name="navSelectedColor">?attr/colorPrimary</item>
<item name="actionModeCloseDrawable">@drawable/ic_navigation_close</item>
<item name="colorButtonNormal">?colorAccent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
Expand Down
27 changes: 27 additions & 0 deletions app/src/main/res/values/attrs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,33 @@
<attr name="fabColorBackground" format="color" />
<attr name="selectedColorPrimary" format="color" />
<attr name="whiteOrTextPrimary" format="color" />
<!-- Text color for the protocol/type label on a profile row. Defaults to
the accent color; Dracula overrides it with its green. -->
<attr name="protocolColor" format="color" />
<!-- Profile name label color. Defaults to primary text; Dracula = yellow. -->
<attr name="profileNameColor" format="color" />
<!-- Service status / connect-button colors. Default to primary text so non-
Dracula themes are unchanged; Dracula tints connected=green, stopped=red. -->
<attr name="statusConnectedColor" format="color" />
<attr name="statusStoppedColor" format="color" />
<!-- Connection-test failure color. Default Material red; Dracula = #ff5555. -->
<attr name="testFailColor" format="color" />
<!-- Bottom-bar speed text (arrows + rate). Default on-primary; Dracula = yellow. -->
<attr name="speedTextColor" format="color" />
<!-- Connect-FAB icon tint when Stopped. Default on-primary; Dracula = yellow
(red was invisible on the pink/purple FAB). Separate from the red
statusStoppedColor used for the status text. -->
<attr name="fabStoppedColor" format="color" />
<!-- Secondary status text (the part after "Connected," / "Success:").
Default on-primary; Dracula = cyan. -->
<attr name="statusDetailColor" format="color" />
<!-- "Testing…" connection-test in-progress text. Default on-primary; Dracula = yellow. -->
<attr name="statusTestingColor" format="color" />
<!-- Proxy route-rule color. Default blue; Dracula = cyan. -->
<attr name="routeProxyColor" format="color" />
<!-- Selected nav-drawer item color (fill tint + icon/text). Default primary;
Dracula uses its accent (pink) to match the toggle-on color. -->
<attr name="navSelectedColor" format="color" />
<!-- Tonal "elevated surface" color for cards (route / group); each theme
variant tints toward its own primary. -->
<attr name="cardElevatedSurfaceColor" format="color" />
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/res/values/colors.xml
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,14 @@
<color name="color_dracula_accent">#FF79C6</color>
<color name="color_dracula_100">#E9DDFB</color>
<color name="color_dracula_300">#9A7BD0</color>
<!-- Dracula green (#50fa7b), used for the protocol/type label. -->
<color name="color_dracula_green">#50FA7B</color>
<!-- Dracula red (#ff5555): failures / stopped state. -->
<color name="color_dracula_red">#FF5555</color>
<!-- Dracula yellow (#f1fa8c): profile name label. -->
<color name="color_dracula_yellow">#F1FA8C</color>
<!-- Dracula cyan (#8be9fd): the Dracula "blue" (e.g. proxy route rules). -->
<color name="color_dracula_cyan">#8BE9FD</color>

<color name="color_ng_black_accent">#9E9E9E</color>
<color name="color_ng_black_primary">#2B2B2B</color>
Expand Down
Loading
Loading