diff --git a/buildSrc/src/main/kotlin/com/orange/ods/gradle/Dependencies.kt b/buildSrc/src/main/kotlin/com/orange/ods/gradle/Dependencies.kt index 8ebe5732c..3c0608fb9 100644 --- a/buildSrc/src/main/kotlin/com/orange/ods/gradle/Dependencies.kt +++ b/buildSrc/src/main/kotlin/com/orange/ods/gradle/Dependencies.kt @@ -26,6 +26,7 @@ object Dependencies { const val composeUiToolingPreview = "androidx.compose.ui:ui-tooling-preview:${Versions.compose}" const val coreKtx = "androidx.core:core-ktx:${Versions.core}" const val customViewPoolingContainer = "androidx.customview:customview-poolingcontainer:${Versions.customViewPoolingContainer}" + const val dataStorePreferences = "androidx.datastore:datastore-preferences:${Versions.dataStorePreferences}" const val firebaseAppDistributionGradlePlugin = "com.google.firebase:firebase-appdistribution-gradle:${Versions.firebaseAppDistributionGradlePlugin}" const val firebaseBom = "com.google.firebase:firebase-bom:${Versions.firebaseBom}" const val firebaseCrashlytics = "com.google.firebase:firebase-crashlytics-ktx" diff --git a/buildSrc/src/main/kotlin/com/orange/ods/gradle/Versions.kt b/buildSrc/src/main/kotlin/com/orange/ods/gradle/Versions.kt index 639a7767a..9dc4b3ff9 100644 --- a/buildSrc/src/main/kotlin/com/orange/ods/gradle/Versions.kt +++ b/buildSrc/src/main/kotlin/com/orange/ods/gradle/Versions.kt @@ -24,10 +24,11 @@ object Versions { const val compose = "1.2.0-rc02" const val core = "1.7.0" const val customViewPoolingContainer = "1.0.0" - const val googleServicesGradlePlugin = "4.3.14" + const val dataStorePreferences = "1.0.0" const val firebaseAppDistributionGradlePlugin = "3.0.1" const val firebaseBom = "30.0.0" const val firebaseCrashlyticsGradlePlugin = "2.8.1" + const val googleServicesGradlePlugin = "4.3.14" const val hilt = "2.44" const val jUnit = "4.13.2" const val kotlin = "1.6.21" diff --git a/changelog.md b/changelog.md index 4a1cb3196..2fe959d9c 100644 --- a/changelog.md +++ b/changelog.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - \[All\] Add `NOTICE.txt` file ([#356](https://github.com/Orange-OpenSource/ods-android/issues/356)) +- \[Demo\] Save the user theme selection in order to reopen the app with this theme [#335](https://github.com/Orange-OpenSource/ods-android/issues/335) - \[Demo\] Add Snackbar component ([#114](https://github.com/Orange-OpenSource/ods-android/issues/114)) - \[Demo\] Display an error message below text fields if customization error switch is on ([#338](https://github.com/Orange-OpenSource/ods-android/issues/338)) - \[Lib\] Add `OdsSnackbar` and `OdsSnackbarHost` composable to manage snackbars display ([#114](https://github.com/Orange-OpenSource/ods-android/issues/114)) @@ -22,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - \[All\] Version numbers in changelog now display changes on GitHub when clicked ([#322](https://github.com/Orange-OpenSource/ods-android/issues/322)) - \[All\] Update documentation [#334](https://github.com/Orange-OpenSource/ods-android/issues/334) - \[All\] Upgrade compile and target SDK versions to 33 [#343](https://github.com/Orange-OpenSource/ods-android/issues/343) +- \[Demo\] Move change theme feature in top app bar by clicking on a palette icon [#335](https://github.com/Orange-OpenSource/ods-android/issues/335) - \[Demo\] Add customization bottom sheets for buttons ([#303](https://github.com/Orange-OpenSource/ods-android/issues/303)) - \[Demo\] Replace action buttons switches by a counter in cards customization bottom sheet ([#327](https://github.com/Orange-OpenSource/ods-android/issues/327)) - \[Demo\] Add customization bottom sheets for sliders ([#307](https://github.com/Orange-OpenSource/ods-android/issues/307)) diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts index 2b81c97d1..ff588da2b 100644 --- a/demo/build.gradle.kts +++ b/demo/build.gradle.kts @@ -129,6 +129,7 @@ dependencies { implementation(Dependencies.browser) implementation(Dependencies.hiltAndroid) kapt(Dependencies.hiltCompiler) + implementation(Dependencies.dataStorePreferences) debugImplementation(Dependencies.composeUiTooling) } diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index b581eac9f..f3259d00e 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -13,7 +13,7 @@ package="com.orange.ods.demo"> by preferencesDataStore(name = "datastore") + +class DataStoreServiceImpl @Inject constructor(private val context: Context) : DataStoreService { + + override suspend fun putString(key: String, value: String) { + val preferenceKey = stringPreferencesKey(key) + context.dataStore.edit { preferences -> + preferences[preferenceKey] = value + } + } + + override suspend fun getString(key: String): String? { + val preferenceKey = stringPreferencesKey(key) + return context.dataStore.data.firstOrNull()?.let { preferences -> + preferences[preferenceKey] + } + } + +} \ No newline at end of file diff --git a/demo/src/main/java/com/orange/ods/demo/domain/DomainModule.kt b/demo/src/main/java/com/orange/ods/demo/domain/DomainModule.kt new file mode 100644 index 000000000..124f60e25 --- /dev/null +++ b/demo/src/main/java/com/orange/ods/demo/domain/DomainModule.kt @@ -0,0 +1,28 @@ +/* + * + * Copyright 2021 Orange + * + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + * / + */ + +package com.orange.ods.demo.domain + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DomainModule { + + @Singleton + @Provides + fun provideDataStoreService(@ApplicationContext context: Context): DataStoreService = DataStoreServiceImpl(context) +} diff --git a/demo/src/main/java/com/orange/ods/demo/ui/MainScreen.kt b/demo/src/main/java/com/orange/ods/demo/ui/MainScreen.kt index 6c03f1dac..7dc4a512a 100644 --- a/demo/src/main/java/com/orange/ods/demo/ui/MainScreen.kt +++ b/demo/src/main/java/com/orange/ods/demo/ui/MainScreen.kt @@ -14,6 +14,7 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -24,23 +25,33 @@ import androidx.compose.material.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.window.Dialog +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavBackStackEntry import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.NavHost import androidx.navigation.navigation import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.orange.ods.compose.text.OdsTextH6 import com.orange.ods.compose.theme.OdsTheme +import com.orange.ods.demo.R import com.orange.ods.demo.ui.about.addAboutGraph import com.orange.ods.demo.ui.components.addComponentsGraph import com.orange.ods.demo.ui.components.tabs.FixedTabRow import com.orange.ods.demo.ui.components.tabs.ScrollableTabRow import com.orange.ods.demo.ui.guidelines.addGuidelinesGraph +import com.orange.ods.demo.ui.utilities.composable.RadioButtonListItem import com.orange.ods.demo.ui.utilities.extension.isDarkModeEnabled import com.orange.ods.theme.OdsThemeConfigurationContract import com.orange.ods.theme.orange.OrangeThemeConfiguration @@ -49,11 +60,18 @@ import com.orange.ods.utilities.extension.orElse @Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES) @Preview(showBackground = true) @Composable -fun MainScreen(themeConfigurations: Set) { +fun MainScreen(themeConfigurations: Set, mainViewModel: MainViewModel = viewModel()) { val isSystemInDarkTheme = isSystemInDarkTheme() val mainState = rememberMainState( themeState = rememberMainThemeState( - currentThemeConfiguration = rememberSaveable { mutableStateOf(getCurrentThemeConfiguration(themeConfigurations)) }, + currentThemeConfiguration = rememberSaveable { + mutableStateOf( + getCurrentThemeConfiguration( + mainViewModel.getUserThemeName(), + themeConfigurations + ) + ) + }, darkModeEnabled = rememberSaveable { mutableStateOf(isSystemInDarkTheme) }, themeConfigurations = themeConfigurations.toList() ) @@ -71,6 +89,8 @@ fun MainScreen(themeConfigurations: Set) { LocalMainThemeManager provides mainState.themeState, LocalOdsDemoGuideline provides mainState.themeState.currentThemeConfiguration.demoGuideline, ) { + var changeThemeDialogVisible by remember { mutableStateOf(false) } + OdsTheme( themeConfiguration = mainState.themeState.currentThemeConfiguration, darkThemeEnabled = configuration.isDarkModeEnabled @@ -85,7 +105,8 @@ fun MainScreen(themeConfigurations: Set) { titleRes = mainState.topAppBarState.titleRes.value, shouldShowUpNavigationIcon = !mainState.shouldShowBottomBar, state = mainState.topAppBarState, - upPress = mainState::upPress + upPress = mainState::upPress, + onChangeThemeActionClick = { changeThemeDialogVisible = true } ) // Display tabs in the top bar if needed MainTabs(mainTabsState = mainState.tabsState) @@ -111,14 +132,58 @@ fun MainScreen(themeConfigurations: Set) { mainNavGraph(navigateToElement = mainState::navigateToElement) } } + + if (changeThemeDialogVisible) { + ChangeThemeDialog( + themeState = mainState.themeState, + dismissDialog = { + changeThemeDialogVisible = false + }, + onThemeSelected = { + mainViewModel.storeUserThemeName(mainState.themeState.currentThemeConfiguration.name) + } + ) + } } } } } -private fun getCurrentThemeConfiguration(themeConfigurations: Set): OdsThemeConfigurationContract { - // If another theme than Orange is injected, select it otherwise take the Orange theme which is always present - return themeConfigurations.firstOrNull { it.name != OrangeThemeConfiguration.OrangeThemeName }.orElse { themeConfigurations.first() } +private fun getCurrentThemeConfiguration(storedUserThemeName: String?, themeConfigurations: Set): OdsThemeConfigurationContract { + // Return the stored user theme configuration if it exists. If not, return the Orange theme configuration or the first existing theme configuration + return themeConfigurations.firstOrNull { it.name == storedUserThemeName } + .orElse { themeConfigurations.firstOrNull { it.name == OrangeThemeConfiguration.OrangeThemeName } } + .orElse { themeConfigurations.first() } +} + +@Composable +private fun ChangeThemeDialog(themeState: MainThemeState, dismissDialog: () -> Unit, onThemeSelected: () -> Unit) { + val selectedRadio = rememberSaveable { mutableStateOf(themeState.currentThemeConfiguration.name) } + + Dialog(onDismissRequest = dismissDialog) { + Column(modifier = Modifier.background(OdsTheme.colors.surface)) { + OdsTextH6( + text = stringResource(R.string.top_app_bar_action_change_theme_desc), + modifier = Modifier + .padding(top = dimensionResource(R.dimen.spacing_m), bottom = dimensionResource(id = R.dimen.spacing_s)) + .padding(horizontal = dimensionResource(R.dimen.screen_horizontal_margin)) + ) + themeState.themeConfigurations.forEach { themeConfiguration -> + RadioButtonListItem( + label = themeConfiguration.name, + selectedRadio = selectedRadio, + currentRadio = themeConfiguration.name, + onClick = { + if (themeConfiguration != themeState.currentThemeConfiguration) { + themeState.currentThemeConfiguration = themeConfiguration + onThemeSelected() + } + dismissDialog() + } + ) + } + } + } } @Composable diff --git a/demo/src/main/java/com/orange/ods/demo/ui/MainTopAppBar.kt b/demo/src/main/java/com/orange/ods/demo/ui/MainTopAppBar.kt index a714a444c..b8a981183 100644 --- a/demo/src/main/java/com/orange/ods/demo/ui/MainTopAppBar.kt +++ b/demo/src/main/java/com/orange/ods/demo/ui/MainTopAppBar.kt @@ -41,7 +41,8 @@ fun MainTopAppBar( titleRes: Int, shouldShowUpNavigationIcon: Boolean, state: MainTopAppBarState, - upPress: () -> Unit + upPress: () -> Unit, + onChangeThemeActionClick: () -> Unit ) { OdsTopAppBar( title = stringResource(id = titleRes), @@ -49,7 +50,7 @@ fun MainTopAppBar( { Icon( imageVector = Icons.Filled.ArrowBack, - contentDescription = stringResource(id = R.string.back_icon_content_description) + contentDescription = stringResource(id = R.string.top_app_bar_back_icon_desc) ) } } else null, @@ -57,25 +58,17 @@ fun MainTopAppBar( actions = { val context = LocalContext.current repeat(state.actionCount.value) { index -> - if (index == 0) { - val configuration = LocalConfiguration.current - val mainThemeManager = LocalMainThemeManager.current - - val painterRes = if (configuration.isDarkModeEnabled) R.drawable.ic_ui_light_mode else R.drawable.ic_ui_dark_mode - val contentDescriptionRes = - if (configuration.isDarkModeEnabled) R.string.theme_changer_icon_content_description_light else R.string.theme_changer_icon_content_description_dark - OdsTopAppBarActionButton( - onClick = { mainThemeManager.darkModeEnabled = !configuration.isDarkModeEnabled }, - painter = painterResource(id = painterRes), - contentDescription = stringResource(id = contentDescriptionRes) - ) - } else { - val action = topAppBarDemoActions[index - 1] - OdsTopAppBarActionButton( - onClick = { clickOnElement(context, context.getString(action.titleRes)) }, - painter = painterResource(id = action.iconRes), - contentDescription = stringResource(id = action.titleRes) - ) + when (index) { + 0 -> TopAppBarChangeThemeActionButton(onClick = onChangeThemeActionClick) + 1 -> TopAppBarChangeModeActionButton() + else -> { + val action = topAppBarDemoActions[index - 1] + OdsTopAppBarActionButton( + onClick = { clickOnElement(context, context.getString(action.titleRes)) }, + painter = painterResource(id = action.iconRes), + contentDescription = stringResource(id = action.titleRes) + ) + } } } if (state.isOverflowMenuEnabled) { @@ -86,6 +79,31 @@ fun MainTopAppBar( ) } +@Composable +private fun TopAppBarChangeThemeActionButton(onClick: () -> Unit) { + OdsTopAppBarActionButton( + onClick = { onClick() }, + painter = painterResource(id = R.drawable.ic_palette), + contentDescription = stringResource(id = R.string.top_app_bar_action_change_theme_desc) + ) +} + +@Composable +private fun TopAppBarChangeModeActionButton() { + val configuration = LocalConfiguration.current + val mainThemeManager = LocalMainThemeManager.current + + val painterRes = if (configuration.isDarkModeEnabled) R.drawable.ic_ui_light_mode else R.drawable.ic_ui_dark_mode + val iconDesc = + if (configuration.isDarkModeEnabled) R.string.top_app_bar_action_change_mode_to_light_desc else R.string.top_app_bar_action_change_mode_to_dark_desc + + OdsTopAppBarActionButton( + onClick = { mainThemeManager.darkModeEnabled = !configuration.isDarkModeEnabled }, + painter = painterResource(id = painterRes), + contentDescription = stringResource(id = iconDesc) + ) +} + @Composable private fun OverflowMenu() { var showMenu by remember { mutableStateOf(false) } diff --git a/demo/src/main/java/com/orange/ods/demo/ui/MainTopAppBarState.kt b/demo/src/main/java/com/orange/ods/demo/ui/MainTopAppBarState.kt index 241eca5c0..a3a9c969e 100644 --- a/demo/src/main/java/com/orange/ods/demo/ui/MainTopAppBarState.kt +++ b/demo/src/main/java/com/orange/ods/demo/ui/MainTopAppBarState.kt @@ -48,7 +48,7 @@ class MainTopAppBarState( companion object { val DefaultConfiguration = TopAppBarConfiguration( isNavigationIconEnabled = true, - actionCount = 1, + actionCount = 2, isOverflowMenuEnabled = false ) } diff --git a/demo/src/main/java/com/orange/ods/demo/ui/MainViewModel.kt b/demo/src/main/java/com/orange/ods/demo/ui/MainViewModel.kt new file mode 100644 index 000000000..14ae774a4 --- /dev/null +++ b/demo/src/main/java/com/orange/ods/demo/ui/MainViewModel.kt @@ -0,0 +1,31 @@ +/* + * + * Copyright 2021 Orange + * + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + * / + */ + +package com.orange.ods.demo.ui + +import androidx.lifecycle.ViewModel +import com.orange.ods.demo.domain.DataStoreService +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.runBlocking +import javax.inject.Inject + +@HiltViewModel +class MainViewModel @Inject constructor(private val dataStoreService: DataStoreService) : ViewModel() { + + companion object { + private const val UserThemeNameKey = "userThemeName" + } + + fun storeUserThemeName(themeName: String) = runBlocking { + dataStoreService.putString(UserThemeNameKey, themeName) + } + + fun getUserThemeName(): String? = runBlocking { dataStoreService.getString(UserThemeNameKey) } +} \ No newline at end of file diff --git a/demo/src/main/java/com/orange/ods/demo/ui/about/AboutScreen.kt b/demo/src/main/java/com/orange/ods/demo/ui/about/AboutScreen.kt index 1e0b5c7e7..7c99f901b 100644 --- a/demo/src/main/java/com/orange/ods/demo/ui/about/AboutScreen.kt +++ b/demo/src/main/java/com/orange/ods/demo/ui/about/AboutScreen.kt @@ -12,7 +12,6 @@ package com.orange.ods.demo.ui.about import android.content.Context import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -22,26 +21,18 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.window.Dialog import com.orange.ods.compose.component.list.OdsListItem import com.orange.ods.compose.text.OdsTextCaption import com.orange.ods.compose.text.OdsTextH4 -import com.orange.ods.compose.theme.OdsTheme import com.orange.ods.demo.R -import com.orange.ods.demo.ui.LocalMainThemeManager import com.orange.ods.demo.ui.LocalMainTopAppBarManager import com.orange.ods.demo.ui.utilities.compat.PackageManagerCompat -import com.orange.ods.demo.ui.utilities.composable.RadioButtonListItem import com.orange.ods.demo.ui.utilities.extension.versionCode import com.orange.ods.utilities.extension.ifNotNull import com.orange.ods.utilities.extension.orElse @@ -50,9 +41,6 @@ import com.orange.ods.utilities.extension.orElse fun AboutScreen(onAboutItemClick: (Long) -> Unit) { LocalMainTopAppBarManager.current.updateTopAppBarTitle(R.string.navigation_item_about) - val mainThemeManager = LocalMainThemeManager.current - var dialogVisible by remember { mutableStateOf(false) } - Column( modifier = Modifier .verticalScroll(rememberScrollState()) @@ -80,34 +68,12 @@ fun AboutScreen(onAboutItemClick: (Long) -> Unit) { Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.spacing_m))) - OdsListItem(text = stringResource(id = R.string.about_menu_theme), modifier = Modifier.clickable { - dialogVisible = true - }) for (aboutItem in aboutItems) { OdsListItem(text = stringResource(id = aboutItem.titleRes), modifier = Modifier.clickable { onAboutItemClick(aboutItem.id) }) } } - - - if (dialogVisible) { - val selectedRadio = remember { mutableStateOf(mainThemeManager.currentThemeConfiguration.name) } - - Dialog(onDismissRequest = { dialogVisible = false }) { - Column(modifier = Modifier.background(OdsTheme.colors.surface)) { - mainThemeManager.themeConfigurations.forEach { themeConfiguration -> - RadioButtonListItem( - label = themeConfiguration.name, - selectedRadio = selectedRadio, - currentRadio = themeConfiguration.name, - onClick = { mainThemeManager.currentThemeConfiguration = themeConfiguration } - ) - } - } - } - } - } private fun getVersion(context: Context): String { diff --git a/demo/src/main/res/drawable/ic_palette.xml b/demo/src/main/res/drawable/ic_palette.xml new file mode 100644 index 000000000..2b27f15de --- /dev/null +++ b/demo/src/main/res/drawable/ic_palette.xml @@ -0,0 +1,4 @@ + + + diff --git a/demo/src/main/res/values/strings.xml b/demo/src/main/res/values/strings.xml index e047d7a8d..c61b1660b 100644 --- a/demo/src/main/res/values/strings.xml +++ b/demo/src/main/res/values/strings.xml @@ -18,9 +18,10 @@ About - Change to dark mode - Change to light mode - Back + Back + Change theme + Change to dark mode + Change to light mode Colour @@ -296,6 +297,5 @@ Legal notice Privacy policy Changelog - Theme \ No newline at end of file