Skip to content

Commit 3028327

Browse files
authored
Add Dracula semantic colors (yellow names, red fails, state-colored connect) (#41)
* 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. * Apply protocolColor green in night Dracula styles Dracula forces night mode on, so the active styles at runtime are the values-night Theme.SagerNet.Dracula / Dialog.Dracula variants. They redeclare the Dracula colors but were missing the protocolColor override, so the label fell back to the base ?accentOrTextSecondary (pink) and the green never applied. Add protocolColor=color_dracula_green to both night styles. Verified in the compiled APK via aapt2: night Dracula styles now resolve protocolColor to @color/color_dracula_green (#ff50fa7b). Fixes the issue Greptile flagged on PR #39. * 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. * Fix FAB tint regression and complete Dracula dialog overrides Addresses reviewer feedback on PR: - P1: applyStateTint set imageTintList to ?textColorPrimary on every theme, which on non-Dracula light themes painted the connect-FAB icon a dark/wrong color, and cleared the tint to null on transient states. Default statusConnectedColor/statusStoppedColor to ?attr/colorOnPrimary (the FAB's normal on-primary icon color) and use colorOnPrimary for transient states, so non-Dracula themes keep their original appearance. StatsBar status text uses the same fallback (it sits on the colorPrimary bar). - Major/P2: add the four missing semantic overrides (profileName/status/ testFail) to Theme.SagerNet.Dialog.Dracula in both day and night so the Dracula dialog palette stays consistent. Verified in compiled APK: base theme status attrs resolve to ?colorOnPrimary; Dracula resolves to yellow/green/red. * 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. * Recolor speeds, stopped-FAB, and status detail to Dracula cyan Per design review: switch speedTextColor (up/down + 0 B/s), fabStoppedColor (stopped airplane icon), and statusDetailColor ('tap to check connection' / '…handshake took Nms') from yellow to Dracula cyan (#8be9fd). Profile names stay yellow; protocol=green, connected=green, stopped/fail status text=red, proxy route=cyan, nav selected=accent. Other themes unchanged. * Keep Testing... yellow while detail text stays cyan Add a dedicated statusTestingColor attr so the connection-test 'Testing…' in-progress text is Dracula yellow, separate from statusDetailColor (cyan) used for the 'tap to check connection' hint and the 'handshake took Nms' detail. Defaults to colorOnPrimary for other themes (unchanged).
1 parent 901529c commit 3028327

12 files changed

Lines changed: 203 additions & 16 deletions

File tree

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -762,14 +762,14 @@ class ConfigurationFragment @JvmOverloads constructor(
762762

763763
2 -> {
764764
profileStatusText = profile.error
765-
profileStatusColor = context.getColour(R.color.material_red_500)
765+
profileStatusColor = context.getColorAttr(R.attr.testFailColor)
766766
}
767767

768768
3 -> {
769769
val err = profile.error ?: ""
770770
val msg = Protocols.genFriendlyMsg(err)
771771
profileStatusText = if (msg != err) msg else getString(R.string.unavailable)
772-
profileStatusColor = context.getColour(R.color.material_red_500)
772+
profileStatusColor = context.getColorAttr(R.attr.testFailColor)
773773
}
774774
}
775775

@@ -1803,6 +1803,7 @@ class ConfigurationFragment @JvmOverloads constructor(
18031803
}
18041804

18051805
profileName.text = proxyEntity.displayName()
1806+
profileName.setTextColor(requireContext().getColorAttr(R.attr.profileNameColor))
18061807
profileType.text = proxyEntity.displayType()
18071808
profileType.setTextColor(requireContext().getProtocolColor(proxyEntity.type))
18081809

@@ -1854,7 +1855,7 @@ class ConfigurationFragment @JvmOverloads constructor(
18541855
profileStatus.text = getString(R.string.available, proxyEntity.ping)
18551856
profileStatus.setTextColor(requireContext().getColour(R.color.material_green_500))
18561857
} else {
1857-
profileStatus.setTextColor(requireContext().getColour(R.color.material_red_500))
1858+
profileStatus.setTextColor(requireContext().getColorAttr(R.attr.testFailColor))
18581859
if (proxyEntity.status == 2) {
18591860
profileStatus.text = proxyEntity.error
18601861
}

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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.nekohasekai.sagernet.widget
22

33
import android.content.Context
4+
import android.content.res.ColorStateList
45
import android.graphics.drawable.Drawable
56
import android.os.Build
67
import android.util.AttributeSet
@@ -17,6 +18,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton
1718
import com.google.android.material.progressindicator.BaseProgressIndicator
1819
import io.nekohasekai.sagernet.R
1920
import io.nekohasekai.sagernet.bg.BaseService
21+
import io.nekohasekai.sagernet.ktx.getColorAttr
2022
import kotlinx.coroutines.Job
2123
import kotlinx.coroutines.delay
2224
import java.util.*
@@ -118,6 +120,7 @@ class ServiceButton @JvmOverloads constructor(
118120
}
119121
checked = state == BaseService.State.Connected
120122
refreshDrawableState()
123+
applyStateTint(state)
121124
val description = context.getText(if (state.canStop) R.string.stop else R.string.connect)
122125
contentDescription = description
123126
TooltipCompat.setTooltipText(this, description)
@@ -129,6 +132,18 @@ class ServiceButton @JvmOverloads constructor(
129132
)
130133
}
131134

135+
private fun applyStateTint(state: BaseService.State) {
136+
// Tint the connect FAB icon by state: connected=green, stopped=red. For
137+
// transient states (and on non-Dracula themes) fall back to colorOnPrimary,
138+
// which is the FAB's normal icon color, so those themes look unchanged.
139+
val attr = when (state) {
140+
BaseService.State.Connected -> R.attr.statusConnectedColor
141+
BaseService.State.Stopped -> R.attr.fabStoppedColor
142+
else -> com.google.android.material.R.attr.colorOnPrimary
143+
}
144+
imageTintList = ColorStateList.valueOf(context.getColorAttr(attr))
145+
}
146+
132147
private fun changeState(icon: AnimatedState, animate: Boolean) {
133148
fun counters(a: AnimatedState, b: AnimatedState): Boolean =
134149
a == iconStopped && b == iconConnecting ||

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

Lines changed: 55 additions & 3 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,6 +82,43 @@ 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+
110+
private fun setStatusColorByState(state: BaseService.State) {
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.
114+
val attr = when (state) {
115+
BaseService.State.Connected -> R.attr.statusConnectedColor
116+
BaseService.State.Stopped, BaseService.State.Stopping -> R.attr.statusStoppedColor
117+
else -> com.google.android.material.R.attr.colorOnPrimary
118+
}
119+
statusText.setTextColor(context.getColorAttr(attr))
120+
}
121+
83122
fun changeState(state: BaseService.State) {
84123
val activity = context as MainActivity
85124
fun postWhenStarted(what: () -> Unit) = activity.lifecycleScope.launch(Dispatchers.Main) {
@@ -89,13 +128,18 @@ class StatsBar @JvmOverloads constructor(
89128
if ((state == BaseService.State.Connected).also { hideOnScroll = it }) {
90129
postWhenStarted {
91130
if (allowShow) performShow()
92-
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+
)
93136
}
94137
} else {
95138
postWhenStarted {
96139
performHide()
97140
}
98141
updateSpeed(0, 0)
142+
setStatusColorByState(state)
99143
setStatus(
100144
context.getText(
101145
when (state) {
@@ -110,6 +154,9 @@ class StatsBar @JvmOverloads constructor(
110154

111155
@SuppressLint("SetTextI18n")
112156
fun updateSpeed(txRate: Long, rxRate: Long) {
157+
val speedColor = context.getColorAttr(R.attr.speedTextColor)
158+
txText.setTextColor(speedColor)
159+
rxText.setTextColor(speedColor)
113160
txText.text = "${
114161
context.getString(
115162
R.string.speed, Formatter.formatFileSize(context, txRate)
@@ -125,27 +172,32 @@ class StatsBar @JvmOverloads constructor(
125172
fun testConnection() {
126173
val activity = context as MainActivity
127174
isEnabled = false
175+
// "Testing…" in the testing color.
176+
statusText.setTextColor(context.getColorAttr(R.attr.statusTestingColor))
128177
setStatus(app.getText(R.string.connection_test_testing))
129178
runOnDefaultDispatcher {
130179
try {
131180
val elapsed = activity.urlTest()
132181
onMainDispatcher {
133182
isEnabled = true
134-
setStatus(
183+
// "Success:" in green; the handshake detail in detail color.
184+
setStatusTwoTone(
135185
app.getString(
136186
if (DataStore.connectionTestURL.startsWith("https://")) {
137187
R.string.connection_test_available
138188
} else {
139189
R.string.connection_test_available_http
140190
}, elapsed
141-
)
191+
), ':',
192+
R.attr.statusConnectedColor, R.attr.statusDetailColor
142193
)
143194
}
144195

145196
} catch (e: Exception) {
146197
Logs.w(e.toString())
147198
onMainDispatcher {
148199
isEnabled = true
200+
statusText.setTextColor(context.getColorAttr(R.attr.statusTestingColor))
149201
setStatus(app.getText(R.string.connection_test_testing))
150202

151203
activity.snackbar(

app/src/main/java/moe/matsuri/nb4a/Protocols.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ object Protocols {
4444
fun Context.getProtocolColor(type: Int): Int {
4545
return when (type) {
4646
TYPE_NEKO -> getColorAttr(android.R.attr.textColorPrimary)
47-
else -> getColorAttr(R.attr.accentOrTextSecondary)
47+
else -> getColorAttr(R.attr.protocolColor)
4848
}
4949
}
5050

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: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@
2626
<item name="android:colorBackground">@color/dracula_background</item>
2727
<item name="colorOnBackground">@color/dracula_on_surface</item>
2828
<item name="android:windowBackground">@color/dracula_background</item>
29+
<!-- Night is Dracula's active code path (it forces night mode on), so the
30+
semantic color overrides must live here too, not only day. -->
31+
<item name="protocolColor">@color/color_dracula_green</item>
32+
<item name="profileNameColor">@color/color_dracula_yellow</item>
33+
<item name="statusConnectedColor">@color/color_dracula_green</item>
34+
<item name="statusStoppedColor">@color/color_dracula_red</item>
35+
<item name="testFailColor">@color/color_dracula_red</item>
36+
<item name="speedTextColor">@color/color_dracula_cyan</item>
37+
<item name="fabStoppedColor">@color/color_dracula_cyan</item>
38+
<item name="statusDetailColor">@color/color_dracula_cyan</item>
39+
<item name="statusTestingColor">@color/color_dracula_yellow</item>
40+
<item name="routeProxyColor">@color/color_dracula_cyan</item>
41+
<item name="navSelectedColor">?attr/colorAccent</item>
2942
</style>
3043

3144
<style name="Theme.SagerNet.Dialog.Dracula" parent="Theme.SagerNet.Dialog">
@@ -40,5 +53,15 @@
4053
<item name="colorOnSurface">@color/dracula_on_surface</item>
4154
<item name="android:colorBackground">@color/dracula_background</item>
4255
<item name="colorOnBackground">@color/dracula_on_surface</item>
56+
<item name="protocolColor">@color/color_dracula_green</item>
57+
<item name="profileNameColor">@color/color_dracula_yellow</item>
58+
<item name="statusConnectedColor">@color/color_dracula_green</item>
59+
<item name="statusStoppedColor">@color/color_dracula_red</item>
60+
<item name="testFailColor">@color/color_dracula_red</item>
61+
<item name="speedTextColor">@color/color_dracula_cyan</item>
62+
<item name="fabStoppedColor">@color/color_dracula_cyan</item>
63+
<item name="statusDetailColor">@color/color_dracula_cyan</item>
64+
<item name="statusTestingColor">@color/color_dracula_yellow</item>
65+
<item name="routeProxyColor">@color/color_dracula_cyan</item>
4366
</style>
4467
</resources>

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@
55
<!-- Default nav-drawer selected-item fill; satisfies the
66
?itemShapeFillColor self-reference in @color/navigation_item. -->
77
<item name="itemShapeFillColor">?colorPrimary</item>
8+
<!-- Protocol/type label color; defaults to accent, Dracula overrides green. -->
9+
<item name="protocolColor">?attr/accentOrTextSecondary</item>
10+
<item name="profileNameColor">?android:attr/textColorPrimary</item>
11+
<item name="statusConnectedColor">?attr/colorOnPrimary</item>
12+
<item name="statusStoppedColor">?attr/colorOnPrimary</item>
13+
<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="statusTestingColor">?attr/colorOnPrimary</item>
18+
<item name="routeProxyColor">@color/color_route_proxy</item>
19+
<item name="navSelectedColor">?attr/colorPrimary</item>
820
<item name="actionModeCloseDrawable">@drawable/ic_navigation_close</item>
921
<item name="colorButtonNormal">?colorAccent</item>
1022
<item name="android:navigationBarColor">@android:color/transparent</item>

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,33 @@
1010
<attr name="fabColorBackground" format="color" />
1111
<attr name="selectedColorPrimary" format="color" />
1212
<attr name="whiteOrTextPrimary" format="color" />
13+
<!-- Text color for the protocol/type label on a profile row. Defaults to
14+
the accent color; Dracula overrides it with its green. -->
15+
<attr name="protocolColor" format="color" />
16+
<!-- Profile name label color. Defaults to primary text; Dracula = yellow. -->
17+
<attr name="profileNameColor" format="color" />
18+
<!-- Service status / connect-button colors. Default to primary text so non-
19+
Dracula themes are unchanged; Dracula tints connected=green, stopped=red. -->
20+
<attr name="statusConnectedColor" format="color" />
21+
<attr name="statusStoppedColor" format="color" />
22+
<!-- Connection-test failure color. Default Material red; Dracula = #ff5555. -->
23+
<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 = cyan. -->
32+
<attr name="statusDetailColor" format="color" />
33+
<!-- "Testing…" connection-test in-progress text. Default on-primary; Dracula = yellow. -->
34+
<attr name="statusTestingColor" format="color" />
35+
<!-- Proxy route-rule color. Default blue; Dracula = cyan. -->
36+
<attr name="routeProxyColor" format="color" />
37+
<!-- Selected nav-drawer item color (fill tint + icon/text). Default primary;
38+
Dracula uses its accent (pink) to match the toggle-on color. -->
39+
<attr name="navSelectedColor" format="color" />
1340
<!-- Tonal "elevated surface" color for cards (route / group); each theme
1441
variant tints toward its own primary. -->
1542
<attr name="cardElevatedSurfaceColor" format="color" />

0 commit comments

Comments
 (0)