Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions app/src/main/java/io/nekohasekai/sagernet/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ object Key {
const val APP_EXPERT = "isExpert"
const val APP_THEME = "appTheme"
const val NIGHT_THEME = "nightTheme"
// Remembers the user's night-mode setting before the dark-only Dracula theme
// forced it on, so it can be restored when switching to another theme.
// Remembers the user's night-mode setting before a dark-only theme (Dracula
// or Dark High Contrast) forced it on, so it can be restored when switching
// to another theme. Storage key kept as "nightThemeBeforeDracula" for
// backward compatibility with previously persisted values.
const val NIGHT_THEME_BEFORE_DRACULA = "nightThemeBeforeDracula"
const val APP_LANGUAGE = "appLanguage"
const val SERVICE_MODE = "serviceMode"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,9 @@ object DataStore : OnPreferenceDataStoreChangeListener {
var isExpert by configurationStore.boolean(Key.APP_EXPERT)
var appTheme by configurationStore.int(Key.APP_THEME)
var nightTheme by configurationStore.stringToInt(Key.NIGHT_THEME)
// -1 = not set (no Dracula override active). Otherwise holds the night-mode
// value to restore when leaving the dark-only Dracula theme.
// -1 = not set (no dark-only-theme override active). Otherwise holds the
// night-mode value to restore when leaving a dark-only theme (Dracula or
// Dark High Contrast). Key name kept for backward compatibility.
var nightThemeBeforeDracula by configurationStore.int(Key.NIGHT_THEME_BEFORE_DRACULA) { -1 }
var appLanguage by configurationStore.string(Key.APP_LANGUAGE) { "" }
var serviceMode by configurationStore.string(Key.SERVICE_MODE) { Key.MODE_VPN }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,22 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
DataStore.initGlobal()
addPreferencesFromResource(R.xml.global_preferences)

val appTheme = findPreference<ColorPickerPreference>(Key.APP_THEME)!!
val appTheme = findPreference<ThemePickerPreference>(Key.APP_THEME)!!
val nightTheme = findPreference<SimpleMenuPreference>(Key.NIGHT_THEME)!!
appTheme.setOnPreferenceChangeListener { _, newTheme ->
if (DataStore.serviceState.started) {
SagerNet.reloadService()
}
val themeId = newTheme as Int
val previousTheme = DataStore.appTheme // still the old value at this point
// Dracula is a dark-only theme: force night mode on so its #282a36
// canvas (values-night) takes effect instead of a washed-out light surface.
// Remember the prior night-mode setting so it can be restored on exit.
if (themeId == Theme.DRACULA) {
if (previousTheme != Theme.DRACULA && DataStore.nightTheme != 1) {
// Dark-only themes (Dracula, Dark High Contrast) only look right in
// night mode: force it on so their dark canvas (values-night) takes
// effect instead of a washed-out light surface. Remember the prior
// night-mode setting so it can be restored when leaving such a theme.
val enteringDarkOnly = themeId in Theme.DARK_ONLY_THEMES
val leavingDarkOnly = previousTheme in Theme.DARK_ONLY_THEMES
if (enteringDarkOnly) {
if (!leavingDarkOnly && DataStore.nightTheme != 1) {
Comment thread
hawkff marked this conversation as resolved.
DataStore.nightThemeBeforeDracula = DataStore.nightTheme
Theme.currentNightMode = 1
// nightTheme.value persists to configurationStore (same key as
Expand All @@ -85,8 +88,8 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
nightTheme.value = "1"
Theme.applyNightTheme()
}
} else if (previousTheme == Theme.DRACULA) {
// Leaving Dracula: restore the night-mode setting we forced on.
} else if (leavingDarkOnly) {
// Leaving a dark-only theme: restore the night-mode setting we forced on.
val restore = DataStore.nightThemeBeforeDracula
if (restore != -1) {
DataStore.nightThemeBeforeDracula = -1
Expand All @@ -107,7 +110,7 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
nightTheme.setOnPreferenceChangeListener { _, newTheme ->
Theme.currentNightMode = (newTheme as String).toInt()
// A manual night-mode change takes precedence: drop any pending
// Dracula restore so we don't override the user's choice later.
// dark-only-theme restore so we don't override the user's choice later.
DataStore.nightThemeBeforeDracula = -1
Theme.applyNightTheme()
true
Expand Down
30 changes: 30 additions & 0 deletions app/src/main/java/io/nekohasekai/sagernet/utils/Theme.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,37 @@ object Theme {
const val VERDANT_MINT = 22
const val DRACULA = 23
const val DYNAMIC = 24
const val DARK_HIGH_CONTRAST = 25

/**
* Themes that only make sense in dark mode: selecting one forces night mode
* on so its dark canvas (values-night) takes effect, and the prior night
* setting is restored on exit (see SettingsPreferenceFragment). Dracula was
* the first such theme; Dark High Contrast (OLED black) is dark-only too.
*/
val DARK_ONLY_THEMES = setOf(DRACULA, DARK_HIGH_CONTRAST)

private fun defaultTheme() = PINK_SSR

/**
* Metadata for a theme shown in the modern named picker.
*
* @param id one of the Theme int constants above (persisted to appTheme)
* @param nameRes display name string resource
* @param previewColor color resource for the preview swatch shown next to the name
*/
data class ThemeInfo(val id: Int, val nameRes: Int, val previewColor: Int)

/**
* Modern, full-fledged M3 themes presented by name in the picker dialog.
* The legacy single-accent palettes stay behind the "Classic colors…" grid.
* Order here is the display order in the dialog.
*/
val MODERN_THEMES: List<ThemeInfo> = listOf(
ThemeInfo(DARK_HIGH_CONTRAST, R.string.theme_dark_high_contrast, R.color.dhc_primary),
ThemeInfo(DYNAMIC, R.string.theme_dynamic, R.color.color_dynamic_swatch),
)

fun apply(context: Context) {
context.setTheme(getTheme())
}
Expand Down Expand Up @@ -77,6 +105,7 @@ object Theme {
BLACK -> R.style.Theme_SagerNet_Black
VERDANT_MINT -> R.style.Theme_SagerNet_VerdantMint
DRACULA -> R.style.Theme_SagerNet_Dracula
DARK_HIGH_CONTRAST -> R.style.Theme_SagerNet_DarkHighContrast
DYNAMIC -> R.style.Theme_SagerNet
else -> getTheme(defaultTheme())
}
Expand Down Expand Up @@ -107,6 +136,7 @@ object Theme {
BLACK -> R.style.Theme_SagerNet_Dialog_Black
VERDANT_MINT -> R.style.Theme_SagerNet_Dialog_VerdantMint
DRACULA -> R.style.Theme_SagerNet_Dialog_Dracula
DARK_HIGH_CONTRAST -> R.style.Theme_SagerNet_Dialog_DarkHighContrast
DYNAMIC -> R.style.Theme_SagerNet_Dialog
else -> getDialogTheme(defaultTheme())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,14 @@ 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.
// Tint the connect FAB icon by state. Connected uses fabConnectedColor
// (defaults to the green connected status color, but themes whose FAB
// background is that same green — e.g. Dark High Contrast — override it
// with a contrasting color so the icon stays visible). Stopped uses
// fabStoppedColor. Transient states fall back to colorOnPrimary, the FAB's
// normal icon color, so unaffected themes look unchanged.
val attr = when (state) {
BaseService.State.Connected -> R.attr.statusConnectedColor
BaseService.State.Connected -> R.attr.fabConnectedColor
BaseService.State.Stopped -> R.attr.fabStoppedColor
else -> com.google.android.material.R.attr.colorOnPrimary
}
Expand Down
118 changes: 0 additions & 118 deletions app/src/main/java/moe/matsuri/nb4a/ui/ColorPickerPreference.kt

This file was deleted.

Loading
Loading