Basic RPN mode implementation

#125
This commit is contained in:
Sad Ellie 2023-11-20 14:16:41 +03:00
parent cf59362708
commit 60e5f1f998
34 changed files with 1933 additions and 244 deletions

View File

@ -136,6 +136,7 @@ internal fun UnittoApp(prefs: AppPreferences?) {
navController = navController,
themmoController = it,
startDestination = prefs.startingScreen,
rpnMode = prefs.rpnMode,
openDrawer = { drawerScope.launch { drawerState.open() } }
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,191 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package 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<BigDecimal>,
) : 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<BigDecimal>,
) : 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<BigDecimal>,
): 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<BigDecimal>,
input: String,
): Pair<BigDecimal, BigDecimal>? {
val first = stack.lastOrNull() ?: return null
val second = input.toBigDecimalOrNull() ?: return null
return first to second
}
private fun <T> List<T>.swapLastTwo(): List<T> {
if (size < 2) return this
return this
.dropLast(2)
.plus(get(lastIndex))
.plus(get(lastIndex - 1))
}
private fun <T> List<T>.rotateUp(): List<T> {
if (size < 2) return this
return this
.drop(1)
.plus(first())
}
private fun <T> List<T>.rotateDown(): List<T> {
if (size < 2) return this
return listOf(last())
.plus(this.dropLast(1))
}

View File

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

View File

@ -88,4 +88,6 @@ interface UserPreferencesRepository {
suspend fun updateAcButton(enabled: Boolean)
suspend fun updateClearInputAfterEquals(enabled: Boolean)
suspend fun updateRpnMode(enabled: Boolean)
}

View File

@ -27,4 +27,5 @@ interface AppPreferences {
val startingScreen: String
val enableToolsExperiment: Boolean
val systemFont: Boolean
val rpnMode: Boolean
}

View File

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

View File

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

View File

@ -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, R> T.letTryOrNull(block: (T) -> R): R? = try {
this?.let(block)
} catch (e: Exception) {

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<BigDecimal>,
val precision: Int,
val outputFormat: Int,
val formatterSymbols: FormatterSymbols,
val allowVibration: Boolean,
val middleZero: Boolean,
) : RPNCalculatorUIState()
}

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.feature.calculator
sealed class RPNInputEdit {
data class Digit(val value: String) : RPNInputEdit()
data object Delete : RPNInputEdit()
data object Dot : RPNInputEdit()
}

View File

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

View File

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

View File

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

View File

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

View File

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

3
password.txt Normal file
View File

@ -0,0 +1,3 @@
009D4E
super secret classified sauce, don't share