Merge branch 'master' into feature/modules

This commit is contained in:
Sad Ellie 2023-01-08 21:21:52 +04:00
commit fd837e6d2c
5 changed files with 170 additions and 121 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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
)
}
}
}
}

View File

@ -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,53 +391,41 @@ 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
when (_unitFrom.value?.group) {
UnitGroup.NUMBER_BASE -> convertAsNumberBase()
else -> convertAsExpression()
}
cancel()
}
}
}
val conversionResult = try {
(unitFrom as NumberBaseUnit).convertToBase(
private fun convertAsNumberBase() {
val conversionResult: String = try {
(_unitFrom.value as NumberBaseUnit).convertToBase(
input = _input.value,
toBase = (unitTo as NumberBaseUnit).base
toBase = (_unitTo.value as NumberBaseUnit).base
)
} catch (e: Exception) {
when (e) {
is ClassCastException -> {
cancel()
return@withContext
}
is ClassCastException -> return
is NumberFormatException, is IllegalArgumentException -> ""
else -> throw e
}
}
_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
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
// AUTOCLOSE ALL BRACKETS
// 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
@ -446,52 +434,41 @@ class MainViewModel @Inject constructor(
// 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
}
is ExpressionException,
is ArithmeticException -> return
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,
val conversionResult: BigDecimal = _unitFrom.value!!.convert(
_unitTo.value!!,
evaluationResult,
_userPrefs.value.digitsPrecision
)
// Converted
_result.update { conversionResult.toStringWith(_userPrefs.value.outputFormat) }
// 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)
}
}
cancel()
}
}
_result.update { conversionResult.toStringWith(_userPrefs.value.outputFormat) }
}
private fun setInputSymbols(symbol: String, add: Boolean = true) {

View File

@ -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(
UnittoButton(
onClick = { onClick(digit) },
onLongClick = onLongClick,
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)
),
onClick = { onClick(digit) },
contentPadding = PaddingValues(0.dp)
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
)
}
}

View File

@ -59,7 +59,7 @@ fun AboutScreen(
}
UnittoLargeTopAppBar(
title = "About Unitto",
title = stringResource(R.string.about_unitto),
navigateUpAction = navigateUpAction
) { padding ->
LazyColumn(contentPadding = padding) {

View File

@ -661,7 +661,6 @@
<string name="separator_setting">Séparateur</string>
<string name="output_format_setting">Format de sortie</string>
<string name="unit_groups_setting">Groupes d\'unités</string>
<string name="currency_rates_note_setting">Wrong currency rates?</string>
<string name="currency_rates_note_title">Note</string>
<string name="currency_rates_note_text">Les taux de change sont mis à jour quotidiennement. L\'application ne permet pas de suivre le marché en temps réel.</string>
<string name="terms_and_conditions">Termes et conditions</string>
@ -681,20 +680,10 @@
<string name="period">Période (42.069,12)</string>
<string name="comma">Virgule (42,069.12)</string>
<string name="spaces">Espaces (42 069.12)</string>
<!-- Output format -->
<string name="output_format_setting_support">Result value formatting</string>
<string name="output_format_setting_info">Engineering strings look like 1E-21</string>
<string name="plain">Défaut</string>
<string name="allow_engineering">Allow engineering</string>
<string name="force_engineering">Force engineering</string>
<!-- Theme -->
<string name="theme_setting_support">App look and feel</string>
<string name="force_auto_mode">Auto</string>
<string name="force_light_mode">Clair</string>
<string name="force_dark_mode">Sombre</string>
<string name="color_theme">Color theme</string>
<string name="force_amoled_mode">AMOLED Noir</string>
<string name="force_amoled_mode_support">Utiliser un fond noir pour les thèmes sombres</string>
<string name="enable_dynamic_colors">Couleurs dynamiques</string>
@ -703,7 +692,6 @@
<!-- MISC. -->
<string name="loading_label">Chargement…</string>
<string name="error_label">Erreur</string>
<string name="copied">Copied %1$s!</string>
<string name="cancel_label">Annuler</string>
<string name="search_bar_placeholder">Rechercher des unités</string>
<string name="search_placeholder">Aucun résultat trouvé</string>