feat(appearance): support material3 dynamic colors

This commit is contained in:
Diego Beraldin 2023-09-06 22:37:07 +02:00
parent a4e8cf7be2
commit 94033dfa89
18 changed files with 223 additions and 48 deletions

View File

@ -0,0 +1,59 @@
package com.github.diegoberaldin.raccoonforlemmy.core.appearance
import android.content.Context
import android.os.Build
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.ThemeState
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.BlackColors
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.ColorSchemeProvider
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.DarkColors
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.LightColors
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.md_theme_black_background
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.md_theme_black_errorContainer
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.md_theme_black_onBackground
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.md_theme_black_onErrorContainer
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.md_theme_black_onPrimaryContainer
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.md_theme_black_onSecondaryContainer
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.md_theme_black_onSurface
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.md_theme_black_onSurfaceVariant
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.md_theme_black_onTertiaryContainer
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.md_theme_black_primaryContainer
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.md_theme_black_secondaryContainer
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.md_theme_black_surface
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.md_theme_black_surfaceVariant
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.md_theme_black_tertiaryContainer
internal class DefaultColorSchemeProvider(private val context: Context) : ColorSchemeProvider {
override val supportsDynamicColors: Boolean
get() {
return Build.VERSION.SDK_INT >= 31
}
override fun getColorScheme(theme: ThemeState, dynamic: Boolean): ColorScheme {
return when (theme) {
ThemeState.Dark -> if (dynamic) dynamicDarkColorScheme(context) else DarkColors
ThemeState.Black -> if (dynamic) dynamicDarkColorScheme(context).copy(
primaryContainer = md_theme_black_primaryContainer,
onPrimaryContainer = md_theme_black_onPrimaryContainer,
secondaryContainer = md_theme_black_secondaryContainer,
onSecondaryContainer = md_theme_black_onSecondaryContainer,
tertiaryContainer = md_theme_black_tertiaryContainer,
onTertiaryContainer = md_theme_black_onTertiaryContainer,
errorContainer = md_theme_black_errorContainer,
onErrorContainer = md_theme_black_onErrorContainer,
background = md_theme_black_background,
onBackground = md_theme_black_onBackground,
surface = md_theme_black_surface,
onSurface = md_theme_black_onSurface,
surfaceVariant = md_theme_black_surfaceVariant,
onSurfaceVariant = md_theme_black_onSurfaceVariant,
) else BlackColors
else -> if (dynamic) dynamicLightColorScheme(context) else LightColors
}
}
}

View File

@ -1,9 +1,23 @@
package com.github.diegoberaldin.raccoonforlemmy.core.appearance.di
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.DefaultColorSchemeProvider
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.repository.ThemeRepository
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.ColorSchemeProvider
import org.koin.dsl.module
import org.koin.java.KoinJavaComponent.inject
actual fun getThemeRepository(): ThemeRepository {
val res: ThemeRepository by inject(ThemeRepository::class.java)
return res
}
actual val nativeAppearanceModule = module {
single<ColorSchemeProvider> {
DefaultColorSchemeProvider(context = get())
}
}
actual fun getColorSchemeProvider(): ColorSchemeProvider {
val res by inject<ColorSchemeProvider>(ColorSchemeProvider::class.java)
return res
}

View File

@ -6,5 +6,7 @@ import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val coreAppearanceModule = module {
includes(nativeAppearanceModule)
singleOf<ThemeRepository>(::DefaultThemeRepository)
}

View File

@ -1,5 +1,9 @@
package com.github.diegoberaldin.raccoonforlemmy.core.appearance.di
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.repository.ThemeRepository
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.ColorSchemeProvider
import org.koin.core.module.Module
expect val nativeAppearanceModule: Module
expect fun getThemeRepository(): ThemeRepository
expect fun getColorSchemeProvider(): ColorSchemeProvider

View File

