Add basic UI for theme switcher

pull/1837/head
Jolanda Verhoef 3 years ago committed by Don Turner
parent e596e76714
commit 080cdc104a

@ -18,6 +18,7 @@ package com.google.samples.apps.nowinandroid.feature.foryou
import android.app.Activity import android.app.Activity
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
@ -46,11 +47,16 @@ import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration.Indefinite import androidx.compose.material3.SnackbarDuration.Indefinite
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
@ -61,7 +67,9 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -71,6 +79,7 @@ import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max import androidx.compose.ui.unit.max
@ -90,6 +99,13 @@ import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.DARK
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.FOLLOW_SYSTEM
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.LIGHT
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.ANDROID
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.DEFAULT
import com.google.samples.apps.nowinandroid.core.model.data.previewAuthors import com.google.samples.apps.nowinandroid.core.model.data.previewAuthors
import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources
import com.google.samples.apps.nowinandroid.core.model.data.previewTopics import com.google.samples.apps.nowinandroid.core.model.data.previewTopics
@ -108,6 +124,7 @@ internal fun ForYouRoute(
val feedState by viewModel.feedState.collectAsStateWithLifecycle() val feedState by viewModel.feedState.collectAsStateWithLifecycle()
val isOffline by viewModel.isOffline.collectAsStateWithLifecycle() val isOffline by viewModel.isOffline.collectAsStateWithLifecycle()
val isSyncing by viewModel.isSyncing.collectAsStateWithLifecycle() val isSyncing by viewModel.isSyncing.collectAsStateWithLifecycle()
val themeState by viewModel.themeState.collectAsStateWithLifecycle()
ForYouScreen( ForYouScreen(
isOffline = isOffline, isOffline = isOffline,
@ -118,6 +135,9 @@ internal fun ForYouRoute(
onAuthorCheckedChanged = viewModel::updateAuthorSelection, onAuthorCheckedChanged = viewModel::updateAuthorSelection,
saveFollowedTopics = viewModel::saveFollowedInterests, saveFollowedTopics = viewModel::saveFollowedInterests,
onNewsResourcesCheckedChanged = viewModel::updateNewsResourceSaved, onNewsResourcesCheckedChanged = viewModel::updateNewsResourceSaved,
themeState = themeState,
onChangeThemeBrand = viewModel::updateThemeBrand,
onChangeDarkThemeConfig = viewModel::updateDarkThemeConfig,
modifier = modifier modifier = modifier
) )
} }
@ -134,8 +154,22 @@ internal fun ForYouScreen(
saveFollowedTopics: () -> Unit, saveFollowedTopics: () -> Unit,
onNewsResourcesCheckedChanged: (String, Boolean) -> Unit, onNewsResourcesCheckedChanged: (String, Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
themeState: Pair<ThemeBrand, DarkThemeConfig> = Pair(DEFAULT, FOLLOW_SYSTEM),
onChangeThemeBrand: (themeBrand: ThemeBrand) -> Unit = {},
onChangeDarkThemeConfig: (darkThemeConfig: DarkThemeConfig) -> Unit = {},
) { ) {
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
var openAccountDialog by remember { mutableStateOf(false) }
if (openAccountDialog) {
AccountDialog(
onDismiss = { openAccountDialog = false },
currentThemeBrand = themeState.first,
currentDarkThemeConfig = themeState.second,
onChangeThemeBrand = onChangeThemeBrand,
onChangeDarkThemeConfig = onChangeDarkThemeConfig
)
}
Scaffold( Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) }, snackbarHost = { SnackbarHost(snackbarHostState) },
@ -148,7 +182,8 @@ internal fun ForYouScreen(
), ),
colors = TopAppBarDefaults.centerAlignedTopAppBarColors( colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent containerColor = Color.Transparent
) ),
onActionClick = { openAccountDialog = true }
) )
}, },
containerColor = Color.Transparent, containerColor = Color.Transparent,
@ -268,6 +303,7 @@ private fun LazyGridScope.interestsSelection(
ForYouInterestsSelectionUiState.Loading, ForYouInterestsSelectionUiState.Loading,
ForYouInterestsSelectionUiState.LoadFailed, ForYouInterestsSelectionUiState.LoadFailed,
ForYouInterestsSelectionUiState.NoInterestsSelection -> Unit ForYouInterestsSelectionUiState.NoInterestsSelection -> Unit
is ForYouInterestsSelectionUiState.WithInterestsSelection -> { is ForYouInterestsSelectionUiState.WithInterestsSelection -> {
item(span = { GridItemSpan(maxLineSpan) }) { item(span = { GridItemSpan(maxLineSpan) }) {
Column(modifier = interestsItemModifier) { Column(modifier = interestsItemModifier) {
@ -433,6 +469,97 @@ fun TopicIcon(
) )
} }
@Composable
fun AccountDialog(
onDismiss: () -> Unit,
currentThemeBrand: ThemeBrand,
currentDarkThemeConfig: DarkThemeConfig,
onChangeThemeBrand: (themeBrand: ThemeBrand) -> Unit,
onChangeDarkThemeConfig: (darkThemeConfig: DarkThemeConfig) -> Unit
) {
AlertDialog(
onDismissRequest = { onDismiss() },
title = {
Text(
text = "Change theme and dark mode",
style = MaterialTheme.typography.titleLarge
)
},
text = {
Column {
Divider()
Column {
AccountDialogThemeChooserRow(
text = "Default",
selected = currentThemeBrand == DEFAULT,
onClick = { onChangeThemeBrand(DEFAULT) }
)
AccountDialogThemeChooserRow(
text = "Android",
selected = currentThemeBrand == ANDROID,
onClick = { onChangeThemeBrand(ANDROID) }
)
}
Divider()
Column(Modifier.selectableGroup()) {
AccountDialogThemeChooserRow(
text = "System default",
selected = currentDarkThemeConfig == FOLLOW_SYSTEM,
onClick = { onChangeDarkThemeConfig(FOLLOW_SYSTEM) }
)
AccountDialogThemeChooserRow(
text = "Light",
selected = currentDarkThemeConfig == LIGHT,
onClick = { onChangeDarkThemeConfig(LIGHT) }
)
AccountDialogThemeChooserRow(
text = "Dark",
selected = currentDarkThemeConfig == DARK,
onClick = { onChangeDarkThemeConfig(DARK) }
)
}
Divider()
}
},
confirmButton = {
Text(
text = "OK",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
.padding(15.dp)
.clickable { onDismiss() }
)
}
)
}
@Composable
fun AccountDialogThemeChooserRow(
text: String,
selected: Boolean,
onClick: () -> Unit
) {
Row(
Modifier
.fillMaxWidth()
.selectable(
selected = selected,
role = Role.RadioButton,
onClick = onClick
)
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selected,
onClick = null
)
Spacer(Modifier.width(8.dp))
Text(text)
}
}
@DevicePreviews @DevicePreviews
@Composable @Composable
fun ForYouScreenPopulatedFeed() { fun ForYouScreenPopulatedFeed() {

@ -31,6 +31,10 @@ import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsStrea
import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesStreamUseCase import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesStreamUseCase
import com.google.samples.apps.nowinandroid.core.domain.GetSortedFollowableAuthorsStreamUseCase import com.google.samples.apps.nowinandroid.core.domain.GetSortedFollowableAuthorsStreamUseCase
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.FOLLOW_SYSTEM
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.DEFAULT
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.feature.foryou.FollowedInterestsUiState.FollowedInterests import com.google.samples.apps.nowinandroid.feature.foryou.FollowedInterestsUiState.FollowedInterests
import com.google.samples.apps.nowinandroid.feature.foryou.FollowedInterestsUiState.None import com.google.samples.apps.nowinandroid.feature.foryou.FollowedInterestsUiState.None
@ -78,6 +82,20 @@ class ForYouViewModel @Inject constructor(
initialValue = Unknown initialValue = Unknown
) )
/**
* The current theme of the app
*/
val themeState: StateFlow<Pair<ThemeBrand, DarkThemeConfig>> =
userDataRepository.userDataStream
.map { userData ->
Pair(userData.themeBrand, userData.darkThemeConfig)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = Pair(DEFAULT, FOLLOW_SYSTEM)
)
/** /**
* The in-progress set of topics to be selected, persisted through process death with a * The in-progress set of topics to be selected, persisted through process death with a
* [SavedStateHandle]. * [SavedStateHandle].
@ -226,6 +244,18 @@ class ForYouViewModel @Inject constructor(
} }
} }
} }
fun updateThemeBrand(themeBrand: ThemeBrand) {
viewModelScope.launch {
userDataRepository.setThemeBrand(themeBrand)
}
}
fun updateDarkThemeConfig(darkThemeConfig: DarkThemeConfig) {
viewModelScope.launch {
userDataRepository.setDarkThemeConfig(darkThemeConfig)
}
}
} }
private fun Flow<List<SaveableNewsResource>>.mapToFeedState(): Flow<NewsFeedUiState> = private fun Flow<List<SaveableNewsResource>>.mapToFeedState(): Flow<NewsFeedUiState> =

Loading…
Cancel
Save