Skip to content

Commit 6dd4ad5

Browse files
committed
Extend Dracula colors: yellow speeds/stopped-FAB, two-tone status, cyan proxy, pink nav
Per design review of the live theme: - speedTextColor: bottom-bar up/down speed text (incl. 0 B/s) -> Dracula yellow. - fabStoppedColor: connect-FAB icon when Stopped -> Dracula yellow (the red strikethrough plane was nearly invisible on the pink/purple FAB). Status TEXT for stopped/shutting-down stays red via statusStoppedColor. - Two-tone status text: 'Connected,' / 'Success:' lead segment in green, the remainder (hint / handshake detail) in yellow (statusDetailColor). 'Testing…' yellow, 'Shutting down…' red. Falls back to single lead color if a locale string lacks the separator (safe, no crash). - routeProxyColor: Proxy route rule -> Dracula cyan #8be9fd (Dracula's blue). - navSelectedColor: selected nav-drawer item (fill + icon/text) -> Dracula accent (pink) to match the toggle-on color. Black keeps its accent; other themes default to colorPrimary (unchanged). All gated via theme attrs that default to prior colors, so the other ~22 themes are visually unchanged. Verified attr resolution in the compiled APK.
1 parent 5e54a71 commit 6dd4ad5

10 files changed

Lines changed: 109 additions & 19 deletions

File tree

app/src/main/java/io/nekohasekai/sagernet/ui/RouteFragment.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import android.view.View
77
import android.view.ViewGroup
88
import androidx.appcompat.widget.Toolbar
99
import androidx.core.content.ContextCompat
10+
import io.nekohasekai.sagernet.ktx.getColorAttr
1011
import androidx.core.view.ViewCompat
1112
import androidx.recyclerview.widget.ItemTouchHelper
1213
import androidx.recyclerview.widget.RecyclerView
@@ -287,13 +288,14 @@ class RouteFragment : ToolbarFragment(R.layout.layout_route), Toolbar.OnMenuItem
287288
routeOutbound.text = rule.displayOutbound()
288289

