Add dark theme settings

This commit is contained in:
Ash 2022-05-05 15:48:35 +08:00
parent ae1e4cb4ee
commit 7933aa4d11
11 changed files with 262 additions and 28 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -14,6 +14,8 @@ import me.ash.reader.ui.ext.dataStore
data class Settings( data class Settings(
val themeIndex: Int = ThemeIndexPreference.default, val themeIndex: Int = ThemeIndexPreference.default,
val customPrimaryColor: String = CustomPrimaryColorPreference.default, val customPrimaryColor: String = CustomPrimaryColorPreference.default,
val darkTheme: DarkThemePreference = DarkThemePreference.default,
val amoledDarkTheme: AmoledDarkThemePreference = AmoledDarkThemePreference.default,
val feedsFilterBarStyle: FeedsFilterBarStylePreference = FeedsFilterBarStylePreference.default, val feedsFilterBarStyle: FeedsFilterBarStylePreference = FeedsFilterBarStylePreference.default,
val feedsFilterBarFilled: FeedsFilterBarFilledPreference = FeedsFilterBarFilledPreference.default, val feedsFilterBarFilled: FeedsFilterBarFilledPreference = FeedsFilterBarFilledPreference.default,
@ -41,6 +43,8 @@ fun Preferences.toSettings(): Settings {
return Settings( return Settings(
themeIndex = ThemeIndexPreference.fromPreferences(this), themeIndex = ThemeIndexPreference.fromPreferences(this),
customPrimaryColor = CustomPrimaryColorPreference.fromPreferences(this), customPrimaryColor = CustomPrimaryColorPreference.fromPreferences(this),
darkTheme = DarkThemePreference.fromPreferences(this),
amoledDarkTheme = AmoledDarkThemePreference.fromPreferences(this),
feedsFilterBarStyle = FeedsFilterBarStylePreference.fromPreferences(this), feedsFilterBarStyle = FeedsFilterBarStylePreference.fromPreferences(this),
feedsFilterBarFilled = FeedsFilterBarFilledPreference.fromPreferences(this), feedsFilterBarFilled = FeedsFilterBarFilledPreference.fromPreferences(this),
@ -60,7 +64,9 @@ fun Preferences.toSettings(): Settings {
flowArticleListImage = FlowArticleListImagePreference.fromPreferences(this), flowArticleListImage = FlowArticleListImagePreference.fromPreferences(this),
flowArticleListDesc = FlowArticleListDescPreference.fromPreferences(this), flowArticleListDesc = FlowArticleListDescPreference.fromPreferences(this),
flowArticleListTime = FlowArticleListTimePreference.fromPreferences(this), flowArticleListTime = FlowArticleListTimePreference.fromPreferences(this),
flowArticleListDateStickyHeader = FlowArticleListDateStickyHeaderPreference.fromPreferences(this), flowArticleListDateStickyHeader = FlowArticleListDateStickyHeaderPreference.fromPreferences(
this
),
flowArticleListTonalElevation = FlowArticleListTonalElevationPreference.fromPreferences(this), flowArticleListTonalElevation = FlowArticleListTonalElevationPreference.fromPreferences(this),
) )
} }
@ -80,6 +86,8 @@ fun SettingsProvider(
CompositionLocalProvider( CompositionLocalProvider(
LocalThemeIndex provides settings.themeIndex, LocalThemeIndex provides settings.themeIndex,
LocalCustomPrimaryColor provides settings.customPrimaryColor, LocalCustomPrimaryColor provides settings.customPrimaryColor,
LocalDarkTheme provides settings.darkTheme,
LocalAmoledDarkTheme provides settings.amoledDarkTheme,
LocalFeedsTopBarTonalElevation provides settings.feedsTopBarTonalElevation, LocalFeedsTopBarTonalElevation provides settings.feedsTopBarTonalElevation,
LocalFeedsGroupListExpand provides settings.feedsGroupListExpand, LocalFeedsGroupListExpand provides settings.feedsGroupListExpand,
@ -110,6 +118,10 @@ val LocalThemeIndex =
compositionLocalOf { ThemeIndexPreference.default } compositionLocalOf { ThemeIndexPreference.default }
val LocalCustomPrimaryColor = val LocalCustomPrimaryColor =
compositionLocalOf { CustomPrimaryColorPreference.default } compositionLocalOf { CustomPrimaryColorPreference.default }
val LocalDarkTheme =
compositionLocalOf<DarkThemePreference> { DarkThemePreference.default }
val LocalAmoledDarkTheme =
compositionLocalOf<AmoledDarkThemePreference> { AmoledDarkThemePreference.default }
val LocalFeedsFilterBarStyle = val LocalFeedsFilterBarStyle =
compositionLocalOf<FeedsFilterBarStylePreference> { FeedsFilterBarStylePreference.default } compositionLocalOf<FeedsFilterBarStylePreference> { FeedsFilterBarStylePreference.default }

View File

@ -13,8 +13,8 @@ import coil.compose.AsyncImage
import coil.imageLoader import coil.imageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import com.caverock.androidsvg.SVG import com.caverock.androidsvg.SVG
import me.ash.reader.data.preference.LocalDarkTheme
import me.ash.reader.ui.svg.parseDynamicColor import me.ash.reader.ui.svg.parseDynamicColor
import me.ash.reader.ui.theme.LocalUseDarkTheme
import me.ash.reader.ui.theme.palette.LocalTonalPalettes import me.ash.reader.ui.theme.palette.LocalTonalPalettes
@Composable @Composable
@ -24,7 +24,7 @@ fun DynamicSVGImage(
contentDescription: String, contentDescription: String,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val useDarkTheme = LocalUseDarkTheme.current val useDarkTheme = LocalDarkTheme.current.isDarkTheme()
val tonalPalettes = LocalTonalPalettes.current val tonalPalettes = LocalTonalPalettes.current
var size by remember { mutableStateOf(IntSize.Zero) } var size by remember { mutableStateOf(IntSize.Zero) }
val pic by remember(tonalPalettes, size) { val pic by remember(tonalPalettes, size) {

View File

@ -130,6 +130,16 @@ sealed class DataStoreKeys<T> {
get() = stringPreferencesKey("customPrimaryColor") get() = stringPreferencesKey("customPrimaryColor")
} }
object DarkTheme : DataStoreKeys<Int>() {
override val key: Preferences.Key<Int>
get() = intPreferencesKey("darkTheme")
}
object AmoledDarkTheme : DataStoreKeys<Boolean>() {
override val key: Preferences.Key<Boolean>
get() = booleanPreferencesKey("amoledDarkTheme")
}
object FeedsFilterBarStyle : DataStoreKeys<Int>() { object FeedsFilterBarStyle : DataStoreKeys<Int>() {
override val key: Preferences.Key<Int> override val key: Preferences.Key<Int>
get() = intPreferencesKey("feedsFilterBarStyle") get() = intPreferencesKey("feedsFilterBarStyle")

View File

@ -14,6 +14,7 @@ import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import me.ash.reader.data.entity.Filter 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.ext.*
import me.ash.reader.ui.page.home.HomeViewAction import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel 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.home.read.ReadPage
import me.ash.reader.ui.page.settings.SettingsPage 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.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.feeds.FeedsPageStyle
import me.ash.reader.ui.page.settings.color.flow.FlowPageStyle 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.interaction.Interaction
import me.ash.reader.ui.page.settings.tips.TipsAndSupport import me.ash.reader.ui.page.settings.tips.TipsAndSupport
import me.ash.reader.ui.page.startup.StartupPage import me.ash.reader.ui.page.startup.StartupPage
import me.ash.reader.ui.theme.AppTheme import me.ash.reader.ui.theme.AppTheme
import me.ash.reader.ui.theme.LocalUseDarkTheme
@OptIn(ExperimentalAnimationApi::class, androidx.compose.material.ExperimentalMaterialApi::class) @OptIn(ExperimentalAnimationApi::class, androidx.compose.material.ExperimentalMaterialApi::class)
@Composable @Composable
@ -86,8 +87,9 @@ fun HomeEntry(
} }
} }
AppTheme { val useDarkTheme = LocalDarkTheme.current.isDarkTheme()
val useDarkTheme = LocalUseDarkTheme.current
AppTheme(useDarkTheme = useDarkTheme) {
rememberSystemUiController().run { rememberSystemUiController().run {
setStatusBarColor(Color.Transparent, !useDarkTheme) setStatusBarColor(Color.Transparent, !useDarkTheme)
@ -129,6 +131,9 @@ fun HomeEntry(
animatedComposable(route = RouteName.COLOR_AND_STYLE) { animatedComposable(route = RouteName.COLOR_AND_STYLE) {
ColorAndStyle(navController) ColorAndStyle(navController)
} }
animatedComposable(route = RouteName.DARK_THEME) {
DarkTheme(navController)
}
animatedComposable(route = RouteName.FEEDS_PAGE_STYLE) { animatedComposable(route = RouteName.FEEDS_PAGE_STYLE) {
FeedsPageStyle(navController) FeedsPageStyle(navController)
} }

View File

@ -14,6 +14,7 @@ object RouteName {
// Color & Style // Color & Style
const val COLOR_AND_STYLE = "color_and_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 FEEDS_PAGE_STYLE = "feeds_page_style"
const val FLOW_PAGE_STYLE = "flow_page_style" const val FLOW_PAGE_STYLE = "flow_page_style"

View File

@ -26,16 +26,12 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.preference.CustomPrimaryColorPreference import me.ash.reader.data.preference.*
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.ui.component.* import me.ash.reader.ui.component.*
import me.ash.reader.ui.page.common.RouteName import me.ash.reader.ui.page.common.RouteName
import me.ash.reader.ui.page.settings.SettingItem import me.ash.reader.ui.page.settings.SettingItem
import me.ash.reader.ui.svg.PALETTE import me.ash.reader.ui.svg.PALETTE
import me.ash.reader.ui.svg.SVGString 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.*
import me.ash.reader.ui.theme.palette.TonalPalettes.Companion.toTonalPalettes import me.ash.reader.ui.theme.palette.TonalPalettes.Companion.toTonalPalettes
import me.ash.reader.ui.theme.palette.dynamic.extractTonalPalettesFromUserWallpaper import me.ash.reader.ui.theme.palette.dynamic.extractTonalPalettesFromUserWallpaper
@ -47,9 +43,11 @@ fun ColorAndStyle(
navController: NavHostController, navController: NavHostController,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val useDarkTheme = LocalUseDarkTheme.current val darkTheme = LocalDarkTheme.current
val darkThemeNot = !darkTheme
val themeIndex = LocalThemeIndex.current val themeIndex = LocalThemeIndex.current
val customPrimaryColor = LocalCustomPrimaryColor.current val customPrimaryColor = LocalCustomPrimaryColor.current
val scope = rememberCoroutineScope()
val wallpaperTonalPalettes = extractTonalPalettesFromUserWallpaper() val wallpaperTonalPalettes = extractTonalPalettesFromUserWallpaper()
var radioButtonSelected by remember { mutableStateOf(if (themeIndex > 4) 0 else 1) } var radioButtonSelected by remember { mutableStateOf(if (themeIndex > 4) 0 else 1) }
@ -151,12 +149,19 @@ fun ColorAndStyle(
) )
SettingItem( SettingItem(
title = stringResource(R.string.dark_theme), title = stringResource(R.string.dark_theme),
desc = stringResource(R.string.use_device_theme), desc = darkTheme.getDesc(context),
enable = false,
separatedActions = true, 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( SettingItem(
title = stringResource(R.string.basic_fonts), title = stringResource(R.string.basic_fonts),

View File

@ -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)
}
}
}
}
}
)
}

View File

@ -4,7 +4,6 @@ import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import me.ash.reader.data.preference.LocalThemeIndex import me.ash.reader.data.preference.LocalThemeIndex
import me.ash.reader.ui.theme.palette.LocalTonalPalettes import me.ash.reader.ui.theme.palette.LocalTonalPalettes
import me.ash.reader.ui.theme.palette.TonalPalettes 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.dynamicDarkColorScheme
import me.ash.reader.ui.theme.palette.dynamicLightColorScheme import me.ash.reader.ui.theme.palette.dynamicLightColorScheme
val LocalUseDarkTheme = compositionLocalOf { false }
@Composable @Composable
fun AppTheme( fun AppTheme(
useDarkTheme: Boolean = isSystemInDarkTheme(), useDarkTheme: Boolean = isSystemInDarkTheme(),
@ -38,7 +35,6 @@ fun AppTheme(
ProvideZcamViewingConditions { ProvideZcamViewingConditions {
CompositionLocalProvider( CompositionLocalProvider(
LocalTonalPalettes provides tonalPalettes.also { it.Preheating() }, LocalTonalPalettes provides tonalPalettes.also { it.Preheating() },
LocalUseDarkTheme provides useDarkTheme,
) { ) {
MaterialTheme( MaterialTheme(
colorScheme = colorScheme =

View File

@ -6,7 +6,7 @@ import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import me.ash.reader.ui.theme.LocalUseDarkTheme import me.ash.reader.data.preference.LocalDarkTheme
@Composable @Composable
fun dynamicLightColorScheme(): ColorScheme { fun dynamicLightColorScheme(): ColorScheme {
@ -68,20 +68,18 @@ fun dynamicDarkColorScheme(): ColorScheme {
) )
} }
@Suppress("NOTHING_TO_INLINE")
@Composable @Composable
inline infix fun Color.onLight(lightColor: Color): Color = infix fun Color.onLight(lightColor: Color): Color =
if (!LocalUseDarkTheme.current) lightColor else this if (!LocalDarkTheme.current.isDarkTheme()) lightColor else this
@Suppress("NOTHING_TO_INLINE")
@Composable @Composable
inline infix fun Color.onDark(darkColor: Color): Color = infix fun Color.onDark(darkColor: Color): Color =
if (LocalUseDarkTheme.current) darkColor else this if (LocalDarkTheme.current.isDarkTheme()) darkColor else this
@Composable @Composable
infix fun Color.alwaysLight(isAlways: Boolean): Color { infix fun Color.alwaysLight(isAlways: Boolean): Color {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
return if (isAlways && LocalUseDarkTheme.current) { return if (isAlways && LocalDarkTheme.current.isDarkTheme()) {
when (this) { when (this) {
colorScheme.primary -> colorScheme.onPrimary colorScheme.primary -> colorScheme.onPrimary
colorScheme.secondary -> colorScheme.onSecondary colorScheme.secondary -> colorScheme.onSecondary