@ -8,6 +8,7 @@ internal class DefaultThemeRepository : ThemeRepository {
override val state = MutableStateFlow<ThemeState>(ThemeState.Light)
override val contentFontScale = MutableStateFlow(1f)
override val navItemTitles = MutableStateFlow(false)
override val dynamicColors = MutableStateFlow(false)
override fun changeTheme(value: ThemeState) {
state.value = value
@ -20,4 +21,8 @@ internal class DefaultThemeRepository : ThemeRepository {
override fun changeNavItemTitles(value: Boolean) {
navItemTitles.value = value
}
override fun changeDynamicColors(value: Boolean) {
dynamicColors.value = value
}
}

View File

@ -11,9 +11,13 @@ interface ThemeRepository {
val navItemTitles: StateFlow<Boolean>
val dynamicColors: StateFlow<Boolean>
fun changeTheme(value: ThemeState)
fun changeContentFontScale(value: Float)
fun changeNavItemTitles(value: Boolean)
fun changeDynamicColors(value: Boolean)
}

View File

@ -6,12 +6,14 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.ThemeState
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.di.getColorSchemeProvider
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.di.getThemeRepository
@Composable
fun AppTheme(
theme: ThemeState,
contentFontScale: Float,
useDynamicColors: Boolean,
content: @Composable () -> Unit,
) {
val repository = remember {
@ -22,11 +24,13 @@ fun AppTheme(
}
val themeState by repository.state.collectAsState()
val colorScheme = when (themeState) {
ThemeState.Dark -> DarkColors
ThemeState.Black -> BlackColors
else -> LightColors
}
val colorSchemeProvider = remember { getColorSchemeProvider() }
val colorScheme = colorSchemeProvider.getColorScheme(
theme = themeState,
dynamic = useDynamicColors
)
MaterialTheme(
colorScheme = colorScheme,
typography = typography,

View File

@ -0,0 +1,10 @@
package com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme
import androidx.compose.material3.ColorScheme
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.ThemeState
interface ColorSchemeProvider {
val supportsDynamicColors: Boolean
fun getColorScheme(theme: ThemeState, dynamic: Boolean): ColorScheme
}

View File

@ -0,0 +1,21 @@
package com.github.diegoberaldin.raccoonforlemmy.core.appearance
import androidx.compose.material3.ColorScheme
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.ThemeState
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.BlackColors
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.ColorSchemeProvider
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.DarkColors
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.LightColors
internal class DefaultColorSchemeProvider : ColorSchemeProvider {
override val supportsDynamicColors = false
override fun getColorScheme(theme: ThemeState, dynamic: Boolean): ColorScheme {
return when (theme) {
ThemeState.Dark -> DarkColors
ThemeState.Black -> BlackColors
else -> LightColors
}
}
}

View File

@ -1,11 +1,23 @@
package com.github.diegoberaldin.raccoonforlemmy.core.appearance.di
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.DefaultColorSchemeProvider
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.repository.ThemeRepository
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.ColorSchemeProvider
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.dsl.module
actual fun getThemeRepository(): ThemeRepository = CoreAppearanceHelper.repository
actual val nativeAppearanceModule = module {
single<ColorSchemeProvider> {
DefaultColorSchemeProvider()
}
}
actual fun getColorSchemeProvider(): ColorSchemeProvider = CoreAppearanceHelper.colorSchemeProvider
object CoreAppearanceHelper : KoinComponent {
internal val repository: ThemeRepository by inject()
internal val colorSchemeProvider: ColorSchemeProvider by inject()
}

View File

@ -12,4 +12,5 @@ object KeyStoreKeys {
const val IncludeNsfw = "includeNsfw"
const val BlurNsfw = "blurNsfw"
const val NavItemTitlesVisible = "navItemTitlesVisible"
const val DynamicColors = "dynamicColors"
}

View File

@ -229,6 +229,21 @@ class SettingsScreen : Screen {
}
)
// dynamic colors
if (uiState.supportsDynamicColors) {
SettingsSwitchRow(
title = stringResource(MR.strings.settings_dynamic_colors),
value = uiState.dynamicColors,
onValueChanged = { value ->
model.reduce(
SettingsScreenMviModel.Intent.ChangeDynamicColors(
value
)
)
}
)
}
// NSFW options
SettingsSwitchRow(
title = stringResource(MR.strings.settings_include_nsfw),

View File

@ -17,6 +17,7 @@ interface SettingsScreenMviModel :
data class ChangeDefaultPostSortType(val value: SortType) : Intent
data class ChangeDefaultCommentSortType(val value: SortType) : Intent
data class ChangeNavBarTitlesVisible(val value: Boolean) : Intent
data class ChangeDynamicColors(val value: Boolean) : Intent
data class ChangeIncludeNsfw(val value: Boolean) : Intent
data class ChangeBlurNsfw(val value: Boolean) : Intent
}
@ -30,6 +31,8 @@ interface SettingsScreenMviModel :
val defaultPostSortType: SortType = SortType.Active,
val defaultCommentSortType: SortType = SortType.New,
val navBarTitlesVisible: Boolean = false,
val supportsDynamicColors: Boolean = false,
val dynamicColors: Boolean = false,
val includeNsfw: Boolean = true,
val blurNsfw: Boolean = true,
val appVersion: String = "",

View File

@ -6,6 +6,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.ThemeState
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.toFontScale
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.toInt
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.repository.ThemeRepository
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.ColorSchemeProvider
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.KeyStoreKeys
@ -25,6 +26,7 @@ import kotlinx.coroutines.launch
class SettingsScreenViewModel(
private val mvi: DefaultMviModel<SettingsScreenMviModel.Intent, SettingsScreenMviModel.UiState, SettingsScreenMviModel.Effect>,
private val themeRepository: ThemeRepository,
private val colorSchemeProvider: ColorSchemeProvider,
private val languageRepository: LanguageRepository,
private val identityRepository: IdentityRepository,
private val keyStore: TemporaryKeyStore,
@ -43,6 +45,9 @@ class SettingsScreenViewModel(
themeRepository.navItemTitles.onEach { value ->
mvi.updateState { it.copy(navBarTitlesVisible = value) }
}.launchIn(this)
themeRepository.dynamicColors.onEach { value ->
mvi.updateState { it.copy(dynamicColors = value) }
}.launchIn(this)
languageRepository.currentLanguage.onEach { lang ->
mvi.updateState { it.copy(lang = lang) }
}.launchIn(this)
@ -62,75 +67,83 @@ class SettingsScreenViewModel(
includeNsfw = keyStore[KeyStoreKeys.IncludeNsfw, true],
blurNsfw = keyStore[KeyStoreKeys.BlurNsfw, true],
appVersion = AppInfo.versionCode,
supportsDynamicColors = colorSchemeProvider.supportsDynamicColors,
)
}
}
override fun reduce(intent: SettingsScreenMviModel.Intent) {
when (intent) {
is SettingsScreenMviModel.Intent.ChangeTheme -> applyTheme(intent.value)
is SettingsScreenMviModel.Intent.ChangeContentFontSize -> applyContentFontScale(intent.value)
is SettingsScreenMviModel.Intent.ChangeLanguage -> changeLanguage(intent.value)
is SettingsScreenMviModel.Intent.ChangeDefaultCommentSortType -> changeDefaultCommentSortType(
intent.value,
)
is SettingsScreenMviModel.Intent.ChangeTheme -> {
changeTheme(intent.value)
}
is SettingsScreenMviModel.Intent.ChangeDefaultListingType -> changeDefaultListingType(
intent.value,
)
is SettingsScreenMviModel.Intent.ChangeContentFontSize -> {
changeContentFontScale(intent.value)
}
is SettingsScreenMviModel.Intent.ChangeDefaultPostSortType -> changeDefaultPostSortType(
intent.value,
)
is SettingsScreenMviModel.Intent.ChangeLanguage -> {
changeLanguage(intent.value)
}
is SettingsScreenMviModel.Intent.ChangeBlurNsfw -> changeBlurNsfw(intent.value)
is SettingsScreenMviModel.Intent.ChangeIncludeNsfw -> changeIncludeNsfw(intent.value)
is SettingsScreenMviModel.Intent.ChangeNavBarTitlesVisible -> changeNavBarTitlesVisible(
intent.value
)
is SettingsScreenMviModel.Intent.ChangeDefaultCommentSortType -> {
changeDefaultCommentSortType(intent.value)
}
is SettingsScreenMviModel.Intent.ChangeDefaultListingType -> {
changeDefaultListingType(intent.value)
}
is SettingsScreenMviModel.Intent.ChangeDefaultPostSortType -> {
changeDefaultPostSortType(intent.value)
}
is SettingsScreenMviModel.Intent.ChangeBlurNsfw -> {
changeBlurNsfw(intent.value)
}
is SettingsScreenMviModel.Intent.ChangeIncludeNsfw -> {
changeIncludeNsfw(intent.value)
}
is SettingsScreenMviModel.Intent.ChangeNavBarTitlesVisible -> {
changeNavBarTitlesVisible(intent.value)
}
is SettingsScreenMviModel.Intent.ChangeDynamicColors -> {
changeDynamicColors(intent.value)
}
}
}
private fun applyTheme(value: ThemeState) {
private fun changeTheme(value: ThemeState) {
themeRepository.changeTheme(value)
mvi.scope.launch(Dispatchers.Main) {
keyStore.save(KeyStoreKeys.UiTheme, value.toInt())
}
keyStore.save(KeyStoreKeys.UiTheme, value.toInt())
}
private fun applyContentFontScale(value: Float) {
private fun changeContentFontScale(value: Float) {
themeRepository.changeContentFontScale(value)
mvi.scope.launch(Dispatchers.Main) {
keyStore.save(KeyStoreKeys.ContentFontScale, value)
}
keyStore.save(KeyStoreKeys.ContentFontScale, value)
}
private fun changeLanguage(value: String) {
languageRepository.changeLanguage(value)
mvi.scope.launch(Dispatchers.Main) {
keyStore.save(KeyStoreKeys.Locale, value)
}
keyStore.save(KeyStoreKeys.Locale, value)
}
private fun changeDefaultListingType(value: ListingType) {
mvi.updateState { it.copy(defaultListingType = value) }
mvi.scope.launch(Dispatchers.Main) {
keyStore.save(KeyStoreKeys.DefaultListingType, value.toInt())
}
keyStore.save(KeyStoreKeys.DefaultListingType, value.toInt())
}
private fun changeDefaultPostSortType(value: SortType) {
mvi.updateState { it.copy(defaultPostSortType = value) }
mvi.scope.launch(Dispatchers.Main) {
keyStore.save(KeyStoreKeys.DefaultPostSortType, value.toInt())
}
keyStore.save(KeyStoreKeys.DefaultPostSortType, value.toInt())
}
private fun changeDefaultCommentSortType(value: SortType) {
mvi.updateState { it.copy(defaultCommentSortType = value) }
mvi.scope.launch(Dispatchers.Main) {
keyStore.save(KeyStoreKeys.DefaultCommentSortType, value.toInt())
}
keyStore.save(KeyStoreKeys.DefaultCommentSortType, value.toInt())
}
private fun changeNavBarTitlesVisible(value: Boolean) {
@ -140,15 +153,16 @@ class SettingsScreenViewModel(
private fun changeIncludeNsfw(value: Boolean) {
mvi.updateState { it.copy(includeNsfw = value) }
mvi.scope.launch(Dispatchers.Main) {
keyStore.save(KeyStoreKeys.IncludeNsfw, value)
}
keyStore.save(KeyStoreKeys.IncludeNsfw, value)
}
private fun changeBlurNsfw(value: Boolean) {
mvi.updateState { it.copy(blurNsfw = value) }
mvi.scope.launch(Dispatchers.Main) {
keyStore.save(KeyStoreKeys.BlurNsfw, value)
}
keyStore.save(KeyStoreKeys.BlurNsfw, value)
}
private fun changeDynamicColors(value: Boolean) {
themeRepository.changeDynamicColors(value)
keyStore.save(KeyStoreKeys.DynamicColors, value)
}
}

View File

@ -15,6 +15,7 @@ val settingsTabModule = module {
themeRepository = get(),
languageRepository = get(),
identityRepository = get(),
colorSchemeProvider = get(),
)
}
}

View File

@ -85,6 +85,7 @@
<string name="settings_include_nsfw">Include NSFW contents</string>
<string name="settings_blur_nsfw">Blur NSFW images</string>
<string name="settings_app_version">App version</string>
<string name="settings_dynamic_colors">Use dynamic colors</string>
<string name="community_button_subscribe">Subscribe</string>
<string name="community_button_subscribed">Subscribed</string>

View File

@ -83,6 +83,7 @@
<string name="settings_include_nsfw">Includi contenuti NSFW</string>
<string name="settings_blur_nsfw">Sfuma immagini NSFW</string>
<string name="settings_app_version">Versione app</string>
<string name="settings_dynamic_colors">Usa colori dinamici</string>
<string name="community_button_subscribe">Iscriviti</string>
<string name="community_button_subscribed">Iscritto</string>

View File

@ -73,13 +73,17 @@ fun App() {
val themeRepository = remember { getThemeRepository() }
val navTitles = keyStore[KeyStoreKeys.NavItemTitlesVisible, false]
val dynamicColors = keyStore[KeyStoreKeys.DynamicColors, false]
LaunchedEffect(Unit) {
themeRepository.changeNavItemTitles(navTitles)
themeRepository.changeDynamicColors(dynamicColors)
}
val useDynamicColors by themeRepository.dynamicColors.collectAsState()
AppTheme(
theme = currentTheme,
contentFontScale = fontScale,
useDynamicColors = useDynamicColors,
) {
val lang by languageRepository.currentLanguage.collectAsState()
LaunchedEffect(lang) {}