From 8319a1753ee14ea5ac4d9874124ffaeab0a51951 Mon Sep 17 00:00:00 2001 From: retanar Date: Tue, 31 Dec 2024 10:01:30 +0200 Subject: [PATCH 01/11] Adding ThemePreferences, slightly reworking colors in AppTheme.kt --- app/build.gradle.kts | 1 + .../featuremodule/template/ui/MainContract.kt | 10 ++++ .../com/featuremodule/template/ui/MainVM.kt | 13 +++++ .../featuremodule/core/ui/theme/AppTheme.kt | 37 ++++++++----- .../data/prefs/ThemePreferences.kt | 55 +++++++++++++++++++ 5 files changed, 102 insertions(+), 14 deletions(-) create mode 100644 data/src/main/java/com/featuremodule/data/prefs/ThemePreferences.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e6ec973..0eaf225 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -81,6 +81,7 @@ dependencies { implementation(projects.feature.foxFeatureApi) implementation(projects.feature.foxFeatureImpl) implementation(projects.core) + implementation(projects.data) implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.splashscreen) diff --git a/app/src/main/java/com/featuremodule/template/ui/MainContract.kt b/app/src/main/java/com/featuremodule/template/ui/MainContract.kt index 6c81a5b..c354415 100644 --- a/app/src/main/java/com/featuremodule/template/ui/MainContract.kt +++ b/app/src/main/java/com/featuremodule/template/ui/MainContract.kt @@ -1,15 +1,25 @@ package com.featuremodule.template.ui +import androidx.compose.material3.ColorScheme import com.featuremodule.core.navigation.NavCommand import com.featuremodule.core.ui.UiEvent import com.featuremodule.core.ui.UiState +import com.featuremodule.core.ui.theme.ColorsDark +import com.featuremodule.core.ui.theme.ColorsLight import kotlinx.coroutines.flow.SharedFlow internal data class State( val commands: SharedFlow, val isLoaded: Boolean = false, + val theme: ThemeState = ThemeState(), ) : UiState +internal data class ThemeState( + val lightColorScheme: ColorScheme = ColorsLight.Default.scheme, + val darkColorScheme: ColorScheme = ColorsDark.Default.scheme, + val useSystemDarkTheme: Boolean = true, +) + internal sealed interface Event : UiEvent { data class OpenNavBarRoute(val route: String, val isSelected: Boolean) : Event } diff --git a/app/src/main/java/com/featuremodule/template/ui/MainVM.kt b/app/src/main/java/com/featuremodule/template/ui/MainVM.kt index 2a6cca4..7d93f01 100644 --- a/app/src/main/java/com/featuremodule/template/ui/MainVM.kt +++ b/app/src/main/java/com/featuremodule/template/ui/MainVM.kt @@ -3,20 +3,33 @@ package com.featuremodule.template.ui import com.featuremodule.core.navigation.NavCommand import com.featuremodule.core.navigation.NavManager import com.featuremodule.core.ui.BaseVM +import com.featuremodule.core.ui.theme.ColorsDark +import com.featuremodule.core.ui.theme.ColorsLight +import com.featuremodule.data.prefs.ThemePreferences import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel internal class MainVM @Inject constructor( private val navManager: NavManager, + private val themePreferences: ThemePreferences, ) : BaseVM() { init { launch { + themePreferences.themeModelFlow.collect { + setState { copy(theme = it.toThemeState()) } + } // Do something useful before loading setState { copy(isLoaded = true) } } } + private fun ThemePreferences.ThemeModel.toThemeState() = ThemeState( + lightColorScheme = ColorsLight.valueOf(lightTheme ?: ColorsLight.Default.name).scheme, + darkColorScheme = ColorsDark.valueOf(darkTheme ?: ColorsDark.Default.name).scheme, + useSystemDarkTheme = useSystemDarkTheme, + ) + override fun initialState() = State(navManager.commands) override fun handleEvent(event: Event) { diff --git a/core/src/main/java/com/featuremodule/core/ui/theme/AppTheme.kt b/core/src/main/java/com/featuremodule/core/ui/theme/AppTheme.kt index 8b42357..a2e7616 100644 --- a/core/src/main/java/com/featuremodule/core/ui/theme/AppTheme.kt +++ b/core/src/main/java/com/featuremodule/core/ui/theme/AppTheme.kt @@ -1,30 +1,19 @@ package com.featuremodule.core.ui.theme import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80, -) - -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40, -) - @Composable fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { val colorScheme = if (darkTheme) { - DarkColorScheme + ColorsDark.Default.scheme } else { - LightColorScheme + ColorsLight.Default.scheme } ProvideAppColors(darkTheme) { @@ -36,6 +25,26 @@ fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () } } +enum class ColorsLight(val scheme: ColorScheme) { + Default( + lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40, + ), + ) +} + +enum class ColorsDark(val scheme: ColorScheme) { + Default( + darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80, + ), + ) +} + /** * A copy of [MaterialTheme] object with only custom colors. Can be responsible for all design, * if needed. diff --git a/data/src/main/java/com/featuremodule/data/prefs/ThemePreferences.kt b/data/src/main/java/com/featuremodule/data/prefs/ThemePreferences.kt new file mode 100644 index 0000000..137a472 --- /dev/null +++ b/data/src/main/java/com/featuremodule/data/prefs/ThemePreferences.kt @@ -0,0 +1,55 @@ +package com.featuremodule.data.prefs + +import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.onStart +import javax.inject.Inject + +class ThemePreferences @Inject constructor( + @ApplicationContext context: Context, +) { + private val preferences = context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE) + + fun setLightTheme(theme: String?) = preferences.edit { putString(KEY_THEME_LIGHT, theme) } + + fun setDarkTheme(theme: String?) = preferences.edit { putString(KEY_THEME_DARK, theme) } + + fun setUseSystemDark(theme: String?) = + preferences.edit { putString(KEY_USE_SYSTEM_DARK, theme) } + + val themeModelFlow = callbackFlow { + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> + // trySendBlocking is used just in case, trySend should be enough too + trySendBlocking(getCurrentPreferences()) + } + preferences.registerOnSharedPreferenceChangeListener(listener) + + awaitClose { preferences.unregisterOnSharedPreferenceChangeListener(listener) } + }.onStart { + emit(getCurrentPreferences()) + } + + fun getCurrentPreferences() = ThemeModel( + lightTheme = preferences.getString(KEY_THEME_LIGHT, null), + darkTheme = preferences.getString(KEY_THEME_DARK, null), + useSystemDarkTheme = preferences.getBoolean(KEY_USE_SYSTEM_DARK, true), + ) + + data class ThemeModel( + val lightTheme: String?, + val darkTheme: String?, + val useSystemDarkTheme: Boolean, + ) + + companion object { + private const val FILE_NAME = "theme_preferences" + private const val KEY_THEME_LIGHT = "key_theme_light" + private const val KEY_THEME_DARK = "key_theme_dark" + private const val KEY_USE_SYSTEM_DARK = "key_use_system_dark" + } +} From 1201744f2eabe23116ea4181f3b447a3b068fbe3 Mon Sep 17 00:00:00 2001 From: retanar Date: Tue, 31 Dec 2024 11:43:18 +0200 Subject: [PATCH 02/11] Adding theme changes to AppTheme, renaming some variables --- .../com/featuremodule/template/MainActivity.kt | 17 +++++++++++++++-- .../com/featuremodule/template/ui/AppContent.kt | 7 ++++++- .../featuremodule/template/ui/MainContract.kt | 6 +++--- .../com/featuremodule/template/ui/MainVM.kt | 11 +++++------ .../com/featuremodule/core/ui/theme/AppTheme.kt | 17 ++++++++++++----- .../data/prefs/ThemePreferences.kt | 14 +++++++------- 6 files changed, 48 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/featuremodule/template/MainActivity.kt b/app/src/main/java/com/featuremodule/template/MainActivity.kt index 7f523fe..4675ab2 100644 --- a/app/src/main/java/com/featuremodule/template/MainActivity.kt +++ b/app/src/main/java/com/featuremodule/template/MainActivity.kt @@ -10,10 +10,15 @@ import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.core.animation.doOnEnd import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import com.featuremodule.core.ui.theme.AppTheme import com.featuremodule.template.ui.AppContent +import com.featuremodule.template.ui.ThemeState import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.MutableStateFlow @@ -29,8 +34,16 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge(statusBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT)) setContent { - AppTheme { - AppContent(updateLoadedState = { isLoaded.value = it }) + var theme by remember { mutableStateOf(ThemeState()) } + AppTheme( + colorsLight = theme.colorsLight, + colorsDark = theme.colorsDark, + switchToDarkWithSystem = theme.switchToDarkWithSystem, + ) { + AppContent( + updateLoadedState = { isLoaded.value = it }, + updateTheme = { theme = it }, + ) } } } diff --git a/app/src/main/java/com/featuremodule/template/ui/AppContent.kt b/app/src/main/java/com/featuremodule/template/ui/AppContent.kt index aae5433..b385cd3 100644 --- a/app/src/main/java/com/featuremodule/template/ui/AppContent.kt +++ b/app/src/main/java/com/featuremodule/template/ui/AppContent.kt @@ -21,8 +21,9 @@ import com.featuremodule.core.util.CollectWithLifecycle @Composable internal fun AppContent( - viewModel: MainVM = hiltViewModel(), updateLoadedState: (isLoaded: Boolean) -> Unit, + updateTheme: (ThemeState) -> Unit, + viewModel: MainVM = hiltViewModel(), ) { val navController = rememberNavController() val backStackEntry by navController.currentBackStackEntryAsState() @@ -33,6 +34,10 @@ internal fun AppContent( updateLoadedState(state.isLoaded) } + LaunchedEffect(state.theme) { + updateTheme(state.theme) + } + state.commands.CollectWithLifecycle { navController.handleCommand(it) } diff --git a/app/src/main/java/com/featuremodule/template/ui/MainContract.kt b/app/src/main/java/com/featuremodule/template/ui/MainContract.kt index c354415..a443417 100644 --- a/app/src/main/java/com/featuremodule/template/ui/MainContract.kt +++ b/app/src/main/java/com/featuremodule/template/ui/MainContract.kt @@ -15,9 +15,9 @@ internal data class State( ) : UiState internal data class ThemeState( - val lightColorScheme: ColorScheme = ColorsLight.Default.scheme, - val darkColorScheme: ColorScheme = ColorsDark.Default.scheme, - val useSystemDarkTheme: Boolean = true, + val colorsLight: ColorScheme = ColorsLight.Default.scheme, + val colorsDark: ColorScheme = ColorsDark.Default.scheme, + val switchToDarkWithSystem: Boolean = true, ) internal sealed interface Event : UiEvent { diff --git a/app/src/main/java/com/featuremodule/template/ui/MainVM.kt b/app/src/main/java/com/featuremodule/template/ui/MainVM.kt index 7d93f01..85590e8 100644 --- a/app/src/main/java/com/featuremodule/template/ui/MainVM.kt +++ b/app/src/main/java/com/featuremodule/template/ui/MainVM.kt @@ -16,18 +16,17 @@ internal class MainVM @Inject constructor( ) : BaseVM() { init { launch { + // The only loading for now is theme loading, so isLoaded is set together themePreferences.themeModelFlow.collect { - setState { copy(theme = it.toThemeState()) } + setState { copy(theme = it.toThemeState(), isLoaded = true) } } - // Do something useful before loading - setState { copy(isLoaded = true) } } } private fun ThemePreferences.ThemeModel.toThemeState() = ThemeState( - lightColorScheme = ColorsLight.valueOf(lightTheme ?: ColorsLight.Default.name).scheme, - darkColorScheme = ColorsDark.valueOf(darkTheme ?: ColorsDark.Default.name).scheme, - useSystemDarkTheme = useSystemDarkTheme, + colorsLight = ColorsLight.valueOf(lightTheme ?: ColorsLight.Default.name).scheme, + colorsDark = ColorsDark.valueOf(darkTheme ?: ColorsDark.Default.name).scheme, + switchToDarkWithSystem = switchToDarkWithSystem, ) override fun initialState() = State(navManager.commands) diff --git a/core/src/main/java/com/featuremodule/core/ui/theme/AppTheme.kt b/core/src/main/java/com/featuremodule/core/ui/theme/AppTheme.kt index a2e7616..d48da21 100644 --- a/core/src/main/java/com/featuremodule/core/ui/theme/AppTheme.kt +++ b/core/src/main/java/com/featuremodule/core/ui/theme/AppTheme.kt @@ -9,14 +9,21 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable @Composable -fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { - val colorScheme = if (darkTheme) { - ColorsDark.Default.scheme +fun AppTheme( + colorsLight: ColorScheme, + colorsDark: ColorScheme, + switchToDarkWithSystem: Boolean, + isSystemDark: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit, +) { + val colorScheme = if (switchToDarkWithSystem && isSystemDark) { + colorsDark } else { - ColorsLight.Default.scheme + colorsLight } - ProvideAppColors(darkTheme) { + // Ignores whether color scheme is dark + ProvideAppColors(isSystemDark) { MaterialTheme( colorScheme = colorScheme, typography = Typography, diff --git a/data/src/main/java/com/featuremodule/data/prefs/ThemePreferences.kt b/data/src/main/java/com/featuremodule/data/prefs/ThemePreferences.kt index 137a472..cf4c84f 100644 --- a/data/src/main/java/com/featuremodule/data/prefs/ThemePreferences.kt +++ b/data/src/main/java/com/featuremodule/data/prefs/ThemePreferences.kt @@ -19,8 +19,8 @@ class ThemePreferences @Inject constructor( fun setDarkTheme(theme: String?) = preferences.edit { putString(KEY_THEME_DARK, theme) } - fun setUseSystemDark(theme: String?) = - preferences.edit { putString(KEY_USE_SYSTEM_DARK, theme) } + fun setSwitchToDarkWithSystem(shouldSwitch: Boolean) = + preferences.edit { putBoolean(KEY_SWITCH_DARK_WITH_SYSTEM, shouldSwitch) } val themeModelFlow = callbackFlow { val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> @@ -37,19 +37,19 @@ class ThemePreferences @Inject constructor( fun getCurrentPreferences() = ThemeModel( lightTheme = preferences.getString(KEY_THEME_LIGHT, null), darkTheme = preferences.getString(KEY_THEME_DARK, null), - useSystemDarkTheme = preferences.getBoolean(KEY_USE_SYSTEM_DARK, true), + switchToDarkWithSystem = preferences.getBoolean(KEY_SWITCH_DARK_WITH_SYSTEM, true), ) data class ThemeModel( val lightTheme: String?, val darkTheme: String?, - val useSystemDarkTheme: Boolean, + val switchToDarkWithSystem: Boolean, ) companion object { private const val FILE_NAME = "theme_preferences" - private const val KEY_THEME_LIGHT = "key_theme_light" - private const val KEY_THEME_DARK = "key_theme_dark" - private const val KEY_USE_SYSTEM_DARK = "key_use_system_dark" + private const val KEY_THEME_LIGHT = "theme_light" + private const val KEY_THEME_DARK = "theme_dark" + private const val KEY_SWITCH_DARK_WITH_SYSTEM = "switch_dark_with_system" } } From cbefe2e814d2d87b0bb549a907dfbc055b9de892 Mon Sep 17 00:00:00 2001 From: retanar Date: Thu, 2 Jan 2025 09:16:14 +0200 Subject: [PATCH 03/11] Another preferences change --- .../featuremodule/template/MainActivity.kt | 2 +- .../featuremodule/template/ui/MainContract.kt | 3 ++- .../com/featuremodule/template/ui/MainVM.kt | 9 +++++--- .../featuremodule/core/ui/theme/AppTheme.kt | 22 ++++++++++++------- .../data/prefs/ThemePreferences.kt | 10 ++++----- 5 files changed, 28 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/featuremodule/template/MainActivity.kt b/app/src/main/java/com/featuremodule/template/MainActivity.kt index 4675ab2..e717629 100644 --- a/app/src/main/java/com/featuremodule/template/MainActivity.kt +++ b/app/src/main/java/com/featuremodule/template/MainActivity.kt @@ -38,7 +38,7 @@ class MainActivity : ComponentActivity() { AppTheme( colorsLight = theme.colorsLight, colorsDark = theme.colorsDark, - switchToDarkWithSystem = theme.switchToDarkWithSystem, + themeStyle = theme.themeStyle, ) { AppContent( updateLoadedState = { isLoaded.value = it }, diff --git a/app/src/main/java/com/featuremodule/template/ui/MainContract.kt b/app/src/main/java/com/featuremodule/template/ui/MainContract.kt index a443417..769ae3a 100644 --- a/app/src/main/java/com/featuremodule/template/ui/MainContract.kt +++ b/app/src/main/java/com/featuremodule/template/ui/MainContract.kt @@ -6,6 +6,7 @@ import com.featuremodule.core.ui.UiEvent import com.featuremodule.core.ui.UiState import com.featuremodule.core.ui.theme.ColorsDark import com.featuremodule.core.ui.theme.ColorsLight +import com.featuremodule.core.ui.theme.ThemeStyle import kotlinx.coroutines.flow.SharedFlow internal data class State( @@ -17,7 +18,7 @@ internal data class State( internal data class ThemeState( val colorsLight: ColorScheme = ColorsLight.Default.scheme, val colorsDark: ColorScheme = ColorsDark.Default.scheme, - val switchToDarkWithSystem: Boolean = true, + val themeStyle: ThemeStyle = ThemeStyle.System, ) internal sealed interface Event : UiEvent { diff --git a/app/src/main/java/com/featuremodule/template/ui/MainVM.kt b/app/src/main/java/com/featuremodule/template/ui/MainVM.kt index 85590e8..fcc0935 100644 --- a/app/src/main/java/com/featuremodule/template/ui/MainVM.kt +++ b/app/src/main/java/com/featuremodule/template/ui/MainVM.kt @@ -5,6 +5,7 @@ import com.featuremodule.core.navigation.NavManager import com.featuremodule.core.ui.BaseVM import com.featuremodule.core.ui.theme.ColorsDark import com.featuremodule.core.ui.theme.ColorsLight +import com.featuremodule.core.ui.theme.ThemeStyle import com.featuremodule.data.prefs.ThemePreferences import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -24,9 +25,11 @@ internal class MainVM @Inject constructor( } private fun ThemePreferences.ThemeModel.toThemeState() = ThemeState( - colorsLight = ColorsLight.valueOf(lightTheme ?: ColorsLight.Default.name).scheme, - colorsDark = ColorsDark.valueOf(darkTheme ?: ColorsDark.Default.name).scheme, - switchToDarkWithSystem = switchToDarkWithSystem, + colorsLight = ColorsLight.entries.find { it.name == lightTheme }?.scheme + ?: ColorsLight.Default.scheme, + colorsDark = ColorsDark.entries.find { it.name == darkTheme }?.scheme + ?: ColorsDark.Default.scheme, + themeStyle = ThemeStyle.entries.find { it.name == themeStyle } ?: ThemeStyle.System, ) override fun initialState() = State(navManager.commands) diff --git a/core/src/main/java/com/featuremodule/core/ui/theme/AppTheme.kt b/core/src/main/java/com/featuremodule/core/ui/theme/AppTheme.kt index d48da21..d5c5186 100644 --- a/core/src/main/java/com/featuremodule/core/ui/theme/AppTheme.kt +++ b/core/src/main/java/com/featuremodule/core/ui/theme/AppTheme.kt @@ -12,18 +12,17 @@ import androidx.compose.runtime.ReadOnlyComposable fun AppTheme( colorsLight: ColorScheme, colorsDark: ColorScheme, - switchToDarkWithSystem: Boolean, - isSystemDark: Boolean = isSystemInDarkTheme(), + themeStyle: ThemeStyle, content: @Composable () -> Unit, ) { - val colorScheme = if (switchToDarkWithSystem && isSystemDark) { - colorsDark - } else { - colorsLight + val isStyleDark = when (themeStyle) { + ThemeStyle.Light -> false + ThemeStyle.Dark -> true + ThemeStyle.System -> isSystemInDarkTheme() } + val colorScheme = if (isStyleDark) colorsDark else colorsLight - // Ignores whether color scheme is dark - ProvideAppColors(isSystemDark) { + ProvideAppColors(isStyleDark) { MaterialTheme( colorScheme = colorScheme, typography = Typography, @@ -52,6 +51,13 @@ enum class ColorsDark(val scheme: ColorScheme) { ) } +/** Sets light or dark theme as active, or switches it with system */ +enum class ThemeStyle { + Light, + Dark, + System +} + /** * A copy of [MaterialTheme] object with only custom colors. Can be responsible for all design, * if needed. diff --git a/data/src/main/java/com/featuremodule/data/prefs/ThemePreferences.kt b/data/src/main/java/com/featuremodule/data/prefs/ThemePreferences.kt index cf4c84f..168aff1 100644 --- a/data/src/main/java/com/featuremodule/data/prefs/ThemePreferences.kt +++ b/data/src/main/java/com/featuremodule/data/prefs/ThemePreferences.kt @@ -19,8 +19,8 @@ class ThemePreferences @Inject constructor( fun setDarkTheme(theme: String?) = preferences.edit { putString(KEY_THEME_DARK, theme) } - fun setSwitchToDarkWithSystem(shouldSwitch: Boolean) = - preferences.edit { putBoolean(KEY_SWITCH_DARK_WITH_SYSTEM, shouldSwitch) } + fun setThemeStyle(theme: String?) = + preferences.edit { putString(KEY_THEME_STYLE, theme) } val themeModelFlow = callbackFlow { val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> @@ -37,19 +37,19 @@ class ThemePreferences @Inject constructor( fun getCurrentPreferences() = ThemeModel( lightTheme = preferences.getString(KEY_THEME_LIGHT, null), darkTheme = preferences.getString(KEY_THEME_DARK, null), - switchToDarkWithSystem = preferences.getBoolean(KEY_SWITCH_DARK_WITH_SYSTEM, true), + themeStyle = preferences.getString(KEY_THEME_STYLE, null), ) data class ThemeModel( val lightTheme: String?, val darkTheme: String?, - val switchToDarkWithSystem: Boolean, + val themeStyle: String?, ) companion object { private const val FILE_NAME = "theme_preferences" private const val KEY_THEME_LIGHT = "theme_light" private const val KEY_THEME_DARK = "theme_dark" - private const val KEY_SWITCH_DARK_WITH_SYSTEM = "switch_dark_with_system" + private const val KEY_THEME_STYLE = "theme_style" } } From 45572ef596ab830919c804e207264383dc34167c Mon Sep 17 00:00:00 2001 From: retanar Date: Thu, 2 Jan 2025 13:22:58 +0200 Subject: [PATCH 04/11] Base ChooseThemeScreen and navigation to it --- feature/homeImpl/build.gradle.kts | 1 + .../featuremodule/homeImpl/HomeGraphEntry.kt | 11 ++ .../homeImpl/theming/ChooseThemeContract.kt | 28 ++++ .../homeImpl/theming/ChooseThemeScreen.kt | 120 ++++++++++++++++++ .../homeImpl/theming/ChooseThemeVM.kt | 74 +++++++++++ .../featuremodule/homeImpl/ui/HomeContract.kt | 1 + .../featuremodule/homeImpl/ui/HomeScreen.kt | 1 + .../com/featuremodule/homeImpl/ui/HomeVM.kt | 4 + 8 files changed, 240 insertions(+) create mode 100644 feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeContract.kt create mode 100644 feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt create mode 100644 feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeVM.kt diff --git a/feature/homeImpl/build.gradle.kts b/feature/homeImpl/build.gradle.kts index 55eb848..06219e8 100644 --- a/feature/homeImpl/build.gradle.kts +++ b/feature/homeImpl/build.gradle.kts @@ -9,6 +9,7 @@ android { dependencies { implementation(projects.feature.homeApi) implementation(projects.feature.featureAApi) + implementation(projects.data) implementation(libs.bundles.exoplayer) implementation(libs.bundles.camerax) diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt index beaf434..9cbbe08 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt @@ -14,6 +14,7 @@ import com.featuremodule.homeImpl.barcode.BarcodeResultScreen import com.featuremodule.homeImpl.camera.TakePhotoScreen import com.featuremodule.homeImpl.exoplayer.ExoplayerScreen import com.featuremodule.homeImpl.imageUpload.ImageUploadScreen +import com.featuremodule.homeImpl.theming.ChooseThemeScreen import com.featuremodule.homeImpl.ui.HomeScreen import com.featuremodule.homeImpl.wifi.WifiScreen @@ -54,6 +55,10 @@ fun NavGraphBuilder.registerHome() { composable(InternalRoutes.WifiDestination.ROUTE) { WifiScreen() } + + composable(InternalRoutes.ChooseThemeDestination.ROUTE) { + ChooseThemeScreen() + } } internal class InternalRoutes { @@ -98,4 +103,10 @@ internal class InternalRoutes { fun constructRoute() = ROUTE } + + object ChooseThemeDestination { + const val ROUTE = "choose_theme" + + fun constructRoute() = ROUTE + } } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeContract.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeContract.kt new file mode 100644 index 0000000..dc78dbc --- /dev/null +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeContract.kt @@ -0,0 +1,28 @@ +package com.featuremodule.homeImpl.theming + +import com.featuremodule.core.ui.UiEvent +import com.featuremodule.core.ui.UiState +import com.featuremodule.core.ui.theme.ColorsDark +import com.featuremodule.core.ui.theme.ColorsLight +import com.featuremodule.core.ui.theme.ThemeStyle + +internal data class State( + val isLoading: Boolean = true, + val previewTheme: ThemeState = ThemeState(), +) : UiState + +internal data class ThemeState( + val colorsLight: ColorsLight = ColorsLight.Default, + val colorsDark: ColorsDark = ColorsDark.Default, + val themeStyle: ThemeStyle = ThemeStyle.System, +) + +internal sealed interface Event : UiEvent { + data class PreviewLightTheme(val colors: ColorsLight) : Event + data class PreviewDarkTheme(val colors: ColorsDark) : Event + data class SetThemeStyle(val themeStyle: ThemeStyle) : Event + data class SaveTheme(val theme: ThemeState) : Event + + data object PopBackIfSaved : Event + data object PopBack : Event +} diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt new file mode 100644 index 0000000..50010d4 --- /dev/null +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt @@ -0,0 +1,120 @@ +package com.featuremodule.homeImpl.theming + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.featuremodule.core.ui.theme.ColorsDark +import com.featuremodule.core.ui.theme.ColorsLight + +@Composable +internal fun ChooseThemeScreen(viewModel: ChooseThemeVM = hiltViewModel()) { + val state by viewModel.state.collectAsStateWithLifecycle() + + Column { + Text( + text = "Light themes", + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(horizontal = 8.dp), + ) + LazyRow( + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(ColorsLight.entries) { + ThemeRadioButton( + name = it.name, + colorScheme = it.scheme, + isSelected = state.previewTheme.colorsLight == it, + onClick = { viewModel.postEvent(Event.PreviewLightTheme(it)) }, + ) + } + } + + Spacer(Modifier.height(24.dp)) + Text( + text = "Dark themes", + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(horizontal = 8.dp), + ) + LazyRow( + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(ColorsDark.entries) { + ThemeRadioButton( + name = it.name, + colorScheme = it.scheme, + isSelected = state.previewTheme.colorsDark == it, + onClick = { viewModel.postEvent(Event.PreviewDarkTheme(it)) }, + ) + } + } + } +} + +@Composable +private fun ThemeRadioButton( + name: String, + colorScheme: ColorScheme, + isSelected: Boolean, + onClick: () -> Unit, +) = Card(modifier = Modifier.clickable { onClick() }) { + Column( + modifier = Modifier.padding(all = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text(name) + Row(modifier = Modifier.size(height = 20.dp, width = 40.dp)) { + Box( + modifier = Modifier + .background( + color = colorScheme.primary, + shape = RoundedCornerShape(topStart = 4.dp, bottomStart = 4.dp), + ) + .fillMaxHeight() + .weight(1f), + ) + Box( + modifier = Modifier + .background( + color = colorScheme.secondary, + shape = RoundedCornerShape(topEnd = 4.dp, bottomEnd = 4.dp), + ) + .fillMaxHeight() + .weight(1f), + ) + } + RadioButton( + selected = isSelected, + onClick = null, + ) + } +} diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeVM.kt new file mode 100644 index 0000000..76f69f8 --- /dev/null +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeVM.kt @@ -0,0 +1,74 @@ +package com.featuremodule.homeImpl.theming + +import com.featuremodule.core.navigation.NavCommand +import com.featuremodule.core.navigation.NavManager +import com.featuremodule.core.ui.BaseVM +import com.featuremodule.core.ui.theme.ColorsDark +import com.featuremodule.core.ui.theme.ColorsLight +import com.featuremodule.core.ui.theme.ThemeStyle +import com.featuremodule.data.prefs.ThemePreferences +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +internal class ChooseThemeVM @Inject constructor( + private val themePreferences: ThemePreferences, + private val navManager: NavManager, +) : BaseVM() { + private lateinit var savedTheme: ThemeState + + init { + launch { + loadSavedTheme() + setState { copy(previewTheme = savedTheme, isLoading = false) } + } + } + + private fun loadSavedTheme() { + savedTheme = themePreferences.getCurrentPreferences().toThemeState() + } + + // Light colors can be assigned dark colors if user sets dark theme with no switching + private fun ThemePreferences.ThemeModel.toThemeState() = ThemeState( + colorsLight = ColorsLight.entries.find { it.name == lightTheme } + ?: ColorsLight.Default, + colorsDark = ColorsDark.entries.find { it.name == darkTheme } + ?: ColorsDark.Default, + themeStyle = ThemeStyle.entries.find { it.name == themeStyle } ?: ThemeStyle.System, + ) + + override fun initialState() = State() + + override fun handleEvent(event: Event) { + when (event) { + is Event.PreviewLightTheme -> setState { + copy(previewTheme = previewTheme.copy(colorsLight = event.colors)) + } + + is Event.PreviewDarkTheme -> setState { + copy(previewTheme = previewTheme.copy(colorsDark = event.colors)) + } + + is Event.SetThemeStyle -> setState { + copy(previewTheme = previewTheme.copy(themeStyle = event.themeStyle)) + } + + is Event.SaveTheme -> saveTheme() + + Event.PopBackIfSaved -> { + if (state.value.previewTheme == savedTheme) { + launch { navManager.navigate(NavCommand.PopBack) } + return + } + } + + Event.PopBack -> launch { navManager.navigate(NavCommand.PopBack) } + } + } + + private fun saveTheme() = with(state.value.previewTheme) { + themePreferences.setLightTheme(colorsLight.name) + themePreferences.setDarkTheme(colorsDark.name) + themePreferences.setThemeStyle(themeStyle.name) + } +} diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeContract.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeContract.kt index 7a385b7..5d2d1cc 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeContract.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeContract.kt @@ -11,4 +11,5 @@ internal sealed interface Event : UiEvent { data object NavigateToCamera : Event data object NavigateToBarcode : Event data object NavigateToWifi : Event + data object NavigateToChooseTheme : Event } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeScreen.kt index 77b31cf..47c44e4 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeScreen.kt @@ -48,6 +48,7 @@ internal fun HomeScreen(route: String?, viewModel: HomeVM = hiltViewModel()) { GenericButton(text = "Camera") { viewModel.postEvent(Event.NavigateToCamera) } GenericButton(text = "Barcode") { viewModel.postEvent(Event.NavigateToBarcode) } GenericButton(text = "Wifi") { viewModel.postEvent(Event.NavigateToWifi) } + GenericButton(text = "Theme") { viewModel.postEvent(Event.NavigateToChooseTheme) } } } } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeVM.kt index 3e6699e..4c54958 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeVM.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeVM.kt @@ -44,6 +44,10 @@ internal class HomeVM @Inject constructor( Event.NavigateToWifi -> navManager.navigate( NavCommand.Forward(InternalRoutes.WifiDestination.constructRoute()), ) + + Event.NavigateToChooseTheme -> navManager.navigate( + NavCommand.Forward(InternalRoutes.ChooseThemeDestination.constructRoute()), + ) } } } From 827eff00a4ee4fd396f94cbe70b6fc8a07602e6b Mon Sep 17 00:00:00 2001 From: retanar Date: Fri, 3 Jan 2025 09:08:16 +0200 Subject: [PATCH 05/11] Added new themes --- .../featuremodule/core/ui/theme/AppTheme.kt | 29 --- .../com/featuremodule/core/ui/theme/Color.kt | 11 - .../core/ui/theme/LocalAppColors.kt | 12 +- .../com/featuremodule/core/ui/theme/Themes.kt | 189 ++++++++++++++++++ .../homeImpl/theming/ChooseThemeScreen.kt | 8 +- 5 files changed, 202 insertions(+), 47 deletions(-) delete mode 100644 core/src/main/java/com/featuremodule/core/ui/theme/Color.kt create mode 100644 core/src/main/java/com/featuremodule/core/ui/theme/Themes.kt diff --git a/core/src/main/java/com/featuremodule/core/ui/theme/AppTheme.kt b/core/src/main/java/com/featuremodule/core/ui/theme/AppTheme.kt index d5c5186..5b24c16 100644 --- a/core/src/main/java/com/featuremodule/core/ui/theme/AppTheme.kt +++ b/core/src/main/java/com/featuremodule/core/ui/theme/AppTheme.kt @@ -3,8 +3,6 @@ package com.featuremodule.core.ui.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable @@ -31,33 +29,6 @@ fun AppTheme( } } -enum class ColorsLight(val scheme: ColorScheme) { - Default( - lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40, - ), - ) -} - -enum class ColorsDark(val scheme: ColorScheme) { - Default( - darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80, - ), - ) -} - -/** Sets light or dark theme as active, or switches it with system */ -enum class ThemeStyle { - Light, - Dark, - System -} - /** * A copy of [MaterialTheme] object with only custom colors. Can be responsible for all design, * if needed. diff --git a/core/src/main/java/com/featuremodule/core/ui/theme/Color.kt b/core/src/main/java/com/featuremodule/core/ui/theme/Color.kt deleted file mode 100644 index cc8af07..0000000 --- a/core/src/main/java/com/featuremodule/core/ui/theme/Color.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.featuremodule.core.ui.theme - -import androidx.compose.ui.graphics.Color - -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) - -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) diff --git a/core/src/main/java/com/featuremodule/core/ui/theme/LocalAppColors.kt b/core/src/main/java/com/featuremodule/core/ui/theme/LocalAppColors.kt index 7d23412..e428be1 100644 --- a/core/src/main/java/com/featuremodule/core/ui/theme/LocalAppColors.kt +++ b/core/src/main/java/com/featuremodule/core/ui/theme/LocalAppColors.kt @@ -13,17 +13,17 @@ val LocalAppColors = staticCompositionLocalOf { AppColors() } * Draft for providing own color hierarchy to be used in the same way as MaterialTheme. */ data class AppColors( - val primary: Color = Purple40, - val secondary: Color = PurpleGrey40, - val tertiary: Color = Pink40, + val primary: Color = ColorsLight.Default.scheme.primary, + val secondary: Color = ColorsLight.Default.scheme.secondary, + val tertiary: Color = ColorsLight.Default.scheme.tertiary, ) private val LightAppColors = AppColors() private val DarkAppColors = AppColors( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80, + primary = ColorsDark.Default.scheme.primary, + secondary = ColorsDark.Default.scheme.secondary, + tertiary = ColorsDark.Default.scheme.tertiary, ) @Composable diff --git a/core/src/main/java/com/featuremodule/core/ui/theme/Themes.kt b/core/src/main/java/com/featuremodule/core/ui/theme/Themes.kt new file mode 100644 index 0000000..be82800 --- /dev/null +++ b/core/src/main/java/com/featuremodule/core/ui/theme/Themes.kt @@ -0,0 +1,189 @@ +package com.featuremodule.core.ui.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color + +enum class ColorsLight(val scheme: ColorScheme) { + Default( + lightColorScheme( + primary = Color(0xFF6650A4), + secondary = Color(0xFF625B71), + tertiary = Color(0xFF7D5260), + ), + ), + Red( + lightColorScheme( + primary = Color(0xFFA50011), + onPrimary = Color(0xFFFFFFFF), + primaryContainer = Color(0xFFEA001D), + onPrimaryContainer = Color(0xFFFFFFFF), + secondary = Color(0xFFB32826), + onSecondary = Color(0xFFFFFFFF), + secondaryContainer = Color(0xFFFF776C), + onSecondaryContainer = Color(0xFF350002), + tertiary = Color(0xFF774300), + onTertiary = Color(0xFFFFFFFF), + tertiaryContainer = Color(0xFFAB6300), + onTertiaryContainer = Color(0xFFFFFFFF), + error = Color(0xFFBA1A1A), + onError = Color(0xFFFFFFFF), + errorContainer = Color(0xFFFFDAD6), + onErrorContainer = Color(0xFF410002), + background = Color(0xFFFFF8F7), + onBackground = Color(0xFF2A1614), + surface = Color(0xFFFFF8F7), + onSurface = Color(0xFF2A1614), + surfaceVariant = Color(0xFFFFDAD6), + onSurfaceVariant = Color(0xFF5F3F3B), + outline = Color(0xFF946E6A), + outlineVariant = Color(0xFFE9BCB7), + scrim = Color(0xFF000000), + inverseSurface = Color(0xFF412B28), + inverseOnSurface = Color(0xFFFFEDEA), + inversePrimary = Color(0xFFFFB4AC), + surfaceDim = Color(0xFFF6D2CE), + surfaceBright = Color(0xFFFFF8F7), + surfaceContainerLowest = Color(0xFFFFFFFF), + surfaceContainerLow = Color(0xFFFFF0EF), + surfaceContainer = Color(0xFFFFE9E6), + surfaceContainerHigh = Color(0xFFFFE2DE), + surfaceContainerHighest = Color(0xFFFFDAD6), + ), + ), + Green( + lightColorScheme( + primary = Color(0xFF3A693B), + onPrimary = Color(0xFFFFFFFF), + primaryContainer = Color(0xFFBBF0B6), + onPrimaryContainer = Color(0xFF002105), + secondary = Color(0xFF52634F), + onSecondary = Color(0xFFFFFFFF), + secondaryContainer = Color(0xFFD5E8CF), + onSecondaryContainer = Color(0xFF101F10), + tertiary = Color(0xFF39656B), + onTertiary = Color(0xFFFFFFFF), + tertiaryContainer = Color(0xFFBCEBF1), + onTertiaryContainer = Color(0xFF001F23), + error = Color(0xFFBA1A1A), + onError = Color(0xFFFFFFFF), + errorContainer = Color(0xFFFFDAD6), + onErrorContainer = Color(0xFF410002), + background = Color(0xFFF7FBF1), + onBackground = Color(0xFF181D17), + surface = Color(0xFFF7FBF1), + onSurface = Color(0xFF181D17), + surfaceVariant = Color(0xFFDEE5D9), + onSurfaceVariant = Color(0xFF424940), + outline = Color(0xFF72796F), + outlineVariant = Color(0xFFC2C9BD), + scrim = Color(0xFF000000), + inverseSurface = Color(0xFF2D322C), + inverseOnSurface = Color(0xFFEEF2E9), + inversePrimary = Color(0xFFA0D49B), + surfaceDim = Color(0xFFD7DBD2), + surfaceBright = Color(0xFFF7FBF1), + surfaceContainerLowest = Color(0xFFFFFFFF), + surfaceContainerLow = Color(0xFFF1F5EC), + surfaceContainer = Color(0xFFEBEFE6), + surfaceContainerHigh = Color(0xFFE6E9E0), + surfaceContainerHighest = Color(0xFFE0E4DB), + ), + ) +} + +enum class ColorsDark(val scheme: ColorScheme) { + Default( + darkColorScheme( + primary = Color(0xFFD0BCFF), + secondary = Color(0xFFCCC2DC), + tertiary = Color(0xFFEFB8C8), + ), + ), + Red( + darkColorScheme( + primary = Color(0xFFFFB4AC), + onPrimary = Color(0xFF690007), + primaryContainer = Color(0xFFDF001B), + onPrimaryContainer = Color(0xFFFFFFFF), + secondary = Color(0xFFFFB4AC), + onSecondary = Color(0xFF690007), + secondaryContainer = Color(0xFF86000C), + onSecondaryContainer = Color(0xFFFFC8C2), + tertiary = Color(0xFFFFB873), + onTertiary = Color(0xFF4B2800), + tertiaryContainer = Color(0xFFA35E00), + onTertiaryContainer = Color(0xFFFFFFFF), + error = Color(0xFFFFB4AB), + onError = Color(0xFF690005), + errorContainer = Color(0xFF93000A), + onErrorContainer = Color(0xFFFFDAD6), + background = Color(0xFF210E0D), + onBackground = Color(0xFFFFDAD6), + surface = Color(0xFF210E0D), + onSurface = Color(0xFFFFDAD6), + surfaceVariant = Color(0xFF5F3F3B), + onSurfaceVariant = Color(0xFFE9BCB7), + outline = Color(0xFFB08783), + outlineVariant = Color(0xFF5F3F3B), + scrim = Color(0xFF000000), + inverseSurface = Color(0xFFFFDAD6), + inverseOnSurface = Color(0xFF412B28), + inversePrimary = Color(0xFFC00016), + surfaceDim = Color(0xFF210E0D), + surfaceBright = Color(0xFF4B3331), + surfaceContainerLowest = Color(0xFF1B0908), + surfaceContainerLow = Color(0xFF2A1614), + surfaceContainer = Color(0xFF2E1A18), + surfaceContainerHigh = Color(0xFF3A2422), + surfaceContainerHighest = Color(0xFF462F2D), + ), + ), + Green( + darkColorScheme( + primary = Color(0xFFA0D49B), + onPrimary = Color(0xFF073910), + primaryContainer = Color(0xFF225025), + onPrimaryContainer = Color(0xFFBBF0B6), + secondary = Color(0xFFB9CCB4), + onSecondary = Color(0xFF253423), + secondaryContainer = Color(0xFF3B4B39), + onSecondaryContainer = Color(0xFFD5E8CF), + tertiary = Color(0xFFA1CED5), + onTertiary = Color(0xFF00363C), + tertiaryContainer = Color(0xFF1F4D53), + onTertiaryContainer = Color(0xFFBCEBF1), + error = Color(0xFFFFB4AB), + onError = Color(0xFF690005), + errorContainer = Color(0xFF93000A), + onErrorContainer = Color(0xFFFFDAD6), + background = Color(0xFF10140F), + onBackground = Color(0xFFE0E4DB), + surface = Color(0xFF10140F), + onSurface = Color(0xFFE0E4DB), + surfaceVariant = Color(0xFF424940), + onSurfaceVariant = Color(0xFFC2C9BD), + outline = Color(0xFF8C9388), + outlineVariant = Color(0xFF424940), + scrim = Color(0xFF000000), + inverseSurface = Color(0xFFE0E4DB), + inverseOnSurface = Color(0xFF2D322C), + inversePrimary = Color(0xFF3A693B), + surfaceDim = Color(0xFF10140F), + surfaceBright = Color(0xFF363A34), + surfaceContainerLowest = Color(0xFF0B0F0A), + surfaceContainerLow = Color(0xFF181D17), + surfaceContainer = Color(0xFF1C211B), + surfaceContainerHigh = Color(0xFF272B25), + surfaceContainerHighest = Color(0xFF323630), + ), + ) +} + +/** Sets light or dark theme as active, or switches it with system */ +enum class ThemeStyle { + Light, + Dark, + System +} diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt index 50010d4..259f6df 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt @@ -102,10 +102,16 @@ private fun ThemeRadioButton( .fillMaxHeight() .weight(1f), ) + Box( + modifier = Modifier + .background(color = colorScheme.secondary) + .fillMaxHeight() + .weight(1f), + ) Box( modifier = Modifier .background( - color = colorScheme.secondary, + color = colorScheme.tertiary, shape = RoundedCornerShape(topEnd = 4.dp, bottomEnd = 4.dp), ) .fillMaxHeight() From cf1749d74eb3785c828b249f1817ac3fc0298da3 Mon Sep 17 00:00:00 2001 From: retanar Date: Fri, 3 Jan 2025 13:24:59 +0200 Subject: [PATCH 06/11] Added preview for chosen themes --- .../homeImpl/theming/ChooseThemeScreen.kt | 58 ++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt index 259f6df..7a21668 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt @@ -1,6 +1,7 @@ package com.featuremodule.homeImpl.theming import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -9,17 +10,25 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedCard import androidx.compose.material3.RadioButton import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -35,7 +44,11 @@ import com.featuremodule.core.ui.theme.ColorsLight internal fun ChooseThemeScreen(viewModel: ChooseThemeVM = hiltViewModel()) { val state by viewModel.state.collectAsStateWithLifecycle() - Column { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + ) { Text( text = "Light themes", fontWeight = FontWeight.SemiBold, @@ -43,7 +56,7 @@ internal fun ChooseThemeScreen(viewModel: ChooseThemeVM = hiltViewModel()) { ) LazyRow( modifier = Modifier.fillMaxWidth(), - contentPadding = PaddingValues(horizontal = 8.dp), + contentPadding = PaddingValues(all = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { items(ColorsLight.entries) { @@ -55,6 +68,7 @@ internal fun ChooseThemeScreen(viewModel: ChooseThemeVM = hiltViewModel()) { ) } } + ThemePreview(state.previewTheme.colorsLight.scheme) Spacer(Modifier.height(24.dp)) Text( @@ -64,7 +78,7 @@ internal fun ChooseThemeScreen(viewModel: ChooseThemeVM = hiltViewModel()) { ) LazyRow( modifier = Modifier.fillMaxWidth(), - contentPadding = PaddingValues(horizontal = 8.dp), + contentPadding = PaddingValues(all = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { items(ColorsDark.entries) { @@ -76,6 +90,7 @@ internal fun ChooseThemeScreen(viewModel: ChooseThemeVM = hiltViewModel()) { ) } } + ThemePreview(state.previewTheme.colorsDark.scheme) } } @@ -124,3 +139,40 @@ private fun ThemeRadioButton( ) } } + +@Composable +private fun ThemePreview(colorScheme: ColorScheme) { + MaterialTheme(colorScheme = colorScheme) { + Column( + modifier = Modifier + .fillMaxWidth() + .background( + color = MaterialTheme.colorScheme.background, + shape = MaterialTheme.shapes.large, + ) + .border( + width = 2.dp, + color = MaterialTheme.colorScheme.primary, + shape = MaterialTheme.shapes.large, + ) + .padding(all = 4.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text("Theme Preview") + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + Button(onClick = {}) { Text("Button") } + OutlinedButton(onClick = {}) { Text("Button") } + TextButton(onClick = {}) { Text("Button") } + } + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + Card(onClick = {}) { + Text(text = "Card", modifier = Modifier.padding(12.dp)) + } + OutlinedCard(onClick = {}) { + Text(text = "Card", modifier = Modifier.padding(12.dp)) + } + } + } + } +} From 0437073a839e95b1c5bf45c5e9a82611e48535e1 Mon Sep 17 00:00:00 2001 From: retanar Date: Wed, 12 Mar 2025 13:47:14 +0200 Subject: [PATCH 07/11] Added SaveOrCloseDialog, fixed few issues --- .../data/prefs/ThemePreferences.kt | 6 + .../homeImpl/theming/ChooseThemeContract.kt | 4 +- .../homeImpl/theming/ChooseThemeScreen.kt | 150 +++++++++++++----- .../homeImpl/theming/ChooseThemeVM.kt | 14 +- 4 files changed, 127 insertions(+), 47 deletions(-) diff --git a/data/src/main/java/com/featuremodule/data/prefs/ThemePreferences.kt b/data/src/main/java/com/featuremodule/data/prefs/ThemePreferences.kt index 168aff1..089af02 100644 --- a/data/src/main/java/com/featuremodule/data/prefs/ThemePreferences.kt +++ b/data/src/main/java/com/featuremodule/data/prefs/ThemePreferences.kt @@ -22,6 +22,12 @@ class ThemePreferences @Inject constructor( fun setThemeStyle(theme: String?) = preferences.edit { putString(KEY_THEME_STYLE, theme) } + fun setAll(themeModel: ThemeModel) = preferences.edit { + putString(KEY_THEME_LIGHT, themeModel.lightTheme) + putString(KEY_THEME_DARK, themeModel.darkTheme) + putString(KEY_THEME_STYLE, themeModel.themeStyle) + } + val themeModelFlow = callbackFlow { val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> // trySendBlocking is used just in case, trySend should be enough too diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeContract.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeContract.kt index dc78dbc..55d7f27 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeContract.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeContract.kt @@ -9,6 +9,7 @@ import com.featuremodule.core.ui.theme.ThemeStyle internal data class State( val isLoading: Boolean = true, val previewTheme: ThemeState = ThemeState(), + val showSaveCloseDialog: Boolean = false, ) : UiState internal data class ThemeState( @@ -21,8 +22,9 @@ internal sealed interface Event : UiEvent { data class PreviewLightTheme(val colors: ColorsLight) : Event data class PreviewDarkTheme(val colors: ColorsDark) : Event data class SetThemeStyle(val themeStyle: ThemeStyle) : Event - data class SaveTheme(val theme: ThemeState) : Event + data object SaveTheme : Event data object PopBackIfSaved : Event data object PopBack : Event + data object HideSaveCloseDialog : Event } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt index 7a21668..c7da4c3 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt @@ -1,5 +1,6 @@ package com.featuremodule.homeImpl.theming +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -27,6 +28,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedCard import androidx.compose.material3.RadioButton +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -34,7 +36,9 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.featuremodule.core.ui.theme.ColorsDark @@ -44,53 +48,73 @@ import com.featuremodule.core.ui.theme.ColorsLight internal fun ChooseThemeScreen(viewModel: ChooseThemeVM = hiltViewModel()) { val state by viewModel.state.collectAsStateWithLifecycle() - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()), - ) { - Text( - text = "Light themes", - fontWeight = FontWeight.SemiBold, - modifier = Modifier.padding(horizontal = 8.dp), - ) - LazyRow( - modifier = Modifier.fillMaxWidth(), - contentPadding = PaddingValues(all = 8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), + // TODO: save button, loader, themestyle chooser + Scaffold { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .verticalScroll(rememberScrollState()), ) { - items(ColorsLight.entries) { - ThemeRadioButton( - name = it.name, - colorScheme = it.scheme, - isSelected = state.previewTheme.colorsLight == it, - onClick = { viewModel.postEvent(Event.PreviewLightTheme(it)) }, - ) + Text( + text = "Light themes", + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(horizontal = 8.dp), + ) + LazyRow( + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(all = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(ColorsLight.entries) { + ThemeRadioButton( + name = it.name, + colorScheme = it.scheme, + isSelected = state.previewTheme.colorsLight == it, + onClick = { viewModel.postEvent(Event.PreviewLightTheme(it)) }, + ) + } } - } - ThemePreview(state.previewTheme.colorsLight.scheme) + ThemePreview(state.previewTheme.colorsLight.scheme) - Spacer(Modifier.height(24.dp)) - Text( - text = "Dark themes", - fontWeight = FontWeight.SemiBold, - modifier = Modifier.padding(horizontal = 8.dp), - ) - LazyRow( - modifier = Modifier.fillMaxWidth(), - contentPadding = PaddingValues(all = 8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - items(ColorsDark.entries) { - ThemeRadioButton( - name = it.name, - colorScheme = it.scheme, - isSelected = state.previewTheme.colorsDark == it, - onClick = { viewModel.postEvent(Event.PreviewDarkTheme(it)) }, - ) + Spacer(Modifier.height(24.dp)) + Text( + text = "Dark themes", + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(horizontal = 8.dp), + ) + LazyRow( + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(all = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(ColorsDark.entries) { + ThemeRadioButton( + name = it.name, + colorScheme = it.scheme, + isSelected = state.previewTheme.colorsDark == it, + onClick = { viewModel.postEvent(Event.PreviewDarkTheme(it)) }, + ) + } } + ThemePreview(state.previewTheme.colorsDark.scheme) } - ThemePreview(state.previewTheme.colorsDark.scheme) + } + + if (state.showSaveCloseDialog) { + SaveOrCloseDialog( + onDismiss = { viewModel.postEvent(Event.HideSaveCloseDialog) }, + onSaveClose = { + viewModel.postEvent(Event.SaveTheme) + viewModel.postEvent(Event.PopBack) + }, + onNoSaveClose = { viewModel.postEvent(Event.PopBack) }, + onNoClose = { viewModel.postEvent(Event.HideSaveCloseDialog) }, + ) + } + + BackHandler { + viewModel.postEvent(Event.PopBackIfSaved) } } @@ -159,7 +183,7 @@ private fun ThemePreview(colorScheme: ColorScheme) { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(2.dp), ) { - Text("Theme Preview") + Text(text = "Theme Preview", color = MaterialTheme.colorScheme.onBackground) Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { Button(onClick = {}) { Text("Button") } OutlinedButton(onClick = {}) { Text("Button") } @@ -176,3 +200,43 @@ private fun ThemePreview(colorScheme: ColorScheme) { } } } + +@Composable +private fun SaveOrCloseDialog( + onDismiss: () -> Unit, + onSaveClose: () -> Unit, + onNoSaveClose: () -> Unit, + onNoClose: () -> Unit, +) { + Dialog(onDismissRequest = onDismiss) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(all = 24.dp)) { + Text( + text = "Changes aren't saved", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + ) + Spacer(Modifier.height(24.dp)) + + Button( + onClick = onSaveClose, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Save and close") + } + OutlinedButton( + onClick = onNoSaveClose, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Don't save and close") + } + OutlinedButton( + onClick = onNoClose, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Don't close") + } + } + } + } +} diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeVM.kt index 76f69f8..27c3aa4 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeVM.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeVM.kt @@ -60,15 +60,23 @@ internal class ChooseThemeVM @Inject constructor( launch { navManager.navigate(NavCommand.PopBack) } return } + setState { copy(showSaveCloseDialog = true) } } Event.PopBack -> launch { navManager.navigate(NavCommand.PopBack) } + + Event.HideSaveCloseDialog -> setState { copy(showSaveCloseDialog = false) } } } private fun saveTheme() = with(state.value.previewTheme) { - themePreferences.setLightTheme(colorsLight.name) - themePreferences.setDarkTheme(colorsDark.name) - themePreferences.setThemeStyle(themeStyle.name) + themePreferences.setAll( + ThemePreferences.ThemeModel( + lightTheme = colorsLight.name, + darkTheme = colorsDark.name, + themeStyle = themeStyle.name, + ), + ) + loadSavedTheme() } } From 2bef59da3a9b14c34f08c906e44e8c542c0d1691 Mon Sep 17 00:00:00 2001 From: retanar Date: Thu, 13 Mar 2025 13:07:13 +0200 Subject: [PATCH 08/11] Added save button --- .../homeImpl/theming/ChooseThemeContract.kt | 1 + .../homeImpl/theming/ChooseThemeScreen.kt | 24 +++++++++++++--- .../homeImpl/theming/ChooseThemeVM.kt | 28 +++++++++---------- .../homeImpl/src/main/res/drawable/save.xml | 11 ++++++++ 4 files changed, 45 insertions(+), 19 deletions(-) create mode 100644 feature/homeImpl/src/main/res/drawable/save.xml diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeContract.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeContract.kt index 55d7f27..54620d5 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeContract.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeContract.kt @@ -9,6 +9,7 @@ import com.featuremodule.core.ui.theme.ThemeStyle internal data class State( val isLoading: Boolean = true, val previewTheme: ThemeState = ThemeState(), + val isThemeSaved: Boolean = true, val showSaveCloseDialog: Boolean = false, ) : UiState diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt index c7da4c3..62db3db 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt @@ -24,6 +24,8 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.ColorScheme +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedCard @@ -35,6 +37,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -43,13 +46,25 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.featuremodule.core.ui.theme.ColorsDark import com.featuremodule.core.ui.theme.ColorsLight +import com.featuremodule.homeImpl.R @Composable internal fun ChooseThemeScreen(viewModel: ChooseThemeVM = hiltViewModel()) { val state by viewModel.state.collectAsStateWithLifecycle() - // TODO: save button, loader, themestyle chooser - Scaffold { innerPadding -> + // TODO: loader, themestyle chooser + Scaffold( + floatingActionButton = { + if (!state.isThemeSaved) { + FloatingActionButton(onClick = { viewModel.postEvent(Event.SaveTheme) }) { + Icon( + painter = painterResource(R.drawable.save), + contentDescription = null, + ) + } + } + }, + ) { innerPadding -> Column( modifier = Modifier .padding(innerPadding) @@ -76,8 +91,8 @@ internal fun ChooseThemeScreen(viewModel: ChooseThemeVM = hiltViewModel()) { } } ThemePreview(state.previewTheme.colorsLight.scheme) - Spacer(Modifier.height(24.dp)) + Text( text = "Dark themes", fontWeight = FontWeight.SemiBold, @@ -98,6 +113,7 @@ internal fun ChooseThemeScreen(viewModel: ChooseThemeVM = hiltViewModel()) { } } ThemePreview(state.previewTheme.colorsDark.scheme) + Spacer(Modifier.height(24.dp)) } } @@ -113,7 +129,7 @@ internal fun ChooseThemeScreen(viewModel: ChooseThemeVM = hiltViewModel()) { ) } - BackHandler { + BackHandler(enabled = !state.isThemeSaved) { viewModel.postEvent(Event.PopBackIfSaved) } } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeVM.kt index 27c3aa4..c2c7bd3 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeVM.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeVM.kt @@ -41,20 +41,10 @@ internal class ChooseThemeVM @Inject constructor( override fun handleEvent(event: Event) { when (event) { - is Event.PreviewLightTheme -> setState { - copy(previewTheme = previewTheme.copy(colorsLight = event.colors)) - } - - is Event.PreviewDarkTheme -> setState { - copy(previewTheme = previewTheme.copy(colorsDark = event.colors)) - } - - is Event.SetThemeStyle -> setState { - copy(previewTheme = previewTheme.copy(themeStyle = event.themeStyle)) - } - - is Event.SaveTheme -> saveTheme() - + is Event.PreviewLightTheme -> updatePreviewTheme { copy(colorsLight = event.colors) } + is Event.PreviewDarkTheme -> updatePreviewTheme { copy(colorsDark = event.colors) } + is Event.SetThemeStyle -> updatePreviewTheme { copy(themeStyle = event.themeStyle) } + Event.SaveTheme -> saveTheme() Event.PopBackIfSaved -> { if (state.value.previewTheme == savedTheme) { launch { navManager.navigate(NavCommand.PopBack) } @@ -64,11 +54,18 @@ internal class ChooseThemeVM @Inject constructor( } Event.PopBack -> launch { navManager.navigate(NavCommand.PopBack) } - Event.HideSaveCloseDialog -> setState { copy(showSaveCloseDialog = false) } } } + private fun updatePreviewTheme(newTheme: ThemeState.() -> ThemeState) = setState { + val updatedTheme = previewTheme.newTheme() + copy( + previewTheme = updatedTheme, + isThemeSaved = updatedTheme == savedTheme, + ) + } + private fun saveTheme() = with(state.value.previewTheme) { themePreferences.setAll( ThemePreferences.ThemeModel( @@ -78,5 +75,6 @@ internal class ChooseThemeVM @Inject constructor( ), ) loadSavedTheme() + setState { copy(isThemeSaved = true) } } } diff --git a/feature/homeImpl/src/main/res/drawable/save.xml b/feature/homeImpl/src/main/res/drawable/save.xml new file mode 100644 index 0000000..5c5be9a --- /dev/null +++ b/feature/homeImpl/src/main/res/drawable/save.xml @@ -0,0 +1,11 @@ + + + + + From a5edc96c578d7a4be11efc70f75d26475c6e8f0c Mon Sep 17 00:00:00 2001 From: retanar Date: Sat, 26 Apr 2025 10:52:33 +0300 Subject: [PATCH 09/11] Added ThemeStyleChooser --- .../homeImpl/theming/ChooseThemeScreen.kt | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt index 62db3db..94b3eca 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt @@ -24,6 +24,8 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.ColorScheme +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -37,6 +39,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -46,13 +49,13 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.featuremodule.core.ui.theme.ColorsDark import com.featuremodule.core.ui.theme.ColorsLight +import com.featuremodule.core.ui.theme.ThemeStyle import com.featuremodule.homeImpl.R @Composable internal fun ChooseThemeScreen(viewModel: ChooseThemeVM = hiltViewModel()) { val state by viewModel.state.collectAsStateWithLifecycle() - // TODO: loader, themestyle chooser Scaffold( floatingActionButton = { if (!state.isThemeSaved) { @@ -71,6 +74,8 @@ internal fun ChooseThemeScreen(viewModel: ChooseThemeVM = hiltViewModel()) { .fillMaxSize() .verticalScroll(rememberScrollState()), ) { + ThemeStyleChooser(state.previewTheme.themeStyle, viewModel::postEvent) + Text( text = "Light themes", fontWeight = FontWeight.SemiBold, @@ -134,6 +139,61 @@ internal fun ChooseThemeScreen(viewModel: ChooseThemeVM = hiltViewModel()) { } } +@Composable +private fun ThemeStyleChooser( + themeStyle: ThemeStyle, + postEvent: (Event) -> Unit, +) { + var isThemeDropdownExpanded = false + + Row( + modifier = Modifier.height(48.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "Theme style", + fontWeight = FontWeight.SemiBold, + modifier = Modifier + .padding(horizontal = 8.dp) + .weight(1f), + ) + + Box { + // Additional clip is needed to limit Ripple effect + Text( + text = themeStyle.toString(), + modifier = Modifier + .padding(horizontal = 8.dp) + .background( + color = MaterialTheme.colorScheme.surfaceContainer, + shape = MaterialTheme.shapes.medium, + ) + .clip(shape = MaterialTheme.shapes.medium) + .clickable { isThemeDropdownExpanded = !isThemeDropdownExpanded } + .padding(all = 8.dp), + ) + + DropdownMenu( + expanded = isThemeDropdownExpanded, + onDismissRequest = { isThemeDropdownExpanded = false }, + ) { + DropdownMenuItem( + text = { Text("Light") }, + onClick = { postEvent(Event.SetThemeStyle(ThemeStyle.Light)) }, + ) + DropdownMenuItem( + text = { Text("Dark") }, + onClick = { postEvent(Event.SetThemeStyle(ThemeStyle.Dark)) }, + ) + DropdownMenuItem( + text = { Text("System") }, + onClick = { postEvent(Event.SetThemeStyle(ThemeStyle.System)) }, + ) + } + } + } +} + @Composable private fun ThemeRadioButton( name: String, From 71696e5c92bb21aa03758987271ab4fee78745b9 Mon Sep 17 00:00:00 2001 From: retanar Date: Sat, 26 Apr 2025 12:11:06 +0300 Subject: [PATCH 10/11] Ktling reformat, some code and theme fixes, and small changes --- .../featuremodule/template/ui/AppContent.kt | 8 ++--- .../featuremodule/core/ui/theme/AppTheme.kt | 15 ++++++++ .../com/featuremodule/core/ui/theme/Themes.kt | 6 ++-- .../data/prefs/ThemePreferences.kt | 3 +- .../homeImpl/theming/ChooseThemeContract.kt | 5 ++- .../homeImpl/theming/ChooseThemeScreen.kt | 36 +++++++++---------- .../homeImpl/theming/ChooseThemeVM.kt | 7 ++-- 7 files changed, 45 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/featuremodule/template/ui/AppContent.kt b/app/src/main/java/com/featuremodule/template/ui/AppContent.kt index b385cd3..b593aec 100644 --- a/app/src/main/java/com/featuremodule/template/ui/AppContent.kt +++ b/app/src/main/java/com/featuremodule/template/ui/AppContent.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -34,7 +33,7 @@ internal fun AppContent( updateLoadedState(state.isLoaded) } - LaunchedEffect(state.theme) { + LaunchedEffect(state.theme, updateTheme) { updateTheme(state.theme) } @@ -51,9 +50,8 @@ internal fun AppContent( currentDestination = backStackEntry?.destination, ) }, - contentWindowInsets = WindowInsets(0), - // Remove this and status bar coloring in AppTheme for edge to edge - modifier = Modifier.windowInsetsPadding(WindowInsets.statusBars), + // Remove this for edge to edge + contentWindowInsets = WindowInsets.statusBars, ) { innerPadding -> AppNavHost( navController = navController, diff --git a/core/src/main/java/com/featuremodule/core/ui/theme/AppTheme.kt b/core/src/main/java/com/featuremodule/core/ui/theme/AppTheme.kt index 5b24c16..b1d7ad4 100644 --- a/core/src/main/java/com/featuremodule/core/ui/theme/AppTheme.kt +++ b/core/src/main/java/com/featuremodule/core/ui/theme/AppTheme.kt @@ -1,10 +1,14 @@ package com.featuremodule.core.ui.theme +import android.app.Activity import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat @Composable fun AppTheme( @@ -20,6 +24,17 @@ fun AppTheme( } val colorScheme = if (isStyleDark) colorsDark else colorsLight + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + WindowCompat.getInsetsController(window, view) + .isAppearanceLightStatusBars = !isStyleDark + WindowCompat.getInsetsController(window, view) + .isAppearanceLightNavigationBars = !isStyleDark + } + } + ProvideAppColors(isStyleDark) { MaterialTheme( colorScheme = colorScheme, diff --git a/core/src/main/java/com/featuremodule/core/ui/theme/Themes.kt b/core/src/main/java/com/featuremodule/core/ui/theme/Themes.kt index be82800..5845379 100644 --- a/core/src/main/java/com/featuremodule/core/ui/theme/Themes.kt +++ b/core/src/main/java/com/featuremodule/core/ui/theme/Themes.kt @@ -90,7 +90,7 @@ enum class ColorsLight(val scheme: ColorScheme) { surfaceContainerHigh = Color(0xFFE6E9E0), surfaceContainerHighest = Color(0xFFE0E4DB), ), - ) + ), } enum class ColorsDark(val scheme: ColorScheme) { @@ -178,12 +178,12 @@ enum class ColorsDark(val scheme: ColorScheme) { surfaceContainerHigh = Color(0xFF272B25), surfaceContainerHighest = Color(0xFF323630), ), - ) + ), } /** Sets light or dark theme as active, or switches it with system */ enum class ThemeStyle { Light, Dark, - System + System, } diff --git a/data/src/main/java/com/featuremodule/data/prefs/ThemePreferences.kt b/data/src/main/java/com/featuremodule/data/prefs/ThemePreferences.kt index 089af02..a8c44b0 100644 --- a/data/src/main/java/com/featuremodule/data/prefs/ThemePreferences.kt +++ b/data/src/main/java/com/featuremodule/data/prefs/ThemePreferences.kt @@ -19,8 +19,7 @@ class ThemePreferences @Inject constructor( fun setDarkTheme(theme: String?) = preferences.edit { putString(KEY_THEME_DARK, theme) } - fun setThemeStyle(theme: String?) = - preferences.edit { putString(KEY_THEME_STYLE, theme) } + fun setThemeStyle(theme: String?) = preferences.edit { putString(KEY_THEME_STYLE, theme) } fun setAll(themeModel: ThemeModel) = preferences.edit { putString(KEY_THEME_LIGHT, themeModel.lightTheme) diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeContract.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeContract.kt index 54620d5..18075ee 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeContract.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeContract.kt @@ -7,7 +7,6 @@ import com.featuremodule.core.ui.theme.ColorsLight import com.featuremodule.core.ui.theme.ThemeStyle internal data class State( - val isLoading: Boolean = true, val previewTheme: ThemeState = ThemeState(), val isThemeSaved: Boolean = true, val showSaveCloseDialog: Boolean = false, @@ -20,8 +19,8 @@ internal data class ThemeState( ) internal sealed interface Event : UiEvent { - data class PreviewLightTheme(val colors: ColorsLight) : Event - data class PreviewDarkTheme(val colors: ColorsDark) : Event + data class SetLightTheme(val colors: ColorsLight) : Event + data class SetDarkTheme(val colors: ColorsDark) : Event data class SetThemeStyle(val themeStyle: ThemeStyle) : Event data object SaveTheme : Event diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt index 94b3eca..0bd86ee 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt @@ -37,6 +37,9 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton 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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -91,7 +94,7 @@ internal fun ChooseThemeScreen(viewModel: ChooseThemeVM = hiltViewModel()) { name = it.name, colorScheme = it.scheme, isSelected = state.previewTheme.colorsLight == it, - onClick = { viewModel.postEvent(Event.PreviewLightTheme(it)) }, + onClick = { viewModel.postEvent(Event.SetLightTheme(it)) }, ) } } @@ -113,7 +116,7 @@ internal fun ChooseThemeScreen(viewModel: ChooseThemeVM = hiltViewModel()) { name = it.name, colorScheme = it.scheme, isSelected = state.previewTheme.colorsDark == it, - onClick = { viewModel.postEvent(Event.PreviewDarkTheme(it)) }, + onClick = { viewModel.postEvent(Event.SetDarkTheme(it)) }, ) } } @@ -140,11 +143,8 @@ internal fun ChooseThemeScreen(viewModel: ChooseThemeVM = hiltViewModel()) { } @Composable -private fun ThemeStyleChooser( - themeStyle: ThemeStyle, - postEvent: (Event) -> Unit, -) { - var isThemeDropdownExpanded = false +private fun ThemeStyleChooser(themeStyle: ThemeStyle, postEvent: (Event) -> Unit) { + var isThemeDropdownExpanded by remember { mutableStateOf(false) } Row( modifier = Modifier.height(48.dp), @@ -177,18 +177,18 @@ private fun ThemeStyleChooser( expanded = isThemeDropdownExpanded, onDismissRequest = { isThemeDropdownExpanded = false }, ) { - DropdownMenuItem( - text = { Text("Light") }, - onClick = { postEvent(Event.SetThemeStyle(ThemeStyle.Light)) }, - ) - DropdownMenuItem( - text = { Text("Dark") }, - onClick = { postEvent(Event.SetThemeStyle(ThemeStyle.Dark)) }, - ) - DropdownMenuItem( - text = { Text("System") }, - onClick = { postEvent(Event.SetThemeStyle(ThemeStyle.System)) }, + @Composable + fun Item(text: String, style: ThemeStyle) = DropdownMenuItem( + text = { Text(text) }, + onClick = { + postEvent(Event.SetThemeStyle(style)) + isThemeDropdownExpanded = false + }, ) + + Item("Light", ThemeStyle.Light) + Item("Dark", ThemeStyle.Dark) + Item("System", ThemeStyle.System) } } } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeVM.kt index c2c7bd3..f8a517e 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeVM.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeVM.kt @@ -20,7 +20,7 @@ internal class ChooseThemeVM @Inject constructor( init { launch { loadSavedTheme() - setState { copy(previewTheme = savedTheme, isLoading = false) } + setState { copy(previewTheme = savedTheme) } } } @@ -28,7 +28,6 @@ internal class ChooseThemeVM @Inject constructor( savedTheme = themePreferences.getCurrentPreferences().toThemeState() } - // Light colors can be assigned dark colors if user sets dark theme with no switching private fun ThemePreferences.ThemeModel.toThemeState() = ThemeState( colorsLight = ColorsLight.entries.find { it.name == lightTheme } ?: ColorsLight.Default, @@ -41,8 +40,8 @@ internal class ChooseThemeVM @Inject constructor( override fun handleEvent(event: Event) { when (event) { - is Event.PreviewLightTheme -> updatePreviewTheme { copy(colorsLight = event.colors) } - is Event.PreviewDarkTheme -> updatePreviewTheme { copy(colorsDark = event.colors) } + is Event.SetLightTheme -> updatePreviewTheme { copy(colorsLight = event.colors) } + is Event.SetDarkTheme -> updatePreviewTheme { copy(colorsDark = event.colors) } is Event.SetThemeStyle -> updatePreviewTheme { copy(themeStyle = event.themeStyle) } Event.SaveTheme -> saveTheme() Event.PopBackIfSaved -> { From 262ecbc32245b271a6cc27dfae298ab34daa194b Mon Sep 17 00:00:00 2001 From: retanar Date: Sat, 26 Apr 2025 13:28:28 +0300 Subject: [PATCH 11/11] Detekt fixes --- .../com/featuremodule/core/ui/theme/Themes.kt | 2 + .../homeImpl/theming/ChooseThemeScreen.kt | 49 ++++++++++--------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/core/src/main/java/com/featuremodule/core/ui/theme/Themes.kt b/core/src/main/java/com/featuremodule/core/ui/theme/Themes.kt index 5845379..a84b1c4 100644 --- a/core/src/main/java/com/featuremodule/core/ui/theme/Themes.kt +++ b/core/src/main/java/com/featuremodule/core/ui/theme/Themes.kt @@ -1,3 +1,5 @@ +@file:Suppress("MagicNumber") + package com.featuremodule.core.ui.theme import androidx.compose.material3.ColorScheme diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt index 0bd86ee..3786b7d 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/theming/ChooseThemeScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState @@ -79,16 +80,7 @@ internal fun ChooseThemeScreen(viewModel: ChooseThemeVM = hiltViewModel()) { ) { ThemeStyleChooser(state.previewTheme.themeStyle, viewModel::postEvent) - Text( - text = "Light themes", - fontWeight = FontWeight.SemiBold, - modifier = Modifier.padding(horizontal = 8.dp), - ) - LazyRow( - modifier = Modifier.fillMaxWidth(), - contentPadding = PaddingValues(all = 8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { + ThemeChooserBlock("Light themes", state.previewTheme.colorsLight.scheme) { items(ColorsLight.entries) { ThemeRadioButton( name = it.name, @@ -98,19 +90,8 @@ internal fun ChooseThemeScreen(viewModel: ChooseThemeVM = hiltViewModel()) { ) } } - ThemePreview(state.previewTheme.colorsLight.scheme) - Spacer(Modifier.height(24.dp)) - Text( - text = "Dark themes", - fontWeight = FontWeight.SemiBold, - modifier = Modifier.padding(horizontal = 8.dp), - ) - LazyRow( - modifier = Modifier.fillMaxWidth(), - contentPadding = PaddingValues(all = 8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { + ThemeChooserBlock("Dark themes", state.previewTheme.colorsDark.scheme) { items(ColorsDark.entries) { ThemeRadioButton( name = it.name, @@ -120,8 +101,6 @@ internal fun ChooseThemeScreen(viewModel: ChooseThemeVM = hiltViewModel()) { ) } } - ThemePreview(state.previewTheme.colorsDark.scheme) - Spacer(Modifier.height(24.dp)) } } @@ -194,6 +173,28 @@ private fun ThemeStyleChooser(themeStyle: ThemeStyle, postEvent: (Event) -> Unit } } +@Composable +private fun ThemeChooserBlock( + title: String, + colorScheme: ColorScheme, + radioButtonsContent: LazyListScope.() -> Unit, +) = Column { + Text( + text = title, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(horizontal = 8.dp), + ) + LazyRow( + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(all = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + radioButtonsContent() + } + ThemePreview(colorScheme) + Spacer(Modifier.height(24.dp)) +} + @Composable private fun ThemeRadioButton( name: String,