From 7cbbb846af4e2f604099823a95944740843bae0e Mon Sep 17 00:00:00 2001 From: Sad Ellie Date: Fri, 9 Feb 2024 23:25:08 +0300 Subject: [PATCH] Refactor unit selector screens decrease duplicate logic in ConverterViewModel --- .../sadellie/unitto/data/common/FlowUtils.kt | 40 +++++ .../feature/converter/ConverterScreen.kt | 32 ++-- .../feature/converter/ConverterViewModel.kt | 135 ---------------- .../feature/converter/LeftSideUIState.kt | 38 ----- ...ideScreen.kt => UnitFromSelectorScreen.kt} | 47 +++--- ...tSideUIState.kt => UnitSelectorUIState.kt} | 39 +++-- .../converter/UnitSelectorViewModel.kt | 149 ++++++++++++++++++ ...tSideScreen.kt => UnitToSelectorScreen.kt} | 60 ++++--- .../feature/converter/components/UnitsList.kt | 19 ++- .../navigation/ConverterNavigation.kt | 118 ++++++++++++-- 10 files changed, 404 insertions(+), 273 deletions(-) delete mode 100644 feature/converter/src/main/java/com/sadellie/unitto/feature/converter/LeftSideUIState.kt rename feature/converter/src/main/java/com/sadellie/unitto/feature/converter/{LeftSideScreen.kt => UnitFromSelectorScreen.kt} (83%) rename feature/converter/src/main/java/com/sadellie/unitto/feature/converter/{RightSideUIState.kt => UnitSelectorUIState.kt} (59%) create mode 100644 feature/converter/src/main/java/com/sadellie/unitto/feature/converter/UnitSelectorViewModel.kt rename feature/converter/src/main/java/com/sadellie/unitto/feature/converter/{RightSideScreen.kt => UnitToSelectorScreen.kt} (81%) diff --git a/data/common/src/main/java/com/sadellie/unitto/data/common/FlowUtils.kt b/data/common/src/main/java/com/sadellie/unitto/data/common/FlowUtils.kt index 4861dd77..087040dc 100644 --- a/data/common/src/main/java/com/sadellie/unitto/data/common/FlowUtils.kt +++ b/data/common/src/main/java/com/sadellie/unitto/data/common/FlowUtils.kt @@ -154,5 +154,45 @@ fun combine( ) } +@Suppress("UNCHECKED_CAST", "UNUSED") +fun combine( + flow: Flow, + flow2: Flow, + flow3: Flow, + flow4: Flow, + flow5: Flow, + flow6: Flow, + flow7: Flow, + flow8: Flow, + flow9: Flow, + flow10: Flow, + transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10) -> R, +): Flow = + kotlinx.coroutines.flow.combine( + flow, + flow2, + flow3, + flow4, + flow5, + flow6, + flow7, + flow8, + flow9, + flow10 + ) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + args[7] as T8, + args[8] as T9, + args[9] as T10, + ) + } + fun Flow.stateIn(scope: CoroutineScope, initialValue: T): StateFlow = stateIn(scope, SharingStarted.WhileSubscribed(5000L), initialValue) diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterScreen.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterScreen.kt index 824aedd8..7291f8d4 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterScreen.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterScreen.kt @@ -97,8 +97,8 @@ import java.util.Locale @Composable internal fun ConverterRoute( viewModel: ConverterViewModel = hiltViewModel(), - navigateToLeftScreen: () -> Unit, - navigateToRightScreen: () -> Unit, + navigateToLeftScreen: (uiState: UnitConverterUIState) -> Unit, + navigateToRightScreen: (uiState: UnitConverterUIState) -> Unit, navigateToMenu: () -> Unit, navigateToSettings: () -> Unit, ) { @@ -124,8 +124,8 @@ internal fun ConverterRoute( @Composable private fun ConverterScreen( uiState: UnitConverterUIState, - navigateToLeftScreen: () -> Unit, - navigateToRightScreen: () -> Unit, + navigateToLeftScreen: (uiState: UnitConverterUIState) -> Unit, + navigateToRightScreen: (uiState: UnitConverterUIState) -> Unit, navigateToSettings: () -> Unit, navigateToMenu: () -> Unit, swapUnits: () -> Unit, @@ -207,9 +207,9 @@ private fun NumberBase( onValueChange: (TextFieldValue) -> Unit, processInput: (String) -> Unit, deleteDigit: () -> Unit, - navigateToLeftScreen: () -> Unit, + navigateToLeftScreen: (uiState: UnitConverterUIState) -> Unit, swapUnits: () -> Unit, - navigateToRightScreen: () -> Unit, + navigateToRightScreen: (uiState: UnitConverterUIState) -> Unit, clearInput: () -> Unit, ) { PortraitLandscape( @@ -239,8 +239,8 @@ private fun NumberBase( unitFromLabel = stringResource(uiState.unitFrom.displayName), unitToLabel = stringResource(uiState.unitTo.displayName), swapUnits = swapUnits, - navigateToLeftScreen = navigateToLeftScreen, - navigateToRightScreen = navigateToRightScreen + navigateToLeftScreen = { navigateToLeftScreen(uiState) }, + navigateToRightScreen = { navigateToRightScreen(uiState) } ) } }, @@ -263,9 +263,9 @@ private fun Default( onFocusOnInput2: (Boolean) -> Unit, processInput: (String) -> Unit, deleteDigit: () -> Unit, - navigateToLeftScreen: () -> Unit, + navigateToLeftScreen: (uiState: UnitConverterUIState) -> Unit, swapUnits: () -> Unit, - navigateToRightScreen: () -> Unit, + navigateToRightScreen: (uiState: UnitConverterUIState) -> Unit, clearInput: () -> Unit, refreshCurrencyRates: (AbstractUnit) -> Unit, addBracket: () -> Unit, @@ -327,7 +327,9 @@ private fun Default( .weight(1f) ) { ExpressionTextField( - modifier = Modifier.fillMaxWidth().weight(1f), + modifier = Modifier + .fillMaxWidth() + .weight(1f), value = uiState.input1, minRatio = 0.7f, onValueChange = onValueChange, @@ -345,7 +347,9 @@ private fun Default( .weight(1f) ) { ExpressionTextField( - modifier = Modifier.fillMaxWidth().weight(1f) + modifier = Modifier + .fillMaxWidth() + .weight(1f) .onFocusEvent { state -> onFocusOnInput2(state.hasFocus) }, value = uiState.input2, minRatio = 0.7f, @@ -405,8 +409,8 @@ private fun Default( unitFromLabel = stringResource(uiState.unitFrom.displayName), unitToLabel = stringResource(uiState.unitTo.displayName), swapUnits = swapUnits, - navigateToLeftScreen = navigateToLeftScreen, - navigateToRightScreen = navigateToRightScreen + navigateToLeftScreen = { navigateToLeftScreen(uiState) }, + navigateToRightScreen = { navigateToRightScreen(uiState) } ) } }, diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterViewModel.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterViewModel.kt index 3bcc19b5..f5ea3f4e 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterViewModel.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterViewModel.kt @@ -33,7 +33,6 @@ import com.sadellie.unitto.data.common.isExpression import com.sadellie.unitto.data.common.stateIn import com.sadellie.unitto.data.converter.UnitID import com.sadellie.unitto.data.model.UnitGroup -import com.sadellie.unitto.data.model.UnitsListSorting import com.sadellie.unitto.data.model.repository.UnitsRepository import com.sadellie.unitto.data.model.repository.UserPreferencesRepository import com.sadellie.unitto.data.model.unit.AbstractUnit @@ -45,12 +44,10 @@ import io.github.sadellie.evaluatto.ExpressionException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -74,13 +71,6 @@ internal class ConverterViewModel @Inject constructor( private val _unitFrom = MutableStateFlow(null) private val _unitTo = MutableStateFlow(null) - private val _leftQuery = MutableStateFlow(TextFieldValue()) - private val _leftUnits = MutableStateFlow>>(emptyMap()) - private val _leftUnitGroup = MutableStateFlow(null) - - private val _rightQuery = MutableStateFlow(TextFieldValue()) - private val _rightUnits = MutableStateFlow>>(emptyMap()) - private val _currenciesState = MutableStateFlow(CurrencyRateUpdateState.Nothing) private var _loadCurrenciesJob: Job? = null @@ -160,81 +150,6 @@ internal class ConverterViewModel @Inject constructor( } .stateIn(viewModelScope, UnitConverterUIState.Loading) - val leftSideUIState = combine( - _unitFrom, - _leftQuery, - _leftUnits, - _leftUnitGroup, - userPrefsRepository.converterPrefs, - unitsRepo.units - ) { unitFrom, query, units, unitGroup, prefs, _ -> - unitFrom ?: return@combine LeftSideUIState.Loading - - return@combine LeftSideUIState.Ready( - unitFrom = unitFrom, - sorting = prefs.unitConverterSorting, - shownUnitGroups = prefs.shownUnitGroups, - favorites = prefs.unitConverterFavoritesOnly, - query = query, - units = units, - unitGroup = unitGroup - ) - } - .mapLatest { - if (it !is LeftSideUIState.Ready) return@mapLatest it - - filterUnitsLeft( - query = it.query, - unitGroup = it.unitGroup, - favoritesOnly = it.favorites, - sorting = it.sorting, - shownUnitGroups = it.shownUnitGroups, - ) - it - } - .stateIn(viewModelScope, SharingStarted.Lazily, LeftSideUIState.Loading) - - val rightSideUIState = combine( - _unitFrom, - _unitTo, - _input1, - _calculation, - _rightQuery, - _rightUnits, - userPrefsRepository.converterPrefs, - _currenciesState, - unitsRepo.units, - ) { unitFrom, unitTo, input, calculation, query, units, prefs, currenciesState, _ -> - unitFrom ?: return@combine RightSideUIState.Loading - unitTo ?: return@combine RightSideUIState.Loading - - return@combine RightSideUIState.Ready( - unitFrom = unitFrom, - unitTo = unitTo, - sorting = prefs.unitConverterSorting, - favorites = prefs.unitConverterFavoritesOnly, - input = (calculation?.toPlainString() ?: input.text).replace(Token.Operator.minus, "-"), - scale = prefs.precision, - outputFormat = prefs.outputFormat, - formatterSymbols = AllFormatterSymbols.getById(prefs.separator), - currencyRateUpdateState = currenciesState, - query = query, - units = units, - ) - } - .mapLatest { - if (it !is RightSideUIState.Ready) return@mapLatest it - - filterUnitsRight( - query = it.query, - unitGroup = it.unitFrom.group, - favoritesOnly = it.favorites, - sorting = it.sorting, - ) - it - } - .stateIn(viewModelScope, SharingStarted.Lazily, RightSideUIState.Loading) - fun swapUnits() { _unitFrom .getAndUpdate { _unitTo.value } @@ -380,56 +295,6 @@ internal class ConverterViewModel @Inject constructor( } } - fun queryChangeLeft(query: TextFieldValue) = _leftQuery.update { query } - - fun queryChangeRight(query: TextFieldValue) = _rightQuery.update { query } - - fun favoritesOnlyChange(enabled: Boolean) = viewModelScope.launch { - userPrefsRepository.updateUnitConverterFavoritesOnly(enabled) - } - - fun updateUnitGroupLeft(unitGroup: UnitGroup?) = _leftUnitGroup.update { unitGroup } - - fun favoriteUnit(unit: AbstractUnit) = viewModelScope.launch { - unitsRepo.favorite(unit) - } - - private fun filterUnitsLeft( - query: TextFieldValue, - unitGroup: UnitGroup?, - favoritesOnly: Boolean, - sorting: UnitsListSorting, - shownUnitGroups: List, - ) = viewModelScope.launch(Dispatchers.Default) { - _leftUnits.update { - unitsRepo.filterUnits( - query = query.text, - unitGroup = unitGroup, - favoritesOnly = favoritesOnly, - hideBrokenUnits = false, - sorting = sorting, - shownUnitGroups = shownUnitGroups - ) - } - } - - private fun filterUnitsRight( - query: TextFieldValue, - unitGroup: UnitGroup?, - favoritesOnly: Boolean, - sorting: UnitsListSorting, - ) = viewModelScope.launch(Dispatchers.Default) { - _rightUnits.update { - unitsRepo.filterUnits( - query = query.text, - unitGroup = unitGroup, - favoritesOnly = favoritesOnly, - hideBrokenUnits = true, - sorting = sorting, - ) - } - } - private fun convertDefault( unitFrom: DefaultUnit, unitTo: DefaultUnit, diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/LeftSideUIState.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/LeftSideUIState.kt deleted file mode 100644 index 7ef72129..00000000 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/LeftSideUIState.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Unitto is a calculator for Android - * Copyright (c) 2023-2024 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.feature.converter - -import androidx.compose.ui.text.input.TextFieldValue -import com.sadellie.unitto.data.model.UnitGroup -import com.sadellie.unitto.data.model.UnitsListSorting -import com.sadellie.unitto.data.model.unit.AbstractUnit - -internal sealed class LeftSideUIState { - data object Loading : LeftSideUIState() - - data class Ready( - val unitFrom: AbstractUnit, - val query: TextFieldValue, - val units: Map> = emptyMap(), - val favorites: Boolean, - val shownUnitGroups: List, - val unitGroup: UnitGroup?, - val sorting: UnitsListSorting, - ) : LeftSideUIState() -} diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/LeftSideScreen.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/UnitFromSelectorScreen.kt similarity index 83% rename from feature/converter/src/main/java/com/sadellie/unitto/feature/converter/LeftSideScreen.kt rename to feature/converter/src/main/java/com/sadellie/unitto/feature/converter/UnitFromSelectorScreen.kt index 6bea6401..8561deed 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/LeftSideScreen.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/UnitFromSelectorScreen.kt @@ -49,31 +49,32 @@ import com.sadellie.unitto.feature.converter.components.UnitsList import java.math.BigDecimal @Composable -internal fun LeftSideRoute( - viewModel: ConverterViewModel, +internal fun UnitFromSelectorRoute( + unitSelectorViewModel: UnitSelectorViewModel, + converterViewModel: ConverterViewModel, navigateUp: () -> Unit, navigateToUnitGroups: () -> Unit, ) { when ( - val uiState = viewModel.leftSideUIState.collectAsStateWithLifecycle().value + val uiState = unitSelectorViewModel.unitFromUIState.collectAsStateWithLifecycle().value ) { - is LeftSideUIState.Loading -> EmptyScreen() - is LeftSideUIState.Ready -> LeftSideScreen( + is UnitSelectorUIState.UnitFrom -> UnitFromSelectorScreen( uiState = uiState, - onQueryChange = viewModel::queryChangeLeft, - toggleFavoritesOnly = viewModel::favoritesOnlyChange, - updateUnitFrom = viewModel::updateUnitFrom, - updateUnitGroup = viewModel::updateUnitGroupLeft, - favoriteUnit = viewModel::favoriteUnit, + onQueryChange = unitSelectorViewModel::updateSelectorQuery, + toggleFavoritesOnly = unitSelectorViewModel::updateShowFavoritesOnly, + updateUnitFrom = converterViewModel::updateUnitFrom, + updateUnitGroup = unitSelectorViewModel::updateSelectedUnitGroup, + favoriteUnit = unitSelectorViewModel::favoriteUnit, navigateUp = navigateUp, navigateToUnitGroups = navigateToUnitGroups, ) + else -> EmptyScreen() } } @Composable -private fun LeftSideScreen( - uiState: LeftSideUIState.Ready, +private fun UnitFromSelectorScreen( + uiState: UnitSelectorUIState.UnitFrom, onQueryChange: (TextFieldValue) -> Unit, toggleFavoritesOnly: (Boolean) -> Unit, updateUnitFrom: (AbstractUnit) -> Unit, @@ -87,8 +88,6 @@ private fun LeftSideScreen( val chipsRowLazyListState = rememberLazyListState() LaunchedEffect(uiState.unitFrom, uiState.shownUnitGroups) { - updateUnitGroup(uiState.unitFrom.group) - kotlin.runCatching { val groupToSelect = uiState.shownUnitGroups.indexOf(uiState.unitFrom.group) if (groupToSelect > -1) { @@ -108,8 +107,8 @@ private fun LeftSideScreen( onQueryChange = onQueryChange, navigateUp = navigateUp, trailingIcon = { - FavoritesButton(uiState.favorites) { - toggleFavoritesOnly(!uiState.favorites) + FavoritesButton(uiState.showFavoritesOnly) { + toggleFavoritesOnly(!uiState.showFavoritesOnly) } }, scrollBehavior = scrollBehavior @@ -119,7 +118,7 @@ private fun LeftSideScreen( modifier = Modifier .padding(start = 8.dp, end = 8.dp, bottom = 4.dp) .fillMaxWidth(), - chosenUnitGroup = uiState.unitGroup, + chosenUnitGroup = uiState.selectedUnitGroup, items = uiState.shownUnitGroups, selectAction = updateUnitGroup, navigateToSettingsAction = navigateToUnitGroups @@ -130,7 +129,7 @@ private fun LeftSideScreen( val resources = LocalContext.current.resources UnitsList( modifier = Modifier.padding(paddingValues), - groupedUnits = uiState.units, + searchResult = uiState.units, navigateToUnitGroups = navigateToUnitGroups, currentUnitId = uiState.unitFrom.id, supportLabel = { resources.getString(it.shortName) }, @@ -146,7 +145,7 @@ private fun LeftSideScreen( @Preview @Composable -private fun LeftSideScreenPreview() { +private fun UnitFromSelectorScreenPreview() { val units: Map> = mapOf( UnitGroup.LENGTH to listOf( NormalUnit(UnitID.meter, BigDecimal.valueOf(1.0E+18), UnitGroup.LENGTH, R.string.unit_meter, R.string.unit_meter_short), @@ -159,14 +158,14 @@ private fun LeftSideScreenPreview() { ) ) - LeftSideScreen( - uiState = LeftSideUIState.Ready( + UnitFromSelectorScreen( + uiState = UnitSelectorUIState.UnitFrom( unitFrom = units.values.first().first(), - units = units, query = TextFieldValue("test"), - favorites = false, + units = UnitSearchResult.Success(units), + selectedUnitGroup = UnitGroup.SPEED, shownUnitGroups = UnitGroup.entries, - unitGroup = units.keys.toList().first(), + showFavoritesOnly = false, sorting = UnitsListSorting.USAGE, ), onQueryChange = {}, diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/RightSideUIState.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/UnitSelectorUIState.kt similarity index 59% rename from feature/converter/src/main/java/com/sadellie/unitto/feature/converter/RightSideUIState.kt rename to feature/converter/src/main/java/com/sadellie/unitto/feature/converter/UnitSelectorUIState.kt index 1148f800..03d6bcec 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/RightSideUIState.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/UnitSelectorUIState.kt @@ -1,6 +1,6 @@ /* * Unitto is a calculator for Android - * Copyright (c) 2023-2024 Elshan Agaev + * Copyright (c) 2024 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 @@ -24,20 +24,39 @@ import com.sadellie.unitto.data.model.UnitGroup import com.sadellie.unitto.data.model.UnitsListSorting import com.sadellie.unitto.data.model.unit.AbstractUnit -internal sealed class RightSideUIState { - data object Loading : RightSideUIState() +internal sealed class UnitSelectorUIState { + data object Loading : UnitSelectorUIState() - data class Ready( + data class UnitFrom( + val query: TextFieldValue, val unitFrom: AbstractUnit, - val unitTo: AbstractUnit, + val shownUnitGroups: List, + val showFavoritesOnly: Boolean, + val units: UnitSearchResult, + val selectedUnitGroup: UnitGroup?, + val sorting: UnitsListSorting, + ) : UnitSelectorUIState() + + data class UnitTo( val query: TextFieldValue, - val units: Map>, - val favorites: Boolean, + val unitFrom: AbstractUnit, + val unitTo: AbstractUnit, + val showFavoritesOnly: Boolean, + val units: UnitSearchResult, + val input: String?, val sorting: UnitsListSorting, - val input: String, val scale: Int, val outputFormat: Int, val formatterSymbols: FormatterSymbols, - val currencyRateUpdateState: CurrencyRateUpdateState, - ) : RightSideUIState() + ) : UnitSelectorUIState() +} + +internal sealed class UnitSearchResult { + data object Empty : UnitSearchResult() + + data object Loading : UnitSearchResult() + + data class Success( + val units: Map> + ) : UnitSearchResult() } diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/UnitSelectorViewModel.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/UnitSelectorViewModel.kt new file mode 100644 index 00000000..e35672ac --- /dev/null +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/UnitSelectorViewModel.kt @@ -0,0 +1,149 @@ +/* + * Unitto is a calculator for Android + * Copyright (c) 2024 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.feature.converter + +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.sadellie.unitto.core.ui.common.textfield.AllFormatterSymbols +import com.sadellie.unitto.data.common.stateIn +import com.sadellie.unitto.data.model.UnitGroup +import com.sadellie.unitto.data.model.repository.UnitsRepository +import com.sadellie.unitto.data.model.repository.UserPreferencesRepository +import com.sadellie.unitto.data.model.unit.AbstractUnit +import com.sadellie.unitto.feature.converter.navigation.inputArg +import com.sadellie.unitto.feature.converter.navigation.unitFromIdArg +import com.sadellie.unitto.feature.converter.navigation.unitGroupArg +import com.sadellie.unitto.feature.converter.navigation.unitToIdArg +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class UnitSelectorViewModel @Inject constructor( + private val userPrefsRepository: UserPreferencesRepository, + private val unitsRepo: UnitsRepository, + savedStateHandle: SavedStateHandle +) : ViewModel() { + private val _query = MutableStateFlow(TextFieldValue()) + private val _searchResults = MutableStateFlow(UnitSearchResult.Loading) + private val _selectedUnitGroup = MutableStateFlow(savedStateHandle.get(unitGroupArg)) + private val _unitFromId = savedStateHandle.get(unitFromIdArg) + private val _unitToId = savedStateHandle.get(unitToIdArg) + private val _input = savedStateHandle.get(inputArg) + + val unitFromUIState: StateFlow = combine( + _query, + _searchResults, + _selectedUnitGroup, + userPrefsRepository.converterPrefs, + ) { query, searchResults, selectedUnitGroup, prefs -> + if (_unitFromId.isNullOrEmpty()) return@combine UnitSelectorUIState.Loading + + return@combine UnitSelectorUIState.UnitFrom( + query = query, + unitFrom = unitsRepo.getById(_unitFromId), + shownUnitGroups = prefs.shownUnitGroups, + showFavoritesOnly = prefs.unitConverterFavoritesOnly, + units = searchResults, + selectedUnitGroup = selectedUnitGroup, + sorting = prefs.unitConverterSorting, + ) + } + .mapLatest { ui -> + if (ui is UnitSelectorUIState.UnitFrom) { + _searchResults.update { + val result = unitsRepo.filterUnits( + query = ui.query.text, + unitGroup = ui.selectedUnitGroup, + favoritesOnly = ui.showFavoritesOnly, + hideBrokenUnits = false, + sorting = ui.sorting, + shownUnitGroups = ui.shownUnitGroups + ) + + if (result.isEmpty()) UnitSearchResult.Empty else UnitSearchResult.Success(result) + } + } + + ui + } + .stateIn(viewModelScope, UnitSelectorUIState.Loading) + + val unitToUIState: StateFlow = combine( + _query, + _searchResults, + userPrefsRepository.converterPrefs, + unitsRepo.units, + ) { query, searchResults, prefs, _ -> + if (_unitFromId.isNullOrEmpty()) return@combine UnitSelectorUIState.Loading + if (_unitToId.isNullOrEmpty()) return@combine UnitSelectorUIState.Loading + + UnitSelectorUIState.UnitTo( + query = query, + unitFrom = unitsRepo.getById(_unitFromId), + unitTo = unitsRepo.getById(_unitToId), + showFavoritesOnly = prefs.unitConverterFavoritesOnly, + units = searchResults, + input = _input, + sorting = prefs.unitConverterSorting, + scale = prefs.precision, + outputFormat = prefs.outputFormat, + formatterSymbols = AllFormatterSymbols.getById(prefs.separator), + ) + } + .mapLatest { ui -> + if (ui is UnitSelectorUIState.UnitTo) { + _searchResults.update { + if (ui.unitFrom.group == UnitGroup.CURRENCY) unitsRepo.updateRates(ui.unitFrom) + + val result = unitsRepo.filterUnits( + query = ui.query.text, + unitGroup = ui.unitFrom.group, + favoritesOnly = ui.showFavoritesOnly, + hideBrokenUnits = true, + sorting = ui.sorting, + ) + + if (result.isEmpty()) UnitSearchResult.Empty else UnitSearchResult.Success(result) + } + } + ui + } + .stateIn(viewModelScope, UnitSelectorUIState.Loading) + + fun updateSelectorQuery(value: TextFieldValue) = _query.update { value } + + fun updateShowFavoritesOnly(value: Boolean) = viewModelScope.launch { + userPrefsRepository.updateUnitConverterFavoritesOnly(value) + } + + fun updateSelectedUnitGroup(value: UnitGroup?) = _selectedUnitGroup.update { value } + + fun favoriteUnit(unit: AbstractUnit) = viewModelScope.launch(Dispatchers.IO) { + unitsRepo.favorite(unit) + } +} diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/RightSideScreen.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/UnitToSelectorScreen.kt similarity index 81% rename from feature/converter/src/main/java/com/sadellie/unitto/feature/converter/RightSideScreen.kt rename to feature/converter/src/main/java/com/sadellie/unitto/feature/converter/UnitToSelectorScreen.kt index cb413666..55df0e29 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/RightSideScreen.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/UnitToSelectorScreen.kt @@ -47,31 +47,31 @@ import com.sadellie.unitto.feature.converter.components.UnitsList import java.math.BigDecimal @Composable -internal fun RightSideRoute( - viewModel: ConverterViewModel, +internal fun UnitToSelectorRoute( + unitSelectorViewModel: UnitSelectorViewModel, + converterViewModel: ConverterViewModel, navigateUp: () -> Unit, navigateToUnitGroups: () -> Unit, ) { when ( - val uiState = viewModel.rightSideUIState.collectAsStateWithLifecycle().value + val uiState = unitSelectorViewModel.unitToUIState.collectAsStateWithLifecycle().value ) { - is RightSideUIState.Loading -> EmptyScreen() - is RightSideUIState.Ready -> - RightSideScreen( - uiState = uiState, - onQueryChange = viewModel::queryChangeRight, - toggleFavoritesOnly = viewModel::favoritesOnlyChange, - updateUnitTo = viewModel::updateUnitTo, - favoriteUnit = viewModel::favoriteUnit, - navigateUp = navigateUp, - navigateToUnitGroups = navigateToUnitGroups, - ) + is UnitSelectorUIState.UnitTo -> UnitToSelectorScreen( + uiState = uiState, + onQueryChange = unitSelectorViewModel::updateSelectorQuery, + toggleFavoritesOnly = unitSelectorViewModel::updateShowFavoritesOnly, + updateUnitTo = converterViewModel::updateUnitTo, + favoriteUnit = unitSelectorViewModel::favoriteUnit, + navigateUp = navigateUp, + navigateToUnitGroups = navigateToUnitGroups, + ) + else -> EmptyScreen() } } @Composable -private fun RightSideScreen( - uiState: RightSideUIState.Ready, +private fun UnitToSelectorScreen( + uiState: UnitSelectorUIState.UnitTo, onQueryChange: (TextFieldValue) -> Unit, toggleFavoritesOnly: (Boolean) -> Unit, updateUnitTo: (AbstractUnit) -> Unit, @@ -89,8 +89,8 @@ private fun RightSideScreen( onQueryChange = onQueryChange, navigateUp = navigateUp, trailingIcon = { - FavoritesButton(uiState.favorites) { - toggleFavoritesOnly(!uiState.favorites) + FavoritesButton(uiState.showFavoritesOnly) { + toggleFavoritesOnly(!uiState.showFavoritesOnly) } }, scrollBehavior = scrollBehavior @@ -100,7 +100,7 @@ private fun RightSideScreen( val resources = LocalContext.current.resources UnitsList( modifier = Modifier.padding(paddingValues), - groupedUnits = uiState.units, + searchResult = uiState.units, navigateToUnitGroups = navigateToUnitGroups, currentUnitId = uiState.unitTo.id, supportLabel = { @@ -112,7 +112,6 @@ private fun RightSideScreen( scale = uiState.scale, outputFormat = uiState.outputFormat, formatterSymbols = uiState.formatterSymbols, - readyCurrencies = uiState.currencyRateUpdateState is CurrencyRateUpdateState.Ready, ) }, onClick = { @@ -128,15 +127,13 @@ private fun RightSideScreen( private fun formatUnitToSupportLabel( unitFrom: AbstractUnit?, unitTo: AbstractUnit?, - input: String, + input: String?, shortName: String, scale: Int, outputFormat: Int, formatterSymbols: FormatterSymbols, - readyCurrencies: Boolean, ): String { - if ((unitFrom?.group == UnitGroup.CURRENCY) and !readyCurrencies) return shortName - if (input.isEmpty()) return shortName + if (input.isNullOrEmpty()) return shortName try { if ((unitFrom is DefaultUnit) and (unitTo is DefaultUnit)) { @@ -168,7 +165,7 @@ private fun formatUnitToSupportLabel( @Preview @Composable -private fun RightSideScreenPreview() { +private fun UnitToSelectorPreview() { val units: Map> = mapOf( UnitGroup.LENGTH to listOf( NormalUnit(UnitID.meter, BigDecimal.valueOf(1.0E+18), UnitGroup.LENGTH, R.string.unit_meter, R.string.unit_meter_short), @@ -181,19 +178,18 @@ private fun RightSideScreenPreview() { ) ) - RightSideScreen( - uiState = RightSideUIState.Ready( + UnitToSelectorScreen( + uiState = UnitSelectorUIState.UnitTo( unitFrom = units.values.first().first(), - units = units, - query = TextFieldValue(), - favorites = false, + unitTo = units.values.first().first(), + query = TextFieldValue("test"), + units = UnitSearchResult.Success(units), + showFavoritesOnly = false, sorting = UnitsListSorting.USAGE, - unitTo = units.values.first()[1], input = "100", scale = 3, outputFormat = OutputFormat.PLAIN, formatterSymbols = FormatterSymbols.Spaces, - currencyRateUpdateState = CurrencyRateUpdateState.Nothing ), onQueryChange = {}, toggleFavoritesOnly = {}, diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/UnitsList.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/UnitsList.kt index cb5212d4..16c1b169 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/UnitsList.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/UnitsList.kt @@ -33,12 +33,13 @@ import com.sadellie.unitto.data.converter.UnitID import com.sadellie.unitto.data.model.UnitGroup import com.sadellie.unitto.data.model.unit.AbstractUnit import com.sadellie.unitto.data.model.unit.NormalUnit +import com.sadellie.unitto.feature.converter.UnitSearchResult import java.math.BigDecimal @Composable internal fun UnitsList( modifier: Modifier, - groupedUnits: Map>, + searchResult: UnitSearchResult, navigateToUnitGroups: () -> Unit, currentUnitId: String, supportLabel: (AbstractUnit) -> String, @@ -47,14 +48,14 @@ internal fun UnitsList( ) { Crossfade( modifier = modifier, - targetState = groupedUnits.isNotEmpty(), + targetState = searchResult, label = "Units list" - ) { hasUnits -> - when (hasUnits) { - true -> LazyColumn( + ) { result -> + when (result) { + is UnitSearchResult.Success -> LazyColumn( modifier = Modifier.fillMaxSize() ) { - groupedUnits.forEach { (group, units) -> + result.units.forEach { (group, units) -> item(group.name) { UnitGroupHeader(Modifier.animateItemPlacement(), group) } @@ -73,11 +74,13 @@ internal fun UnitsList( } } - false -> SearchPlaceholder( + UnitSearchResult.Empty -> SearchPlaceholder( onButtonClick = navigateToUnitGroups, supportText = stringResource(R.string.converter_no_results_support), buttonLabel = stringResource(R.string.open_settings_label) ) + + UnitSearchResult.Loading -> Unit } } } @@ -100,7 +103,7 @@ private fun PreviewUnitsList() { UnitsList( modifier = Modifier.fillMaxSize(), - groupedUnits = groupedUnits, + searchResult = UnitSearchResult.Success(units = groupedUnits), navigateToUnitGroups = {}, currentUnitId = UnitID.mile, supportLabel = { resources.getString(it.shortName) }, diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/navigation/ConverterNavigation.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/navigation/ConverterNavigation.kt index 1033121b..288f0bb0 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/navigation/ConverterNavigation.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/navigation/ConverterNavigation.kt @@ -22,19 +22,42 @@ import androidx.compose.runtime.remember import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.navArgument import androidx.navigation.navDeepLink import com.sadellie.unitto.core.ui.model.DrawerItem import com.sadellie.unitto.core.ui.unittoComposable import com.sadellie.unitto.core.ui.unittoNavigation +import com.sadellie.unitto.data.model.UnitGroup import com.sadellie.unitto.feature.converter.ConverterRoute import com.sadellie.unitto.feature.converter.ConverterViewModel -import com.sadellie.unitto.feature.converter.LeftSideRoute -import com.sadellie.unitto.feature.converter.RightSideRoute +import com.sadellie.unitto.feature.converter.CurrencyRateUpdateState +import com.sadellie.unitto.feature.converter.UnitConverterUIState +import com.sadellie.unitto.feature.converter.UnitFromSelectorRoute +import com.sadellie.unitto.feature.converter.UnitToSelectorRoute private val graph = DrawerItem.Converter.graph private val start = DrawerItem.Converter.start -private const val LEFT = "left" -private const val RIGHT = "right" + +private const val UNIT_FROM = "unitFromSelector" +private const val UNIT_TO = "unitToSelector" +internal const val unitGroupArg = "unitGroupArg" +internal const val unitFromIdArg = "unitFromId" +internal const val unitToIdArg = "unitToIdArg" +internal const val inputArg = "inputArg" + +private const val UNIT_FROM_ROUTE = "$UNIT_FROM/{$unitFromIdArg}/{$unitGroupArg}" +private const val UNIT_TO_ROUTE = "$UNIT_TO/{$unitFromIdArg}/{$unitToIdArg}/{$inputArg}" +private fun NavHostController.navigateLeft( + unitFromId: String, + unitGroup: UnitGroup, +) = navigate("$UNIT_FROM/$unitFromId/$unitGroup") + +private fun NavHostController.navigateRight( + unitFromId: String, + unitToId: String, + input: String?, +) = navigate("$UNIT_TO/$unitFromId/$unitToId/$input") fun NavGraphBuilder.converterGraph( openDrawer: () -> Unit, @@ -58,36 +81,107 @@ fun NavGraphBuilder.converterGraph( ConverterRoute( viewModel = parentViewModel, - navigateToLeftScreen = { navController.navigate(LEFT) }, - navigateToRightScreen = { navController.navigate(RIGHT) }, + // Navigation logic is here, but should actually be in ConverterScreen + navigateToLeftScreen = { uiState: UnitConverterUIState -> + when (uiState) { + is UnitConverterUIState.Default -> navController + .navigateLeft(uiState.unitFrom.id, uiState.unitFrom.group) + + is UnitConverterUIState.NumberBase -> navController + .navigateLeft(uiState.unitFrom.id, uiState.unitFrom.group) + + else -> Unit + } + }, + navigateToRightScreen = { uiState: UnitConverterUIState -> + when (uiState) { + is UnitConverterUIState.Default -> { + // Don't allow converting if still loading currencies + val convertingCurrencies = uiState.unitFrom.group == UnitGroup.CURRENCY + val currenciesReady = + uiState.currencyRateUpdateState is CurrencyRateUpdateState.Ready + + val input: String? = if (convertingCurrencies and !currenciesReady) { + null + } else { + (uiState.calculation?.toPlainString() ?: uiState.input1.text) + .ifEmpty { null } + } + + navController.navigateRight( + uiState.unitFrom.id, + uiState.unitTo.id, + input + ) + } + + is UnitConverterUIState.NumberBase -> { + val input = uiState.input.text.ifEmpty { null } + navController.navigateRight( + uiState.unitFrom.id, + uiState.unitTo.id, + input + ) + } + + UnitConverterUIState.Loading -> Unit + } + }, navigateToSettings = navigateToSettings, navigateToMenu = openDrawer ) } - unittoComposable(LEFT) { backStackEntry -> + unittoComposable( + route = UNIT_FROM_ROUTE, + arguments = listOf( + navArgument(unitFromIdArg) { + type = NavType.StringType + }, + navArgument(unitGroupArg) { + type = NavType.EnumType(UnitGroup::class.java) + }, + ) + ) { backStackEntry -> val parentEntry = remember(backStackEntry) { navController.getBackStackEntry(graph) } val parentViewModel = hiltViewModel(parentEntry) - LeftSideRoute( - viewModel = parentViewModel, + UnitFromSelectorRoute( + unitSelectorViewModel = hiltViewModel(), + converterViewModel = parentViewModel, navigateUp = navController::navigateUp, navigateToUnitGroups = navigateToUnitGroups ) } - unittoComposable(RIGHT) { backStackEntry -> + unittoComposable( + route = UNIT_TO_ROUTE, + arguments = listOf( + navArgument(unitFromIdArg) { + type = NavType.StringType + }, + navArgument(unitToIdArg) { + type = NavType.StringType + }, + navArgument(inputArg) { + type = NavType.StringType + nullable = true + defaultValue = null + }, + ) + ) { backStackEntry -> val parentEntry = remember(backStackEntry) { navController.getBackStackEntry(graph) } val parentViewModel = hiltViewModel(parentEntry) - RightSideRoute( - viewModel = parentViewModel, + UnitToSelectorRoute( + unitSelectorViewModel = hiltViewModel(), + converterViewModel = parentViewModel, navigateUp = navController::navigateUp, navigateToUnitGroups = navigateToUnitGroups )