diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index d7115c4a..71ad5d90 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -110,6 +110,7 @@ dependencies {
implementation(libs.com.google.accompanist.systemuicontroller)
implementation(project(mapOf("path" to ":feature:converter")))
+ implementation(project(mapOf("path" to ":feature:calculator")))
implementation(project(mapOf("path" to ":feature:settings")))
implementation(project(mapOf("path" to ":feature:unitslist")))
implementation(project(mapOf("path" to ":feature:tools")))
diff --git a/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt b/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt
index 30633b17..f9599b0d 100644
--- a/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt
+++ b/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt
@@ -21,6 +21,8 @@ package com.sadellie.unitto
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
+import com.sadellie.unitto.feature.calculator.navigation.calculatorScreen
+import com.sadellie.unitto.feature.calculator.navigation.navigateToCalculator
import com.sadellie.unitto.feature.converter.MainViewModel
import com.sadellie.unitto.feature.converter.navigation.converterRoute
import com.sadellie.unitto.feature.converter.navigation.converterScreen
@@ -83,9 +85,12 @@ fun UnittoNavigation(
toolsScreen(
navigateUpAction = navController::navigateUp,
+ navigateToCalculator = navController::navigateToCalculator,
navigateToEpoch = navController::navigateToEpoch
)
+ calculatorScreen(navigateUpAction = navController::navigateUp)
+
epochScreen(
navigateUpAction = navController::navigateUp
)
diff --git a/core/base/src/main/java/com/sadellie/unitto/core/base/KeypadSymbols.kt b/core/base/src/main/java/com/sadellie/unitto/core/base/KeypadSymbols.kt
index 692d0c4e..f1794c31 100644
--- a/core/base/src/main/java/com/sadellie/unitto/core/base/KeypadSymbols.kt
+++ b/core/base/src/main/java/com/sadellie/unitto/core/base/KeypadSymbols.kt
@@ -57,6 +57,17 @@ const val KEY_RIGHT_BRACKET = ")"
const val KEY_EXPONENT = "^"
const val KEY_SQRT = "√"
+const val KEY_PI = "π"
+const val KEY_FACTORIAL = "!"
+const val KEY_SIN = "sin("
+const val KEY_COS = "cos("
+const val KEY_TAN = "tan("
+const val KEY_E_SMALL = "e"
+const val KEY_MODULO = "#"
+const val KEY_LN = "ln("
+const val KEY_LOG = "log("
+const val KEY_PERCENT = "%"
+const val KEY_EVALUATE = "="
val OPERATORS by lazy {
listOf(
diff --git a/core/base/src/main/res/values/strings.xml b/core/base/src/main/res/values/strings.xml
index 8b12ba6f..5d1b021f 100644
--- a/core/base/src/main/res/values/strings.xml
+++ b/core/base/src/main/res/values/strings.xml
@@ -1022,6 +1022,10 @@
This screen is part of an experiment. It may change in the future.
Click to read more!
+
+ Calculator
+ Calculate, but don\'t convert
+
Number of decimal places
Converted values may have a precision higher than the preferred one.
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt
index 801eebc9..18599db6 100644
--- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt
@@ -29,14 +29,18 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import com.sadellie.unitto.core.ui.theme.NumbersTextStyleTitleLarge
+import com.sadellie.unitto.core.ui.theme.NumbersTextStyleTitleSmall
/**
* Button for keyboard
@@ -87,3 +91,105 @@ fun KeyboardButton(
if (isPressed and allowVibration) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
}
}
+
+@Composable
+fun BasicKeyboardButton(
+ modifier: Modifier,
+ onClick: () -> Unit,
+ onLongClick: (() -> Unit)?,
+ containerColor: Color,
+ contentColor: Color,
+ text: String,
+ textColor: Color,
+ fontSize: TextUnit,
+ allowVibration: Boolean
+) {
+ val view = LocalView.current
+ val interactionSource = remember { MutableInteractionSource() }
+ val isPressed by interactionSource.collectIsPressedAsState()
+ val cornerRadius: Int by animateIntAsState(
+ targetValue = if (isPressed) 30 else 50,
+ animationSpec = tween(easing = FastOutSlowInEasing),
+ )
+
+ UnittoButton(
+ modifier = modifier,
+ onClick = onClick,
+ onLongClick = onLongClick,
+ shape = RoundedCornerShape(cornerRadius),
+ containerColor = containerColor,
+ contentColor = contentColor,
+ interactionSource = interactionSource
+ ) {
+ Text(
+ text = text,
+ style = NumbersTextStyleTitleLarge,
+ color = textColor,
+ fontSize = fontSize
+ )
+ }
+
+ LaunchedEffect(key1 = isPressed) {
+ if (isPressed and allowVibration) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
+ }
+}
+
+@Composable
+fun KeyboardButtonLight(
+ modifier: Modifier,
+ symbol: String,
+ onClick: (String) -> Unit,
+ onLongClick: (() -> Unit)? = null,
+ allowVibration: Boolean = false
+) {
+ BasicKeyboardButton(
+ modifier = modifier,
+ onClick = { onClick(symbol) },
+ onLongClick = onLongClick,
+ containerColor = MaterialTheme.colorScheme.inverseOnSurface,
+ contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
+ text = symbol,
+ textColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ fontSize = TextUnit.Unspecified,
+ allowVibration = allowVibration,
+ )
+}
+
+@Composable
+fun KeyboardButtonFilled(
+ modifier: Modifier,
+ symbol: String,
+ onClick: (String) -> Unit,
+ onLongClick: (() -> Unit)? = null,
+ allowVibration: Boolean = false
+) {
+ BasicKeyboardButton(
+ modifier = modifier,
+ onClick = { onClick(symbol) },
+ onLongClick = onLongClick,
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
+ text = symbol,
+ textColor = MaterialTheme.colorScheme.onSecondaryContainer,
+ fontSize = TextUnit.Unspecified,
+ allowVibration = allowVibration
+ )
+}
+
+@Composable
+fun KeyboardButtonAdditional(
+ modifier: Modifier,
+ symbol: String,
+ onClick: (String) -> Unit
+) {
+ TextButton(
+ onClick = { onClick(symbol) },
+ modifier = modifier
+ ) {
+ Text(
+ text = symbol,
+ color = MaterialTheme.colorScheme.onSecondaryContainer,
+ style = NumbersTextStyleTitleSmall
+ )
+ }
+}
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/theme/Type.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/theme/Type.kt
index 79950164..5fa08f65 100644
--- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/theme/Type.kt
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/theme/Type.kt
@@ -64,6 +64,15 @@ val NumbersTextStyleTitleLarge = TextStyle(
letterSpacing = 0.sp,
)
+// This text style is used for secondary keyboard button
+val NumbersTextStyleTitleSmall = TextStyle(
+ fontFamily = Lato,
+ fontWeight = FontWeight.W500,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.sp,
+)
+
val AppTypography = Typography(
displayLarge = TextStyle(
fontFamily = Montserrat,
diff --git a/feature/calculator/.gitignore b/feature/calculator/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/calculator/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/calculator/build.gradle.kts b/feature/calculator/build.gradle.kts
new file mode 100644
index 00000000..68815800
--- /dev/null
+++ b/feature/calculator/build.gradle.kts
@@ -0,0 +1,38 @@
+/*
+ * 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 .
+ */
+
+plugins {
+ id("unitto.library")
+ id("unitto.library.compose")
+ id("unitto.library.feature")
+ id("unitto.android.hilt")
+}
+
+android {
+ namespace = "com.sadellie.unitto.feature.calculator"
+}
+
+dependencies {
+ testImplementation(libs.junit)
+ implementation(libs.org.mariuszgromada.math.mxparser)
+ implementation(libs.com.github.sadellie.themmo)
+
+ implementation(project(mapOf("path" to ":data:userprefs")))
+ implementation(project(mapOf("path" to ":data:unitgroups")))
+ implementation(project(mapOf("path" to ":data:units")))
+}
diff --git a/feature/calculator/consumer-rules.pro b/feature/calculator/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/feature/calculator/src/main/AndroidManifest.xml b/feature/calculator/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..232257bf
--- /dev/null
+++ b/feature/calculator/src/main/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/AngleMode.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/AngleMode.kt
new file mode 100644
index 00000000..67318a58
--- /dev/null
+++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/AngleMode.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.feature.calculator
+
+internal enum class AngleMode { DEG, RAD }
diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt
new file mode 100644
index 00000000..514f2786
--- /dev/null
+++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.feature.calculator
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sadellie.unitto.core.ui.common.UnittoTopAppBar
+import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayMedium
+import com.sadellie.unitto.feature.calculator.components.CalculatorKeyboard
+import com.sadellie.unitto.feature.calculator.components.InputTextField
+
+@Composable
+internal fun CalculatorRoute(
+ navigateUpAction: () -> Unit,
+ viewModel: CalculatorViewModel = hiltViewModel()
+) {
+ val uiState = viewModel.uiState.collectAsStateWithLifecycle()
+
+ CalculatorScreen(
+ uiState = uiState.value,
+ navigateUpAction = navigateUpAction,
+ addSymbol = viewModel::addSymbol,
+ clearSymbols = viewModel::clearSymbols,
+ deleteSymbol = viewModel::deleteSymbol,
+ onCursorChange = viewModel::onCursorChange,
+ toggleAngleMode = viewModel::toggleCalculatorMode,
+ evaluate = viewModel::evaluate
+ )
+}
+
+@Composable
+private fun CalculatorScreen(
+ uiState: CalculatorUIState,
+ navigateUpAction: () -> Unit,
+ addSymbol: (String) -> Unit,
+ clearSymbols: () -> Unit,
+ deleteSymbol: () -> Unit,
+ onCursorChange: (IntRange) -> Unit,
+ toggleAngleMode: () -> Unit,
+ evaluate: () -> Unit
+) {
+ UnittoTopAppBar(
+ title = stringResource(R.string.calculator),
+ navigateUpAction = navigateUpAction,
+ ) {
+ Column(Modifier.padding(it)) {
+ InputTextField(
+ modifier = Modifier.fillMaxWidth(),
+ value = TextFieldValue(
+ text = uiState.input,
+ selection = TextRange(uiState.selection.first, uiState.selection.last)
+ ),
+ onCursorChange = onCursorChange
+ )
+ AnimatedVisibility(visible = uiState.output.isNotEmpty()) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ // Quick fix to prevent the UI from crashing
+ text = uiState.output,
+ textAlign = TextAlign.End,
+ softWrap = false,
+ color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
+ style = NumbersTextStyleDisplayMedium,
+ )
+ }
+ CalculatorKeyboard(
+ modifier = Modifier,
+ addSymbol = addSymbol,
+ clearSymbols = clearSymbols,
+ deleteSymbol = deleteSymbol,
+ toggleAngleMode = toggleAngleMode,
+ angleMode = uiState.angleMode,
+ evaluate = evaluate
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun PreviewCalculatorScreen() {
+ CalculatorScreen(
+ uiState = CalculatorUIState(),
+ navigateUpAction = {},
+ addSymbol = {},
+ clearSymbols = {},
+ deleteSymbol = {},
+ onCursorChange = {},
+ toggleAngleMode = {},
+ evaluate = {}
+ )
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..c572c0c6
--- /dev/null
+++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorUIState.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.feature.calculator
+
+internal data class CalculatorUIState(
+ val input: String = "",
+ val output: String = "",
+ val selection: IntRange = 0..0,
+ val angleMode: AngleMode = AngleMode.RAD
+)
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
new file mode 100644
index 00000000..1f1daf40
--- /dev/null
+++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt
@@ -0,0 +1,191 @@
+/*
+ * 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.feature.calculator
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.sadellie.unitto.core.base.KEY_LEFT_BRACKET
+import com.sadellie.unitto.core.base.KEY_MINUS
+import com.sadellie.unitto.core.base.KEY_MINUS_DISPLAY
+import com.sadellie.unitto.core.base.KEY_RIGHT_BRACKET
+import com.sadellie.unitto.data.setMinimumRequiredScale
+import com.sadellie.unitto.data.toStringWith
+import com.sadellie.unitto.data.trimZeros
+import com.sadellie.unitto.data.userprefs.UserPreferences
+import com.sadellie.unitto.data.userprefs.UserPreferencesRepository
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import org.mariuszgromada.math.mxparser.Expression
+import java.math.BigDecimal
+import javax.inject.Inject
+import org.mariuszgromada.math.mxparser.mXparser as MathParser
+
+@HiltViewModel
+internal class CalculatorViewModel @Inject constructor(
+ userPrefsRepository: UserPreferencesRepository
+) : ViewModel() {
+ private val _userPrefs: StateFlow =
+ userPrefsRepository.userPreferencesFlow.stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(5000L),
+ UserPreferences()
+ )
+
+ private val _input: MutableStateFlow = MutableStateFlow("")
+ private val _output: MutableStateFlow = MutableStateFlow("")
+ private val _selection: MutableStateFlow = MutableStateFlow(IntRange(0, 0))
+ private val _angleMode: MutableStateFlow = MutableStateFlow(AngleMode.RAD)
+
+ val uiState = combine(
+ _input, _output, _selection, _angleMode
+ ) { input, output, selection, angleMode ->
+ return@combine CalculatorUIState(
+ input = input,
+ output = output,
+ selection = selection,
+ angleMode = angleMode
+ )
+ }.stateIn(
+ viewModelScope, SharingStarted.WhileSubscribed(5000L), CalculatorUIState()
+ )
+
+ fun addSymbol(symbol: String) {
+ val selection = _selection.value
+ _input.update {
+ if (it.isEmpty()) symbol else it.replaceRange(selection.first, selection.last, symbol)
+ }
+ _selection.update { it.first + symbol.length..it.first + symbol.length }
+ }
+
+ fun deleteSymbol() {
+ val selection = _selection.value
+ val newSelectionStart = when (selection.last) {
+ 0 -> return
+ selection.first -> _selection.value.first - 1
+ else -> _selection.value.first
+ }
+
+ _selection.update { newSelectionStart..newSelectionStart }
+ _input.update { it.removeRange(newSelectionStart, selection.last) }
+ }
+
+ fun clearSymbols() {
+ _selection.update { 0..0 }
+ _input.update { "" }
+ }
+
+ fun toggleCalculatorMode() {
+ _angleMode.update {
+ if (it == AngleMode.DEG) {
+ MathParser.setRadiansMode()
+ AngleMode.RAD
+ } else {
+ MathParser.setDegreesMode()
+ AngleMode.DEG
+ }
+ }
+ }
+
+ // Called when user clicks "=" on a keyboard
+ fun evaluate() {
+ if (!Expression(_input.value.clean).checkSyntax()) return
+
+ _input.update { _output.value }
+ _selection.update { _input.value.length.._input.value.length }
+ _output.update { "" }
+ }
+
+ fun onCursorChange(selection: IntRange) {
+ _selection.update { selection }
+ }
+
+ private fun calculateInput() {
+ // Input is empty, don't calculate
+ if (_input.value.isEmpty()) {
+ _output.update { "" }
+ return
+ }
+
+ val calculated = Expression(_input.value.clean).calculate()
+
+ // Calculation error, return NaN
+ if (calculated.isNaN() or calculated.isInfinite()) {
+ _output.update { calculated.toString() }
+ return
+ }
+
+ val calculatedBigDecimal = calculated
+ .toBigDecimal()
+ .setMinimumRequiredScale(_userPrefs.value.digitsPrecision)
+ .trimZeros()
+
+ try {
+ val inputBigDecimal = BigDecimal(_input.value)
+
+ // Input and output are identical values
+ if (inputBigDecimal.compareTo(calculatedBigDecimal) == 0) {
+ _output.update { "" }
+ return
+ }
+ } catch (e: NumberFormatException) {
+ // Cannot compare input and output
+ }
+ _output.update {
+ calculatedBigDecimal.toStringWith(_userPrefs.value.outputFormat)
+ }
+ return
+ }
+
+ /**
+ * Clean input so that there are no syntax errors
+ */
+ private val String.clean: String
+ get() {
+ val leftBrackets = count { it.toString() == KEY_LEFT_BRACKET }
+ val rightBrackets = count { it.toString() == KEY_RIGHT_BRACKET }
+ val neededBrackets = leftBrackets - rightBrackets
+ return this
+ .replace(KEY_MINUS_DISPLAY, KEY_MINUS)
+ .plus(KEY_RIGHT_BRACKET.repeat(neededBrackets.coerceAtLeast(0)))
+ }
+
+ init {
+ /**
+ * mxParser uses some unnecessary rounding for doubles. It causes expressions like 9999^9999
+ * to load CPU very much. We use BigDecimal to achieve same result without CPU overload.
+ */
+ MathParser.setCanonicalRounding(false)
+
+ // Observe and invoke calculation without UI lag.
+ viewModelScope.launch(Dispatchers.Default) {
+ merge(_userPrefs, _input, _angleMode).collectLatest {
+ calculateInput()
+ }
+ }
+ }
+}
diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/CalculatorKeyboard.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/CalculatorKeyboard.kt
new file mode 100644
index 00000000..69ec035b
--- /dev/null
+++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/CalculatorKeyboard.kt
@@ -0,0 +1,182 @@
+/*
+ * 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.feature.calculator.components
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ExpandLess
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.sadellie.unitto.core.base.KEY_0
+import com.sadellie.unitto.core.base.KEY_1
+import com.sadellie.unitto.core.base.KEY_2
+import com.sadellie.unitto.core.base.KEY_3
+import com.sadellie.unitto.core.base.KEY_4
+import com.sadellie.unitto.core.base.KEY_5
+import com.sadellie.unitto.core.base.KEY_6
+import com.sadellie.unitto.core.base.KEY_7
+import com.sadellie.unitto.core.base.KEY_8
+import com.sadellie.unitto.core.base.KEY_9
+import com.sadellie.unitto.core.base.KEY_CLEAR
+import com.sadellie.unitto.core.base.KEY_COS
+import com.sadellie.unitto.core.base.KEY_DIVIDE_DISPLAY
+import com.sadellie.unitto.core.base.KEY_DOT
+import com.sadellie.unitto.core.base.KEY_EVALUATE
+import com.sadellie.unitto.core.base.KEY_EXPONENT
+import com.sadellie.unitto.core.base.KEY_E_SMALL
+import com.sadellie.unitto.core.base.KEY_FACTORIAL
+import com.sadellie.unitto.core.base.KEY_LEFT_BRACKET
+import com.sadellie.unitto.core.base.KEY_LN
+import com.sadellie.unitto.core.base.KEY_LOG
+import com.sadellie.unitto.core.base.KEY_MINUS_DISPLAY
+import com.sadellie.unitto.core.base.KEY_MODULO
+import com.sadellie.unitto.core.base.KEY_MULTIPLY_DISPLAY
+import com.sadellie.unitto.core.base.KEY_PERCENT
+import com.sadellie.unitto.core.base.KEY_PI
+import com.sadellie.unitto.core.base.KEY_PLUS
+import com.sadellie.unitto.core.base.KEY_RIGHT_BRACKET
+import com.sadellie.unitto.core.base.KEY_SIN
+import com.sadellie.unitto.core.base.KEY_SQRT
+import com.sadellie.unitto.core.base.KEY_TAN
+import com.sadellie.unitto.core.ui.common.KeyboardButtonAdditional
+import com.sadellie.unitto.core.ui.common.KeyboardButtonFilled
+import com.sadellie.unitto.core.ui.common.KeyboardButtonLight
+import com.sadellie.unitto.feature.calculator.AngleMode
+
+@Composable
+internal fun CalculatorKeyboard(
+ modifier: Modifier,
+ addSymbol: (String) -> Unit,
+ clearSymbols: () -> Unit,
+ deleteSymbol: () -> Unit,
+ toggleAngleMode: () -> Unit,
+ angleMode: AngleMode,
+ evaluate: () -> Unit
+) {
+ var showAdditional: Boolean by remember { mutableStateOf(false) }
+ val expandRotation: Float by animateFloatAsState(
+ targetValue = if (showAdditional) 0f else 180f,
+ animationSpec = tween(easing = FastOutSlowInEasing)
+ )
+
+ Column(
+ modifier = modifier
+ ) {
+ val weightModifier = Modifier.weight(1f)
+ val mainButtonModifier = Modifier
+ .fillMaxSize()
+ .weight(1f)
+ .padding(4.dp)
+
+ Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
+ // Additional buttons
+ Column(modifier = weightModifier) {
+ Row(Modifier, horizontalArrangement = Arrangement.spacedBy(2.dp)) {
+ KeyboardButtonAdditional(weightModifier, KEY_SQRT, onClick = addSymbol)
+ KeyboardButtonAdditional(weightModifier, KEY_PI, onClick = addSymbol)
+ KeyboardButtonAdditional(weightModifier, KEY_EXPONENT, onClick = addSymbol)
+ KeyboardButtonAdditional(weightModifier, KEY_FACTORIAL, onClick = addSymbol)
+ }
+ AnimatedVisibility(visible = showAdditional) {
+ Column {
+ Row(Modifier, horizontalArrangement = Arrangement.spacedBy(2.dp)) {
+ KeyboardButtonAdditional(weightModifier, angleMode.name, onClick = { toggleAngleMode() })
+ KeyboardButtonAdditional(weightModifier, KEY_SIN, onClick = addSymbol)
+ KeyboardButtonAdditional(weightModifier, KEY_COS, onClick = addSymbol)
+ KeyboardButtonAdditional(weightModifier, KEY_TAN, onClick = addSymbol)
+ }
+ Row(Modifier, horizontalArrangement = Arrangement.spacedBy(2.dp)) {
+ KeyboardButtonAdditional(weightModifier, KEY_MODULO, onClick = addSymbol)
+ KeyboardButtonAdditional(weightModifier, KEY_E_SMALL, onClick = addSymbol)
+ KeyboardButtonAdditional(weightModifier, KEY_LN, onClick = addSymbol)
+ KeyboardButtonAdditional(weightModifier, KEY_LOG, onClick = addSymbol)
+ }
+ }
+ }
+ }
+ // Expand/Collapse
+ IconButton({ showAdditional = !showAdditional }) {
+ Icon(Icons.Default.ExpandLess, null, Modifier.rotate(expandRotation))
+ }
+ }
+
+ Row(weightModifier) {
+ KeyboardButtonFilled(mainButtonModifier, KEY_LEFT_BRACKET, addSymbol)
+ KeyboardButtonFilled(mainButtonModifier, KEY_RIGHT_BRACKET, addSymbol)
+ KeyboardButtonFilled(mainButtonModifier, KEY_PERCENT, addSymbol)
+ KeyboardButtonFilled(mainButtonModifier, KEY_DIVIDE_DISPLAY, addSymbol)
+ }
+
+ Row(weightModifier) {
+ KeyboardButtonLight(mainButtonModifier, KEY_7, addSymbol)
+ KeyboardButtonLight(mainButtonModifier, KEY_8, addSymbol)
+ KeyboardButtonLight(mainButtonModifier, KEY_9, addSymbol)
+ KeyboardButtonFilled(mainButtonModifier, KEY_MULTIPLY_DISPLAY, addSymbol)
+ }
+ Row(weightModifier) {
+ KeyboardButtonLight(mainButtonModifier, KEY_4, addSymbol)
+ KeyboardButtonLight(mainButtonModifier, KEY_5, addSymbol)
+ KeyboardButtonLight(mainButtonModifier, KEY_6, addSymbol)
+ KeyboardButtonFilled(mainButtonModifier, KEY_MINUS_DISPLAY, addSymbol)
+ }
+ Row(weightModifier) {
+ KeyboardButtonLight(mainButtonModifier, KEY_1, addSymbol)
+ KeyboardButtonLight(mainButtonModifier, KEY_2, addSymbol)
+ KeyboardButtonLight(mainButtonModifier, KEY_3, addSymbol)
+ KeyboardButtonFilled(mainButtonModifier, KEY_PLUS, addSymbol)
+ }
+ Row(weightModifier) {
+ KeyboardButtonLight(mainButtonModifier, KEY_0, addSymbol)
+ KeyboardButtonLight(mainButtonModifier, KEY_DOT, addSymbol)
+ KeyboardButtonLight(mainButtonModifier, KEY_CLEAR, { deleteSymbol() }, onLongClick = clearSymbols)
+ KeyboardButtonFilled(mainButtonModifier, KEY_EVALUATE, { evaluate() })
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun PreviewCalculatorKeyboard() {
+ CalculatorKeyboard(
+ modifier = Modifier,
+ addSymbol = {},
+ clearSymbols = {},
+ deleteSymbol = {},
+ toggleAngleMode = {},
+ angleMode = AngleMode.DEG,
+ evaluate = {}
+ )
+}
diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/InputTextField.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/InputTextField.kt
new file mode 100644
index 00000000..b49360f3
--- /dev/null
+++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/InputTextField.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.feature.calculator.components
+
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalTextInputService
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.style.TextAlign
+import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayLarge
+
+@Composable
+internal fun InputTextField(
+ modifier: Modifier,
+ value: TextFieldValue,
+ onCursorChange: (IntRange) -> Unit
+) {
+ CompositionLocalProvider(
+ // FIXME Can't paste if this is null
+ LocalTextInputService provides null
+ ) {
+ BasicTextField(
+ modifier = modifier,
+ value = value,
+ onValueChange = { onCursorChange(it.selection.start..it.selection.end) },
+ textStyle = NumbersTextStyleDisplayLarge.copy(
+ textAlign = TextAlign.End,
+ color = MaterialTheme.colorScheme.onBackground
+ ),
+ minLines = 1,
+ maxLines = 1,
+ )
+ }
+}
diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/navigation/CalculatorNavigation.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/navigation/CalculatorNavigation.kt
new file mode 100644
index 00000000..763f68c5
--- /dev/null
+++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/navigation/CalculatorNavigation.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.feature.calculator.navigation
+
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import androidx.navigation.navDeepLink
+import com.sadellie.unitto.feature.calculator.CalculatorRoute
+
+private const val calculatorRoute = "calculator_route"
+
+fun NavController.navigateToCalculator() {
+ navigate(calculatorRoute)
+}
+
+fun NavGraphBuilder.calculatorScreen(
+ navigateUpAction: () -> Unit
+) {
+ composable(
+ route = calculatorRoute,
+ deepLinks = listOf(
+ navDeepLink { uriPattern = "app://com.sadellie.unitto/$calculatorRoute" }
+ )) {
+ CalculatorRoute(navigateUpAction = navigateUpAction)
+ }
+}
diff --git a/feature/tools/src/main/java/com/sadellie/unitto/feature/tools/ToolsScreen.kt b/feature/tools/src/main/java/com/sadellie/unitto/feature/tools/ToolsScreen.kt
index dda4bfc9..d868feed 100644
--- a/feature/tools/src/main/java/com/sadellie/unitto/feature/tools/ToolsScreen.kt
+++ b/feature/tools/src/main/java/com/sadellie/unitto/feature/tools/ToolsScreen.kt
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Calculate
import androidx.compose.material.icons.filled.Schedule
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
@@ -23,6 +24,7 @@ import com.sadellie.unitto.core.ui.common.UnittoLargeTopAppBar
@Composable
internal fun ToolsScreen(
navigateUpAction: () -> Unit,
+ navigateToCalculator: () -> Unit,
navigateToEpoch: () -> Unit
) {
UnittoLargeTopAppBar(
@@ -57,6 +59,19 @@ internal fun ToolsScreen(
}
}
}
+ item {
+ ListItem(
+ leadingContent = {
+ Icon(
+ Icons.Default.Calculate,
+ stringResource(R.string.calculator)
+ )
+ },
+ headlineText = { Text(stringResource(R.string.calculator)) },
+ supportingText = { Text(stringResource(R.string.calculator_support)) },
+ modifier = Modifier.clickable { navigateToCalculator() }
+ )
+ }
item {
ListItem(
leadingContent = {
@@ -79,6 +94,7 @@ internal fun ToolsScreen(
internal fun PreviewToolsScreen() {
ToolsScreen(
navigateUpAction = {},
- navigateToEpoch = {}
+ navigateToEpoch = {},
+ navigateToCalculator = {}
)
}
\ No newline at end of file
diff --git a/feature/tools/src/main/java/com/sadellie/unitto/feature/tools/navigation/ToolsNavigation.kt b/feature/tools/src/main/java/com/sadellie/unitto/feature/tools/navigation/ToolsNavigation.kt
index 30ff5f15..462dcb2a 100644
--- a/feature/tools/src/main/java/com/sadellie/unitto/feature/tools/navigation/ToolsNavigation.kt
+++ b/feature/tools/src/main/java/com/sadellie/unitto/feature/tools/navigation/ToolsNavigation.kt
@@ -31,11 +31,13 @@ fun NavController.navigateToTools() {
fun NavGraphBuilder.toolsScreen(
navigateUpAction: () -> Unit,
+ navigateToCalculator: () -> Unit,
navigateToEpoch: () -> Unit
) {
composable(toolsRoute) {
ToolsScreen(
navigateUpAction = navigateUpAction,
+ navigateToCalculator = navigateToCalculator,
navigateToEpoch = navigateToEpoch
)
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 35977dcb..ff4dc0fd 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -29,6 +29,7 @@ orgBurnoutcrewComposereorderable = "0.9.6"
comGithubSadellieExprk = "e55cba8f41"
androidGradlePlugin = "7.4.1"
kotlin = "1.8.0"
+mxParser = "5.2.1"
[libraries]
androidx-core = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" }
@@ -42,7 +43,7 @@ org-jetbrains-kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "androidxComposeUi" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "androidxComposeUi" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "androidxComposeUi" }
-androidx-compose-ui-test-junit4 = { group= "androidx.compose.ui", name = "ui-test-junit4", version.ref = "androidxCompose" }
+androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "androidxCompose" }
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "androidxCompose" }
androidx-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" }
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycleRuntimeCompose" }
@@ -63,6 +64,7 @@ com-github-sadellie-themmo = { group = "com.github.sadellie", name = "themmo", v
org-burnoutcrew-composereorderable = { group = "org.burnoutcrew.composereorderable", name = "reorderable", version.ref = "orgBurnoutcrewComposereorderable" }
com-github-sadellie-exprk = { group = "com.github.sadellie", name = "ExprK", version.ref = "comGithubSadellieExprk" }
android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" }
+org-mariuszgromada-math-mxparser = { group = "org.mariuszgromada.math", name = "MathParser.org-mXparser", version.ref = "mxParser" }
# classpath
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 5b05d959..96749ac9 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -22,6 +22,7 @@ include(":core:base")
include(":core:ui")
include(":feature:converter")
include(":feature:unitslist")
+include(":feature:calculator")
include(":feature:settings")
include(":feature:tools")
include(":feature:epoch")