From 2bd56a7c3e2b350f2f41e7bf466b73e5551f8ab5 Mon Sep 17 00:00:00 2001 From: Sad Ellie Date: Fri, 6 Jan 2023 20:48:24 +0400 Subject: [PATCH 1/4] Fixed long click #15 --- .../unitto/screens/common/UnittoButton.kt | 86 +++++++++++++++++++ .../screens/main/components/KeyboardButton.kt | 26 +++--- 2 files changed, 98 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/com/sadellie/unitto/screens/common/UnittoButton.kt diff --git a/app/src/main/java/com/sadellie/unitto/screens/common/UnittoButton.kt b/app/src/main/java/com/sadellie/unitto/screens/common/UnittoButton.kt new file mode 100644 index 00000000..f95a624a --- /dev/null +++ b/app/src/main/java/com/sadellie/unitto/screens/common/UnittoButton.kt @@ -0,0 +1,86 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 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.common + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.semantics.Role + +@Composable +fun UnittoButton( + onClick: () -> Unit, + onLongClick: (() -> Unit)?, + modifier: Modifier = Modifier, + shape: Shape, + containerColor: Color, + contentColor: Color, + border: BorderStroke? = null, + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, + interactionSource: MutableInteractionSource, + content: @Composable RowScope.() -> Unit +) { + Surface( + modifier = modifier.clip(shape).combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + interactionSource = interactionSource, + indication = rememberRipple(), + role = Role.Button, + ), + color = containerColor, + contentColor = contentColor, + border = border + ) { + CompositionLocalProvider(LocalContentColor provides contentColor) { + ProvideTextStyle(value = MaterialTheme.typography.labelLarge) { + Row( + Modifier + .defaultMinSize( + minWidth = ButtonDefaults.MinWidth, + minHeight = ButtonDefaults.MinHeight + ) + .padding(contentPadding), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + content = content + ) + } + } + } +} diff --git a/app/src/main/java/com/sadellie/unitto/screens/main/components/KeyboardButton.kt b/app/src/main/java/com/sadellie/unitto/screens/main/components/KeyboardButton.kt index b63a2b08..5f19f3fb 100644 --- a/app/src/main/java/com/sadellie/unitto/screens/main/components/KeyboardButton.kt +++ b/app/src/main/java/com/sadellie/unitto/screens/main/components/KeyboardButton.kt @@ -26,7 +26,6 @@ import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -34,6 +33,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.sadellie.unitto.screens.common.UnittoButton import com.sadellie.unitto.ui.theme.NumbersTextStyleTitleLarge /** @@ -51,7 +51,7 @@ fun KeyboardButton( modifier: Modifier = Modifier, digit: String, isPrimary: Boolean = true, - onLongClick: () -> Unit = {}, + onLongClick: (() -> Unit)? = null, onClick: (String) -> Unit = {} ) { val interactionSource = remember { MutableInteractionSource() } @@ -59,24 +59,22 @@ fun KeyboardButton( val cornerRadius: Int by animateIntAsState( targetValue = if (isPressed) 30 else 50, animationSpec = tween(easing = FastOutSlowInEasing), - finishedListener = { if (it == 30) onLongClick() }) + ) - Button( - modifier = modifier, - interactionSource = interactionSource, - shape = RoundedCornerShape(cornerRadius), - colors = ButtonDefaults.buttonColors( - containerColor = if (isPrimary) MaterialTheme.colorScheme.inverseOnSurface else MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer, - disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) - ), + UnittoButton( onClick = { onClick(digit) }, - contentPadding = PaddingValues(0.dp) + onLongClick = onLongClick, + modifier = modifier, + shape = RoundedCornerShape(cornerRadius), + containerColor = if (isPrimary) MaterialTheme.colorScheme.inverseOnSurface else MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + contentPadding = PaddingValues(0.dp), + interactionSource = interactionSource, ) { Text( text = digit, style = NumbersTextStyleTitleLarge, - color = if (isPrimary) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSecondaryContainer, + color = if (isPrimary) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSecondaryContainer ) } } From f55a5bdfeb28558eaf98cb051867fbda0f24deac Mon Sep 17 00:00:00 2001 From: Sad Ellie Date: Sat, 7 Jan 2023 14:44:13 +0400 Subject: [PATCH 2/4] Fixed "About Unitto" localization. --- .../java/com/sadellie/unitto/screens/setttings/AboutScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/sadellie/unitto/screens/setttings/AboutScreen.kt b/app/src/main/java/com/sadellie/unitto/screens/setttings/AboutScreen.kt index decc9358..924b9363 100644 --- a/app/src/main/java/com/sadellie/unitto/screens/setttings/AboutScreen.kt +++ b/app/src/main/java/com/sadellie/unitto/screens/setttings/AboutScreen.kt @@ -59,7 +59,7 @@ fun AboutScreen( } UnittoLargeTopAppBar( - title = "About Unitto", + title = stringResource(R.string.about_unitto), navigateUpAction = navigateUpAction ) { padding -> LazyColumn(contentPadding = padding) { From 366cfb9768747890225a76553e61b3ef1ac06c2c Mon Sep 17 00:00:00 2001 From: Sad Ellie Date: Sat, 7 Jan 2023 14:49:59 +0400 Subject: [PATCH 3/4] Fixed French translation. --- app/src/main/res/values-fr/strings.xml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index c77d4a34..6c1f96e7 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -661,7 +661,6 @@ Séparateur Format de sortie Groupes d\'unités - Wrong currency rates? Note Les taux de change sont mis à jour quotidiennement. L\'application ne permet pas de suivre le marché en temps réel. Termes et conditions @@ -681,20 +680,10 @@ Période (42.069,12) Virgule (42,069.12) Espaces (42 069.12) - - - Result value formatting - Engineering strings look like 1E-21 Défaut - Allow engineering - Force engineering - - - App look and feel Auto Clair Sombre - Color theme AMOLED Noir Utiliser un fond noir pour les thèmes sombres Couleurs dynamiques @@ -703,7 +692,6 @@ Chargement… Erreur - Copied %1$s! Annuler Rechercher des unités Aucun résultat trouvé From 080ddbbd4c91161a40470e02e1bf65694df4c754 Mon Sep 17 00:00:00 2001 From: Sad Ellie Date: Sun, 8 Jan 2023 16:17:16 +0400 Subject: [PATCH 4/4] Refactor input processing Fixed precision for calculated results --- .../unitto/screens/main/MainViewModel.kt | 165 ++++++++---------- 1 file changed, 71 insertions(+), 94 deletions(-) diff --git a/app/src/main/java/com/sadellie/unitto/screens/main/MainViewModel.kt b/app/src/main/java/com/sadellie/unitto/screens/main/MainViewModel.kt index 6533d726..c65c909c 100644 --- a/app/src/main/java/com/sadellie/unitto/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/sadellie/unitto/screens/main/MainViewModel.kt @@ -47,6 +47,7 @@ import com.sadellie.unitto.data.OPERATORS import com.sadellie.unitto.data.combine import com.sadellie.unitto.data.preferences.UserPreferences import com.sadellie.unitto.data.preferences.UserPreferencesRepository +import com.sadellie.unitto.data.setMinimumRequiredScale import com.sadellie.unitto.data.toStringWith import com.sadellie.unitto.data.trimZeros import com.sadellie.unitto.data.units.AbstractUnit @@ -59,6 +60,8 @@ import com.sadellie.unitto.data.units.database.MyBasedUnitsRepository import com.sadellie.unitto.data.units.remote.CurrencyApi import com.sadellie.unitto.data.units.remote.CurrencyUnitResponse import dagger.hilt.android.lifecycle.HiltViewModel +import java.math.BigDecimal +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow @@ -73,9 +76,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.math.BigDecimal -import java.math.RoundingMode -import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( @@ -391,107 +391,84 @@ class MainViewModel @Inject constructor( } private suspend fun convertInput() { - if (_unitFrom.value?.group == UnitGroup.NUMBER_BASE) { - convertAsNumberBase() - } else { - convertAsExpression() - } - } - - private suspend fun convertAsNumberBase() { + // Loading don't do anything + if ((_unitFrom.value == null) or (_unitTo.value == null)) return withContext(Dispatchers.Default) { while (isActive) { - // Units are still loading, don't convert anything yet - val unitFrom = _unitFrom.value ?: return@withContext - val unitTo = _unitTo.value ?: return@withContext - - val conversionResult = try { - (unitFrom as NumberBaseUnit).convertToBase( - input = _input.value, - toBase = (unitTo as NumberBaseUnit).base - ) - } catch (e: Exception) { - when (e) { - is ClassCastException -> { - cancel() - return@withContext - } - is NumberFormatException, is IllegalArgumentException -> "" - else -> throw e - } + when (_unitFrom.value?.group) { + UnitGroup.NUMBER_BASE -> convertAsNumberBase() + else -> convertAsExpression() } - _result.update { conversionResult } cancel() } } } - private suspend fun convertAsExpression() { - withContext(Dispatchers.Default) { - while (isActive) { - // Units are still loading, don't convert anything yet - val unitFrom = _unitFrom.value ?: return@withContext - val unitTo = _unitTo.value ?: return@withContext - - // First we clean the input from garbage at the end - var cleanInput = _input.value.dropLastWhile { !it.isDigit() } - - // Now we close open brackets that user didn't close - // AUTOCLOSE ALL BRACKETS - val leftBrackets = _input.value.count { it.toString() == KEY_LEFT_BRACKET } - val rightBrackets = _input.value.count { it.toString() == KEY_RIGHT_BRACKET } - val neededBrackets = leftBrackets - rightBrackets - if (neededBrackets > 0) cleanInput += KEY_RIGHT_BRACKET.repeat(neededBrackets) - - // Now we evaluate expression in input - val evaluationResult: BigDecimal = try { - Expressions().eval(cleanInput) - .setScale(_userPrefs.value.digitsPrecision, RoundingMode.HALF_EVEN) - .trimZeros() - } catch (e: Exception) { - when (e) { - is ExpressionException, - is ArrayIndexOutOfBoundsException, - is IndexOutOfBoundsException, - is NumberFormatException, - is ArithmeticException -> { - // Invalid expression, can't do anything further - cancel() - return@withContext - } - else -> throw e - } - } - - // Evaluated. Hide calculated result if no expression entered. - // 123.456 will be true - // -123.456 will be true - // -123.456-123 will be false (first minus gets removed, ending with 123.456) - if (_input.value.removePrefix(KEY_MINUS).all { it.toString() !in OPERATORS }) { - // No operators - _calculated.update { null } - } else { - _calculated.update { - evaluationResult.toStringWith( - _userPrefs.value.outputFormat - ) - } - } - - // Now we just convert. - // We can use evaluation result here, input is valid - val conversionResult: BigDecimal = unitFrom.convert( - unitTo, - evaluationResult, - _userPrefs.value.digitsPrecision - ) - - // Converted - _result.update { conversionResult.toStringWith(_userPrefs.value.outputFormat) } - - cancel() + private fun convertAsNumberBase() { + val conversionResult: String = try { + (_unitFrom.value as NumberBaseUnit).convertToBase( + input = _input.value, + toBase = (_unitTo.value as NumberBaseUnit).base + ) + } catch (e: Exception) { + when (e) { + is ClassCastException -> return + is NumberFormatException, is IllegalArgumentException -> "" + else -> throw e } } + _result.update { conversionResult } + } + + private fun convertAsExpression() { + // First we clean the input from garbage at the end + var cleanInput = _input.value.dropLastWhile { !it.isDigit() } + + // Now we close open brackets that user didn't close + // AUTO-CLOSE ALL BRACKETS + val leftBrackets = _input.value.count { it.toString() == KEY_LEFT_BRACKET } + val rightBrackets = _input.value.count { it.toString() == KEY_RIGHT_BRACKET } + val neededBrackets = leftBrackets - rightBrackets + if (neededBrackets > 0) cleanInput += KEY_RIGHT_BRACKET.repeat(neededBrackets) + + // Now we evaluate expression in input + val evaluationResult: BigDecimal = try { + Expressions().eval(cleanInput) + } catch (e: Exception) { + when (e) { + is ArrayIndexOutOfBoundsException, + is IndexOutOfBoundsException, + is NumberFormatException, + is ExpressionException, + is ArithmeticException -> return + else -> throw e + } + } + + // Now we just convert. + // We can use evaluation result here, input is valid + val conversionResult: BigDecimal = _unitFrom.value!!.convert( + _unitTo.value!!, + evaluationResult, + _userPrefs.value.digitsPrecision + ) + + // Evaluated. Hide calculated result if no expression entered. + // 123.456 will be true + // -123.456 will be true + // -123.456-123 will be false (first minus gets removed, ending with 123.456) + _calculated.update { + if (_input.value.removePrefix(KEY_MINUS).all { it.toString() !in OPERATORS }) { + null + } else { + evaluationResult + .setMinimumRequiredScale(_userPrefs.value.digitsPrecision) + .trimZeros() + .toStringWith(_userPrefs.value.outputFormat) + } + } + + _result.update { conversionResult.toStringWith(_userPrefs.value.outputFormat) } } private fun setInputSymbols(symbol: String, add: Boolean = true) {