diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/TextFieldValueExtensions.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/TextFieldValueExtensions.kt index 3df15e5b..c6ecd876 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/TextFieldValueExtensions.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/TextFieldValueExtensions.kt @@ -20,6 +20,7 @@ package com.sadellie.unitto.core.ui.common.textfield import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.SavedStateHandle import com.sadellie.unitto.core.base.Token fun TextFieldValue.addTokens(tokens: String): TextFieldValue { @@ -131,6 +132,18 @@ fun TextFieldValue.deleteTokens(): TextFieldValue { ) } +/** + * Tries to get a [TextFieldValue]. Places cursor at the end. + * + * @receiver [SavedStateHandle] Where to look for. + * @param key Key to find + * @return [TextFieldValue] with cursor at the end. + */ +fun SavedStateHandle.getTextField(key: String): TextFieldValue = + with(get(key) ?: "") { + TextFieldValue(this, TextRange(this.length)) + } + /** * !!! Recursive !!! (one wrong step and you are dead 💀) */ diff --git a/feature/calculator/src/androidTest/java/com/sadellie/unitto/feature/calculator/CalculatorScreenTest.kt b/feature/calculator/src/androidTest/java/com/sadellie/unitto/feature/calculator/CalculatorScreenTest.kt index 9dd18b6a..b5677e8c 100644 --- a/feature/calculator/src/androidTest/java/com/sadellie/unitto/feature/calculator/CalculatorScreenTest.kt +++ b/feature/calculator/src/androidTest/java/com/sadellie/unitto/feature/calculator/CalculatorScreenTest.kt @@ -63,7 +63,7 @@ class CalculatorScreenTest { CalculatorScreen( uiState = CalculatorUIState.Ready( input = TextFieldValue(), - output = CalculationResult.Default(), + output = CalculationResult.Empty, radianMode = false, precision = 3, outputFormat = OutputFormat.PLAIN, @@ -97,7 +97,7 @@ class CalculatorScreenTest { CalculatorScreen( uiState = CalculatorUIState.Ready( input = TextFieldValue(), - output = CalculationResult.Default(), + output = CalculationResult.Empty, radianMode = false, precision = 3, outputFormat = OutputFormat.PLAIN, diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorUIState.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorUIState.kt index 4f30a926..5b8d1d87 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorUIState.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorUIState.kt @@ -45,6 +45,8 @@ internal sealed class CalculatorUIState { sealed class CalculationResult { data class Default(val text: String = "") : CalculationResult() + data object Empty: CalculationResult() + data object DivideByZeroError : CalculationResult() { @StringRes val label: Int = R.string.calculator_divide_by_zero_error diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt index ff7eaf76..eda1af6d 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt @@ -28,6 +28,7 @@ import com.sadellie.unitto.core.ui.common.textfield.AllFormatterSymbols import com.sadellie.unitto.core.ui.common.textfield.addBracket import com.sadellie.unitto.core.ui.common.textfield.addTokens import com.sadellie.unitto.core.ui.common.textfield.deleteTokens +import com.sadellie.unitto.core.ui.common.textfield.getTextField import com.sadellie.unitto.data.common.isExpression import com.sadellie.unitto.data.common.setMinimumRequiredScale import com.sadellie.unitto.data.common.stateIn @@ -45,6 +46,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.math.BigDecimal import javax.inject.Inject @@ -55,24 +57,23 @@ internal class CalculatorViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, ) : ViewModel() { private val _inputKey = "CALCULATOR_INPUT" - private val _input = MutableStateFlow( - with(savedStateHandle[_inputKey] ?: "") { - TextFieldValue(this, TextRange(this.length)) - } - ) - private val _result = MutableStateFlow(CalculationResult.Default()) + private val _input = MutableStateFlow(savedStateHandle.getTextField(_inputKey)) + private val _result = MutableStateFlow(CalculationResult.Empty) private val _equalClicked = MutableStateFlow(false) + private val _prefs = userPrefsRepository.calculatorPrefs + .stateIn(viewModelScope, null) val uiState: StateFlow = combine( _input, _result, - userPrefsRepository.calculatorPrefs, + _prefs, calculatorHistoryRepository.historyFlow, - _equalClicked, - ) { input, result, prefs, history, showError -> + ) { input, result, prefs, history -> + prefs ?: return@combine CalculatorUIState.Loading + return@combine CalculatorUIState.Ready( input = input, - output = if (!showError and (result !is CalculationResult.Default)) CalculationResult.Default() else result, + output = result, radianMode = prefs.radianMode, precision = prefs.precision, outputFormat = prefs.outputFormat, @@ -85,12 +86,31 @@ internal class CalculatorViewModel @Inject constructor( ) } .mapLatest { ui -> - calculate( - input = ui.input.text, - radianMode = ui.radianMode, - outputFormat = ui.outputFormat, - precision = ui.precision - ) + if (ui !is CalculatorUIState.Ready) return@mapLatest ui + if (_equalClicked.value) return@mapLatest ui + + if (!ui.input.text.isExpression()) { + _result.update { CalculationResult.Empty } + return@mapLatest ui + } + + _result.update { + try { + CalculationResult.Default( + calculate( + input = ui.input.text, + radianMode = ui.radianMode, + ) + .setMinimumRequiredScale(ui.precision) + .trimZeros() + .toStringWith(ui.outputFormat) + ) + } catch (e: ExpressionException.DivideByZero) { + CalculationResult.Empty + } catch (e: Exception) { + CalculationResult.Empty + } + } ui } @@ -130,9 +150,11 @@ internal class CalculatorViewModel @Inject constructor( } fun clearInput() = _input.update { + _equalClicked.update { false } savedStateHandle[_inputKey] = "" TextFieldValue() } + fun onCursorChange(selection: TextRange) = _input.update { it.copy(selection = selection) } fun updateRadianMode(newValue: Boolean) = viewModelScope.launch { @@ -143,56 +165,47 @@ internal class CalculatorViewModel @Inject constructor( calculatorHistoryRepository.clear() } - fun evaluate() = viewModelScope.launch(Dispatchers.IO) { - when (val result = _result.value) { - is CalculationResult.Default -> { - calculatorHistoryRepository.add( - expression = _input.value.text.replace("-", Token.Operator.minus), - result = result.text - ) - _input.update { TextFieldValue(result.text, TextRange(result.text.length)) } - _result.update { CalculationResult.Default() } - _equalClicked.update { true } - } + fun evaluate() = viewModelScope.launch { + val prefs = _prefs.value ?: return@launch + if (_equalClicked.value) return@launch + if (!_input.value.text.isExpression()) return@launch - is CalculationResult.DivideByZeroError -> { - _equalClicked.update { true } - } - - is CalculationResult.Error -> { - // skip for generic error (bad expression and stuff - } - } - } - - private fun calculate( - input: String, - radianMode: Boolean, - outputFormat: Int, - precision: Int, - ) = viewModelScope.launch(Dispatchers.Default) { - if (!input.isExpression()) { - _result.update { CalculationResult.Default() } + val result = try { + calculate(_input.value.text, prefs.radianMode) + } catch (e: ExpressionException.DivideByZero) { + _equalClicked.update { true } + _result.update { CalculationResult.DivideByZeroError } + return@launch + } catch (e: ExpressionException.FactorialCalculation) { + _equalClicked.update { true } + _result.update { CalculationResult.Error } + return@launch + } catch (e: Exception) { + _equalClicked.update { true } + _result.update { CalculationResult.Error } return@launch } + .setMinimumRequiredScale(prefs.precision) + .trimZeros() + .toStringWith(prefs.outputFormat) - _result.update { - try { - CalculationResult.Default( - Expression(input, radianMode = radianMode) - .calculate() - .also { - if (it > BigDecimal.valueOf(Double.MAX_VALUE)) throw ExpressionException.TooBig() - } - .setMinimumRequiredScale(precision) - .trimZeros() - .toStringWith(outputFormat) - ) - } catch (e: ExpressionException.DivideByZero) { - CalculationResult.DivideByZeroError - } catch (e: Exception) { - CalculationResult.Error + calculatorHistoryRepository.add( + expression = _input.value.text.replace("-", Token.Operator.minus), + result = result + ) + + _input.update { TextFieldValue(result, TextRange(result.length)) } + _result.update { CalculationResult.Empty } + } + + private suspend fun calculate( + input: String, + radianMode: Boolean, + ): BigDecimal = withContext(Dispatchers.Default) { + Expression(input, radianMode) + .calculate() + .also { + if (it > BigDecimal.valueOf(Double.MAX_VALUE)) throw ExpressionException.TooBig() } - } } } diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/TextBox.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/TextBox.kt index a1c991f6..64669cd4 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/TextBox.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/TextBox.kt @@ -22,6 +22,7 @@ import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.sizeIn @@ -83,6 +84,15 @@ fun TextBox( ) if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) { when (output) { + is CalculationResult.Empty -> { + Spacer( + modifier = Modifier + .weight(2f) + .fillMaxWidth() + .padding(horizontal = 8.dp) + ) + } + is CalculationResult.Default -> { var outputTF by remember(output) { mutableStateOf(TextFieldValue(output.text)) 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 40fdc23b..c8452950 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 @@ -28,6 +28,7 @@ import com.sadellie.unitto.core.ui.common.textfield.AllFormatterSymbols import com.sadellie.unitto.core.ui.common.textfield.addBracket import com.sadellie.unitto.core.ui.common.textfield.addTokens import com.sadellie.unitto.core.ui.common.textfield.deleteTokens +import com.sadellie.unitto.core.ui.common.textfield.getTextField import com.sadellie.unitto.data.common.combine import com.sadellie.unitto.data.common.isExpression import com.sadellie.unitto.data.common.stateIn @@ -64,11 +65,7 @@ internal class ConverterViewModel @Inject constructor( ) : ViewModel() { private val converterInputKey = "CONVERTER_INPUT" - private val _input = MutableStateFlow( - with(savedStateHandle[converterInputKey] ?: "") { - TextFieldValue(this, TextRange(this.length)) - } - ) + private val _input = MutableStateFlow(savedStateHandle.getTextField(converterInputKey)) private val _calculation = MutableStateFlow(null) private val _result = MutableStateFlow(ConverterResult.Loading) private val _unitFrom = MutableStateFlow(null)