Skip to content

Commit 3bd3854

Browse files
committed
Add Material 3 Dark High Contrast theme (OLED black)
A full-fledged M3 dark-only theme on a true black (#000000) canvas: green-primary action accent (GitHub style) with a bright blue secondary for links/selection and high-contrast near-white text. - Theme.DARK_HIGH_CONTRAST (id 25) mapped to new Theme.SagerNet. DarkHighContrast styles (day declares the full dark canvas so it never flashes light; night repeats it as the active forced-night path). - Full M3 role overrides: surface/background/containers, surfaceVariant, outline/outlineVariant, secondary/tertiary, plus status semantics (green/red/yellow) and route/proxy blue. - Generalize the dark-only night-mode handling: selecting any theme in Theme.DARK_ONLY_THEMES (Dracula, Dark High Contrast) forces night mode on and restores the prior setting on exit. Storage key kept for compat. - Surfaced by name in the modern picker list (green preview swatch); excluded from the classic grid. - Picker select() now follows the standard Preference contract (notify-then-persist) so the fragment sees the true previous theme.
1 parent 37661e1 commit 3bd3854

9 files changed

Lines changed: 242 additions & 16 deletions

File tree

app/src/main/java/io/nekohasekai/sagernet/Constants.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ object Key {
1414
const val APP_EXPERT = "isExpert"
1515
const val APP_THEME = "appTheme"
1616
const val NIGHT_THEME = "nightTheme"
17-
// Remembers the user's night-mode setting before the dark-only Dracula theme
18-
// forced it on, so it can be restored when switching to another theme.
17+
// Remembers the user's night-mode setting before a dark-only theme (Dracula
18+
// or Dark High Contrast) forced it on, so it can be restored when switching
19+
// to another theme. Storage key kept as "nightThemeBeforeDracula" for
20+
// backward compatibility with previously persisted values.
1921
const val NIGHT_THEME_BEFORE_DRACULA = "nightThemeBeforeDracula"
2022
const val APP_LANGUAGE = "appLanguage"
2123
const val SERVICE_MODE = "serviceMode"

app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,9 @@ object DataStore : OnPreferenceDataStoreChangeListener {
103103
var isExpert by configurationStore.boolean(Key.APP_EXPERT)
104104
var appTheme by configurationStore.int(Key.APP_THEME)
105105
var nightTheme by configurationStore.stringToInt(Key.NIGHT_THEME)
106-
// -1 = not set (no Dracula override active). Otherwise holds the night-mode
107-
// value to restore when leaving the dark-only Dracula theme.
106+
// -1 = not set (no dark-only-theme override active). Otherwise holds the
107+
// night-mode value to restore when leaving a dark-only theme (Dracula or
108+
// Dark High Contrast). Key name kept for backward compatibility.
108109
var nightThemeBeforeDracula by configurationStore.int(Key.NIGHT_THEME_BEFORE_DRACULA) { -1 }
109110
var appLanguage by configurationStore.string(Key.APP_LANGUAGE) { "" }
110111
var serviceMode by configurationStore.string(Key.SERVICE_MODE) { Key.MODE_VPN }

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

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,14 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
7272
}
7373
val themeId = newTheme as Int
7474
val previousTheme = DataStore.appTheme // still the old value at this point
75-
// Dracula is a dark-only theme: force night mode on so its #282a36
76-
// canvas (values-night) takes effect instead of a washed-out light surface.
77-
// Remember the prior night-mode setting so it can be restored on exit.
78-
if (themeId == Theme.DRACULA) {
79-
if (previousTheme != Theme.DRACULA && DataStore.nightTheme != 1) {
75+
// Dark-only themes (Dracula, Dark High Contrast) only look right in
76+
// night mode: force it on so their dark canvas (values-night) takes
77+
// effect instead of a washed-out light surface. Remember the prior
78+
// night-mode setting so it can be restored when leaving such a theme.
79+
val enteringDarkOnly = themeId in Theme.DARK_ONLY_THEMES
80+
val leavingDarkOnly = previousTheme in Theme.DARK_ONLY_THEMES
81+
if (enteringDarkOnly) {
82+
if (!leavingDarkOnly && DataStore.nightTheme != 1) {
8083
DataStore.nightThemeBeforeDracula = DataStore.nightTheme
8184
Theme.currentNightMode = 1
8285
// nightTheme.value persists to configurationStore (same key as
@@ -85,8 +88,8 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
8588
nightTheme.value = "1"
8689
Theme.applyNightTheme()
8790
}
88-
} else if (previousTheme == Theme.DRACULA) {
89-
// Leaving Dracula: restore the night-mode setting we forced on.
91+
} else if (leavingDarkOnly) {
92+
// Leaving a dark-only theme: restore the night-mode setting we forced on.
9093
val restore = DataStore.nightThemeBeforeDracula
9194
if (restore != -1) {
9295
DataStore.nightThemeBeforeDracula = -1
@@ -107,7 +110,7 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
107110
nightTheme.setOnPreferenceChangeListener { _, newTheme ->
108111
Theme.currentNightMode = (newTheme as String).toInt()
109112
// A manual night-mode change takes precedence: drop any pending
110-
// Dracula restore so we don't override the user's choice later.
113+
// dark-only-theme restore so we don't override the user's choice later.
111114
DataStore.nightThemeBeforeDracula = -1
112115
Theme.applyNightTheme()
113116
true

app/src/main/java/io/nekohasekai/sagernet/utils/Theme.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ object Theme {
3333
const val VERDANT_MINT = 22
3434
const val DRACULA = 23
3535
const val DYNAMIC = 24
36+
const val DARK_HIGH_CONTRAST = 25
37+
38+
/**
39+
* Themes that only make sense in dark mode: selecting one forces night mode
40+
* on so its dark canvas (values-night) takes effect, and the prior night
41+
* setting is restored on exit (see SettingsPreferenceFragment). Dracula was
42+
* the first such theme; Dark High Contrast (OLED black) is dark-only too.
43+
*/
44+
val DARK_ONLY_THEMES = setOf(DRACULA, DARK_HIGH_CONTRAST)
3645

3746
private fun defaultTheme() = PINK_SSR
3847

@@ -51,6 +60,7 @@ object Theme {
5160
* Order here is the display order in the dialog.
5261
*/
5362
val MODERN_THEMES: List<ThemeInfo> = listOf(
63+
ThemeInfo(DARK_HIGH_CONTRAST, R.string.theme_dark_high_contrast, R.color.dhc_primary),
5464
ThemeInfo(DYNAMIC, R.string.theme_dynamic, R.color.color_dynamic_swatch),
5565
)
5666

@@ -95,6 +105,7 @@ object Theme {
95105
BLACK -> R.style.Theme_SagerNet_Black
96106
VERDANT_MINT -> R.style.Theme_SagerNet_VerdantMint
97107
DRACULA -> R.style.Theme_SagerNet_Dracula
108+
DARK_HIGH_CONTRAST -> R.style.Theme_SagerNet_DarkHighContrast
98109
DYNAMIC -> R.style.Theme_SagerNet
99110
else -> getTheme(defaultTheme())
100111
}
@@ -125,6 +136,7 @@ object Theme {
125136
BLACK -> R.style.Theme_SagerNet_Dialog_Black
126137
VERDANT_MINT -> R.style.Theme_SagerNet_Dialog_VerdantMint
127138
DRACULA -> R.style.Theme_SagerNet_Dialog_Dracula
139+
DARK_HIGH_CONTRAST -> R.style.Theme_SagerNet_Dialog_DarkHighContrast
128140
DYNAMIC -> R.style.Theme_SagerNet_Dialog
129141
else -> getDialogTheme(defaultTheme())
130142
}

app/src/main/java/moe/matsuri/nb4a/ui/ThemePickerPreference.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,9 +225,12 @@ class ThemePickerPreference
225225
}
226226

227227
private fun select(themeId: Int) {
228-
// Match the legacy swatch-grid ordering: persist first, then notify the
229-
// change listener (SettingsPreferenceFragment applies/recreates).
230-
persistInt(themeId)
231-
callChangeListener(themeId)
228+
// Notify the change listener first (while DataStore.appTheme still holds the
229+
// previous theme, which SettingsPreferenceFragment reads to drive dark-only
230+
// night-mode handling), then persist if accepted. This is the standard
231+
// Preference contract; the listener always returns true here.
232+
if (callChangeListener(themeId)) {
233+
persistInt(themeId)
234+
}
232235
}
233236
}

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

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,89 @@
8787
<item name="statusTestingColor">@color/color_dracula_yellow</item>
8888
<item name="routeProxyColor">@color/color_dracula_cyan</item>
8989
</style>
90+
91+
<!-- Dark High Contrast (OLED) night variant. Night is this theme's active path
92+
(it forces night mode on), so the full canvas + M3 roles are repeated here
93+
to match the day style declared in values/themes.xml. -->
94+
<style name="Theme.SagerNet.DarkHighContrast" parent="Theme.SagerNet">
95+
<item name="colorPrimary">@color/dhc_primary</item>
96+
<item name="colorPrimaryDark">@color/dhc_primary_dark</item>
97+
<item name="colorAccent">@color/dhc_accent</item>
98+
<item name="colorOnPrimary">@color/dhc_on_primary</item>
99+
<item name="colorMaterial100">@color/dhc_100</item>
100+
<item name="colorMaterial300">@color/dhc_300</item>
101+
<item name="cardElevatedSurfaceColor">@color/card_elevated_dhc</item>
102+
103+
<item name="colorSurface">@color/dhc_surface</item>
104+
<item name="colorOnSurface">@color/dhc_on_surface</item>
105+
<item name="android:colorBackground">@color/dhc_background</item>
106+
<item name="colorOnBackground">@color/dhc_on_surface</item>
107+
<item name="android:windowBackground">@color/dhc_background</item>
108+
<item name="colorPrimaryContainer">@color/dhc_container</item>
109+
<item name="colorOnPrimaryContainer">@color/dhc_on_surface</item>
110+
<item name="colorSecondary">@color/dhc_accent</item>
111+
<item name="colorSecondaryContainer">@color/dhc_container</item>
112+
<item name="colorOnSecondaryContainer">@color/dhc_on_surface</item>
113+
<item name="colorTertiary">@color/dhc_blue</item>
114+
<item name="colorTertiaryContainer">@color/dhc_container</item>
115+
<item name="colorOnTertiaryContainer">@color/dhc_on_surface</item>
116+
<item name="colorSurfaceVariant">@color/dhc_surface_variant</item>
117+
<item name="colorOnSurfaceVariant">@color/dhc_on_surface_variant</item>
118+
<item name="colorOutline">@color/dhc_outline</item>
119+
<item name="colorOutlineVariant">@color/dhc_outline_variant</item>
120+
<item name="accentOrTextSecondary">@color/dhc_accent</item>
121+
<item name="accentOrTextPrimary">@color/dhc_accent</item>
122+
<item name="primaryOrTextSecondary">@color/dhc_primary</item>
123+
<item name="primaryOrTextPrimary">@color/dhc_primary</item>
124+
<item name="protocolColor">@color/dhc_green</item>
125+
<item name="profileNameColor">@color/dhc_on_surface</item>
126+
<item name="statusConnectedColor">@color/dhc_green</item>
127+
<item name="statusStoppedColor">@color/dhc_red</item>
128+
<item name="statusConnectingColor">@color/dhc_yellow</item>
129+
<item name="testFailColor">@color/dhc_red</item>
130+
<item name="speedTextColor">@color/dhc_accent</item>
131+
<item name="fabStoppedColor">@color/dhc_on_primary</item>
132+
<item name="statusDetailColor">@color/dhc_accent</item>
133+
<item name="statusTestingColor">@color/dhc_yellow</item>
134+
<item name="routeProxyColor">@color/dhc_blue</item>
135+
<item name="navSelectedColor">?attr/colorPrimary</item>
136+
</style>
137+
138+
<style name="Theme.SagerNet.Dialog.DarkHighContrast" parent="Theme.SagerNet.Dialog">
139+
<item name="colorPrimary">@color/dhc_primary</item>
140+
<item name="colorPrimaryDark">@color/dhc_primary_dark</item>
141+
<item name="colorAccent">@color/dhc_accent</item>
142+
<item name="colorOnPrimary">@color/dhc_on_primary</item>
143+
<item name="colorMaterial100">@color/dhc_100</item>
144+
<item name="colorMaterial300">@color/dhc_300</item>
145+
<item name="cardElevatedSurfaceColor">@color/card_elevated_dhc</item>
146+
147+
<item name="colorSurface">@color/dhc_surface</item>
148+
<item name="colorOnSurface">@color/dhc_on_surface</item>
149+
<item name="android:colorBackground">@color/dhc_background</item>
150+
<item name="colorOnBackground">@color/dhc_on_surface</item>
151+
<item name="colorPrimaryContainer">@color/dhc_container</item>
152+
<item name="colorOnPrimaryContainer">@color/dhc_on_surface</item>
153+
<item name="colorSecondary">@color/dhc_accent</item>
154+
<item name="colorSecondaryContainer">@color/dhc_container</item>
155+
<item name="colorOnSecondaryContainer">@color/dhc_on_surface</item>
156+
<item name="colorTertiary">@color/dhc_blue</item>
157+
<item name="colorTertiaryContainer">@color/dhc_container</item>
158+
<item name="colorOnTertiaryContainer">@color/dhc_on_surface</item>
159+
<item name="colorSurfaceVariant">@color/dhc_surface_variant</item>
160+
<item name="colorOnSurfaceVariant">@color/dhc_on_surface_variant</item>
161+
<item name="colorOutline">@color/dhc_outline</item>
162+
<item name="colorOutlineVariant">@color/dhc_outline_variant</item>
163+
<item name="protocolColor">@color/dhc_green</item>
164+
<item name="profileNameColor">@color/dhc_on_surface</item>
165+
<item name="statusConnectedColor">@color/dhc_green</item>
166+
<item name="statusStoppedColor">@color/dhc_red</item>
167+
<item name="statusConnectingColor">@color/dhc_yellow</item>
168+
<item name="testFailColor">@color/dhc_red</item>
169+
<item name="speedTextColor">@color/dhc_accent</item>
170+
<item name="fabStoppedColor">@color/dhc_on_primary</item>
171+
<item name="statusDetailColor">@color/dhc_accent</item>
172+
<item name="statusTestingColor">@color/dhc_yellow</item>
173+
<item name="routeProxyColor">@color/dhc_blue</item>
174+
</style>
90175
</resources>

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,4 +374,30 @@
374374
<color name="dracula_surface">#343746</color>
375375
<color name="dracula_on_surface">#F8F8F2</color>
376376

377+
<!-- Dark High Contrast (OLED): a full Material 3 dark-only theme on a true
378+
black canvas. Green-primary (GitHub action-button style) with a bright
379+
blue secondary for links/selection; maximally legible near-white text.
380+
The theme forces night mode on, so the live canvas comes from
381+
values-night; these base declarations exist only so resources always
382+
resolve in a non-night configuration. -->
383+
<color name="dhc_primary">#3FB950</color> <!-- green action accent -->
384+
<color name="dhc_primary_dark">#2EA043</color> <!-- darker green (status bar) -->
385+
<color name="dhc_on_primary">#0A0A0A</color> <!-- dark text on green -->
386+
<color name="dhc_accent">#71B7FF</color> <!-- bright blue secondary -->
387+
<color name="dhc_blue">#4493F8</color> <!-- link/route blue -->
388+
<color name="dhc_background">#000000</color> <!-- true OLED black -->
389+
<color name="dhc_surface">#0D0D0D</color>
390+
<color name="dhc_surface_variant">#161616</color>
391+
<color name="dhc_container">#1C1C1C</color>
392+
<color name="dhc_on_surface">#F0F6FC</color> <!-- high-contrast near-white -->
393+
<color name="dhc_on_surface_variant">#C9D1D9</color>
394+
<color name="dhc_outline">#3D444D</color>
395+
<color name="dhc_outline_variant">#21262D</color>
396+
<color name="dhc_green">#3FB950</color>
397+
<color name="dhc_red">#F85149</color>
398+
<color name="dhc_yellow">#D29922</color>
399+
<color name="dhc_100">#1C1C1C</color> <!-- colorMaterial100 stand-in -->
400+
<color name="dhc_300">#2EA043</color> <!-- colorMaterial300 stand-in -->
401+
<color name="card_elevated_dhc">#0D0D0D</color>
402+
377403
</resources>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<string name="menu_about">About</string>
1616
<string name="theme">Theme</string>
1717
<string name="theme_dynamic">Dynamic (Material You)</string>
18+
<string name="theme_dark_high_contrast">Dark High Contrast</string>
1819
<string name="theme_classic_colors">Classic colors…</string>
1920
<string name="theme_classic_color_swatch">Color theme %1$d</string>
2021
<string name="document">Document</string>

0 commit comments

Comments
 (0)