From 60e5f1f998c69bafd7c3927d95fd00b12f3f3767 Mon Sep 17 00:00:00 2001 From: Sad Ellie Date: Mon, 20 Nov 2023 14:16:41 +0300 Subject: [PATCH] Basic RPN mode implementation #125 --- .../java/com/sadellie/unitto/UnittoApp.kt | 1 + .../com/sadellie/unitto/UnittoNavigation.kt | 6 +- .../unitto/core/ui/common/KeyboardButton.kt | 4 +- .../core/ui/common/key/unittoicons/Down.kt | 60 ++++ .../core/ui/common/key/unittoicons/Enter.kt | 62 +++++ .../core/ui/common/key/unittoicons/Pop.kt | 137 ++++++++++ .../core/ui/common/key/unittoicons/Swap.kt | 71 +++++ .../core/ui/common/key/unittoicons/Unary.kt | 75 +++++ .../core/ui/common/key/unittoicons/Up.kt | 60 ++++ .../common/textfield/FixedInputTextFIeld.kt | 92 +++++++ .../unitto/data/common/BigDecimalUtils.kt | 6 +- .../sadellie/evaluatto/BigDecimalMath.kt | 103 +++++++ .../github/sadellie/evaluatto/Expression.kt | 87 ------ .../sadellie/evaluatto/RPNCalculation.kt | 37 +++ .../io/github/sadellie/evaluatto/RPNEngine.kt | 191 +++++++++++++ .../sadellie/evaluatto/RPNEngineKtTest.kt | 256 ++++++++++++++++++ .../repository/UserPreferencesRepository.kt | 2 + .../data/model/userprefs/AppPreferences.kt | 1 + .../unitto/data/userprefs/PreferenceModels.kt | 1 + .../unitto/data/userprefs/PrefsKeys.kt | 29 +- .../unitto/data/userprefs/UserPreferences.kt | 13 +- .../feature/calculator/DecimalToFraction.kt | 2 +- .../unitto/feature/calculator/InputBox.kt | 120 ++++++++ .../calculator/RPNCalculatorKeyboard.kt | 149 ++++++++++ .../feature/calculator/RPNCalculatorScreen.kt | 134 +++++++++ .../calculator/RPNCalculatorUIState.kt | 37 +++ .../calculator/RPNCalculatorViewModel.kt | 118 ++++++++ .../unitto/feature/calculator/RPNInputEdit.kt | 25 ++ .../calculator/components/HistoryList.kt | 109 +------- .../navigation/CalculatorNavigation.kt | 19 +- .../calculator/CalculatorSettingsScreen.kt | 116 +++++--- .../calculator/CalculatorSettingsUIState.kt | 30 ++ .../calculator/CalculatorSettingsViewModel.kt | 21 +- password.txt | 3 + 34 files changed, 1933 insertions(+), 244 deletions(-) create mode 100644 core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Down.kt create mode 100644 core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Enter.kt create mode 100644 core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Pop.kt create mode 100644 core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Swap.kt create mode 100644 core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Unary.kt create mode 100644 core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Up.kt create mode 100644 core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FixedInputTextFIeld.kt create mode 100644 data/evaluatto/src/main/java/io/github/sadellie/evaluatto/BigDecimalMath.kt create mode 100644 data/evaluatto/src/main/java/io/github/sadellie/evaluatto/RPNCalculation.kt create mode 100644 data/evaluatto/src/main/java/io/github/sadellie/evaluatto/RPNEngine.kt create mode 100644 data/evaluatto/src/test/java/io/github/sadellie/evaluatto/RPNEngineKtTest.kt create mode 100644 feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/InputBox.kt create mode 100644 feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorKeyboard.kt create mode 100644 feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorScreen.kt create mode 100644 feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorUIState.kt create mode 100644 feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorViewModel.kt create mode 100644 feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNInputEdit.kt create mode 100644 feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsUIState.kt create mode 100644 password.txt diff --git a/app/src/main/java/com/sadellie/unitto/UnittoApp.kt b/app/src/main/java/com/sadellie/unitto/UnittoApp.kt index c5a0b30b..7cdf511b 100644 --- a/app/src/main/java/com/sadellie/unitto/UnittoApp.kt +++ b/app/src/main/java/com/sadellie/unitto/UnittoApp.kt @@ -136,6 +136,7 @@ internal fun UnittoApp(prefs: AppPreferences?) { navController = navController, themmoController = it, startDestination = prefs.startingScreen, + rpnMode = prefs.rpnMode, openDrawer = { drawerScope.launch { drawerState.open() } } ) } diff --git a/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt b/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt index 20161a5f..df1e9dd4 100644 --- a/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt +++ b/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt @@ -40,7 +40,8 @@ internal fun UnittoNavigation( navController: NavHostController, themmoController: ThemmoController, startDestination: String, - openDrawer: () -> Unit + openDrawer: () -> Unit, + rpnMode: Boolean, ) { NavHost( navController = navController, @@ -62,7 +63,8 @@ internal fun UnittoNavigation( ) calculatorGraph( - navigateToMenu = openDrawer, + openDrawer = openDrawer, + rpnMode = rpnMode, navigateToSettings = navController::navigateToSettings ) 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 a77994a7..9af4232b 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 @@ -22,7 +22,6 @@ import android.view.HapticFeedbackConstants import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -30,6 +29,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalView @@ -72,7 +72,7 @@ fun BasicKeyboardButton( Icon( imageVector = icon, contentDescription = null, - modifier = Modifier.fillMaxHeight(contentHeight), + modifier = Modifier.matchParentSize().scale(contentHeight), tint = iconColor ) } diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Down.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Down.kt new file mode 100644 index 00000000..8a72b795 --- /dev/null +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Down.kt @@ -0,0 +1,60 @@ +/* + * 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.core.ui.common.key.unittoicons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp +import com.sadellie.unitto.core.ui.common.key.UnittoIcons + +@Suppress("UnusedReceiverParameter") +val UnittoIcons.Down: ImageVector + get() { + if (_down != null) { + return _down!! + } + _down = Builder(name = "Down", defaultWidth = 170.0.dp, defaultHeight = 170.0.dp, + viewportWidth = 170.0f, viewportHeight = 170.0f).apply { + path(fill = SolidColor(Color(0xFF1C1B1F)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero) { + moveTo(79.0f, 37.0f) + verticalLineTo(110.05f) + lineTo(45.4f, 76.45f) + lineTo(37.0f, 85.0f) + lineTo(85.0f, 133.0f) + lineTo(133.0f, 85.0f) + lineTo(124.6f, 76.45f) + lineTo(91.0f, 110.05f) + verticalLineTo(37.0f) + horizontalLineTo(79.0f) + close() + } + } + .build() + return _down!! + } + +private var _down: ImageVector? = null diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Enter.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Enter.kt new file mode 100644 index 00000000..71706837 --- /dev/null +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Enter.kt @@ -0,0 +1,62 @@ +/* + * 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.core.ui.common.key.unittoicons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp +import com.sadellie.unitto.core.ui.common.key.UnittoIcons + +@Suppress("UnusedReceiverParameter") +val UnittoIcons.Enter: ImageVector + get() { + if (_enter != null) { + return _enter!! + } + _enter = Builder(name = "Enter", defaultWidth = 170.0.dp, defaultHeight = 170.0.dp, + viewportWidth = 170.0f, viewportHeight = 170.0f).apply { + path(fill = SolidColor(Color(0xFF1C1B1F)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero) { + moveTo(128.0f, 37.0f) + lineTo(128.0f, 104.765f) + lineTo(64.894f, 104.765f) + lineTo(85.224f, 124.953f) + lineTo(77.176f, 133.0f) + lineTo(43.294f, 99.118f) + lineTo(77.318f, 65.094f) + lineTo(85.224f, 73.141f) + lineTo(64.894f, 93.471f) + lineTo(116.706f, 93.471f) + lineTo(116.706f, 37.0f) + lineTo(128.0f, 37.0f) + close() + } + } + .build() + return _enter!! + } + +private var _enter: ImageVector? = null diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Pop.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Pop.kt new file mode 100644 index 00000000..ac4da0c5 --- /dev/null +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Pop.kt @@ -0,0 +1,137 @@ +/* + * 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.core.ui.common.key.unittoicons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp +import com.sadellie.unitto.core.ui.common.key.UnittoIcons + +@Suppress("UnusedReceiverParameter") +val UnittoIcons.Pop: ImageVector + get() { + if (_pop != null) { + return _pop!! + } + _pop = Builder(name = "Pop", defaultWidth = 274.0.dp, defaultHeight = 170.0.dp, + viewportWidth = 274.0f, viewportHeight = 170.0f).apply { + path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero) { + moveTo(47.507f, 87.46f) + curveTo(51.159f, 87.46f, 54.371f, 86.976f, 57.143f, 86.008f) + curveTo(59.959f, 85.04f, 62.313f, 83.698f, 64.205f, 81.982f) + curveTo(66.141f, 80.222f, 67.593f, 78.132f, 68.561f, 75.712f) + curveTo(69.529f, 73.292f, 70.013f, 70.63f, 70.013f, 67.726f) + curveTo(70.013f, 61.698f, 68.143f, 56.99f, 64.403f, 53.602f) + curveTo(60.707f, 50.214f, 55.075f, 48.52f, 47.507f, 48.52f) + horizontalLineTo(32.327f) + verticalLineTo(87.46f) + horizontalLineTo(47.507f) + close() + moveTo(47.507f, 38.422f) + curveTo(53.491f, 38.422f, 58.683f, 39.126f, 63.083f, 40.534f) + curveTo(67.527f, 41.898f, 71.201f, 43.856f, 74.105f, 46.408f) + curveTo(77.009f, 48.96f, 79.165f, 52.04f, 80.573f, 55.648f) + curveTo(82.025f, 59.256f, 82.751f, 63.282f, 82.751f, 67.726f) + curveTo(82.751f, 72.126f, 81.981f, 76.152f, 80.441f, 79.804f) + curveTo(78.901f, 83.456f, 76.635f, 86.602f, 73.643f, 89.242f) + curveTo(70.695f, 91.882f, 67.021f, 93.95f, 62.621f, 95.446f) + curveTo(58.265f, 96.898f, 53.227f, 97.624f, 47.507f, 97.624f) + horizontalLineTo(32.327f) + verticalLineTo(133.0f) + horizontalLineTo(19.589f) + verticalLineTo(38.422f) + horizontalLineTo(47.507f) + close() + moveTo(183.61f, 85.744f) + curveTo(183.61f, 92.828f, 182.488f, 99.34f, 180.244f, 105.28f) + curveTo(178.0f, 111.176f, 174.832f, 116.258f, 170.74f, 120.526f) + curveTo(166.648f, 124.794f, 161.72f, 128.116f, 155.956f, 130.492f) + curveTo(150.236f, 132.824f, 143.9f, 133.99f, 136.948f, 133.99f) + curveTo(129.996f, 133.99f, 123.66f, 132.824f, 117.94f, 130.492f) + curveTo(112.22f, 128.116f, 107.314f, 124.794f, 103.222f, 120.526f) + curveTo(99.13f, 116.258f, 95.962f, 111.176f, 93.718f, 105.28f) + curveTo(91.474f, 99.34f, 90.352f, 92.828f, 90.352f, 85.744f) + curveTo(90.352f, 78.66f, 91.474f, 72.17f, 93.718f, 66.274f) + curveTo(95.962f, 60.334f, 99.13f, 55.23f, 103.222f, 50.962f) + curveTo(107.314f, 46.65f, 112.22f, 43.306f, 117.94f, 40.93f) + curveTo(123.66f, 38.554f, 129.996f, 37.366f, 136.948f, 37.366f) + curveTo(143.9f, 37.366f, 150.236f, 38.554f, 155.956f, 40.93f) + curveTo(161.72f, 43.306f, 166.648f, 46.65f, 170.74f, 50.962f) + curveTo(174.832f, 55.23f, 178.0f, 60.334f, 180.244f, 66.274f) + curveTo(182.488f, 72.17f, 183.61f, 78.66f, 183.61f, 85.744f) + close() + moveTo(170.476f, 85.744f) + curveTo(170.476f, 79.936f, 169.684f, 74.722f, 168.1f, 70.102f) + curveTo(166.516f, 65.482f, 164.272f, 61.588f, 161.368f, 58.42f) + curveTo(158.464f, 55.208f, 154.944f, 52.744f, 150.808f, 51.028f) + curveTo(146.672f, 49.312f, 142.052f, 48.454f, 136.948f, 48.454f) + curveTo(131.888f, 48.454f, 127.29f, 49.312f, 123.154f, 51.028f) + curveTo(119.018f, 52.744f, 115.476f, 55.208f, 112.528f, 58.42f) + curveTo(109.624f, 61.588f, 107.38f, 65.482f, 105.796f, 70.102f) + curveTo(104.212f, 74.722f, 103.42f, 79.936f, 103.42f, 85.744f) + curveTo(103.42f, 91.552f, 104.212f, 96.766f, 105.796f, 101.386f) + curveTo(107.38f, 105.962f, 109.624f, 109.856f, 112.528f, 113.068f) + curveTo(115.476f, 116.236f, 119.018f, 118.678f, 123.154f, 120.394f) + curveTo(127.29f, 122.066f, 131.888f, 122.902f, 136.948f, 122.902f) + curveTo(142.052f, 122.902f, 146.672f, 122.066f, 150.808f, 120.394f) + curveTo(154.944f, 118.678f, 158.464f, 116.236f, 161.368f, 113.068f) + curveTo(164.272f, 109.856f, 166.516f, 105.962f, 168.1f, 101.386f) + curveTo(169.684f, 96.766f, 170.476f, 91.552f, 170.476f, 85.744f) + close() + moveTo(227.208f, 87.46f) + curveTo(230.86f, 87.46f, 234.072f, 86.976f, 236.844f, 86.008f) + curveTo(239.66f, 85.04f, 242.014f, 83.698f, 243.906f, 81.982f) + curveTo(245.842f, 80.222f, 247.294f, 78.132f, 248.262f, 75.712f) + curveTo(249.23f, 73.292f, 249.714f, 70.63f, 249.714f, 67.726f) + curveTo(249.714f, 61.698f, 247.844f, 56.99f, 244.104f, 53.602f) + curveTo(240.408f, 50.214f, 234.776f, 48.52f, 227.208f, 48.52f) + horizontalLineTo(212.028f) + verticalLineTo(87.46f) + horizontalLineTo(227.208f) + close() + moveTo(227.208f, 38.422f) + curveTo(233.192f, 38.422f, 238.384f, 39.126f, 242.784f, 40.534f) + curveTo(247.228f, 41.898f, 250.902f, 43.856f, 253.806f, 46.408f) + curveTo(256.71f, 48.96f, 258.866f, 52.04f, 260.274f, 55.648f) + curveTo(261.726f, 59.256f, 262.452f, 63.282f, 262.452f, 67.726f) + curveTo(262.452f, 72.126f, 261.682f, 76.152f, 260.142f, 79.804f) + curveTo(258.602f, 83.456f, 256.336f, 86.602f, 253.344f, 89.242f) + curveTo(250.396f, 91.882f, 246.722f, 93.95f, 242.322f, 95.446f) + curveTo(237.966f, 96.898f, 232.928f, 97.624f, 227.208f, 97.624f) + horizontalLineTo(212.028f) + verticalLineTo(133.0f) + horizontalLineTo(199.29f) + verticalLineTo(38.422f) + horizontalLineTo(227.208f) + close() + } + } + .build() + return _pop!! + } + +private var _pop: ImageVector? = null diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Swap.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Swap.kt new file mode 100644 index 00000000..0e27b343 --- /dev/null +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Swap.kt @@ -0,0 +1,71 @@ +/* + * 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.core.ui.common.key.unittoicons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp +import com.sadellie.unitto.core.ui.common.key.UnittoIcons + +@Suppress("UnusedReceiverParameter") +val UnittoIcons.Swap: ImageVector + get() { + if (_swap != null) { + return _swap!! + } + _swap = Builder(name = "Swap", defaultWidth = 170.0.dp, defaultHeight = 170.0.dp, + viewportWidth = 170.0f, viewportHeight = 170.0f).apply { + path(fill = SolidColor(Color(0xFF1C1B1F)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero) { + moveTo(66.2f, 89.8f) + verticalLineTo(55.36f) + lineTo(53.84f, 67.72f) + lineTo(47.0f, 61.0f) + lineTo(71.0f, 37.0f) + lineTo(95.0f, 61.0f) + lineTo(88.16f, 67.72f) + lineTo(75.8f, 55.36f) + verticalLineTo(89.8f) + horizontalLineTo(66.2f) + close() + moveTo(99.8f, 133.0f) + lineTo(75.8f, 109.0f) + lineTo(82.64f, 102.28f) + lineTo(95.0f, 114.64f) + verticalLineTo(80.2f) + horizontalLineTo(104.6f) + verticalLineTo(114.64f) + lineTo(116.96f, 102.28f) + lineTo(123.8f, 109.0f) + lineTo(99.8f, 133.0f) + close() + } + } + .build() + return _swap!! + } + +private var _swap: ImageVector? = null diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Unary.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Unary.kt new file mode 100644 index 00000000..c348a11f --- /dev/null +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Unary.kt @@ -0,0 +1,75 @@ +/* + * 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.core.ui.common.key.unittoicons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp +import com.sadellie.unitto.core.ui.common.key.UnittoIcons + +@Suppress("UnusedReceiverParameter") +val UnittoIcons.Unary: ImageVector + get() { + if (_unary != null) { + return _unary!! + } + _unary = Builder(name = "Unary", defaultWidth = 170.0.dp, defaultHeight = 170.0.dp, + viewportWidth = 170.0f, viewportHeight = 170.0f).apply { + path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero) { + moveTo(46.0f, 71.125f) + horizontalLineTo(124.0f) + verticalLineTo(81.363f) + horizontalLineTo(46.0f) + verticalLineTo(71.125f) + close() + } + path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero) { + moveTo(90.85f, 37.0f) + lineTo(90.85f, 115.0f) + horizontalLineTo(80.613f) + lineTo(80.613f, 37.0f) + lineTo(90.85f, 37.0f) + close() + } + path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero) { + moveTo(46.0f, 124.0f) + horizontalLineTo(124.0f) + verticalLineTo(133.0f) + horizontalLineTo(46.0f) + verticalLineTo(124.0f) + close() + } + } + .build() + return _unary!! + } + +private var _unary: ImageVector? = null diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Up.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Up.kt new file mode 100644 index 00000000..fc0dad9e --- /dev/null +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Up.kt @@ -0,0 +1,60 @@ +/* + * 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.core.ui.common.key.unittoicons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp +import com.sadellie.unitto.core.ui.common.key.UnittoIcons + +@Suppress("UnusedReceiverParameter") +val UnittoIcons.Up: ImageVector + get() { + if (_up != null) { + return _up!! + } + _up = Builder(name = "Up", defaultWidth = 170.0.dp, defaultHeight = 170.0.dp, viewportWidth + = 170.0f, viewportHeight = 170.0f).apply { + path(fill = SolidColor(Color(0xFF1C1B1F)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero) { + moveTo(79.0f, 133.0f) + verticalLineTo(59.95f) + lineTo(45.4f, 93.55f) + lineTo(37.0f, 85.0f) + lineTo(85.0f, 37.0f) + lineTo(133.0f, 85.0f) + lineTo(124.6f, 93.55f) + lineTo(91.0f, 59.95f) + verticalLineTo(133.0f) + horizontalLineTo(79.0f) + close() + } + } + .build() + return _up!! + } + +private var _up: ImageVector? = null diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FixedInputTextFIeld.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FixedInputTextFIeld.kt new file mode 100644 index 00000000..1629a32e --- /dev/null +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FixedInputTextFIeld.kt @@ -0,0 +1,92 @@ +/* + * 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.core.ui.common.textfield + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +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.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalTextInputService +import androidx.compose.ui.platform.LocalTextToolbar +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.sadellie.unitto.core.ui.theme.LocalNumberTypography + +@Composable +fun FixedInputTextField( + modifier: Modifier = Modifier, + value: String, + formatterSymbols: FormatterSymbols, + textColor: Color, + onClick: (cleanValue: String) -> Unit +) { + val clipboardManager = LocalClipboardManager.current + val expression = value.take(1000) + var expressionValue by remember(expression) { + mutableStateOf(TextFieldValue(expression, TextRange(expression.length))) + } + + val expressionInteractionSource = remember(expression) { MutableInteractionSource() } + LaunchedEffect(expressionInteractionSource) { + expressionInteractionSource.interactions.collect { + if (it is PressInteraction.Release) onClick(expression.clearAndFilterExpression(formatterSymbols)) + } + } + + CompositionLocalProvider( + LocalTextInputService provides null, + LocalTextToolbar provides UnittoTextToolbar( + view = LocalView.current, + copyCallback = { + clipboardManager.copyWithoutGrouping(expressionValue, formatterSymbols) + expressionValue = expressionValue.copy(selection = TextRange(expressionValue.selection.end)) + } + ) + ) { + BasicTextField( + value = expressionValue, + onValueChange = { expressionValue = it }, + maxLines = 1, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .horizontalScroll(rememberScrollState(), reverseScrolling = true), + textStyle = LocalNumberTypography.current.displaySmall.copy(color = textColor, textAlign = TextAlign.End), + readOnly = true, + visualTransformation = ExpressionTransformer(formatterSymbols), + interactionSource = expressionInteractionSource + ) + } +} diff --git a/data/common/src/main/java/com/sadellie/unitto/data/common/BigDecimalUtils.kt b/data/common/src/main/java/com/sadellie/unitto/data/common/BigDecimalUtils.kt index 4354814c..94f12cfb 100644 --- a/data/common/src/main/java/com/sadellie/unitto/data/common/BigDecimalUtils.kt +++ b/data/common/src/main/java/com/sadellie/unitto/data/common/BigDecimalUtils.kt @@ -24,6 +24,7 @@ import java.math.RoundingMode import kotlin.math.floor import kotlin.math.log10 +// TODO Use everywhere fun BigDecimal.format( scale: Int, outputFormat: Int @@ -34,6 +35,7 @@ fun BigDecimal.format( .toStringWith(outputFormat) } +// TODO Move tests and mark as internal /** * Shorthand function to use correct `toString` method according to [outputFormat]. */ @@ -76,5 +78,7 @@ fun BigDecimal.setMinimumRequiredScale(prefScale: Int): BigDecimal { * Removes all trailing zeroes. */ fun BigDecimal.trimZeros(): BigDecimal { - return if (this.compareTo(BigDecimal.ZERO) == 0) BigDecimal.ZERO else this.stripTrailingZeros() + return if (this.isEqualTo(BigDecimal.ZERO)) BigDecimal.ZERO else this.stripTrailingZeros() } + +fun BigDecimal.isEqualTo(bd: BigDecimal): Boolean = compareTo(bd) == 0 diff --git a/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/BigDecimalMath.kt b/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/BigDecimalMath.kt new file mode 100644 index 00000000..fe5031c6 --- /dev/null +++ b/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/BigDecimalMath.kt @@ -0,0 +1,103 @@ +/* + * 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 io.github.sadellie.evaluatto + +import java.math.BigDecimal +import java.math.MathContext +import java.math.RoundingMode +import kotlin.math.acos +import kotlin.math.asin +import kotlin.math.atan +import kotlin.math.pow + +internal fun BigDecimal.sin(radianMode: Boolean): BigDecimal { + val angle: Double = if (radianMode) this.toDouble() else Math.toRadians(this.toDouble()) + return kotlin.math.sin(angle).toBigDecimal() +} + +internal fun BigDecimal.arsin(radianMode: Boolean): BigDecimal { + val angle: Double = asin(this.toDouble()) + return (if (radianMode) angle else Math.toDegrees(angle)).toBigDecimal() +} + +internal fun BigDecimal.cos(radianMode: Boolean): BigDecimal { + val angle: Double = if (radianMode) this.toDouble() else Math.toRadians(this.toDouble()) + return kotlin.math.cos(angle).toBigDecimal() +} + +internal fun BigDecimal.arcos(radianMode: Boolean): BigDecimal { + val angle: Double = acos(this.toDouble()) + return (if (radianMode) angle else Math.toDegrees(angle)).toBigDecimal() +} + +internal fun BigDecimal.tan(radianMode: Boolean): BigDecimal { + val angle: Double = if (radianMode) this.toDouble() else Math.toRadians(this.toDouble()) + return kotlin.math.tan(angle).toBigDecimal() +} + +internal fun BigDecimal.artan(radianMode: Boolean): BigDecimal { + val angle: Double = atan(this.toDouble()) + return (if (radianMode) angle else Math.toDegrees(angle)).toBigDecimal() +} + +internal fun BigDecimal.ln(): BigDecimal { + return kotlin.math.ln(this.toDouble()).toBigDecimal() +} + +internal fun BigDecimal.log(): BigDecimal { + return kotlin.math.log(this.toDouble(), 10.0).toBigDecimal() +} + +internal fun BigDecimal.exp(): BigDecimal { + return kotlin.math.exp(this.toDouble()).toBigDecimal() +} + +internal fun BigDecimal.pow(n: BigDecimal): BigDecimal { + val mathContext: MathContext = MathContext.DECIMAL64 + + var right = n + val signOfRight = right.signum() + right = right.multiply(signOfRight.toBigDecimal()) + val remainderOfRight = right.remainder(BigDecimal.ONE) + val n2IntPart = right.subtract(remainderOfRight) + val intPow = pow(n2IntPart.intValueExact(), mathContext) + val doublePow = BigDecimal( + toDouble().pow(remainderOfRight.toDouble()) + ) + + var result = intPow.multiply(doublePow, mathContext) + if (signOfRight == -1) result = + BigDecimal.ONE.divide(result, mathContext.precision, RoundingMode.HALF_UP) + + return result +} + +internal fun BigDecimal.factorial(): BigDecimal { + if (this.remainder(BigDecimal.ONE).compareTo(BigDecimal.ZERO) != 0) throw ExpressionException.FactorialCalculation() + if (this < BigDecimal.ZERO) throw ExpressionException.FactorialCalculation() + if (this > maxFactorial) throw ExpressionException.TooBig() + + var expr = this + for (i in 1 until this.toInt()) { + expr *= BigDecimal(i) + } + return expr +} + +private val maxFactorial by lazy { BigDecimal("9999") } diff --git a/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/Expression.kt b/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/Expression.kt index ef30d655..93164c57 100644 --- a/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/Expression.kt +++ b/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/Expression.kt @@ -21,20 +21,7 @@ package io.github.sadellie.evaluatto import com.sadellie.unitto.core.base.MAX_PRECISION import com.sadellie.unitto.core.base.Token import java.math.BigDecimal -import java.math.MathContext import java.math.RoundingMode -import kotlin.math.acos -import kotlin.math.asin -import kotlin.math.atan -import kotlin.math.cos -import kotlin.math.exp -import kotlin.math.ln -import kotlin.math.log -import kotlin.math.pow -import kotlin.math.sin -import kotlin.math.tan - -private val maxFactorial by lazy { BigDecimal("9999") } sealed class ExpressionException(override val message: String): Exception(message) { class DivideByZero : ExpressionException("Can't divide by zero") @@ -240,77 +227,3 @@ class Expression( return expr } } - -private fun BigDecimal.sin(radianMode: Boolean): BigDecimal { - val angle: Double = if (radianMode) this.toDouble() else Math.toRadians(this.toDouble()) - return sin(angle).toBigDecimal() -} - -private fun BigDecimal.arsin(radianMode: Boolean): BigDecimal { - val angle: Double = asin(this.toDouble()) - return (if (radianMode) angle else Math.toDegrees(angle)).toBigDecimal() -} - -private fun BigDecimal.cos(radianMode: Boolean): BigDecimal { - val angle: Double = if (radianMode) this.toDouble() else Math.toRadians(this.toDouble()) - return cos(angle).toBigDecimal() -} - -private fun BigDecimal.arcos(radianMode: Boolean): BigDecimal { - val angle: Double = acos(this.toDouble()) - return (if (radianMode) angle else Math.toDegrees(angle)).toBigDecimal() -} - -private fun BigDecimal.tan(radianMode: Boolean): BigDecimal { - val angle: Double = if (radianMode) this.toDouble() else Math.toRadians(this.toDouble()) - return tan(angle).toBigDecimal() -} - -private fun BigDecimal.artan(radianMode: Boolean): BigDecimal { - val angle: Double = atan(this.toDouble()) - return (if (radianMode) angle else Math.toDegrees(angle)).toBigDecimal() -} - -private fun BigDecimal.ln(): BigDecimal { - return ln(this.toDouble()).toBigDecimal() -} - -private fun BigDecimal.log(): BigDecimal { - return log(this.toDouble(), 10.0).toBigDecimal() -} - -private fun BigDecimal.exp(): BigDecimal { - return exp(this.toDouble()).toBigDecimal() -} - -private fun BigDecimal.pow(n: BigDecimal): BigDecimal { - val mathContext: MathContext = MathContext.DECIMAL64 - - var right = n - val signOfRight = right.signum() - right = right.multiply(signOfRight.toBigDecimal()) - val remainderOfRight = right.remainder(BigDecimal.ONE) - val n2IntPart = right.subtract(remainderOfRight) - val intPow = pow(n2IntPart.intValueExact(), mathContext) - val doublePow = BigDecimal( - toDouble().pow(remainderOfRight.toDouble()) - ) - - var result = intPow.multiply(doublePow, mathContext) - if (signOfRight == -1) result = - BigDecimal.ONE.divide(result, mathContext.precision, RoundingMode.HALF_UP) - - return result -} - -private fun BigDecimal.factorial(): BigDecimal { - if (this.remainder(BigDecimal.ONE).compareTo(BigDecimal.ZERO) != 0) throw ExpressionException.FactorialCalculation() - if (this < BigDecimal.ZERO) throw ExpressionException.FactorialCalculation() - if (this > maxFactorial) throw ExpressionException.TooBig() - - var expr = this - for (i in 1 until this.toInt()) { - expr *= BigDecimal(i) - } - return expr -} diff --git a/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/RPNCalculation.kt b/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/RPNCalculation.kt new file mode 100644 index 00000000..0b108442 --- /dev/null +++ b/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/RPNCalculation.kt @@ -0,0 +1,37 @@ +/* + * 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 io.github.sadellie.evaluatto + +sealed class RPNCalculation { + data object Negate : RPNCalculation() + + data object Clear : RPNCalculation() + data object Enter : RPNCalculation() + data object RotateUp : RPNCalculation() + data object RotateDown : RPNCalculation() + data object Swap : RPNCalculation() + data object Pop : RPNCalculation() + + data object Plus : RPNCalculation() + data object Minus : RPNCalculation() + data object Multiply : RPNCalculation() + data object Divide : RPNCalculation() + data object Percent : RPNCalculation() + data object Power : RPNCalculation() // unused +} diff --git a/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/RPNEngine.kt b/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/RPNEngine.kt new file mode 100644 index 00000000..dcf2af00 --- /dev/null +++ b/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/RPNEngine.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 io.github.sadellie.evaluatto + +import com.sadellie.unitto.core.base.MAX_PRECISION +import java.math.BigDecimal +import java.math.RoundingMode + +sealed class RPNResult { + + /** + * Both input and stack were changed. + * + * @property input New input. Empty when `null`. + * @property stack New stack. + */ + data class Result( + val input: BigDecimal?, + val stack: List, + ) : RPNResult() + + /** + * Only input has been changed. + * + * @property input New input. + */ + data class NewInput( + val input: BigDecimal, + ) : RPNResult() + + /** + * Only stack has been changed. + * + * @property stack New stack. + */ + data class NewStack( + val stack: List, + ) : RPNResult() + + /** + * Something is wrong. Input/stack is empty or there ane not enough stack objects. + */ + data object BadInput : RPNResult() + + /** + * Dividing by zero, duh + */ + data object DivideByZero : RPNResult() +} + +// vroom vroom mfs +// overdose on early returns +fun RPNCalculation.perform( + input: String, + stack: List, +): RPNResult { + when (this) { + RPNCalculation.Clear -> { + return RPNResult.Result(null, emptyList()) + } + + RPNCalculation.Enter -> { + val inputBD = input.toBigDecimalOrNull() ?: return RPNResult.BadInput + return RPNResult.Result(null, stack + inputBD) + } + + RPNCalculation.Negate -> { + val inputBD = input.toBigDecimalOrNull() ?: return RPNResult.BadInput + val result = inputBD.negate() + return RPNResult.NewInput(result) + } + + RPNCalculation.RotateUp -> { + if (stack.size < 2) return RPNResult.BadInput + return RPNResult.NewStack(stack.rotateUp()) + } + + RPNCalculation.RotateDown -> { + if (stack.size < 2) return RPNResult.BadInput + return RPNResult.NewStack(stack.rotateDown()) + } + + RPNCalculation.Swap -> { + if (stack.isEmpty()) return RPNResult.BadInput + if (input.isEmpty()) { + // Swap last 2 in stack + if (stack.size < 2) return RPNResult.BadInput + return RPNResult.NewStack(stack.swapLastTwo()) + } + + // Swap last and input + val (lastFromStack, inputBD) = operands(stack, input) ?: return RPNResult.BadInput + return RPNResult.Result(lastFromStack, stack.dropLast(1) + inputBD) + } + + RPNCalculation.Pop -> { + val lastStacked = stack.lastOrNull() ?: return RPNResult.BadInput + return RPNResult.Result(lastStacked, stack.dropLast(1)) + } + + RPNCalculation.Plus -> { + val (lastFromStack, inputBD) = operands(stack, input) ?: return RPNResult.BadInput + val result = lastFromStack.plus(inputBD) + return RPNResult.Result(result, stack.dropLast(1)) + } + + RPNCalculation.Minus -> { + val (lastFromStack, inputBD) = operands(stack, input) ?: return RPNResult.BadInput + val result = lastFromStack.minus(inputBD) + return RPNResult.Result(result, stack.dropLast(1)) + } + + RPNCalculation.Multiply -> { + val (lastFromStack, inputBD) = operands(stack, input) ?: return RPNResult.BadInput + val result = lastFromStack.multiply(inputBD) + return RPNResult.Result(result, stack.dropLast(1)) + } + + RPNCalculation.Divide -> { + val (lastFromStack, inputBD) = operands(stack, input) ?: return RPNResult.BadInput + if (inputBD.compareTo(BigDecimal.ZERO) == 0) return RPNResult.DivideByZero + + val result = lastFromStack.divide(inputBD, MAX_PRECISION, RoundingMode.HALF_EVEN) + return RPNResult.Result(result, stack.dropLast(1)) + } + + RPNCalculation.Percent -> { + val (lastFromStack, inputBD) = operands(stack, input) ?: return RPNResult.BadInput + // 100 * 24 / 100 = + val result = lastFromStack + .multiply(inputBD) + .divide(bigDecimalHundred, MAX_PRECISION, RoundingMode.HALF_EVEN) + return RPNResult.Result(result, stack.dropLast(1)) + } + + RPNCalculation.Power -> { + val (lastFromStack, inputBD) = operands(stack, input) ?: return RPNResult.BadInput + val result = lastFromStack.pow(inputBD) + return RPNResult.Result(result, stack.dropLast(1)) + } + } +} + +private val bigDecimalHundred by lazy { BigDecimal("100") } + +private fun operands( + stack: List, + input: String, +): Pair? { + val first = stack.lastOrNull() ?: return null + val second = input.toBigDecimalOrNull() ?: return null + + return first to second +} + +private fun List.swapLastTwo(): List { + if (size < 2) return this + return this + .dropLast(2) + .plus(get(lastIndex)) + .plus(get(lastIndex - 1)) +} + +private fun List.rotateUp(): List { + if (size < 2) return this + return this + .drop(1) + .plus(first()) +} + +private fun List.rotateDown(): List { + if (size < 2) return this + return listOf(last()) + .plus(this.dropLast(1)) +} diff --git a/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/RPNEngineKtTest.kt b/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/RPNEngineKtTest.kt new file mode 100644 index 00000000..b8be0354 --- /dev/null +++ b/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/RPNEngineKtTest.kt @@ -0,0 +1,256 @@ +/* + * 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 io.github.sadellie.evaluatto + +import com.sadellie.unitto.core.base.MAX_PRECISION +import org.junit.Assert.assertEquals +import org.junit.Test +import java.math.BigDecimal + +class RPNEngineKtTest { + + @Test + fun testBadOperands() { + // no funny business if input and/or stack is empty + val actual = RPNCalculation.Divide.perform( + input = "", + stack = emptyList() + ) + + assertEquals(RPNResult.BadInput, actual) + } + + @Test + fun testDivide() { + val actual = RPNCalculation.Divide.perform( + input = "2", + stack = listOf(BigDecimal("5")) + ) + + if (actual !is RPNResult.Result) throw Exception("Wrong return") + + assertEquals(BigDecimal("2.5").setScale(MAX_PRECISION), actual.input) + assertEquals(emptyList(), actual.stack) + } + + @Test + fun testDivideByZero() { + val actual = RPNCalculation.Divide.perform( + input = "0", + stack = listOf(BigDecimal("5")) + ) + + assertEquals(RPNResult.DivideByZero, actual) + } + + @Test + fun testMinus() { + val actual = RPNCalculation.Minus.perform( + input = "2", + stack = listOf(BigDecimal("5")) + ) + + if (actual !is RPNResult.Result) throw Exception("Wrong return") + + assertEquals(BigDecimal("3"), actual.input) + assertEquals(emptyList(), actual.stack) + } + + @Test + fun testMultiply() { + val actual = RPNCalculation.Multiply.perform( + input = "2", + stack = listOf(BigDecimal("5")) + ) + + if (actual !is RPNResult.Result) throw Exception("Wrong return") + + assertEquals(BigDecimal("10"), actual.input) + assertEquals(emptyList(), actual.stack) + } + + @Test + fun testNegate() { + val actual = RPNCalculation.Negate.perform( + input = "2", + stack = listOf(BigDecimal("5")) + ) + + if (actual !is RPNResult.NewInput) throw Exception("Wrong return") + + assertEquals(BigDecimal("-2"), actual.input) + } + + @Test + fun testPercent() { + val actual = RPNCalculation.Percent.perform( + input = "150", + stack = listOf(BigDecimal("69")) + ) + + if (actual !is RPNResult.Result) throw Exception("Wrong return") + + assertEquals(BigDecimal("103.5").setScale(MAX_PRECISION), actual.input) + assertEquals(emptyList(), actual.stack) + } + + @Test + fun testPlus() { + val actual = RPNCalculation.Plus.perform( + input = "150", + stack = listOf(BigDecimal("69")) + ) + + if (actual !is RPNResult.Result) throw Exception("Wrong return") + + assertEquals(BigDecimal("219"), actual.input) + assertEquals(emptyList(), actual.stack) + } + + @Test + fun testPower() { + val actual = RPNCalculation.Power.perform( + input = "3", + stack = listOf(BigDecimal("2")) + ) + + if (actual !is RPNResult.Result) throw Exception("Wrong return") + + assertEquals(BigDecimal("8"), actual.input) + assertEquals(emptyList(), actual.stack) + } + + @Test + fun testRotateUp() { + val actual = RPNCalculation.RotateUp.perform( + input = "", + stack = listOf(BigDecimal("1"), BigDecimal("2"), BigDecimal("3")) + ) + + if (actual !is RPNResult.NewStack) throw Exception("Wrong return") + + assertEquals(listOf(BigDecimal("2"), BigDecimal("3"), BigDecimal("1")), actual.stack) + } + + @Test + fun testRotateDown() { + val actual = RPNCalculation.RotateDown.perform( + input = "", + stack = listOf(BigDecimal("1"), BigDecimal("2"), BigDecimal("3")) + ) + + if (actual !is RPNResult.NewStack) throw Exception("Wrong return") + + assertEquals(listOf(BigDecimal("3"), BigDecimal("1"), BigDecimal("2")), actual.stack) + } + + @Test + fun testPop() { + val actual = RPNCalculation.Pop.perform( + input = "", + stack = listOf(BigDecimal("1"), BigDecimal("2"), BigDecimal("3")) + ) + + if (actual !is RPNResult.Result) throw Exception("Wrong return") + + assertEquals(BigDecimal("3"), actual.input) + assertEquals(listOf(BigDecimal("1"), BigDecimal("2")), actual.stack) + } + + @Test + fun testClear() { + val actual = RPNCalculation.Clear.perform( + input = "3", + stack = listOf(BigDecimal("2")) + ) + + if (actual !is RPNResult.Result) throw Exception("Wrong return") + + assertEquals(null, actual.input) + assertEquals(emptyList(), actual.stack) + } + + @Test + fun testEnter() { + val actual = RPNCalculation.Enter.perform( + input = "3", + stack = listOf(BigDecimal("2")) + ) + + if (actual !is RPNResult.Result) throw Exception("Wrong return") + + assertEquals(null, actual.input) + assertEquals(listOf(BigDecimal("2"), BigDecimal("3")), actual.stack) + } + + @Test + fun testSwap() { + val actual = RPNCalculation.Swap.perform( + input = "3", + stack = listOf(BigDecimal("2")) + ) + + if (actual !is RPNResult.Result) throw Exception("Wrong return") + + assertEquals(BigDecimal("2"), actual.input) + assertEquals(listOf(BigDecimal("3")), actual.stack) + } + + @Test + fun testSwapEmptyInput() { + val actual = RPNCalculation.Swap.perform( + input = "", + stack = listOf(BigDecimal("1"), BigDecimal("2")) + ) + + if (actual !is RPNResult.NewStack) throw Exception("Wrong return") + + assertEquals(listOf(BigDecimal("2"), BigDecimal("1")), actual.stack) + } + + @Test + fun testSwapEmptyInputNotEnoughInStack() { + val actual = RPNCalculation.Swap.perform( + input = "", + stack = listOf(BigDecimal("1")) + ) + + assertEquals(RPNResult.BadInput, actual) + } + + @Test + fun testSwapEmptyStack() { + val actual = RPNCalculation.Swap.perform( + input = "123", + stack = emptyList() + ) + + assertEquals(RPNResult.BadInput, actual) + } + + @Test + fun testSwapEmptyBoth() { + val actual = RPNCalculation.Swap.perform( + input = "", + stack = emptyList() + ) + + assertEquals(RPNResult.BadInput, actual) + } +} diff --git a/data/model/src/main/java/com/sadellie/unitto/data/model/repository/UserPreferencesRepository.kt b/data/model/src/main/java/com/sadellie/unitto/data/model/repository/UserPreferencesRepository.kt index 1157240f..95fcce03 100644 --- a/data/model/src/main/java/com/sadellie/unitto/data/model/repository/UserPreferencesRepository.kt +++ b/data/model/src/main/java/com/sadellie/unitto/data/model/repository/UserPreferencesRepository.kt @@ -88,4 +88,6 @@ interface UserPreferencesRepository { suspend fun updateAcButton(enabled: Boolean) suspend fun updateClearInputAfterEquals(enabled: Boolean) + + suspend fun updateRpnMode(enabled: Boolean) } diff --git a/data/model/src/main/java/com/sadellie/unitto/data/model/userprefs/AppPreferences.kt b/data/model/src/main/java/com/sadellie/unitto/data/model/userprefs/AppPreferences.kt index 25708eae..82ba34fa 100644 --- a/data/model/src/main/java/com/sadellie/unitto/data/model/userprefs/AppPreferences.kt +++ b/data/model/src/main/java/com/sadellie/unitto/data/model/userprefs/AppPreferences.kt @@ -27,4 +27,5 @@ interface AppPreferences { val startingScreen: String val enableToolsExperiment: Boolean val systemFont: Boolean + val rpnMode: Boolean } diff --git a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PreferenceModels.kt b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PreferenceModels.kt index c24bb23c..03dc71e1 100644 --- a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PreferenceModels.kt +++ b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PreferenceModels.kt @@ -41,6 +41,7 @@ data class AppPreferencesImpl( override val startingScreen: String, override val enableToolsExperiment: Boolean, override val systemFont: Boolean, + override val rpnMode: Boolean, ) : AppPreferences data class GeneralPreferencesImpl( diff --git a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PrefsKeys.kt b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PrefsKeys.kt index 7ecd722b..c70c2a51 100644 --- a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PrefsKeys.kt +++ b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PrefsKeys.kt @@ -24,28 +24,35 @@ import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey internal object PrefsKeys { + // COMMON val THEMING_MODE = stringPreferencesKey("THEMING_MODE_PREF_KEY") val ENABLE_DYNAMIC_THEME = booleanPreferencesKey("ENABLE_DYNAMIC_THEME_PREF_KEY") val ENABLE_AMOLED_THEME = booleanPreferencesKey("ENABLE_AMOLED_THEME_PREF_KEY") val CUSTOM_COLOR = longPreferencesKey("CUSTOM_COLOR_PREF_KEY") val MONET_MODE = stringPreferencesKey("MONET_MODE_PREF_KEY") + val STARTING_SCREEN = stringPreferencesKey("STARTING_SCREEN_PREF_KEY") + val ENABLE_TOOLS_EXPERIMENT = booleanPreferencesKey("ENABLE_TOOLS_EXPERIMENT_PREF_KEY") + val SYSTEM_FONT = booleanPreferencesKey("SYSTEM_FONT_PREF_KEY") + val ENABLE_VIBRATIONS = booleanPreferencesKey("ENABLE_VIBRATIONS_PREF_KEY") + val MIDDLE_ZERO = booleanPreferencesKey("MIDDLE_ZERO_PREF_KEY") + val AC_BUTTON = booleanPreferencesKey("AC_BUTTON_PREF_KEY") + val RPN_MODE = booleanPreferencesKey("RPN_MODE_PREF_KEY") + + // FORMATTER val DIGITS_PRECISION = intPreferencesKey("DIGITS_PRECISION_PREF_KEY") val SEPARATOR = intPreferencesKey("SEPARATOR_PREF_KEY") val OUTPUT_FORMAT = intPreferencesKey("OUTPUT_FORMAT_PREF_KEY") + + // CALCULATOR + val RADIAN_MODE = booleanPreferencesKey("RADIAN_MODE_PREF_KEY") + val PARTIAL_HISTORY_VIEW = booleanPreferencesKey("PARTIAL_HISTORY_VIEW_PREF_KEY") + val CLEAR_INPUT_AFTER_EQUALS = booleanPreferencesKey("CLEAR_INPUT_AFTER_EQUALS_PREF_KEY") + + // UNIT CONVERTER val LATEST_LEFT_SIDE = stringPreferencesKey("LATEST_LEFT_SIDE_PREF_KEY") val LATEST_RIGHT_SIDE = stringPreferencesKey("LATEST_RIGHT_SIDE_PREF_KEY") val SHOWN_UNIT_GROUPS = stringPreferencesKey("SHOWN_UNIT_GROUPS_PREF_KEY") - val ENABLE_VIBRATIONS = booleanPreferencesKey("ENABLE_VIBRATIONS_PREF_KEY") - val ENABLE_TOOLS_EXPERIMENT = booleanPreferencesKey("ENABLE_TOOLS_EXPERIMENT_PREF_KEY") - val STARTING_SCREEN = stringPreferencesKey("STARTING_SCREEN_PREF_KEY") - val RADIAN_MODE = booleanPreferencesKey("RADIAN_MODE_PREF_KEY") - val UNIT_CONVERTER_FAVORITES_ONLY = - booleanPreferencesKey("UNIT_CONVERTER_FAVORITES_ONLY_PREF_KEY") + val UNIT_CONVERTER_FAVORITES_ONLY = booleanPreferencesKey("UNIT_CONVERTER_FAVORITES_ONLY_PREF_KEY") val UNIT_CONVERTER_FORMAT_TIME = booleanPreferencesKey("UNIT_CONVERTER_FORMAT_TIME_PREF_KEY") val UNIT_CONVERTER_SORTING = stringPreferencesKey("UNIT_CONVERTER_SORTING_PREF_KEY") - val MIDDLE_ZERO = booleanPreferencesKey("MIDDLE_ZERO_PREF_KEY") - val SYSTEM_FONT = booleanPreferencesKey("SYSTEM_FONT_PREF_KEY") - val PARTIAL_HISTORY_VIEW = booleanPreferencesKey("PARTIAL_HISTORY_VIEW_PREF_KEY") - val AC_BUTTON = booleanPreferencesKey("AC_BUTTON_PREF_KEY") - val CLEAR_INPUT_AFTER_EQUALS = booleanPreferencesKey("CLEAR_INPUT_AFTER_EQUALS_PREF_KEY") } diff --git a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt index e74567b0..c126d308 100644 --- a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt +++ b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt @@ -63,7 +63,8 @@ class UserPreferencesRepositoryImpl @Inject constructor( monetMode = preferences.getMonetMode(), startingScreen = preferences.getStartingScreen(), enableToolsExperiment = preferences.getEnableToolsExperiment(), - systemFont = preferences.getSystemFont() + systemFont = preferences.getSystemFont(), + rpnMode = preferences.getRpnMode(), ) } @@ -287,6 +288,12 @@ class UserPreferencesRepositoryImpl @Inject constructor( preferences[PrefsKeys.CLEAR_INPUT_AFTER_EQUALS] = enabled } } + + override suspend fun updateRpnMode(enabled: Boolean) { + dataStore.edit { preferences -> + preferences[PrefsKeys.RPN_MODE] = enabled + } + } } private fun Preferences.getEnableDynamicTheme(): Boolean { @@ -387,6 +394,10 @@ private fun Preferences.getClearInputAfterEquals(): Boolean { return this[PrefsKeys.CLEAR_INPUT_AFTER_EQUALS] ?: true } +private fun Preferences.getRpnMode(): Boolean { + return this[PrefsKeys.RPN_MODE] ?: false +} + private inline fun T.letTryOrNull(block: (T) -> R): R? = try { this?.let(block) } catch (e: Exception) { diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/DecimalToFraction.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/DecimalToFraction.kt index c89edcd4..61374872 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/DecimalToFraction.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/DecimalToFraction.kt @@ -19,6 +19,7 @@ package com.sadellie.unitto.feature.calculator import com.sadellie.unitto.core.base.Token +import com.sadellie.unitto.data.common.isEqualTo import java.math.BigDecimal import java.math.BigInteger import java.math.RoundingMode @@ -101,5 +102,4 @@ private fun BigDecimal.repeatingDecimals(): String? { return null } -private fun BigDecimal.isEqualTo(bd: BigDecimal): Boolean = compareTo(bd) == 0 private val maxDenominator by lazy { BigInteger("1000000000") } diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/InputBox.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/InputBox.kt new file mode 100644 index 00000000..006384c4 --- /dev/null +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/InputBox.kt @@ -0,0 +1,120 @@ +/* + * 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.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.sadellie.unitto.core.base.OutputFormat +import com.sadellie.unitto.core.ui.common.textfield.ExpressionTextField +import com.sadellie.unitto.core.ui.common.textfield.FixedInputTextField +import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols +import com.sadellie.unitto.data.common.format +import java.math.BigDecimal + +@Composable +internal fun InputBox( + modifier: Modifier, + input: TextFieldValue, + onCursorChange: (TextRange) -> Unit, + stack: List, + formatterSymbols: FormatterSymbols, + precision: Int, + outputFormat: Int, +) { + val listState = rememberLazyListState() + + LaunchedEffect(stack) { + listState.animateScrollToItem(stack.lastIndex.coerceAtLeast(0)) + } + + Column( + modifier = modifier + .clip(RoundedCornerShape(24.dp)) + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceVariant) + .padding(start = 12.dp, end = 12.dp, bottom = 12.dp), + verticalArrangement = Arrangement.Bottom + ) { + LazyColumn( + modifier = Modifier.weight(1f), + state = listState, + verticalArrangement = Arrangement.Bottom, + ) { + items(stack) { + FixedInputTextField( + modifier = Modifier.fillMaxWidth(), + value = it.format(precision, outputFormat), + formatterSymbols = formatterSymbols, + textColor = MaterialTheme.colorScheme.onSurfaceVariant, + onClick = {} + ) + } + } + + ExpressionTextField( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(0.25f), + value = input, + minRatio = 0.6f, + onCursorChange = onCursorChange, + formatterSymbols = formatterSymbols, + textColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} + +@Preview(device = "spec:width=1080px,height=2160px,dpi=440") +@Composable +fun PreviewInputBox() { + InputBox( + modifier = Modifier.fillMaxSize(), + input = TextFieldValue("123456.789"), + onCursorChange = {}, + stack = listOf( + BigDecimal("123456.7890"), + BigDecimal("123456.7890"), + BigDecimal("123456.7890"), + BigDecimal("123456.7890"), + BigDecimal("123456.7890"), + BigDecimal("123456.7890"), + ), + formatterSymbols = FormatterSymbols.Spaces, + precision = 3, + outputFormat = OutputFormat.PLAIN + ) +} diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorKeyboard.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorKeyboard.kt new file mode 100644 index 00000000..b517b75b --- /dev/null +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorKeyboard.kt @@ -0,0 +1,149 @@ +/* + * 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.foundation.layout.Arrangement +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.sadellie.unitto.core.base.Token +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.core.ui.common.KeyboardButtonTertiary +import com.sadellie.unitto.core.ui.common.key.UnittoIcons +import com.sadellie.unitto.core.ui.common.key.unittoicons.Backspace +import com.sadellie.unitto.core.ui.common.key.unittoicons.Clear +import com.sadellie.unitto.core.ui.common.key.unittoicons.Comma +import com.sadellie.unitto.core.ui.common.key.unittoicons.Divide +import com.sadellie.unitto.core.ui.common.key.unittoicons.Dot +import com.sadellie.unitto.core.ui.common.key.unittoicons.Down +import com.sadellie.unitto.core.ui.common.key.unittoicons.Enter +import com.sadellie.unitto.core.ui.common.key.unittoicons.Key0 +import com.sadellie.unitto.core.ui.common.key.unittoicons.Key1 +import com.sadellie.unitto.core.ui.common.key.unittoicons.Key2 +import com.sadellie.unitto.core.ui.common.key.unittoicons.Key3 +import com.sadellie.unitto.core.ui.common.key.unittoicons.Key4 +import com.sadellie.unitto.core.ui.common.key.unittoicons.Key5 +import com.sadellie.unitto.core.ui.common.key.unittoicons.Key6 +import com.sadellie.unitto.core.ui.common.key.unittoicons.Key7 +import com.sadellie.unitto.core.ui.common.key.unittoicons.Key8 +import com.sadellie.unitto.core.ui.common.key.unittoicons.Key9 +import com.sadellie.unitto.core.ui.common.key.unittoicons.Minus +import com.sadellie.unitto.core.ui.common.key.unittoicons.Multiply +import com.sadellie.unitto.core.ui.common.key.unittoicons.Percent +import com.sadellie.unitto.core.ui.common.key.unittoicons.Plus +import com.sadellie.unitto.core.ui.common.key.unittoicons.Pop +import com.sadellie.unitto.core.ui.common.key.unittoicons.Swap +import com.sadellie.unitto.core.ui.common.key.unittoicons.Unary +import com.sadellie.unitto.core.ui.common.key.unittoicons.Up +import io.github.sadellie.evaluatto.RPNCalculation + +@OptIn(ExperimentalLayoutApi::class) +@Composable +internal fun RPNCalculatorKeyboard( + modifier: Modifier, + fractional: String, + middleZero: Boolean, + allowVibration: Boolean, + onCalculationClick: (RPNCalculation) -> Unit, + onInputEditClick: (RPNInputEdit) -> Unit, +) { + val fractionalIcon = remember(fractional) { if (fractional == Token.Digit.dot) UnittoIcons.Dot else UnittoIcons.Comma } + + val columns = 4 + val mainButtonRows = 5f + val additionalButtonRows = 1f + val additionalButtonRowFactor = 0.7f // How much smaller are the additional buttons than the main buttons + val additionalButtonIconHeight = 0.65f + val fillFactor = 0.92f + + val rows = remember { mainButtonRows + additionalButtonRows * additionalButtonRowFactor } + val height = remember { fillFactor / rows } + val width = remember { fillFactor / columns } + + FlowRow( + maxItemsInEachRow = columns, + modifier = modifier, + horizontalArrangement = Arrangement.SpaceAround, + verticalArrangement = Arrangement.SpaceAround + ) { + val aModifier = Modifier + .fillMaxHeight(height * additionalButtonRowFactor) + .fillMaxWidth(width) + + val bModifier = Modifier + .fillMaxHeight(height) + .fillMaxWidth(width) + + KeyboardButtonAdditional(modifier = aModifier, icon = UnittoIcons.Swap, allowVibration = allowVibration, contentHeight = additionalButtonIconHeight) { onCalculationClick(RPNCalculation.Swap) } + KeyboardButtonAdditional(modifier = aModifier, icon = UnittoIcons.Up, allowVibration = allowVibration, contentHeight = additionalButtonIconHeight) { onCalculationClick(RPNCalculation.RotateUp) } + KeyboardButtonAdditional(modifier = aModifier, icon = UnittoIcons.Down, allowVibration = allowVibration, contentHeight = additionalButtonIconHeight) { onCalculationClick(RPNCalculation.RotateDown) } + KeyboardButtonAdditional(modifier = aModifier, icon = UnittoIcons.Pop, allowVibration = allowVibration, contentHeight = additionalButtonIconHeight) { onCalculationClick(RPNCalculation.Pop) } + + KeyboardButtonTertiary(modifier = bModifier, icon = UnittoIcons.Clear, allowVibration = allowVibration) { onCalculationClick(RPNCalculation.Clear) } + KeyboardButtonFilled(modifier = bModifier, icon = UnittoIcons.Unary, allowVibration = allowVibration) { onCalculationClick(RPNCalculation.Negate) } + KeyboardButtonFilled(modifier = bModifier, icon = UnittoIcons.Percent, allowVibration = allowVibration) { onCalculationClick(RPNCalculation.Percent) } + KeyboardButtonFilled(modifier = bModifier, icon = UnittoIcons.Divide, allowVibration = allowVibration) { onCalculationClick(RPNCalculation.Divide) } + + KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key7, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._7)) } + KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key8, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._8)) } + KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key9, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._9)) } + KeyboardButtonFilled(modifier = bModifier, icon = UnittoIcons.Multiply, allowVibration = allowVibration) { onCalculationClick(RPNCalculation.Multiply) } + + KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key4, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._4)) } + KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key5, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._5)) } + KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key6, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._6)) } + KeyboardButtonFilled(modifier = bModifier, icon = UnittoIcons.Minus, allowVibration = allowVibration) { onCalculationClick(RPNCalculation.Minus) } + + KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key1, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._1)) } + KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key2, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._2)) } + KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key3, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._3)) } + KeyboardButtonFilled(modifier = bModifier, icon = UnittoIcons.Plus, allowVibration = allowVibration) { onCalculationClick(RPNCalculation.Plus) } + + if (middleZero) { + KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key0, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._0)) } + KeyboardButtonLight(modifier = bModifier, icon = fractionalIcon, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Dot) } + } else { + KeyboardButtonLight(modifier = bModifier, icon = fractionalIcon, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Dot) } + KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key0, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._0)) } + } + KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Backspace, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Delete) } + KeyboardButtonFilled(modifier = bModifier, icon = UnittoIcons.Enter, allowVibration = allowVibration) { onCalculationClick(RPNCalculation.Enter) } + } +} + +@Preview +@Composable +private fun PreviewKeyboard() { + RPNCalculatorKeyboard( + modifier = Modifier.fillMaxSize(), + fractional = Token.Digit.dot, + middleZero = false, + allowVibration = false, + onCalculationClick = {}, + onInputEditClick = {} + ) +} diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorScreen.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorScreen.kt new file mode 100644 index 00000000..49cc302c --- /dev/null +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorScreen.kt @@ -0,0 +1,134 @@ +/* + * 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.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.sadellie.unitto.core.base.OutputFormat +import com.sadellie.unitto.core.base.R +import com.sadellie.unitto.core.ui.common.MenuButton +import com.sadellie.unitto.core.ui.common.SettingsButton +import com.sadellie.unitto.core.ui.common.UnittoEmptyScreen +import com.sadellie.unitto.core.ui.common.UnittoScreenWithTopBar +import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols +import io.github.sadellie.evaluatto.RPNCalculation +import java.math.BigDecimal + +@Composable +internal fun RPNCalculatorRoute( + openDrawer: () -> Unit, + navigateToSettings: () -> Unit, + viewModel: RPNCalculatorViewModel = hiltViewModel(), +) { + when (val uiState = viewModel.uiState.collectAsStateWithLifecycle().value) { + RPNCalculatorUIState.Loading -> UnittoEmptyScreen() + is RPNCalculatorUIState.Ready -> RPNCalculatorScreen( + uiState = uiState, + openDrawer = openDrawer, + navigateToSettings = navigateToSettings, + onCursorChange = viewModel::onCursorChange, + onCalculationClick = viewModel::onCalculationClick, + onInputEditClick = viewModel::onInputEdit + ) + } +} + +@Composable +internal fun RPNCalculatorScreen( + uiState: RPNCalculatorUIState.Ready, + openDrawer: () -> Unit, + navigateToSettings: () -> Unit, + onCursorChange: (TextRange) -> Unit, + onCalculationClick: (RPNCalculation) -> Unit, + onInputEditClick: (RPNInputEdit) -> Unit, +) { + UnittoScreenWithTopBar( + title = { Text(stringResource(id = R.string.calculator_title)) }, + navigationIcon = { MenuButton(openDrawer) }, + actions = { SettingsButton(navigateToSettings) } + ) { paddingValues -> + Column( + Modifier.padding(paddingValues) + ) { + InputBox( + modifier = Modifier + .padding(8.dp) + .fillMaxHeight(0.3f), + input = uiState.input, + stack = uiState.stack, + formatterSymbols = uiState.formatterSymbols, + precision = uiState.precision, + outputFormat = uiState.outputFormat, + onCursorChange = onCursorChange + ) + RPNCalculatorKeyboard( + modifier = Modifier + .padding(horizontal = 4.dp) + .fillMaxSize(), + fractional = uiState.formatterSymbols.fractional, + middleZero = uiState.middleZero, + allowVibration = uiState.allowVibration, + onCalculationClick = onCalculationClick, + onInputEditClick = onInputEditClick + ) + } + } +} + +@Preview(widthDp = 432, heightDp = 1008, device = "spec:parent=pixel_5,orientation=portrait") +@Preview(widthDp = 432, heightDp = 864, device = "spec:parent=pixel_5,orientation=portrait") +@Preview(widthDp = 597, heightDp = 1393, device = "spec:parent=pixel_5,orientation=portrait") +@Composable +private fun RPNCalculatorScreenPreview() { + RPNCalculatorScreen( + uiState = RPNCalculatorUIState.Ready( + input = TextFieldValue("test"), + stack = listOf( + BigDecimal("123456.7890"), + BigDecimal("123456.7890"), + BigDecimal("123456.7890"), + BigDecimal("123456.7890"), + BigDecimal("123456.7890"), + BigDecimal("123456.7890"), + ), + precision = 3, + outputFormat = OutputFormat.PLAIN, + formatterSymbols = FormatterSymbols.Spaces, + allowVibration = true, + middleZero = true, + ), + openDrawer = {}, + navigateToSettings = {}, + onCalculationClick = {}, + onInputEditClick = {}, + onCursorChange = {} + ) +} diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorUIState.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorUIState.kt new file mode 100644 index 00000000..18b6e21b --- /dev/null +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorUIState.kt @@ -0,0 +1,37 @@ +/* + * 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.ui.text.input.TextFieldValue +import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols +import java.math.BigDecimal + +internal sealed class RPNCalculatorUIState { + data object Loading : RPNCalculatorUIState() + + data class Ready( + val input: TextFieldValue, + val stack: List, + val precision: Int, + val outputFormat: Int, + val formatterSymbols: FormatterSymbols, + val allowVibration: Boolean, + val middleZero: Boolean, + ) : RPNCalculatorUIState() +} diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorViewModel.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorViewModel.kt new file mode 100644 index 00000000..ec9b6269 --- /dev/null +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorViewModel.kt @@ -0,0 +1,118 @@ +/* + * 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.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.sadellie.unitto.core.base.Token +import com.sadellie.unitto.core.ui.common.textfield.AllFormatterSymbols +import com.sadellie.unitto.core.ui.common.textfield.addTokens +import com.sadellie.unitto.core.ui.common.textfield.deleteTokens +import com.sadellie.unitto.core.ui.common.textfield.getTextField +import com.sadellie.unitto.data.common.format +import com.sadellie.unitto.data.common.stateIn +import com.sadellie.unitto.data.model.repository.UserPreferencesRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import io.github.sadellie.evaluatto.RPNCalculation +import io.github.sadellie.evaluatto.RPNResult +import io.github.sadellie.evaluatto.perform +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.math.BigDecimal +import javax.inject.Inject + +@HiltViewModel +internal class RPNCalculatorViewModel @Inject constructor( + userPrefsRepository: UserPreferencesRepository, + private val savedStateHandle: SavedStateHandle, +) : ViewModel() { + + private val _inputKey = "RPN_CALCULATOR_INPUT" + private val _input = MutableStateFlow(savedStateHandle.getTextField(_inputKey)) + private val _stack = MutableStateFlow(emptyList()) + private val _prefs = userPrefsRepository.calculatorPrefs.stateIn(viewModelScope, null) + + val uiState = combine( + _prefs, + _input, + _stack + ) { prefs, input, stack -> + prefs ?: return@combine RPNCalculatorUIState.Loading + + return@combine RPNCalculatorUIState.Ready( + input = input, + stack = stack, + precision = prefs.precision, + outputFormat = prefs.outputFormat, + formatterSymbols = AllFormatterSymbols.getById(prefs.separator), + allowVibration = prefs.enableVibrations, + middleZero = prefs.middleZero + ) + } + .stateIn(viewModelScope, RPNCalculatorUIState.Loading) + + fun onInputEdit(action: RPNInputEdit) { + val input = _input.value + val newInput = when (action) { + is RPNInputEdit.Digit -> input.addTokens(action.value) + RPNInputEdit.Dot -> { + if (_input.value.text.contains(Token.Digit.dot)) return + input.addTokens(Token.Digit.dot) + } + RPNInputEdit.Delete -> input.deleteTokens() + } + + savedStateHandle[_inputKey] = newInput.text + } + + fun onCalculationClick(action: RPNCalculation) = viewModelScope.launch { + val prefs = _prefs.value ?: return@launch + + val newResult = withContext(Dispatchers.Default) { + action.perform(_input.value.text, _stack.value) + } + + when (newResult) { + is RPNResult.Result -> { + val newInput = newResult.input?.format(prefs.precision, prefs.outputFormat) ?: "" + _input.update { TextFieldValue(newInput, TextRange(newInput.length)) } + _stack.update { newResult.stack } + } + + is RPNResult.NewStack -> { + _stack.update { newResult.stack } + } + + is RPNResult.NewInput -> { + val newInput = newResult.input.format(prefs.precision, prefs.outputFormat) + _input.update { TextFieldValue(newInput, TextRange(newInput.length)) } + } + RPNResult.BadInput, RPNResult.DivideByZero -> Unit + } + } + + fun onCursorChange(selection: TextRange) = _input.update { it.copy(selection = selection) } +} diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNInputEdit.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNInputEdit.kt new file mode 100644 index 00000000..b6a2fe46 --- /dev/null +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNInputEdit.kt @@ -0,0 +1,25 @@ +/* + * 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 + +sealed class RPNInputEdit { + data class Digit(val value: String) : RPNInputEdit() + data object Delete : RPNInputEdit() + data object Dot : RPNInputEdit() +} diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt index 544531c9..3ffb18f8 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt @@ -19,53 +19,30 @@ package com.sadellie.unitto.feature.calculator.components import androidx.compose.foundation.background -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.BasicTextField import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.History import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.LocalTextInputService -import androidx.compose.ui.platform.LocalTextToolbar -import androidx.compose.ui.platform.LocalView 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.compose.ui.unit.dp import com.sadellie.unitto.core.base.R -import com.sadellie.unitto.core.ui.common.textfield.ExpressionTransformer +import com.sadellie.unitto.core.ui.common.textfield.FixedInputTextField import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols -import com.sadellie.unitto.core.ui.common.textfield.UnittoTextToolbar -import com.sadellie.unitto.core.ui.common.textfield.clearAndFilterExpression -import com.sadellie.unitto.core.ui.common.textfield.copyWithoutGrouping -import com.sadellie.unitto.core.ui.theme.LocalNumberTypography import com.sadellie.unitto.data.model.HistoryItem import java.text.SimpleDateFormat import java.util.Locale @@ -151,83 +128,23 @@ private fun HistoryListItem( formatterSymbols: FormatterSymbols, addTokens: (String) -> Unit, ) { - val clipboardManager = LocalClipboardManager.current - val expression = historyItem.expression.take(1000) - var expressionValue by remember(expression) { - mutableStateOf(TextFieldValue(expression, TextRange(expression.length))) - } - val result = historyItem.result.take(1000) - var resultValue by remember(result) { - mutableStateOf(TextFieldValue(result, TextRange(result.length))) - } - - val expressionInteractionSource = remember(expression) { MutableInteractionSource() } - LaunchedEffect(expressionInteractionSource) { - expressionInteractionSource.interactions.collect { - if (it is PressInteraction.Release) addTokens(expression.clearAndFilterExpression(formatterSymbols)) - } - } - - val resultInteractionSource = remember(result) { MutableInteractionSource() } - LaunchedEffect(resultInteractionSource) { - resultInteractionSource.interactions.collect { - if (it is PressInteraction.Release) addTokens(result.clearAndFilterExpression(formatterSymbols)) - } - } - Column( modifier = modifier.height(HistoryItemHeight), verticalArrangement = Arrangement.Center ) { - CompositionLocalProvider( - LocalTextInputService provides null, - LocalTextToolbar provides UnittoTextToolbar( - view = LocalView.current, - copyCallback = { - clipboardManager.copyWithoutGrouping(expressionValue, formatterSymbols) - expressionValue = expressionValue.copy(selection = TextRange(expressionValue.selection.end)) - } - ) - ) { - BasicTextField( - value = expressionValue, - onValueChange = { expressionValue = it }, - maxLines = 1, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp) - .horizontalScroll(rememberScrollState(), reverseScrolling = true), - textStyle = LocalNumberTypography.current.displaySmall.copy(color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.End), - readOnly = true, - visualTransformation = ExpressionTransformer(formatterSymbols), - interactionSource = expressionInteractionSource - ) - } + FixedInputTextField( + value = historyItem.expression, + formatterSymbols = formatterSymbols, + textColor = MaterialTheme.colorScheme.onSurfaceVariant, + onClick = addTokens, + ) - CompositionLocalProvider( - LocalTextInputService provides null, - LocalTextToolbar provides UnittoTextToolbar( - view = LocalView.current, - copyCallback = { - clipboardManager.copyWithoutGrouping(resultValue, formatterSymbols) - resultValue = resultValue.copy(selection = TextRange(resultValue.selection.end)) - } - ) - ) { - BasicTextField( - value = resultValue, - onValueChange = { resultValue = it }, - maxLines = 1, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp) - .horizontalScroll(rememberScrollState(), reverseScrolling = true), - textStyle = LocalNumberTypography.current.displaySmall.copy(color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), textAlign = TextAlign.End), - readOnly = true, - visualTransformation = ExpressionTransformer(formatterSymbols), - interactionSource = resultInteractionSource - ) - } + FixedInputTextField( + value = historyItem.result, + formatterSymbols = formatterSymbols, + textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + onClick = addTokens, + ) } } 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 index fbae4ef0..fbabfcc5 100644 --- 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 @@ -24,12 +24,14 @@ import com.sadellie.unitto.core.base.TopLevelDestinations import com.sadellie.unitto.core.ui.unittoComposable import com.sadellie.unitto.core.ui.unittoNavigation import com.sadellie.unitto.feature.calculator.CalculatorRoute +import com.sadellie.unitto.feature.calculator.RPNCalculatorRoute private val graph = TopLevelDestinations.Calculator.graph private val start = TopLevelDestinations.Calculator.start fun NavGraphBuilder.calculatorGraph( - navigateToMenu: () -> Unit, + rpnMode: Boolean, + openDrawer: () -> Unit, navigateToSettings: () -> Unit ) { unittoNavigation( @@ -40,10 +42,17 @@ fun NavGraphBuilder.calculatorGraph( ) ) { unittoComposable(start) { - CalculatorRoute( - navigateToMenu = navigateToMenu, - navigateToSettings = navigateToSettings - ) + if (rpnMode) { + RPNCalculatorRoute( + openDrawer = openDrawer, + navigateToSettings = navigateToSettings + ) + } else { + CalculatorRoute( + navigateToMenu = openDrawer, + navigateToSettings = navigateToSettings + ) + } } } } diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsScreen.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsScreen.kt index 270b5eb1..4f5cada4 100644 --- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsScreen.kt +++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsScreen.kt @@ -18,73 +18,111 @@ package com.sadellie.unitto.feature.settings.calculator -import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Backspace import androidx.compose.material.icons.filled.Timer +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.sadellie.unitto.core.base.OutputFormat import com.sadellie.unitto.core.base.R -import com.sadellie.unitto.core.base.Separator import com.sadellie.unitto.core.ui.common.NavigateUpButton import com.sadellie.unitto.core.ui.common.UnittoEmptyScreen import com.sadellie.unitto.core.ui.common.UnittoListItem import com.sadellie.unitto.core.ui.common.UnittoScreenWithLargeTopBar -import com.sadellie.unitto.data.model.userprefs.CalculatorPreferences -import com.sadellie.unitto.data.userprefs.CalculatorPreferencesImpl @Composable internal fun CalculatorSettingsRoute( viewModel: CalculatorSettingsViewModel = hiltViewModel(), navigateUpAction: () -> Unit, ) { - when (val prefs = viewModel.prefs.collectAsStateWithLifecycle().value) { - null -> UnittoEmptyScreen() + when (val prefs = viewModel.uiState.collectAsStateWithLifecycle().value) { + CalculatorSettingsUIState.Loading -> UnittoEmptyScreen() else -> { CalculatorSettingsScreen( - prefs = prefs, + uiState = prefs, navigateUpAction = navigateUpAction, updatePartialHistoryView = viewModel::updatePartialHistoryView, updateClearInputAfterEquals = viewModel::updateClearInputAfterEquals, + updateRpnMode = viewModel::updateRpnMode, ) } } } +// TODO Translate @Composable private fun CalculatorSettingsScreen( - prefs: CalculatorPreferences, + uiState: CalculatorSettingsUIState, navigateUpAction: () -> Unit, updatePartialHistoryView: (Boolean) -> Unit, updateClearInputAfterEquals: (Boolean) -> Unit, + updateRpnMode: (Boolean) -> Unit, ) { UnittoScreenWithLargeTopBar( title = stringResource(R.string.calculator_title), navigationIcon = { NavigateUpButton(navigateUpAction) } ) { padding -> - LazyColumn(contentPadding = padding) { - item("partial history") { - UnittoListItem( - headlineText = stringResource(R.string.settings_partial_history_view), - icon = Icons.Default.Timer, - supportingText = stringResource(R.string.settings_partial_history_view_support), - switchState = prefs.partialHistoryView, - onSwitchChange = updatePartialHistoryView - ) + Column(Modifier.padding(padding)) { + SingleChoiceSegmentedButtonRow( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + SegmentedButton( + selected = uiState is CalculatorSettingsUIState.Standard, + onClick = { updateRpnMode(false) }, + shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2), + ) { + Text("Standard") + } + SegmentedButton( + selected = uiState == CalculatorSettingsUIState.RPN, + onClick = { updateRpnMode(true) }, + shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2), + ) { + Text("RPN") + } } - item("clear input") { - UnittoListItem( - headlineText = stringResource(R.string.settings_clear_input), - icon = Icons.AutoMirrored.Filled.Backspace, - supportingText = stringResource(R.string.settings_clear_input_support), - switchState = prefs.clearInputAfterEquals, - onSwitchChange = updateClearInputAfterEquals - ) + Crossfade( + targetState = uiState, + label = "Mode switch" + ) { state -> + when (state) { + is CalculatorSettingsUIState.Standard -> { + Column { + UnittoListItem( + headlineText = stringResource(R.string.settings_partial_history_view), + icon = Icons.Default.Timer, + supportingText = stringResource(R.string.settings_partial_history_view_support), + switchState = state.partialHistoryView, + onSwitchChange = updatePartialHistoryView + ) + + UnittoListItem( + headlineText = stringResource(R.string.settings_clear_input), + icon = Icons.AutoMirrored.Filled.Backspace, + supportingText = stringResource(R.string.settings_clear_input_support), + switchState = state.clearInputAfterEquals, + onSwitchChange = updateClearInputAfterEquals + ) + } + } + + else -> Unit + } } } } @@ -92,21 +130,27 @@ private fun CalculatorSettingsScreen( @Preview @Composable -private fun PreviewCalculatorSettingsScreen() { +private fun PreviewCalculatorSettingsScreenStandard() { CalculatorSettingsScreen( - prefs = CalculatorPreferencesImpl( - radianMode = false, - enableVibrations = false, - separator = Separator.SPACE, - middleZero = false, + uiState = CalculatorSettingsUIState.Standard( partialHistoryView = true, - precision = 3, - outputFormat = OutputFormat.PLAIN, - acButton = true, - clearInputAfterEquals = true, + clearInputAfterEquals = false ), navigateUpAction = {}, updatePartialHistoryView = {}, updateClearInputAfterEquals = {}, + updateRpnMode = {} + ) +} + +@Preview +@Composable +private fun PreviewCalculatorSettingsScreenRPN() { + CalculatorSettingsScreen( + uiState = CalculatorSettingsUIState.RPN, + navigateUpAction = {}, + updatePartialHistoryView = {}, + updateClearInputAfterEquals = {}, + updateRpnMode = {} ) } diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsUIState.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsUIState.kt new file mode 100644 index 00000000..f7fa8ecf --- /dev/null +++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsUIState.kt @@ -0,0 +1,30 @@ +/* + * 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.settings.calculator + +internal sealed class CalculatorSettingsUIState { + data object Loading : CalculatorSettingsUIState() + + data object RPN : CalculatorSettingsUIState() + + data class Standard( + val partialHistoryView: Boolean, + val clearInputAfterEquals: Boolean, + ) : CalculatorSettingsUIState() +} diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsViewModel.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsViewModel.kt index e887b5dc..e7ba599a 100644 --- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsViewModel.kt +++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsViewModel.kt @@ -23,6 +23,7 @@ import androidx.lifecycle.viewModelScope import com.sadellie.unitto.data.common.stateIn import com.sadellie.unitto.data.model.repository.UserPreferencesRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import javax.inject.Inject @@ -30,8 +31,20 @@ import javax.inject.Inject internal class CalculatorSettingsViewModel @Inject constructor( private val userPrefsRepository: UserPreferencesRepository, ) : ViewModel() { - val prefs = userPrefsRepository.calculatorPrefs - .stateIn(viewModelScope, null) + val uiState = combine( + userPrefsRepository.appPrefs, + userPrefsRepository.calculatorPrefs, + ) { app, calc -> + if (app.rpnMode) { + CalculatorSettingsUIState.RPN + } else { + CalculatorSettingsUIState.Standard( + partialHistoryView = calc.partialHistoryView, + clearInputAfterEquals = calc.clearInputAfterEquals + ) + } + } + .stateIn(viewModelScope, CalculatorSettingsUIState.Loading) fun updatePartialHistoryView(enabled: Boolean) = viewModelScope.launch { userPrefsRepository.updatePartialHistoryView(enabled) @@ -40,4 +53,8 @@ internal class CalculatorSettingsViewModel @Inject constructor( fun updateClearInputAfterEquals(enabled: Boolean) = viewModelScope.launch { userPrefsRepository.updateClearInputAfterEquals(enabled) } + + fun updateRpnMode(enabled: Boolean) = viewModelScope.launch { + userPrefsRepository.updateRpnMode(enabled) + } } diff --git a/password.txt b/password.txt new file mode 100644 index 00000000..bd27d794 --- /dev/null +++ b/password.txt @@ -0,0 +1,3 @@ +009D4E + +super secret classified sauce, don't share