diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index 252afb1ce..c482f0edf 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -18,6 +18,7 @@ package com.google.samples.apps.nowinandroid.feature.foryou import android.app.Activity import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box 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.items 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.RoundedCornerShape +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarDuration.Indefinite import androidx.compose.material3.SnackbarHost @@ -61,7 +67,9 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp 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.FollowableTopic 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.previewNewsResources import com.google.samples.apps.nowinandroid.core.model.data.previewTopics @@ -108,6 +124,7 @@ internal fun ForYouRoute( val feedState by viewModel.feedState.collectAsStateWithLifecycle() val isOffline by viewModel.isOffline.collectAsStateWithLifecycle() val isSyncing by viewModel.isSyncing.collectAsStateWithLifecycle() + val themeState by viewModel.themeState.collectAsStateWithLifecycle() ForYouScreen( isOffline = isOffline, @@ -118,6 +135,9 @@ internal fun ForYouRoute( onAuthorCheckedChanged = viewModel::updateAuthorSelection, saveFollowedTopics = viewModel::saveFollowedInterests, onNewsResourcesCheckedChanged = viewModel::updateNewsResourceSaved, + themeState = themeState, + onChangeThemeBrand = viewModel::updateThemeBrand, + onChangeDarkThemeConfig = viewModel::updateDarkThemeConfig, modifier = modifier ) } @@ -134,8 +154,22 @@ internal fun ForYouScreen( saveFollowedTopics: () -> Unit, onNewsResourcesCheckedChanged: (String, Boolean) -> Unit, modifier: Modifier = Modifier, + themeState: Pair = Pair(DEFAULT, FOLLOW_SYSTEM), + onChangeThemeBrand: (themeBrand: ThemeBrand) -> Unit = {}, + onChangeDarkThemeConfig: (darkThemeConfig: DarkThemeConfig) -> Unit = {}, ) { 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( snackbarHost = { SnackbarHost(snackbarHostState) }, @@ -148,7 +182,8 @@ internal fun ForYouScreen( ), colors = TopAppBarDefaults.centerAlignedTopAppBarColors( containerColor = Color.Transparent - ) + ), + onActionClick = { openAccountDialog = true } ) }, containerColor = Color.Transparent, @@ -268,6 +303,7 @@ private fun LazyGridScope.interestsSelection( ForYouInterestsSelectionUiState.Loading, ForYouInterestsSelectionUiState.LoadFailed, ForYouInterestsSelectionUiState.NoInterestsSelection -> Unit + is ForYouInterestsSelectionUiState.WithInterestsSelection -> { item(span = { GridItemSpan(maxLineSpan) }) { 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 @Composable fun ForYouScreenPopulatedFeed() { diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt index 8cbf39c31..3c20719e6 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt @@ -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.GetSortedFollowableAuthorsStreamUseCase 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.feature.foryou.FollowedInterestsUiState.FollowedInterests import com.google.samples.apps.nowinandroid.feature.foryou.FollowedInterestsUiState.None @@ -78,6 +82,20 @@ class ForYouViewModel @Inject constructor( initialValue = Unknown ) + /** + * The current theme of the app + */ + val themeState: StateFlow> = + 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 * [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>.mapToFeedState(): Flow =