289290
// set text color based on route type
290-
val colorRes = when (rule.outbound) {
291-
-2L -> R.color.color_route_block // block: red
292-
-1L -> R.color.color_route_direct // direct: green
293-
0L -> R.color.color_route_proxy // proxy: blue
294-
else -> R.color.color_route_config // config: purple
291+
val ctx = itemView.context
292+
val outboundColor = when (rule.outbound) {
293+
-2L -> ContextCompat.getColor(ctx, R.color.color_route_block) // block: red
294+
-1L -> ContextCompat.getColor(ctx, R.color.color_route_direct) // direct: green
295+
0L -> ctx.getColorAttr(R.attr.routeProxyColor) // proxy: blue/cyan
296+
else -> ContextCompat.getColor(ctx, R.color.color_route_config) // config: purple
295297
}
296-
routeOutbound.setTextColor(ContextCompat.getColor(itemView.context, colorRes))
298+
routeOutbound.setTextColor(outboundColor)
297299

298300
itemView.setOnClickListener {
299301
enableSwitch.performClick()

app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ class ServiceButton @JvmOverloads constructor(
138138
// which is the FAB's normal icon color, so those themes look unchanged.
139139
val attr = when (state) {
140140
BaseService.State.Connected -> R.attr.statusConnectedColor
141-
BaseService.State.Stopped -> R.attr.statusStoppedColor
141+
BaseService.State.Stopped -> R.attr.fabStoppedColor
142142
else -> com.google.android.material.R.attr.colorOnPrimary
143143
}
144144
imageTintList = ColorStateList.valueOf(context.getColorAttr(attr))

app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package io.nekohasekai.sagernet.widget
22

33
import android.annotation.SuppressLint
44
import android.content.Context
5+
import android.text.SpannableStringBuilder
56
import android.text.format.Formatter
7+
import android.text.style.ForegroundColorSpan
68
import android.util.AttributeSet
79
import android.view.View
810
import android.widget.TextView
@@ -80,36 +82,64 @@ class StatsBar @JvmOverloads constructor(
8082
TooltipCompat.setTooltipText(this, text)
8183
}
8284

85+
// Two-tone status: color the lead segment (split at [sep], kept with the lead)
86+
// and the remainder separately. Used for "Connected, …" and "Success: …".
87+
private fun setStatusTwoTone(
88+
full: CharSequence, sep: Char, leadAttr: Int, restAttr: Int
89+
) {
90+
val s = full.toString()
91+
val idx = s.indexOf(sep)
92+
if (idx < 0) {
93+
statusText.setTextColor(context.getColorAttr(leadAttr))
94+
setStatus(full)
95+
return
96+
}
97+
val cut = idx + 1 // keep the separator char with the lead segment
98+
val span = SpannableStringBuilder(s)
99+
span.setSpan(
100+
ForegroundColorSpan(context.getColorAttr(leadAttr)),
101+
0, cut, SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
102+
)
103+
span.setSpan(
104+
ForegroundColorSpan(context.getColorAttr(restAttr)),
105+
cut, s.length, SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
106+
)
107+
setStatus(span)
108+
}
109+
83110
private fun setStatusColorByState(state: BaseService.State) {
84-
// Connected = statusConnectedColor, Stopped = statusStoppedColor, transient
85-
// states use colorOnPrimary (the bar's normal on-primary text color). For
86-
// non-Dracula themes the connected/stopped attrs also default to
87-
// colorOnPrimary, so there is no visible change.
111+
// Connected = statusConnectedColor (green), Stopped/Stopping = statusStoppedColor
112+
// (red — "Shutting down…" reads as red), Connecting/other = colorOnPrimary.
113+
// Non-Dracula themes default these attrs to colorOnPrimary, so no change.
88114
val attr = when (state) {
89115
BaseService.State.Connected -> R.attr.statusConnectedColor
90-
BaseService.State.Stopped -> R.attr.statusStoppedColor
116+
BaseService.State.Stopped, BaseService.State.Stopping -> R.attr.statusStoppedColor
91117
else -> com.google.android.material.R.attr.colorOnPrimary
92118
}
93119
statusText.setTextColor(context.getColorAttr(attr))
94120
}
95121

96122
fun changeState(state: BaseService.State) {
97123
val activity = context as MainActivity
98-
setStatusColorByState(state)
99124
fun postWhenStarted(what: () -> Unit) = activity.lifecycleScope.launch(Dispatchers.Main) {
100125
delay(100L)
101126
activity.whenStarted { what() }
102127
}
103128
if ((state == BaseService.State.Connected).also { hideOnScroll = it }) {
104129
postWhenStarted {
105130
if (allowShow) performShow()
106-
setStatus(app.getText(R.string.vpn_connected))
131+
// "Connected," in green; the "tap to check connection" hint in detail color.
132+
setStatusTwoTone(
133+
app.getText(R.string.vpn_connected), ',',
134+
R.attr.statusConnectedColor, R.attr.statusDetailColor
135+
)
107136
}
108137
} else {
109138
postWhenStarted {
110139
performHide()
111140
}
112141
updateSpeed(0, 0)
142+
setStatusColorByState(state)
113143
setStatus(
114144
context.getText(
115145
when (state) {
@@ -124,6 +154,9 @@ class StatsBar @JvmOverloads constructor(
124154

125155
@SuppressLint("SetTextI18n")
126156
fun updateSpeed(txRate: Long, rxRate: Long) {
157+
val speedColor = context.getColorAttr(R.attr.speedTextColor)
158+
txText.setTextColor(speedColor)
159+
rxText.setTextColor(speedColor)
127160
txText.text = "${
128161
context.getString(
129162
R.string.speed, Formatter.formatFileSize(context, txRate)
@@ -139,27 +172,32 @@ class StatsBar @JvmOverloads constructor(
139172
fun testConnection() {
140173
val activity = context as MainActivity
141174
isEnabled = false
175+
// "Testing…" in the detail color.
176+
statusText.setTextColor(context.getColorAttr(R.attr.statusDetailColor))
142177
setStatus(app.getText(R.string.connection_test_testing))
143178
runOnDefaultDispatcher {
144179
try {
145180
val elapsed = activity.urlTest()
146181
onMainDispatcher {
147182
isEnabled = true
148-
setStatus(
183+
// "Success:" in green; the handshake detail in detail color.
184+
setStatusTwoTone(
149185
app.getString(
150186
if (DataStore.connectionTestURL.startsWith("https://")) {
151187
R.string.connection_test_available
152188
} else {
153189
R.string.connection_test_available_http
154190
}, elapsed
155-
)
191+
), ':',
192+
R.attr.statusConnectedColor, R.attr.statusDetailColor
156193
)
157194
}
158195

159196
} catch (e: Exception) {
160197
Logs.w(e.toString())
161198
onMainDispatcher {
162199
isEnabled = true
200+
statusText.setTextColor(context.getColorAttr(R.attr.statusDetailColor))
163201
setStatus(app.getText(R.string.connection_test_testing))
164202

165203
activity.snackbar(
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<selector xmlns:android="http://schemas.android.com/apk/res/android">
3-
<item android:color="?attr/colorPrimary" android:alpha="0.16" android:state_checked="true" />
3+
<item android:color="?attr/navSelectedColor" android:alpha="0.16" android:state_checked="true" />
44
<item android:color="@android:color/transparent" />
55
</selector>
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<selector xmlns:android="http://schemas.android.com/apk/res/android">
3-
<item android:color="?attr/primaryOrTextSecondary" android:state_checked="true" />
3+
<item android:color="?attr/navSelectedColor" android:state_checked="true" />
44
<item android:color="?android:textColorPrimary" android:state_checked="false" />
5-
</selector>
5+
</selector>

app/src/main/res/values-night/themes.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@
3333
<item name="statusConnectedColor">@color/color_dracula_green</item>
3434
<item name="statusStoppedColor">@color/color_dracula_red</item>
3535
<item name="testFailColor">@color/color_dracula_red</item>
36+
<item name="speedTextColor">@color/color_dracula_yellow</item>
37+
<item name="fabStoppedColor">@color/color_dracula_yellow</item>
38+
<item name="statusDetailColor">@color/color_dracula_yellow</item>
39+
<item name="routeProxyColor">@color/color_dracula_cyan</item>
40+
<item name="navSelectedColor">?attr/colorAccent</item>
3641
</style>
3742

3843
<style name="Theme.SagerNet.Dialog.Dracula" parent="Theme.SagerNet.Dialog">
@@ -52,5 +57,9 @@
5257
<item name="statusConnectedColor">@color/color_dracula_green</item>
5358
<item name="statusStoppedColor">@color/color_dracula_red</item>
5459
<item name="testFailColor">@color/color_dracula_red</item>
60+
<item name="speedTextColor">@color/color_dracula_yellow</item>
61+
<item name="fabStoppedColor">@color/color_dracula_yellow</item>
62+
<item name="statusDetailColor">@color/color_dracula_yellow</item>
63+
<item name="routeProxyColor">@color/color_dracula_cyan</item>
5564
</style>
5665
</resources>

app/src/main/res/values-v26/themes.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
<item name="statusConnectedColor">?attr/colorOnPrimary</item>
1212
<item name="statusStoppedColor">?attr/colorOnPrimary</item>
1313
<item name="testFailColor">@color/material_red_500</item>
14+
<item name="speedTextColor">?attr/colorOnPrimary</item>
15+
<item name="fabStoppedColor">?attr/colorOnPrimary</item>
16+
<item name="statusDetailColor">?attr/colorOnPrimary</item>
17+
<item name="routeProxyColor">@color/color_route_proxy</item>
18+
<item name="navSelectedColor">?attr/colorPrimary</item>
1419
<item name="actionModeCloseDrawable">@drawable/ic_navigation_close</item>
1520
<item name="colorButtonNormal">?colorAccent</item>
1621
<item name="android:navigationBarColor">@android:color/transparent</item>

app/src/main/res/values/attrs.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@
2121
<attr name="statusStoppedColor" format="color" />
2222
<!-- Connection-test failure color. Default Material red; Dracula = #ff5555. -->
2323
<attr name="testFailColor" format="color" />
24+
<!-- Bottom-bar speed text (arrows + rate). Default on-primary; Dracula = yellow. -->
25+
<attr name="speedTextColor" format="color" />
26+
<!-- Connect-FAB icon tint when Stopped. Default on-primary; Dracula = yellow
27+
(red was invisible on the pink/purple FAB). Separate from the red
28+
statusStoppedColor used for the status text. -->
29+
<attr name="fabStoppedColor" format="color" />
30+
<!-- Secondary status text (the part after "Connected," / "Success:").
31+
Default on-primary; Dracula = yellow. -->
32+
<attr name="statusDetailColor" format="color" />
33+
<!-- Proxy route-rule color. Default blue; Dracula = cyan. -->
34+
<attr name="routeProxyColor" format="color" />
35+
<!-- Selected nav-drawer item color (fill tint + icon/text). Default primary;
36+
Dracula uses its accent (pink) to match the toggle-on color. -->
37+
<attr name="navSelectedColor" format="color" />
2438
<!-- Tonal "elevated surface" color for cards (route / group); each theme
2539
variant tints toward its own primary. -->
2640
<attr name="cardElevatedSurfaceColor" format="color" />

app/src/main/res/values/colors.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@
306306
<color name="color_dracula_red">#FF5555</color>
307307
<!-- Dracula yellow (#f1fa8c): profile name label. -->
308308
<color name="color_dracula_yellow">#F1FA8C</color>
309+
<!-- Dracula cyan (#8be9fd): the Dracula "blue" (e.g. proxy route rules). -->
310+
<color name="color_dracula_cyan">#8BE9FD</color>
309311

310312
<color name="color_ng_black_accent">#9E9E9E</color>
311313
<color name="color_ng_black_primary">#2B2B2B</color>

app/src/main/res/values/themes.xml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
<item name="statusConnectedColor">?attr/colorOnPrimary</item>
1111
<item name="statusStoppedColor">?attr/colorOnPrimary</item>
1212
<item name="testFailColor">@color/material_red_500</item>
13+
<item name="speedTextColor">?attr/colorOnPrimary</item>
14+
<item name="fabStoppedColor">?attr/colorOnPrimary</item>
15+
<item name="statusDetailColor">?attr/colorOnPrimary</item>
16+
<item name="routeProxyColor">@color/color_route_proxy</item>
17+
<item name="navSelectedColor">?attr/colorPrimary</item>
1318
<!-- Default nav-drawer selected-item fill; also satisfies the
1419
?itemShapeFillColor self-reference in @color/navigation_item so the
1520
ColorStateList resolves for every theme. Only Black overrides it;
@@ -76,6 +81,11 @@
7681
<item name="statusConnectedColor">?attr/colorOnPrimary</item>
7782
<item name="statusStoppedColor">?attr/colorOnPrimary</item>
7883
<item name="testFailColor">@color/material_red_500</item>
84+
<item name="speedTextColor">?attr/colorOnPrimary</item>
85+
<item name="fabStoppedColor">?attr/colorOnPrimary</item>
86+
<item name="statusDetailColor">?attr/colorOnPrimary</item>
87+
<item name="routeProxyColor">@color/color_route_proxy</item>
88+
<item name="navSelectedColor">?attr/colorPrimary</item>
7989
<item name="itemShapeFillColor">?colorPrimary</item>
8090
<item name="actionModeCloseDrawable">@drawable/ic_navigation_close</item>
8191
<item name="colorButtonNormal">?colorAccent</item>
@@ -598,6 +608,7 @@
598608
<item name="selectedColorPrimary">@color/color_ng_black_accent</item>
599609
<item name="fabColorBackground">@color/color_ng_black_accent</item>
600610
<item name="itemShapeFillColor">@color/color_ng_black_accent</item>
611+
<item name="navSelectedColor">@color/color_ng_black_accent</item>
601612

602613
<item name="accentOrTextSecondary">?android:textColorSecondary</item>
603614
<item name="accentOrTextPrimary">?android:textColorPrimary</item>
@@ -630,6 +641,11 @@
630641
<item name="statusConnectedColor">@color/color_dracula_green</item>
631642
<item name="statusStoppedColor">@color/color_dracula_red</item>
632643
<item name="testFailColor">@color/color_dracula_red</item>
644+
<item name="speedTextColor">@color/color_dracula_yellow</item>
645+
<item name="fabStoppedColor">@color/color_dracula_yellow</item>
646+
<item name="statusDetailColor">@color/color_dracula_yellow</item>
647+
<item name="routeProxyColor">@color/color_dracula_cyan</item>
648+
<item name="navSelectedColor">?attr/colorAccent</item>
633649
</style>
634650

635651
<style name="Theme.SagerNet.Dialog.Black" parent="Theme.SagerNet.Dialog">
@@ -671,6 +687,10 @@
671687
<item name="statusConnectedColor">@color/color_dracula_green</item>
672688
<item name="statusStoppedColor">@color/color_dracula_red</item>
673689
<item name="testFailColor">@color/color_dracula_red</item>
690+
<item name="speedTextColor">@color/color_dracula_yellow</item>
691+
<item name="fabStoppedColor">@color/color_dracula_yellow</item>
692+
<item name="statusDetailColor">@color/color_dracula_yellow</item>
693+
<item name="routeProxyColor">@color/color_dracula_cyan</item>
674694
</style>
675695

676696
<style name="Theme.Start" parent="Theme.MaterialComponents.DayNight">

0 commit comments

Comments
 (0)