diff --git a/app/src/main/java/io/nekohasekai/sagernet/ktx/Dialogs.kt b/app/src/main/java/io/nekohasekai/sagernet/ktx/Dialogs.kt index 3dbc532d5..24e5a40a1 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ktx/Dialogs.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ktx/Dialogs.kt @@ -18,7 +18,7 @@ fun Fragment.alert(text: String) = requireContext().alert(text) fun AlertDialog.tryToShow() { try { - val activity = context as Activity + val activity = context.unwrap() if (!activity.isFinishing) { show() } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt b/app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt index 8c43f98b8..2906e4e56 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt @@ -63,6 +63,21 @@ import kotlin.reflect.KProperty0 fun String?.blankAsNull(): String? = if (isNullOrBlank()) null else this +/** + * Resolve the hosting object of type [T] (e.g. the Activity / LifecycleOwner) from a View's + * Context. Material 3 themes wrap the view context in a ContextThemeWrapper, so a direct + * `context as Activity` cast throws ClassCastException; walk the ContextWrapper.baseContext + * chain instead. + */ +inline fun Context.unwrap(): T { + var ctx: Context? = this + while (ctx != null) { + if (ctx is T) return ctx + ctx = (ctx as? android.content.ContextWrapper)?.baseContext + } + error("Could not unwrap ${T::class.java.simpleName} from context") +} + inline fun Iterable.forEachTry(action: (T) -> Unit) { var result: Exception? = null for (element in this) try { diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/ThemedActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/ThemedActivity.kt index 15d72f221..1825b7385 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/ThemedActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ThemedActivity.kt @@ -12,6 +12,7 @@ import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.color.DynamicColors import com.google.android.material.snackbar.Snackbar import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.database.DataStore @@ -33,6 +34,11 @@ abstract class ThemedActivity : AppCompatActivity { } Theme.applyNightTheme() + // Only the explicit Dynamic (Material You) theme should use wallpaper colors. + // The hand-picked themes keep their legacy palettes instead of being reseeded + // into Material 3's generated tonal roles. + if (!isDialog) applyDynamicColors() + super.onCreate(savedInstanceState) uiMode = resources.configuration.uiMode @@ -63,6 +69,18 @@ abstract class ThemedActivity : AppCompatActivity { themeResId = resId } + /** + * Apply Material 3 dynamic color ONLY when the user explicitly picks the Dynamic + * (Material You) theme, and only on Android 12+ where a wallpaper palette exists. + * The hand-designed themes keep their own colors untouched — forcing a content-based + * reseed on them mangled their palettes into arbitrary M3 tones. + */ + private fun applyDynamicColors() { + if (DataStore.appTheme == Theme.DYNAMIC) { + DynamicColors.applyToActivityIfAvailable(this) + } + } + override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/Theme.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/Theme.kt index 0f24ad364..2e228a58c 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/utils/Theme.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/utils/Theme.kt @@ -32,6 +32,7 @@ object Theme { const val BLACK = 21 const val VERDANT_MINT = 22 const val DRACULA = 23 + const val DYNAMIC = 24 private fun defaultTheme() = PINK_SSR @@ -76,6 +77,7 @@ object Theme { BLACK -> R.style.Theme_SagerNet_Black VERDANT_MINT -> R.style.Theme_SagerNet_VerdantMint DRACULA -> R.style.Theme_SagerNet_Dracula + DYNAMIC -> R.style.Theme_SagerNet else -> getTheme(defaultTheme()) } } @@ -105,6 +107,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 + DYNAMIC -> R.style.Theme_SagerNet_Dialog else -> getDialogTheme(defaultTheme()) } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt b/app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt index 6a578a823..d5b69e7e2 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt @@ -20,6 +20,7 @@ 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 io.nekohasekai.sagernet.ktx.unwrap import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -66,11 +67,12 @@ class ServiceButton @JvmOverloads constructor( private val iconConnecting by lazy { AnimatedState(R.drawable.ic_service_connecting) { hideProgress() - delayedAnimation = (context as LifecycleOwner).lifecycleScope.launch { + val owner = context.unwrap() + delayedAnimation = owner.lifecycleScope.launch { delay(context.resources.getInteger(android.R.integer.config_mediumAnimTime) + 1000L) // Gate the UI mutation on STARTED so a delayed progress reveal doesn't run // while the activity is stopped (the old launchWhenStarted suspended here). - (context as LifecycleOwner).withStarted { + owner.withStarted { isIndeterminate = true show() } diff --git a/app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt b/app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt index a6cdde97a..c3db54ab2 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt @@ -122,7 +122,7 @@ class StatsBar @JvmOverloads constructor( } fun changeState(state: BaseService.State) { - val activity = context as MainActivity + val activity = context.unwrap() fun postWhenStarted(what: () -> Unit) = activity.lifecycleScope.launch(Dispatchers.Main) { delay(100L) activity.withStarted { what() } @@ -172,7 +172,7 @@ class StatsBar @JvmOverloads constructor( } fun testConnection() { - val activity = context as MainActivity + val activity = context.unwrap() isEnabled = false // "Testing…" in the testing color. statusText.setTextColor(context.getColorAttr(R.attr.statusTestingColor)) diff --git a/app/src/main/res/layout/layout_app_list.xml b/app/src/main/res/layout/layout_app_list.xml index 2370531fb..976e405c9 100644 --- a/app/src/main/res/layout/layout_app_list.xml +++ b/app/src/main/res/layout/layout_app_list.xml @@ -28,7 +28,7 @@ android:theme="?actionBarTheme" android:touchscreenBlocksFocus="false" app:layout_collapseMode="pin" - app:popupTheme="@style/ThemeOverlay.AppCompat.DayNight" + app:popupTheme="@style/ThemeOverlay.SagerNet.Toolbar.Popup" app:title="@string/app_name" /> \ No newline at end of file diff --git a/app/src/main/res/layout/layout_apps.xml b/app/src/main/res/layout/layout_apps.xml index 67463768d..41c9f2f83 100644 --- a/app/src/main/res/layout/layout_apps.xml +++ b/app/src/main/res/layout/layout_apps.xml @@ -28,7 +28,7 @@ android:theme="?actionBarTheme" android:touchscreenBlocksFocus="false" app:layout_collapseMode="pin" - app:popupTheme="@style/ThemeOverlay.AppCompat.DayNight" + app:popupTheme="@style/ThemeOverlay.SagerNet.Toolbar.Popup" app:title="@string/app_name" /> - - - - - @@ -99,6 +76,35 @@ + + + + + + + android:theme="@style/ThemeOverlay.Material3.Dark.ActionBar"> + app:popupTheme="@style/ThemeOverlay.SagerNet.Toolbar.Popup" /> diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index b14f9246c..cb7b8dfa0 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -5,6 +5,11 @@ #1FFFFFFF + + #938F99 + #EF5350 #66BB6A diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index e146ce488..c1abd02a6 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -26,6 +26,18 @@ @color/dracula_background @color/dracula_on_surface @color/dracula_background + + @color/color_dracula_container + @color/dracula_on_surface + @color/color_dracula_container + @color/dracula_on_surface + @color/color_dracula_container + @color/dracula_on_surface + @color/color_dracula_surface_variant + @color/dracula_on_surface + @color/color_dracula_outline @color/color_dracula_green @@ -54,6 +66,15 @@ @color/dracula_on_surface @color/dracula_background @color/dracula_on_surface + @color/color_dracula_container + @color/dracula_on_surface + @color/color_dracula_container + @color/dracula_on_surface + @color/color_dracula_container + @color/dracula_on_surface + @color/color_dracula_surface_variant + @color/dracula_on_surface + @color/color_dracula_outline @color/color_dracula_green @color/color_dracula_yellow @color/color_dracula_green diff --git a/app/src/main/res/values-v26/themes.xml b/app/src/main/res/values-v26/themes.xml index 58a704e87..895cc4c39 100644 --- a/app/src/main/res/values-v26/themes.xml +++ b/app/src/main/res/values-v26/themes.xml @@ -1,7 +1,9 @@ - - - - - - - + + + + + - + + - - @@ -233,10 +304,10 @@ 0dp 0dp - - @@ -244,16 +315,16 @@ rounded @dimen/button_corner_radius - - - @@ -270,6 +341,7 @@ @color/material_amber_500 @color/material_amber_700 @color/material_amber_accent_200 + @color/black @color/material_amber_100 @color/material_amber_300 @color/card_elevated_amber @@ -378,6 +450,7 @@ @color/material_lime_500 @color/material_lime_700 @color/material_lime_accent_200 + @color/black @color/material_lime_100 @color/material_lime_300 @color/card_elevated_lime @@ -423,6 +496,7 @@ @color/material_yellow_500 @color/material_yellow_700 @color/material_yellow_accent_200 + @color/black @color/material_yellow_100 @color/material_yellow_300 @color/card_elevated_yellow @@ -433,6 +507,7 @@ @color/material_amber_500 @color/material_amber_700 @color/material_amber_accent_200 + @color/black @color/material_amber_100 @color/material_amber_300 @color/card_elevated_amber @@ -541,6 +616,7 @@ @color/material_lime_500 @color/material_lime_700 @color/material_lime_accent_200 + @color/black @color/material_lime_100 @color/material_lime_300 @color/card_elevated_lime @@ -595,6 +671,7 @@ @color/material_yellow_500 @color/material_yellow_700 @color/material_yellow_accent_200 + @color/black @color/material_yellow_100 @color/material_yellow_300 @color/card_elevated_yellow @@ -610,10 +687,17 @@ @color/card_elevated_black @color/color_ng_black_accent - @color/color_ng_black_accent + @color/color_ng_black_primary @color/color_ng_black_accent @color/color_ng_black_accent + + @color/color_ng_black_container + @color/color_ng_black_container + @color/color_ng_black_container + @color/color_ng_black_surface_variant + @color/color_ng_black_accent + ?android:textColorSecondary ?android:textColorPrimary ?android:textColorSecondary @@ -663,9 +747,16 @@ @color/card_elevated_black @color/color_ng_black_accent - @color/color_ng_black_accent + @color/color_ng_black_primary @color/color_ng_black_accent + + @color/color_ng_black_container + @color/color_ng_black_container + @color/color_ng_black_container + @color/color_ng_black_surface_variant + @color/color_ng_black_accent + ?android:textColorSecondary ?android:textColorPrimary ?android:textColorSecondary @@ -701,7 +792,7 @@ @color/color_dracula_cyan -