mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-18 16:25:27 +02:00
Added Calculator (#2)
Note: First iteration of this feature, will change the navigation, cursor and copy/paste/cut.
This commit is contained in:
parent
b963cdc644
commit
5ab779d136
@ -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")))
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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(
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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
1
feature/calculator/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
38
feature/calculator/build.gradle.kts
Normal file
38
feature/calculator/build.gradle.kts
Normal 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")))
|
||||
}
|
0
feature/calculator/consumer-rules.pro
Normal file
0
feature/calculator/consumer-rules.pro
Normal file
22
feature/calculator/src/main/AndroidManifest.xml
Normal file
22
feature/calculator/src/main/AndroidManifest.xml
Normal 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>
|
@ -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 }
|
@ -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 = {}
|
||||
)
|
||||
}
|
@ -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
|
||||
)
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 = {}
|
||||
)
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 = {}
|
||||
)
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
|
@ -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" }
|
||||
|
@ -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")
|
||||
|
Loading…
x
Reference in New Issue
Block a user