From 7933aa4d11dd59e241e2ba28c34e72a222e6db52 Mon Sep 17 00:00:00 2001 From: Ash Date: Thu, 5 May 2022 15:48:35 +0800 Subject: [PATCH] Add dark theme settings --- .../preference/AmoledDarkThemePreference.kt | 41 +++++++ .../data/preference/DarkThemePreference.kt | 66 ++++++++++++ .../me/ash/reader/data/preference/Settings.kt | 14 ++- .../reader/ui/component/DynamicSVGImage.kt | 4 +- .../java/me/ash/reader/ui/ext/DataStoreExt.kt | 10 ++ .../me/ash/reader/ui/page/common/HomeEntry.kt | 11 +- .../me/ash/reader/ui/page/common/RouteName.kt | 1 + .../ui/page/settings/color/ColorAndStyle.kt | 25 +++-- .../ui/page/settings/color/DarkTheme.kt | 100 ++++++++++++++++++ .../main/java/me/ash/reader/ui/theme/Theme.kt | 4 - .../ui/theme/palette/DynamicTonalPalette.kt | 14 ++- 11 files changed, 262 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/me/ash/reader/data/preference/AmoledDarkThemePreference.kt create mode 100644 app/src/main/java/me/ash/reader/data/preference/DarkThemePreference.kt create mode 100644 app/src/main/java/me/ash/reader/ui/page/settings/color/DarkTheme.kt diff --git a/app/src/main/java/me/ash/reader/data/preference/AmoledDarkThemePreference.kt b/app/src/main/java/me/ash/reader/data/preference/AmoledDarkThemePreference.kt new file mode 100644 index 0000000..30e2c85 --- /dev/null +++ b/app/src/main/java/me/ash/reader/data/preference/AmoledDarkThemePreference.kt @@ -0,0 +1,41 @@ +package me.ash.reader.data.preference + +import android.content.Context +import androidx.datastore.preferences.core.Preferences +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import me.ash.reader.ui.ext.DataStoreKeys +import me.ash.reader.ui.ext.dataStore +import me.ash.reader.ui.ext.put + +sealed class AmoledDarkThemePreference(val value: Boolean) : Preference() { + object ON : AmoledDarkThemePreference(true) + object OFF : AmoledDarkThemePreference(false) + + override fun put(context: Context, scope: CoroutineScope) { + scope.launch { + context.dataStore.put( + DataStoreKeys.AmoledDarkTheme, + value + ) + } + } + + companion object { + val default = OFF + val values = listOf(ON, OFF) + + fun fromPreferences(preferences: Preferences) = + when (preferences[DataStoreKeys.AmoledDarkTheme.key]) { + true -> ON + false -> OFF + else -> default + } + } +} + +operator fun AmoledDarkThemePreference.not(): AmoledDarkThemePreference = + when (value) { + true -> AmoledDarkThemePreference.OFF + false -> AmoledDarkThemePreference.ON + } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/preference/DarkThemePreference.kt b/app/src/main/java/me/ash/reader/data/preference/DarkThemePreference.kt new file mode 100644 index 0000000..a5329df --- /dev/null +++ b/app/src/main/java/me/ash/reader/data/preference/DarkThemePreference.kt @@ -0,0 +1,66 @@ +package me.ash.reader.data.preference + +import android.content.Context +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import androidx.datastore.preferences.core.Preferences +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import me.ash.reader.R +import me.ash.reader.ui.ext.DataStoreKeys +import me.ash.reader.ui.ext.dataStore +import me.ash.reader.ui.ext.put + +sealed class DarkThemePreference(val value: Int) : Preference() { + object UseDeviceTheme : DarkThemePreference(0) + object ON : DarkThemePreference(1) + object OFF : DarkThemePreference(2) + + override fun put(context: Context, scope: CoroutineScope) { + scope.launch { + context.dataStore.put( + DataStoreKeys.DarkTheme, + value + ) + } + } + + fun getDesc(context: Context): String = + when (this) { + UseDeviceTheme -> context.getString(R.string.use_device_theme) + ON -> context.getString(R.string.on) + OFF -> context.getString(R.string.off) + } + + @Composable + fun isDarkTheme(): Boolean = when (this) { + UseDeviceTheme -> isSystemInDarkTheme() + ON -> true + OFF -> false + } + + companion object { + val default = UseDeviceTheme + val values = listOf(UseDeviceTheme, ON, OFF) + + fun fromPreferences(preferences: Preferences) = + when (preferences[DataStoreKeys.DarkTheme.key]) { + 0 -> UseDeviceTheme + 1 -> ON + 2 -> OFF + else -> default + } + } +} + +@Composable +operator fun DarkThemePreference.not(): DarkThemePreference = + when (this) { + DarkThemePreference.UseDeviceTheme -> if (isSystemInDarkTheme()) { + DarkThemePreference.OFF + } else { + DarkThemePreference.ON + } + DarkThemePreference.ON -> DarkThemePreference.OFF + DarkThemePreference.OFF -> DarkThemePreference.ON + } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/preference/Settings.kt b/app/src/main/java/me/ash/reader/data/preference/Settings.kt index b1d4022..c97e359 100644 --- a/app/src/main/java/me/ash/reader/data/preference/Settings.kt +++ b/app/src/main/java/me/ash/reader/data/preference/Settings.kt @@ -14,6 +14,8 @@ import me.ash.reader.ui.ext.dataStore data class Settings( val themeIndex: Int = ThemeIndexPreference.default, val customPrimaryColor: String = CustomPrimaryColorPreference.default, + val darkTheme: DarkThemePreference = DarkThemePreference.default, + val amoledDarkTheme: AmoledDarkThemePreference = AmoledDarkThemePreference.default, val feedsFilterBarStyle: FeedsFilterBarStylePreference = FeedsFilterBarStylePreference.default, val feedsFilterBarFilled: FeedsFilterBarFilledPreference = FeedsFilterBarFilledPreference.default, @@ -41,6 +43,8 @@ fun Preferences.toSettings(): Settings { return Settings( themeIndex = ThemeIndexPreference.fromPreferences(this), customPrimaryColor = CustomPrimaryColorPreference.fromPreferences(this), + darkTheme = DarkThemePreference.fromPreferences(this), + amoledDarkTheme = AmoledDarkThemePreference.fromPreferences(this), feedsFilterBarStyle = FeedsFilterBarStylePreference.fromPreferences(this), feedsFilterBarFilled = FeedsFilterBarFilledPreference.fromPreferences(this), @@ -60,7 +64,9 @@ fun Preferences.toSettings(): Settings { flowArticleListImage = FlowArticleListImagePreference.fromPreferences(this), flowArticleListDesc = FlowArticleListDescPreference.fromPreferences(this), flowArticleListTime = FlowArticleListTimePreference.fromPreferences(this), - flowArticleListDateStickyHeader = FlowArticleListDateStickyHeaderPreference.fromPreferences(this), + flowArticleListDateStickyHeader = FlowArticleListDateStickyHeaderPreference.fromPreferences( + this + ), flowArticleListTonalElevation = FlowArticleListTonalElevationPreference.fromPreferences(this), ) } @@ -80,6 +86,8 @@ fun SettingsProvider( CompositionLocalProvider( LocalThemeIndex provides settings.themeIndex, LocalCustomPrimaryColor provides settings.customPrimaryColor, + LocalDarkTheme provides settings.darkTheme, + LocalAmoledDarkTheme provides settings.amoledDarkTheme, LocalFeedsTopBarTonalElevation provides settings.feedsTopBarTonalElevation, LocalFeedsGroupListExpand provides settings.feedsGroupListExpand, @@ -110,6 +118,10 @@ val LocalThemeIndex = compositionLocalOf { ThemeIndexPreference.default } val LocalCustomPrimaryColor = compositionLocalOf { CustomPrimaryColorPreference.default } +val LocalDarkTheme = + compositionLocalOf { DarkThemePreference.default } +val LocalAmoledDarkTheme = + compositionLocalOf { AmoledDarkThemePreference.default } val LocalFeedsFilterBarStyle = compositionLocalOf { FeedsFilterBarStylePreference.default } diff --git a/app/src/main/java/me/ash/reader/ui/component/DynamicSVGImage.kt b/app/src/main/java/me/ash/reader/ui/component/DynamicSVGImage.kt index 27048b1..f5a7e19 100644 --- a/app/src/main/java/me/ash/reader/ui/component/DynamicSVGImage.kt +++ b/app/src/main/java/me/ash/reader/ui/component/DynamicSVGImage.kt @@ -13,8 +13,8 @@ import coil.compose.AsyncImage import coil.imageLoader import coil.request.ImageRequest import com.caverock.androidsvg.SVG +import me.ash.reader.data.preference.LocalDarkTheme import me.ash.reader.ui.svg.parseDynamicColor -import me.ash.reader.ui.theme.LocalUseDarkTheme import me.ash.reader.ui.theme.palette.LocalTonalPalettes @Composable @@ -24,7 +24,7 @@ fun DynamicSVGImage( contentDescription: String, ) { val context = LocalContext.current - val useDarkTheme = LocalUseDarkTheme.current + val useDarkTheme = LocalDarkTheme.current.isDarkTheme() val tonalPalettes = LocalTonalPalettes.current var size by remember { mutableStateOf(IntSize.Zero) } val pic by remember(tonalPalettes, size) { diff --git a/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt b/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt index 79728c4..68dc81b 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt @@ -130,6 +130,16 @@ sealed class DataStoreKeys { get() = stringPreferencesKey("customPrimaryColor") } + object DarkTheme : DataStoreKeys() { + override val key: Preferences.Key + get() = intPreferencesKey("darkTheme") + } + + object AmoledDarkTheme : DataStoreKeys() { + override val key: Preferences.Key + get() = booleanPreferencesKey("amoledDarkTheme") + } + object FeedsFilterBarStyle : DataStoreKeys() { override val key: Preferences.Key get() = intPreferencesKey("feedsFilterBarStyle") diff --git a/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt b/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt index 4aed762..f8b3943 100644 --- a/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt +++ b/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt @@ -14,6 +14,7 @@ import com.google.accompanist.navigation.animation.AnimatedNavHost import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.google.accompanist.systemuicontroller.rememberSystemUiController import me.ash.reader.data.entity.Filter +import me.ash.reader.data.preference.LocalDarkTheme import me.ash.reader.ui.ext.* import me.ash.reader.ui.page.home.HomeViewAction import me.ash.reader.ui.page.home.HomeViewModel @@ -22,13 +23,13 @@ import me.ash.reader.ui.page.home.flow.FlowPage import me.ash.reader.ui.page.home.read.ReadPage import me.ash.reader.ui.page.settings.SettingsPage import me.ash.reader.ui.page.settings.color.ColorAndStyle +import me.ash.reader.ui.page.settings.color.DarkTheme import me.ash.reader.ui.page.settings.color.feeds.FeedsPageStyle import me.ash.reader.ui.page.settings.color.flow.FlowPageStyle import me.ash.reader.ui.page.settings.interaction.Interaction import me.ash.reader.ui.page.settings.tips.TipsAndSupport import me.ash.reader.ui.page.startup.StartupPage import me.ash.reader.ui.theme.AppTheme -import me.ash.reader.ui.theme.LocalUseDarkTheme @OptIn(ExperimentalAnimationApi::class, androidx.compose.material.ExperimentalMaterialApi::class) @Composable @@ -86,8 +87,9 @@ fun HomeEntry( } } - AppTheme { - val useDarkTheme = LocalUseDarkTheme.current + val useDarkTheme = LocalDarkTheme.current.isDarkTheme() + + AppTheme(useDarkTheme = useDarkTheme) { rememberSystemUiController().run { setStatusBarColor(Color.Transparent, !useDarkTheme) @@ -129,6 +131,9 @@ fun HomeEntry( animatedComposable(route = RouteName.COLOR_AND_STYLE) { ColorAndStyle(navController) } + animatedComposable(route = RouteName.DARK_THEME) { + DarkTheme(navController) + } animatedComposable(route = RouteName.FEEDS_PAGE_STYLE) { FeedsPageStyle(navController) } diff --git a/app/src/main/java/me/ash/reader/ui/page/common/RouteName.kt b/app/src/main/java/me/ash/reader/ui/page/common/RouteName.kt index baf917a..2de9d92 100644 --- a/app/src/main/java/me/ash/reader/ui/page/common/RouteName.kt +++ b/app/src/main/java/me/ash/reader/ui/page/common/RouteName.kt @@ -14,6 +14,7 @@ object RouteName { // Color & Style const val COLOR_AND_STYLE = "color_and_style" + const val DARK_THEME = "dark_theme" const val FEEDS_PAGE_STYLE = "feeds_page_style" const val FLOW_PAGE_STYLE = "flow_page_style" diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/color/ColorAndStyle.kt b/app/src/main/java/me/ash/reader/ui/page/settings/color/ColorAndStyle.kt index 9a675ca..e958d23 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/color/ColorAndStyle.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/color/ColorAndStyle.kt @@ -26,16 +26,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import me.ash.reader.R -import me.ash.reader.data.preference.CustomPrimaryColorPreference -import me.ash.reader.data.preference.LocalCustomPrimaryColor -import me.ash.reader.data.preference.LocalThemeIndex -import me.ash.reader.data.preference.ThemeIndexPreference +import me.ash.reader.data.preference.* import me.ash.reader.ui.component.* import me.ash.reader.ui.page.common.RouteName import me.ash.reader.ui.page.settings.SettingItem import me.ash.reader.ui.svg.PALETTE import me.ash.reader.ui.svg.SVGString -import me.ash.reader.ui.theme.LocalUseDarkTheme import me.ash.reader.ui.theme.palette.* import me.ash.reader.ui.theme.palette.TonalPalettes.Companion.toTonalPalettes import me.ash.reader.ui.theme.palette.dynamic.extractTonalPalettesFromUserWallpaper @@ -47,9 +43,11 @@ fun ColorAndStyle( navController: NavHostController, ) { val context = LocalContext.current - val useDarkTheme = LocalUseDarkTheme.current + val darkTheme = LocalDarkTheme.current + val darkThemeNot = !darkTheme val themeIndex = LocalThemeIndex.current val customPrimaryColor = LocalCustomPrimaryColor.current + val scope = rememberCoroutineScope() val wallpaperTonalPalettes = extractTonalPalettesFromUserWallpaper() var radioButtonSelected by remember { mutableStateOf(if (themeIndex > 4) 0 else 1) } @@ -151,12 +149,19 @@ fun ColorAndStyle( ) SettingItem( title = stringResource(R.string.dark_theme), - desc = stringResource(R.string.use_device_theme), - enable = false, + desc = darkTheme.getDesc(context), separatedActions = true, - onClick = {}, + onClick = { + navController.navigate(RouteName.DARK_THEME) { + launchSingleTop = true + } + }, ) { - Switch(activated = useDarkTheme, enable = false) + Switch( + activated = darkTheme.isDarkTheme() + ) { + darkThemeNot.put(context, scope) + } } SettingItem( title = stringResource(R.string.basic_fonts), diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/color/DarkTheme.kt b/app/src/main/java/me/ash/reader/ui/page/settings/color/DarkTheme.kt new file mode 100644 index 0000000..a927f23 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/settings/color/DarkTheme.kt @@ -0,0 +1,100 @@ +package me.ash.reader.ui.page.settings.color + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import me.ash.reader.R +import me.ash.reader.data.preference.DarkThemePreference +import me.ash.reader.data.preference.LocalAmoledDarkTheme +import me.ash.reader.data.preference.LocalDarkTheme +import me.ash.reader.data.preference.not +import me.ash.reader.ui.component.DisplayText +import me.ash.reader.ui.component.FeedbackIconButton +import me.ash.reader.ui.component.Subtitle +import me.ash.reader.ui.component.Switch +import me.ash.reader.ui.page.settings.SettingItem +import me.ash.reader.ui.theme.palette.onLight + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DarkTheme( + navController: NavHostController, +) { + val context = LocalContext.current + val darkTheme = LocalDarkTheme.current + val amoledDarkTheme = LocalAmoledDarkTheme.current + val scope = rememberCoroutineScope() + + Scaffold( + modifier = Modifier + .background(MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface) + .statusBarsPadding() + .navigationBarsPadding(), + containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface, + topBar = { + SmallTopAppBar( + colors = TopAppBarDefaults.smallTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface + ), + title = {}, + navigationIcon = { + FeedbackIconButton( + imageVector = Icons.Rounded.ArrowBack, + contentDescription = stringResource(R.string.back), + tint = MaterialTheme.colorScheme.onSurface + ) { + navController.popBackStack() + } + }, + actions = {} + ) + }, + content = { + LazyColumn { + item { + DisplayText(text = stringResource(R.string.dark_theme), desc = "") + } + item { + DarkThemePreference.values.map { + SettingItem( + title = it.getDesc(context), + onClick = { + it.put(context, scope) + }, + ) { + RadioButton(selected = it == darkTheme, onClick = { + it.put(context, scope) + }) + } + } + Subtitle( + modifier = Modifier.padding(horizontal = 24.dp), + text = "其他", + ) + SettingItem( + title = "Amoled 的深色主题", + onClick = { + (!amoledDarkTheme).put(context, scope) + }, + ) { + Switch(activated = amoledDarkTheme.value) { + (!amoledDarkTheme).put(context, scope) + } + } + } + } + } + ) +} diff --git a/app/src/main/java/me/ash/reader/ui/theme/Theme.kt b/app/src/main/java/me/ash/reader/ui/theme/Theme.kt index 694093b..cdb48dd 100644 --- a/app/src/main/java/me/ash/reader/ui/theme/Theme.kt +++ b/app/src/main/java/me/ash/reader/ui/theme/Theme.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.compositionLocalOf import me.ash.reader.data.preference.LocalThemeIndex import me.ash.reader.ui.theme.palette.LocalTonalPalettes import me.ash.reader.ui.theme.palette.TonalPalettes @@ -13,8 +12,6 @@ import me.ash.reader.ui.theme.palette.dynamic.extractTonalPalettesFromUserWallpa import me.ash.reader.ui.theme.palette.dynamicDarkColorScheme import me.ash.reader.ui.theme.palette.dynamicLightColorScheme -val LocalUseDarkTheme = compositionLocalOf { false } - @Composable fun AppTheme( useDarkTheme: Boolean = isSystemInDarkTheme(), @@ -38,7 +35,6 @@ fun AppTheme( ProvideZcamViewingConditions { CompositionLocalProvider( LocalTonalPalettes provides tonalPalettes.also { it.Preheating() }, - LocalUseDarkTheme provides useDarkTheme, ) { MaterialTheme( colorScheme = diff --git a/app/src/main/java/me/ash/reader/ui/theme/palette/DynamicTonalPalette.kt b/app/src/main/java/me/ash/reader/ui/theme/palette/DynamicTonalPalette.kt index 3df4107..db48ae6 100644 --- a/app/src/main/java/me/ash/reader/ui/theme/palette/DynamicTonalPalette.kt +++ b/app/src/main/java/me/ash/reader/ui/theme/palette/DynamicTonalPalette.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color -import me.ash.reader.ui.theme.LocalUseDarkTheme +import me.ash.reader.data.preference.LocalDarkTheme @Composable fun dynamicLightColorScheme(): ColorScheme { @@ -68,20 +68,18 @@ fun dynamicDarkColorScheme(): ColorScheme { ) } -@Suppress("NOTHING_TO_INLINE") @Composable -inline infix fun Color.onLight(lightColor: Color): Color = - if (!LocalUseDarkTheme.current) lightColor else this +infix fun Color.onLight(lightColor: Color): Color = + if (!LocalDarkTheme.current.isDarkTheme()) lightColor else this -@Suppress("NOTHING_TO_INLINE") @Composable -inline infix fun Color.onDark(darkColor: Color): Color = - if (LocalUseDarkTheme.current) darkColor else this +infix fun Color.onDark(darkColor: Color): Color = + if (LocalDarkTheme.current.isDarkTheme()) darkColor else this @Composable infix fun Color.alwaysLight(isAlways: Boolean): Color { val colorScheme = MaterialTheme.colorScheme - return if (isAlways && LocalUseDarkTheme.current) { + return if (isAlways && LocalDarkTheme.current.isDarkTheme()) { when (this) { colorScheme.primary -> colorScheme.onPrimary colorScheme.secondary -> colorScheme.onSecondary