Added Calculator (#2)

Note: First iteration of this feature, will change the navigation, cursor and copy/paste/cut.
This commit is contained in:
Sad Ellie 2023-02-10 12:01:29 +04:00
parent b963cdc644
commit 5ab779d136
21 changed files with 857 additions and 2 deletions

View File

@ -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")))

View File

@ -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
)

View File

@ -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(

View File

@ -1022,6 +1022,10 @@
<string name="tools_notice_description">This screen is part of an experiment. It may change in the future.</string>
<string name="click_to_read_more">Click to read more!</string>
<!--Calculator-->
<string name="calculator">Calculator</string>
<string name="calculator_support">Calculate, but don\'t convert</string>
<!--Precision-->
<string name="precision_setting_support">Number of decimal places</string>
<string name="precision_setting_info">Converted values may have a precision higher than the preferred one.</string>

View File

@ -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
)
}
}

View File

@ -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,

1
feature/calculator/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

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

View File

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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/>.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.feature.calculator
internal enum class AngleMode { DEG, RAD }

View File

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

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<UserPreferences> =
userPrefsRepository.userPreferencesFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000L),
UserPreferences()
)
private val _input: MutableStateFlow<String> = MutableStateFlow("")
private val _output: MutableStateFlow<String> = MutableStateFlow("")
private val _selection: MutableStateFlow<IntRange> = MutableStateFlow(IntRange(0, 0))
private val _angleMode: MutableStateFlow<AngleMode> = 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()
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -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 = {}
)
}

View File

@ -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
)
}

View File

@ -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" }

View File

@ -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")