diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7628070c..7ccfe25b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -162,4 +162,7 @@ dependencies { // Retrofit with Moshi Converter implementation("com.squareup.retrofit2:converter-moshi:2.9.0") + + // Themmo + implementation("com.github.sadellie:themmo:0.0.2") } \ No newline at end of file diff --git a/app/src/main/java/com/sadellie/unitto/MainActivity.kt b/app/src/main/java/com/sadellie/unitto/MainActivity.kt index 44254f8f..67e12fad 100644 --- a/app/src/main/java/com/sadellie/unitto/MainActivity.kt +++ b/app/src/main/java/com/sadellie/unitto/MainActivity.kt @@ -22,17 +22,21 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels +import androidx.compose.animation.core.tween +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.sadellie.unitto.data.NavRoutes.ABOUT_SCREEN import com.sadellie.unitto.data.NavRoutes.LEFT_LIST_SCREEN import com.sadellie.unitto.data.NavRoutes.MAIN_SCREEN import com.sadellie.unitto.data.NavRoutes.RIGHT_LIST_SCREEN import com.sadellie.unitto.data.NavRoutes.SETTINGS_SCREEN -import com.sadellie.unitto.data.preferences.AppTheme +import com.sadellie.unitto.data.NavRoutes.THEMES_SCREEN import com.sadellie.unitto.screens.MainViewModel import com.sadellie.unitto.screens.about.AboutScreen import com.sadellie.unitto.screens.main.MainScreen @@ -40,34 +44,58 @@ import com.sadellie.unitto.screens.second.LeftSideScreen import com.sadellie.unitto.screens.second.RightSideScreen import com.sadellie.unitto.screens.second.SecondViewModel import com.sadellie.unitto.screens.setttings.SettingsScreen -import com.sadellie.unitto.ui.theme.UnittoTheme +import com.sadellie.unitto.screens.theming.ThemesScreen +import com.sadellie.unitto.screens.theming.ThemesViewModel +import com.sadellie.unitto.ui.theme.AppTypography +import com.sadellie.unitto.ui.theme.DarkThemeColors +import com.sadellie.unitto.ui.theme.LightThemeColors import dagger.hilt.android.AndroidEntryPoint +import io.github.sadellie.themmo.Themmo +import io.github.sadellie.themmo.ThemmoController +import io.github.sadellie.themmo.rememberThemmoController @AndroidEntryPoint class MainActivity : ComponentActivity() { private val mainViewModel: MainViewModel by viewModels() private val secondViewModel: SecondViewModel by viewModels() + private val themesViewModel: ThemesViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - val navController = rememberNavController() - val currentAppTheme: Int = mainViewModel.currentTheme + themesViewModel.collectThemeOptions() - // We don't draw anything until we know what theme we need to use - if (currentAppTheme != AppTheme.NOT_SET) { - UnittoTheme( - currentAppTheme = currentAppTheme - ) { - UnittoApp( - navController = navController, - mainViewModel = mainViewModel, - secondViewModel = secondViewModel - ) - } + val themmoController = rememberThemmoController( + lightColorScheme = LightThemeColors, + darkColorScheme = DarkThemeColors, + // Anything below will not called if theming mode is still loading from DataStore + themingMode = themesViewModel.themingMode ?: return@setContent, + dynamicThemeEnabled = themesViewModel.enableDynamic, + amoledThemeEnabled = themesViewModel.enableAmoled + ) + val navController = rememberNavController() + val sysUiController = rememberSystemUiController() + + Themmo( + themmoController = themmoController, + typography = AppTypography, + animationSpec = tween(150) + ) { + val backgroundColor = MaterialTheme.colorScheme.background + + UnittoApp( + navController = navController, + mainViewModel = mainViewModel, + secondViewModel = secondViewModel, + themesViewModel = themesViewModel, + themmoController = it + ) + + SideEffect { sysUiController.setSystemBarsColor(backgroundColor) } } + } } @@ -81,7 +109,9 @@ class MainActivity : ComponentActivity() { fun UnittoApp( navController: NavHostController, mainViewModel: MainViewModel, - secondViewModel: SecondViewModel + secondViewModel: SecondViewModel, + themesViewModel: ThemesViewModel, + themmoController: ThemmoController ) { NavHost( navController = navController, @@ -123,6 +153,14 @@ fun UnittoApp( ) } + composable(THEMES_SCREEN) { + ThemesScreen( + navigateUpAction = { navController.navigateUp() }, + themmoController = themmoController, + viewModel = themesViewModel + ) + } + composable(ABOUT_SCREEN) { AboutScreen(navigateUpAction = { navController.navigateUp() }) } diff --git a/app/src/main/java/com/sadellie/unitto/data/NavRoutes.kt b/app/src/main/java/com/sadellie/unitto/data/NavRoutes.kt index b62e2fe7..a3ecbd7f 100644 --- a/app/src/main/java/com/sadellie/unitto/data/NavRoutes.kt +++ b/app/src/main/java/com/sadellie/unitto/data/NavRoutes.kt @@ -28,5 +28,6 @@ object NavRoutes { const val RIGHT_LIST_SCREEN = "RightScreen" const val SETTINGS_SCREEN = "SettingsScreen" + const val THEMES_SCREEN = "ThemesScreen" const val ABOUT_SCREEN = "AboutScreen" } diff --git a/app/src/main/java/com/sadellie/unitto/data/preferences/AppTheme.kt b/app/src/main/java/com/sadellie/unitto/data/preferences/AppTheme.kt deleted file mode 100644 index d5e6db66..00000000 --- a/app/src/main/java/com/sadellie/unitto/data/preferences/AppTheme.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Unitto is a unit converter for Android - * Copyright (c) 2022-2022 Elshan Agaev - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.sadellie.unitto.data.preferences - -import android.os.Build -import com.sadellie.unitto.R - - -/** - * All possible state of theme in the app - */ -object AppTheme { - // Used on app launch when we don't know which theme to use - const val NOT_SET = 0 - - const val AUTO = 1 - const val LIGHT = 2 - const val DARK = 3 - const val LIGHT_DYNAMIC = 4 - const val DARK_DYNAMIC = 5 - const val AMOLED = 6 -} - -/** - * Device specific map of available themes. Used in settings - */ -val APP_THEMES: Map by lazy { - // Dynamic themes are only for Android 8.1 and later - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - mapOf( - AppTheme.AUTO to R.string.force_auto_mode, - AppTheme.LIGHT to R.string.force_light_mode, - AppTheme.DARK to R.string.force_dark_mode, - AppTheme.AMOLED to R.string.force_amoled_mode, - AppTheme.LIGHT_DYNAMIC to R.string.force_light_dynamic_mode, - AppTheme.DARK_DYNAMIC to R.string.force_dark_dynamic_mode, - ) - } else { - mapOf( - AppTheme.AUTO to R.string.force_auto_mode, - AppTheme.LIGHT to R.string.force_light_mode, - AppTheme.DARK to R.string.force_dark_mode, - AppTheme.AMOLED to R.string.force_amoled_mode, - ) - } -} diff --git a/app/src/main/java/com/sadellie/unitto/data/preferences/UserPreferences.kt b/app/src/main/java/com/sadellie/unitto/data/preferences/UserPreferences.kt index d9a5e670..1cc4b570 100644 --- a/app/src/main/java/com/sadellie/unitto/data/preferences/UserPreferences.kt +++ b/app/src/main/java/com/sadellie/unitto/data/preferences/UserPreferences.kt @@ -22,19 +22,24 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import com.sadellie.unitto.data.units.AbstractUnit import com.sadellie.unitto.data.units.MyUnitIDS +import io.github.sadellie.themmo.ThemingMode import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map +import okio.IOException import javax.inject.Inject - /** * Represents user preferences that are user across the app * - * @property currentAppTheme Current [AppTheme] to be used + * @property themingMode [ThemingMode] from Themmo + * @property enableDynamicTheme Use dynamic color scheme + * @property enableAmoledTheme Use amoled color scheme * @property digitsPrecision Current [PRECISIONS]. Number of digits in fractional part * @property separator Current [Separator] that used to separate thousands * @property outputFormat Current [OutputFormat] that is applied to converted value (not input) @@ -43,7 +48,9 @@ import javax.inject.Inject * @property enableAnalytics Whether or not user wants to share application usage data */ data class UserPreferences( - val currentAppTheme: Int, + val themingMode: ThemingMode, + val enableDynamicTheme: Boolean, + val enableAmoledTheme: Boolean, val digitsPrecision: Int, val separator: Int, val outputFormat: Int, @@ -61,7 +68,9 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS * Keys for DataStore */ private object PrefsKeys { - val CURRENT_APP_THEME = intPreferencesKey("CURRENT_APP_THEME") + val THEMING_MODE = stringPreferencesKey("THEMING_MODE_PREF_KEY") + val ENABLE_DYNAMIC_THEME = booleanPreferencesKey("ENABLE_DYNAMIC_THEME_PREF_KEY") + val ENABLE_AMOLED_THEME = booleanPreferencesKey("ENABLE_AMOLED_THEME_PREF_KEY") val DIGITS_PRECISION = intPreferencesKey("DIGITS_PRECISION_PREF_KEY") val SEPARATOR = intPreferencesKey("SEPARATOR_PREF_KEY") val OUTPUT_FORMAT = intPreferencesKey("OUTPUT_FORMAT_PREF_KEY") @@ -71,9 +80,21 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS } val userPreferencesFlow: Flow = dataStore.data + .catch { exception -> + if (exception is IOException) { + emit(emptyPreferences()) + } else { + throw exception + } + } .map { preferences -> - val currentAppTheme: Int = - preferences[PrefsKeys.CURRENT_APP_THEME] ?: AppTheme.AUTO + val themingMode: ThemingMode = + preferences[PrefsKeys.THEMING_MODE]?.let { ThemingMode.valueOf(it) } + ?: ThemingMode.AUTO + val enableDynamicTheme: Boolean = + preferences[PrefsKeys.ENABLE_DYNAMIC_THEME] ?: false + val enableAmoledTheme: Boolean = + preferences[PrefsKeys.ENABLE_AMOLED_THEME] ?: false val digitsPrecision: Int = preferences[PrefsKeys.DIGITS_PRECISION] ?: 3 val separator: Int = @@ -88,7 +109,9 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS preferences[PrefsKeys.ENABLE_ANALYTICS] ?: true UserPreferences( - currentAppTheme = currentAppTheme, + themingMode = themingMode, + enableDynamicTheme = enableDynamicTheme, + enableAmoledTheme = enableAmoledTheme, digitsPrecision = digitsPrecision, separator = separator, outputFormat = outputFormat, @@ -98,17 +121,6 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS ) } - /** - * Update current theme preference in DataStore - * - * @param appTheme [AppTheme] to change to - */ - suspend fun updateCurrentAppTheme(appTheme: Int) { - dataStore.edit { preferences -> - preferences[PrefsKeys.CURRENT_APP_THEME] = appTheme - } - } - /** * Update precision preference in DataStore * @@ -166,4 +178,37 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS preferences[PrefsKeys.LATEST_RIGHT_SIDE] = rightSideUnit.unitId } } + + /** + * Update [ThemingMode]. Saves value as a string. + * + * @param themingMode [ThemingMode] to save. + */ + suspend fun updateThemingMode(themingMode: ThemingMode) { + dataStore.edit { preferences -> + preferences[PrefsKeys.THEMING_MODE] = themingMode.name + } + } + + /** + * Update preference on whether or not generate color scheme from device wallpaper. + * + * @param enabled True if user wants to enable this feature. + */ + suspend fun updateDynamicTheme(enabled: Boolean) { + dataStore.edit { preferences -> + preferences[PrefsKeys.ENABLE_DYNAMIC_THEME] = enabled + } + } + + /** + * Update preference on whether or not use true black colors. + * + * @param enabled True if user wants to enable this feature. + */ + suspend fun updateAmoledTheme(enabled: Boolean) { + dataStore.edit { preferences -> + preferences[PrefsKeys.ENABLE_AMOLED_THEME] = enabled + } + } } diff --git a/app/src/main/java/com/sadellie/unitto/screens/MainViewModel.kt b/app/src/main/java/com/sadellie/unitto/screens/MainViewModel.kt index b4fe3637..fb4ccfd1 100644 --- a/app/src/main/java/com/sadellie/unitto/screens/MainViewModel.kt +++ b/app/src/main/java/com/sadellie/unitto/screens/MainViewModel.kt @@ -30,7 +30,6 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics import com.sadellie.unitto.data.KEY_0 import com.sadellie.unitto.data.KEY_DOT import com.sadellie.unitto.data.KEY_MINUS -import com.sadellie.unitto.data.preferences.AppTheme import com.sadellie.unitto.data.preferences.MAX_PRECISION import com.sadellie.unitto.data.preferences.OutputFormat import com.sadellie.unitto.data.preferences.Separator @@ -59,8 +58,6 @@ class MainViewModel @Inject constructor( private val allUnitsRepository: AllUnitsRepository ) : ViewModel() { - var currentTheme: Int by mutableStateOf(AppTheme.NOT_SET) - private set var precision: Int by mutableStateOf(3) private set var separator: Int by mutableStateOf(Separator.SPACES) @@ -70,15 +67,6 @@ class MainViewModel @Inject constructor( var enableAnalytics: Boolean by mutableStateOf(false) private set - /** - * See [UserPreferencesRepository.updateCurrentAppTheme] - */ - fun updateCurrentAppTheme(appTheme: Int) { - viewModelScope.launch { - userPrefsRepository.updateCurrentAppTheme(appTheme) - } - } - /** * See [UserPreferencesRepository.updateDigitsPrecision] */ @@ -407,13 +395,11 @@ class MainViewModel @Inject constructor( * * Any change in user preferences will update mutableStateOf so composables, that rely on this * values recompose when actually needed. For example, when you change output format, composable - * in MainActivity will not be recomposed even though it needs currentTheme which is also in - * user preferences/ + * in MainActivity will not be recomposed. */ private fun observePreferenceChanges() { viewModelScope.launch { userPrefsRepository.userPreferencesFlow.collect { userPref -> - currentTheme = userPref.currentAppTheme precision = userPref.digitsPrecision separator = userPref.separator.also { Formatter.setSeparator(it) } outputFormat = userPref.outputFormat diff --git a/app/src/main/java/com/sadellie/unitto/screens/setttings/SettingsScreen.kt b/app/src/main/java/com/sadellie/unitto/screens/setttings/SettingsScreen.kt index 33cbaec3..3575bb06 100644 --- a/app/src/main/java/com/sadellie/unitto/screens/setttings/SettingsScreen.kt +++ b/app/src/main/java/com/sadellie/unitto/screens/setttings/SettingsScreen.kt @@ -18,7 +18,6 @@ package com.sadellie.unitto.screens.setttings -import android.os.Build import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -31,7 +30,6 @@ import com.sadellie.unitto.BuildConfig import com.sadellie.unitto.R import com.sadellie.unitto.data.NavRoutes.ABOUT_SCREEN import com.sadellie.unitto.data.NavRoutes.THEMES_SCREEN -import com.sadellie.unitto.data.preferences.APP_THEMES import com.sadellie.unitto.data.preferences.OUTPUT_FORMAT import com.sadellie.unitto.data.preferences.PRECISIONS import com.sadellie.unitto.data.preferences.SEPARATORS @@ -91,7 +89,7 @@ fun SettingsScreen( SettingsListItem( stringResource(R.string.theme_setting), stringResource(R.string.theme_setting_support) - ) { dialogState = DialogState.THEME } + ) { navControllerAction(THEMES_SCREEN) } } // CURRENCY RATE NOTE @@ -192,19 +190,6 @@ fun SettingsScreen( supportText = stringResource(id = R.string.output_format_setting_info) ) } - DialogState.THEME -> { - AlertDialogWithList( - title = stringResource(id = R.string.theme_setting), - listItems = APP_THEMES, - selectedItemIndex = mainViewModel.currentTheme, - selectAction = { mainViewModel.updateCurrentAppTheme(it) }, - dismissAction = { resetDialog() }, - // Show note for users with devices that support custom Dynamic theming - supportText = if (Build.VERSION.SDK_INT in (Build.VERSION_CODES.O_MR1..Build.VERSION_CODES.R)) stringResource( - id = R.string.theme_setting_info - ) else null - ) - } DialogState.CURRENCY_RATE -> { AlertDialogWithList( title = stringResource(id = R.string.currency_rates_note_title), @@ -222,5 +207,5 @@ fun SettingsScreen( * All possible states for alert dialog that opens when user clicks on settings. */ private enum class DialogState { - NONE, PRECISION, SEPARATOR, OUTPUT_FORMAT, THEME, CURRENCY_RATE, + NONE, PRECISION, SEPARATOR, OUTPUT_FORMAT, CURRENCY_RATE, } diff --git a/app/src/main/java/com/sadellie/unitto/screens/setttings/components/SettingsListItem.kt b/app/src/main/java/com/sadellie/unitto/screens/setttings/components/SettingsListItem.kt index e1dc941c..cfbf9421 100644 --- a/app/src/main/java/com/sadellie/unitto/screens/setttings/components/SettingsListItem.kt +++ b/app/src/main/java/com/sadellie/unitto/screens/setttings/components/SettingsListItem.kt @@ -25,12 +25,21 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable +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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow @@ -121,3 +130,59 @@ fun SettingsListItem( ) = BasicSettingsListItem(label, supportText, { onSwitchChange(switchState) }) { Switch(checked = switchState, onCheckedChange = { onSwitchChange(!it) }) } + +/** + * Represents one item in list on Settings screen with drop-down menu. + * + * @param label Main text. + * @param supportText Text that is located below label. + * @param allOptions Options in drop-down menu. + * @param selected Selected option. + * @param onSelectedChange Action to perform when drop-down menu item is selected. + */ +@Composable +fun SettingsListItem( + label: String, + supportText: String? = null, + allOptions: Collection, + selected: T, + onSelectedChange: (T) -> Unit +) = BasicSettingsListItem(label, supportText, {}) { + var dropDownExpanded by rememberSaveable { mutableStateOf(false) } + var currentOption by rememberSaveable { mutableStateOf(selected) } + + ExposedDropdownMenuBox( + modifier = Modifier, + expanded = dropDownExpanded, + onExpandedChange = { dropDownExpanded = it } + ) { + OutlinedTextField( + modifier = Modifier.widthIn(1.dp), + value = currentOption.toString(), + onValueChange = {}, + readOnly = true, + singleLine = true, + enabled = false, + colors = TextFieldDefaults.outlinedTextFieldColors( + disabledBorderColor = MaterialTheme.colorScheme.outline, + disabledTextColor = MaterialTheme.colorScheme.onSurface, + ) + ) + ExposedDropdownMenu( + modifier = Modifier.exposedDropdownSize(), + expanded = dropDownExpanded, + onDismissRequest = { dropDownExpanded = false } + ) { + allOptions.forEach { + DropdownMenuItem( + text = { Text(it.toString()) }, + onClick = { + currentOption = it + onSelectedChange(it) + dropDownExpanded = false + } + ) + } + } + } +} diff --git a/app/src/main/java/com/sadellie/unitto/screens/theming/ThemesScreen.kt b/app/src/main/java/com/sadellie/unitto/screens/theming/ThemesScreen.kt new file mode 100644 index 00000000..e12c7eb4 --- /dev/null +++ b/app/src/main/java/com/sadellie/unitto/screens/theming/ThemesScreen.kt @@ -0,0 +1,89 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2022 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.screens.theming + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.sadellie.unitto.R +import com.sadellie.unitto.screens.common.UnittoLargeTopAppBar +import com.sadellie.unitto.screens.setttings.components.SettingsListItem +import io.github.sadellie.themmo.ThemingMode +import io.github.sadellie.themmo.ThemmoController + +@Composable +fun ThemesScreen( + navigateUpAction: () -> Unit = {}, + themmoController: ThemmoController, + viewModel: ThemesViewModel +) { + UnittoLargeTopAppBar( + title = stringResource(id = R.string.theme_setting), + navigateUpAction = navigateUpAction + ) { paddingValues -> + LazyColumn(contentPadding = paddingValues) { + item { + SettingsListItem( + label = stringResource(R.string.theme_setting), + allOptions = ThemingMode.values().toList(), + selected = themmoController.currentThemingMode, + onSelectedChange = { + themmoController.setThemingMode(it) + viewModel.updateThemingMode(it) + } + ) + } + + item { + SettingsListItem( + label = stringResource(R.string.enable_dynamic_colors), + supportText = stringResource(R.string.enable_dynamic_colors_support), + switchState = themmoController.isDynamicThemeEnabled, + onSwitchChange = { + themmoController.enableDynamicTheme(!it) + viewModel.updateDynamicTheme(!it) + } + ) + } + + item { + AnimatedVisibility( + visible = (themmoController.currentThemingMode != ThemingMode.FORCE_LIGHT), + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut(), + ) { + SettingsListItem( + label = stringResource(R.string.force_amoled_mode), + supportText = stringResource(R.string.force_amoled_mode_support), + switchState = themmoController.isAmoledThemeEnabled, + onSwitchChange = { + themmoController.enableAmoledTheme(!it) + viewModel.updateAmoledTheme(!it) + } + ) + } + } + } + } +} diff --git a/app/src/main/java/com/sadellie/unitto/screens/theming/ThemesViewModel.kt b/app/src/main/java/com/sadellie/unitto/screens/theming/ThemesViewModel.kt new file mode 100644 index 00000000..c0d911c6 --- /dev/null +++ b/app/src/main/java/com/sadellie/unitto/screens/theming/ThemesViewModel.kt @@ -0,0 +1,80 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2022 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.screens.theming + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.sadellie.unitto.data.preferences.UserPreferencesRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import io.github.sadellie.themmo.ThemingMode +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ThemesViewModel @Inject constructor( + private val userPrefsRepository: UserPreferencesRepository +) : ViewModel() { + + /** + * @see [UserPreferencesRepository.updateThemingMode] + */ + fun updateThemingMode(themingMode: ThemingMode) { + viewModelScope.launch { + userPrefsRepository.updateThemingMode(themingMode) + } + } + + /** + * @see [UserPreferencesRepository.updateDynamicTheme] + */ + fun updateDynamicTheme(enabled: Boolean) { + viewModelScope.launch { + userPrefsRepository.updateDynamicTheme(enabled) + } + } + + /** + * @see [UserPreferencesRepository.updateAmoledTheme] + */ + fun updateAmoledTheme(enabled: Boolean) { + viewModelScope.launch { + userPrefsRepository.updateAmoledTheme(enabled) + } + } + + var themingMode: ThemingMode? by mutableStateOf(null) + var enableDynamic by mutableStateOf(false) + var enableAmoled by mutableStateOf(false) + + /** + * Collect saved theming options. Used on app launch. + */ + fun collectThemeOptions() { + viewModelScope.launch { + val userPref = userPrefsRepository.userPreferencesFlow.first() + themingMode = userPref.themingMode + enableDynamic = userPref.enableDynamicTheme + enableAmoled = userPref.enableAmoledTheme + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sadellie/unitto/ui/theme/Color.kt b/app/src/main/java/com/sadellie/unitto/ui/theme/Color.kt index 83956851..ed289108 100644 --- a/app/src/main/java/com/sadellie/unitto/ui/theme/Color.kt +++ b/app/src/main/java/com/sadellie/unitto/ui/theme/Color.kt @@ -73,5 +73,3 @@ val md_theme_dark_onSurfaceVariant = Color(0xFFc1c9be) val md_theme_dark_outline = Color(0xFF8b9389) val md_theme_dark_inverseOnSurface = Color(0xFF1a1c19) val md_theme_dark_inverseSurface = Color(0xFFe1e3dd) - -val md_theme_amoled_black = Color(0xFF000000) diff --git a/app/src/main/java/com/sadellie/unitto/ui/theme/ColorSchemeUtils.kt b/app/src/main/java/com/sadellie/unitto/ui/theme/ColorSchemeUtils.kt deleted file mode 100644 index f72918d3..00000000 --- a/app/src/main/java/com/sadellie/unitto/ui/theme/ColorSchemeUtils.kt +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Unitto is a unit converter for Android - * Copyright (c) 2022-2022 Elshan Agaev - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.sadellie.unitto.ui.theme - -import android.app.WallpaperManager -import android.content.Context -import android.os.Build -import androidx.compose.material3.ColorScheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Stable -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.luminance - - -/** - * Shifts colors to make them brighter - */ -@Stable -private fun Color.shiftTo255(ratio: Float): Color { - return this - .copy( - alpha, - red + (1.0f - red) * ratio, - green + (1.0f - green) * ratio, - blue + (1.0f - blue) * ratio, - ) -} - -/** - * Shifts colors to make them darker - */ -@Stable -private fun Color.shiftTo0(ratio: Float): Color { - return this - .copy( - alpha, - red * (1.0f - ratio), - green * (1.0f - ratio), - blue * (1.0f - ratio), - ) -} - -/** - * Decides which colors fits the best for the color that is used as a background - */ -@Stable -private fun Color.getAppropriateTextColor(): Color { - return if (luminance() > 0.5) Color.Black else Color.White -} - -/** - * Function to return dynamic light theme. Determines which API to use based on Android version - */ -fun dynamicLightTheme(context: Context): ColorScheme { - return when { - // Android 12+ devices use new api - Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> dynamicLightColorScheme(context) - // Dynamic theming for devices with Android 8.1 up to Android 11 - Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> { - // Wallpaper colors can be null. We return default theme for such cases - val wallpaperColors = WallpaperManager.getInstance(context) - .getWallpaperColors(WallpaperManager.FLAG_SYSTEM) - ?: return LightThemeColors - - val primary = Color( - red = wallpaperColors.primaryColor.red(), - green = wallpaperColors.primaryColor.green(), - blue = wallpaperColors.primaryColor.blue() - ) - // Secondary color can be null. For such cases we just generate it from primary - val secondary: Color = primary.shiftTo255(0.5f) - val background = primary.shiftTo255(0.9f) - val onBackground = background.getAppropriateTextColor() - - return lightColorScheme( - // Settings screen group text, units screen units group text - primary = primary, - // Switch thumb color - onPrimary = primary.getAppropriateTextColor(), - onPrimaryContainer = primary.shiftTo0(0.7f), - // Selected unit, group, keyboard buttons - secondaryContainer = secondary, - onSecondaryContainer = secondary.getAppropriateTextColor(), - // Background color for all screens - background = background, - // Main screen input/output text color - onBackground = onBackground, - // Collapsable top bar background - surface = background, - // Main screen Unitto text, disabled buttons - // Settings screen title and back icon, dialog window title, not selected radio button color - // Third party licenses screen title and back icon - onSurface = onBackground, - surfaceVariant = primary.shiftTo255(0.5f), - // Main Screen settings icon, Not selected chips text, short unit names - // Settings items secondary text - onSurfaceVariant = primary.shiftTo0(0.80f), - // Chips outline and cards outline on Third party licenses screen - outline = primary.shiftTo0(0.8f), - ) - } - // Just in case - else -> LightThemeColors - } -} - -/** - * Function to return dynamic light theme. Determines which API to use based on Android version - */ -fun dynamicDarkTheme(context: Context): ColorScheme { - return when { - // Android 12+ devices use new api - Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> dynamicDarkColorScheme(context) - // Dynamic theming for devices with Android 8.1 up to Android 11 - Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> { - // Wallpaper colors can be null. We return default theme for such cases - val wallpaperColors = WallpaperManager.getInstance(context) - .getWallpaperColors(WallpaperManager.FLAG_SYSTEM) - ?: return DarkThemeColors - - val primary = Color( - red = wallpaperColors.primaryColor.red(), - green = wallpaperColors.primaryColor.green(), - blue = wallpaperColors.primaryColor.blue() - ) - // Secondary color can be null. For such cases we just generate it from primary - val secondary: Color = primary.shiftTo0(0.7f) - val background = primary.shiftTo0(0.9f) - val onBackground = background.getAppropriateTextColor() - - return darkColorScheme( - // Settings screen group text, units screen units group text - primary = primary, - // Switch thumb color - onPrimary = primary.getAppropriateTextColor(), - onPrimaryContainer = primary.shiftTo255(0.7f), - // Selected unit, group, keyboard buttons - secondaryContainer = secondary, - onSecondaryContainer = secondary.getAppropriateTextColor(), - // Background color for all screens - background = background, - // Main screen input/output text color - onBackground = onBackground, - // Collapsable top bar background - surface = background, - // Main screen Unitto text, disabled buttons - // Settings screen title and back icon, dialog window title, not selected radio button color - // Third party licenses screen title and back icon - onSurface = onBackground, - surfaceVariant = primary.shiftTo0(0.5f), - // Main Screen settings icon, Not selected chips text, short unit names - // Settings items secondary text - onSurfaceVariant = primary.shiftTo255(0.80f), - // Chips outline and cards outline on Third party licenses screen - outline = primary.shiftTo255(0.8f), - ) - } - // Just in case - else -> DarkThemeColors - } -} diff --git a/app/src/main/java/com/sadellie/unitto/ui/theme/ColorSchemes.kt b/app/src/main/java/com/sadellie/unitto/ui/theme/ColorSchemes.kt index a5e4ad87..a92b86da 100644 --- a/app/src/main/java/com/sadellie/unitto/ui/theme/ColorSchemes.kt +++ b/app/src/main/java/com/sadellie/unitto/ui/theme/ColorSchemes.kt @@ -80,10 +80,3 @@ val DarkThemeColors by lazy { inverseSurface = md_theme_dark_inverseSurface, ) } - -val AmoledThemeColors by lazy { - DarkThemeColors.copy( - background = md_theme_amoled_black, - surface = md_theme_amoled_black, - ) -} diff --git a/app/src/main/java/com/sadellie/unitto/ui/theme/Theme.kt b/app/src/main/java/com/sadellie/unitto/ui/theme/Theme.kt deleted file mode 100644 index 237d5022..00000000 --- a/app/src/main/java/com/sadellie/unitto/ui/theme/Theme.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Unitto is a unit converter for Android - * Copyright (c) 2022-2022 Elshan Agaev - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.sadellie.unitto.ui.theme - -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.platform.LocalContext -import com.google.accompanist.systemuicontroller.rememberSystemUiController -import com.sadellie.unitto.data.preferences.AppTheme - - -@Composable -fun UnittoTheme( - currentAppTheme: Int, - content: @Composable () -> Unit -) { - // Dynamic color is only for Android 12 and higher - val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 - val sysUiController = rememberSystemUiController() - - val colors = when (currentAppTheme) { - AppTheme.DARK -> DarkThemeColors - AppTheme.LIGHT -> LightThemeColors - AppTheme.AMOLED -> AmoledThemeColors - AppTheme.LIGHT_DYNAMIC -> dynamicLightTheme(LocalContext.current) - AppTheme.DARK_DYNAMIC -> dynamicDarkTheme(LocalContext.current) - // When it is set to Auto - else -> { - when { - dynamicColor and !isSystemInDarkTheme() -> dynamicLightTheme(LocalContext.current) - dynamicColor and isSystemInDarkTheme() -> dynamicDarkTheme(LocalContext.current) - !dynamicColor and !isSystemInDarkTheme() -> LightThemeColors - !dynamicColor and isSystemInDarkTheme() -> DarkThemeColors - // This case is here just in case, not actually used - else -> LightThemeColors - } - } - } - - MaterialTheme( - colorScheme = colors, - typography = AppTypography, - content = content - ) - - SideEffect { - sysUiController.setSystemBarsColor( - color = colors.background - ) - } -} diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index 7df1b4ab..001fa879 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -675,7 +675,7 @@ Settings - Theme + Themes Precision Separator Output format @@ -717,8 +717,6 @@ Light Dark AMOLED Dark - Dynamic light - Dynamic dark Loading… @@ -738,5 +736,8 @@ Clear input Add or remove unit from favorites Empty search result + Dynamic colors + Generate theme from your current wallpaper + Use black background for dark themes \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 5b2bc248..44984314 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -492,7 +492,7 @@ Перевести из Перевести в Настройки - Тема + Темы Точность Разделитель Формат вывода @@ -523,8 +523,6 @@ Светлая Темная Темная AMOLED - Динамическая светлая - Динамическая темная Загрузка… Ошибка Скопировано %1$s! @@ -673,5 +671,8 @@ Отправлять статистику использования Все данные анонимны и зашифрованы Название версии + Динамичные цвета + Использовать цвета обоев рабочего стола + Использовать черный фон в темных темах \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cebae7f1..41c6add4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -936,7 +936,7 @@ Settings - Theme + Themes Precision Separator Output format @@ -991,8 +991,9 @@ Light Dark AMOLED Dark - Dynamic light - Dynamic dark + Use black background for dark themes + Dynamic colors + Generate theme from your current wallpaper Loading… diff --git a/settings.gradle.kts b/settings.gradle.kts index 1e49e70b..110e6142 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,6 +3,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven(url = "https://jitpack.io") } } rootProject.name = "Unitto"