Merge pull request #47 from Ashinch/feature/dark-theme

Add dark theme settings
This commit is contained in:
Ashinch 2022-05-05 20:58:05 +08:00 committed by GitHub
commit 4d2d857676
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 282 additions and 33 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(
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> { DarkThemePreference.default }
val LocalAmoledDarkTheme =
compositionLocalOf<AmoledDarkThemePreference> { AmoledDarkThemePreference.default }
val LocalFeedsFilterBarStyle =
compositionLocalOf<FeedsFilterBarStylePreference> { FeedsFilterBarStylePreference.default }

View File

@ -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,10 +24,10 @@ 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) {
val pic by remember(useDarkTheme, tonalPalettes, size) {
mutableStateOf(
PictureDrawable(
SVG.getFromString(svgImageString.parseDynamicColor(tonalPalettes, useDarkTheme))

View File

@ -130,6 +130,16 @@ sealed class DataStoreKeys<T> {
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>() {
override val key: Preferences.Key<Int>
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.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)
}

View File

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

View File

@ -2,7 +2,6 @@ package me.ash.reader.ui.page.home.read
import android.util.Log
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
@ -77,7 +76,7 @@ fun ReadPage(
}
Scaffold(
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
containerColor = MaterialTheme.colorScheme.surface,
topBar = {},
content = {
Box(Modifier.fillMaxSize()) {

View File

@ -19,6 +19,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.ash.reader.ui.theme.palette.LocalTonalPalettes
import me.ash.reader.ui.theme.palette.onDark
@Composable
fun SettingItem(
@ -31,6 +33,8 @@ fun SettingItem(
onClick: () -> Unit,
action: (@Composable () -> Unit)? = null
) {
val tonalPalettes = LocalTonalPalettes.current
Surface(
modifier = modifier
.clickable { onClick() }
@ -71,7 +75,8 @@ fun SettingItem(
Divider(
modifier = Modifier
.padding(start = 16.dp)
.size(1.dp, 32.dp)
.size(1.dp, 32.dp),
color = tonalPalettes neutralVariant 80 onDark (tonalPalettes neutralVariant 30),
)
}
Box(Modifier.padding(start = 16.dp)) {

View File

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

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 = stringResource(R.string.other),
)
SettingItem(
title = stringResource(R.string.amoled_dark_theme),
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.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 =

View File

@ -6,7 +6,8 @@ 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.LocalAmoledDarkTheme
import me.ash.reader.data.preference.LocalDarkTheme
@Composable
fun dynamicLightColorScheme(): ColorScheme {
@ -41,6 +42,8 @@ fun dynamicLightColorScheme(): ColorScheme {
@Composable
fun dynamicDarkColorScheme(): ColorScheme {
val palettes = LocalTonalPalettes.current
val amoledDarkTheme = LocalAmoledDarkTheme.current
return darkColorScheme(
primary = palettes primary 80,
onPrimary = palettes primary 20,
@ -57,7 +60,7 @@ fun dynamicDarkColorScheme(): ColorScheme {
onTertiaryContainer = palettes tertiary 90,
background = palettes neutral 10,
onBackground = palettes neutral 90,
surface = palettes neutral 10,
surface = palettes neutral if (amoledDarkTheme.value) 0 else 10,
onSurface = palettes neutral 90,
surfaceVariant = palettes neutralVariant 30,
onSurfaceVariant = palettes neutralVariant 80,
@ -68,20 +71,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

View File

@ -102,6 +102,10 @@
<string name="style">样式</string>
<string name="dark_theme">深色主题</string>
<string name="use_device_theme">跟随系统设置</string>
<string name="on">开启</string>
<string name="off">关闭</string>
<string name="amoled_dark_theme">Amoled 深色主题</string>
<string name="other">其他</string>
<string name="tonal_elevation">色调海拔</string>
<string name="fonts">字体</string>
<string name="basic_fonts">基本字体</string>

View File

@ -103,6 +103,10 @@
<string name="style">Style</string>
<string name="dark_theme">Dark Theme</string>
<string name="use_device_theme">Use Device Theme</string>
<string name="on">On</string>
<string name="off">Off</string>
<string name="other">Other</string>
<string name="amoled_dark_theme">Amoled Dark Theme</string>
<string name="tonal_elevation">Tonal Elevation</string>
<string name="fonts">Fonts</string>
<string name="basic_fonts">Basic Fonts</string>