75 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/sosauce/cutecalc/ui/screens/calculator/CalculatorViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.sosauce.cutecalc.ui.screens.calculator
2 |
3 | import android.app.Application
4 | import androidx.compose.foundation.text.input.TextFieldState
5 | import androidx.compose.foundation.text.input.clearText
6 | import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
7 | import androidx.compose.runtime.getValue
8 | import androidx.compose.runtime.mutableStateOf
9 | import androidx.compose.runtime.setValue
10 | import androidx.compose.runtime.snapshotFlow
11 | import androidx.lifecycle.AndroidViewModel
12 | import androidx.lifecycle.viewModelScope
13 | import com.sosauce.cutecalc.data.actions.CalcAction
14 | import com.sosauce.cutecalc.data.calculator.Evaluator
15 | import com.sosauce.cutecalc.data.datastore.getDecimalPrecision
16 | import com.sosauce.cutecalc.utils.backspace
17 | import com.sosauce.cutecalc.utils.insertText
18 | import com.sosauce.cutecalc.utils.isErrorMessage
19 | import kotlinx.coroutines.flow.MutableStateFlow
20 | import kotlinx.coroutines.flow.asStateFlow
21 | import kotlinx.coroutines.flow.collectLatest
22 | import kotlinx.coroutines.flow.first
23 | import kotlinx.coroutines.flow.update
24 | import kotlinx.coroutines.launch
25 |
26 | class CalculatorViewModel(
27 | private val application: Application
28 | ) : AndroidViewModel(application) {
29 |
30 |
31 | val textFieldState = TextFieldState()
32 | var evaluatedCalculation by mutableStateOf("")
33 | private set
34 |
35 | private val _previewShowErrors = MutableStateFlow(false)
36 | val previewShowErrors = _previewShowErrors.asStateFlow()
37 |
38 |
39 | init {
40 | viewModelScope.launch {
41 | snapshotFlow { textFieldState.text.toString() }
42 | .collectLatest { text ->
43 | val decimalPrecision =
44 | getDecimalPrecision(application.applicationContext).first()
45 | evaluatedCalculation = if (textFieldState.text.isEmpty()) {
46 | // there's currently a bug that will keep the preview to the last result even if this is empty, my head hurts too much to search a real fix atm
47 | ""
48 | } else {
49 | Evaluator.eval(text, decimalPrecision)
50 | }
51 | }
52 | }
53 | }
54 |
55 | fun handleAction(action: CalcAction) {
56 | _previewShowErrors.update { false }
57 |
58 | when (action) {
59 | is CalcAction.GetResult -> {
60 | if (evaluatedCalculation.isErrorMessage()) {
61 | _previewShowErrors.update { true }
62 | } else {
63 | textFieldState.setTextAndPlaceCursorAtEnd(evaluatedCalculation)
64 | }
65 | }
66 |
67 | is CalcAction.AddToField -> textFieldState.insertText(action.value)
68 | is CalcAction.ResetField -> textFieldState.clearText()
69 | is CalcAction.Backspace -> textFieldState.backspace()
70 | }
71 | }
72 |
73 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | CuteCalc
3 | CuteCalc is a cute and elegant calculator app for Android !
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | ---
28 |
👀 Overview
29 |
30 | - Very lightweight (~1.2 Mb APK size) !
31 | - No permissions needed !
32 | - Material 3 Expressive Design !
33 | - Very fast and feature-rich !
34 |
35 | ---
36 | 🤔 Why ?
37 |
38 | I am 15 y/o and have been into computers ever since I was ~10, growing up, I always thought about how software could do anything someone could dream of, so I started learning multiple languages until stepping upon Kotlin. Since then, I've learnt and started to build Android apps, and CuteCalc is my first project upon, I hope, alot more.
39 |
40 | ---
41 | 💬 Contact Me
42 |
43 | [Discord server](https://discord.gg/c6aCu4yjbu)
44 |
45 | [Email](sosauce_dev@protonmail.com)
46 |
47 | ---
48 | ❤️ Support
49 |
50 | If you wish to support me, you can see how to do so on [my website](https://sosauce.github.io/support/)
51 |
52 | ---
53 |
54 | ⚠️ Copyright
55 |
56 | Copyright (c)2025 sosauce
57 |
58 | This program is free software: you can redistribute it and/or modify
59 | it under the terms of the GNU General Public License as published by
60 | the Free Software Foundation, either version 3 of the License, or
61 | (at your option) any later version.
62 |
63 | This program is distributed in the hope that it will be useful,
64 | but WITHOUT ANY WARRANTY; without even the implied warranty of
65 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
66 | GNU General Public License for more details.
67 |
68 | The above copyright notice, this permission notice, and its license shall be included in all copies or substantial portions of the Software.
69 |
70 | You can find a copy of the GNU General Public License v3 [here](https://www.gnu.org/licenses/)
71 | ---
72 | #### You can find the SHA-256 [here](https://sosauce.github.io/projects/)
73 |
74 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2 |
3 | plugins {
4 | alias(libs.plugins.androidApplication)
5 | alias(libs.plugins.kotlin)
6 | alias(libs.plugins.compose.compiler)
7 | alias(libs.plugins.ksp)
8 | }
9 |
10 |
11 |
12 | android {
13 | namespace = "com.sosauce.cutecalc"
14 | compileSdk = 36
15 |
16 | defaultConfig {
17 |
18 | applicationId = "com.sosauce.cutecalc"
19 | minSdk = 23
20 | targetSdk = 36
21 | versionCode = 40001
22 | versionName = "3.6.4"
23 | ndk {
24 | //noinspection ChromeOsAbiSupport
25 | abiFilters += arrayOf("arm64-v8a", "armeabi-v7a")
26 | }
27 |
28 | }
29 |
30 | applicationVariants.all {
31 | val variant = this
32 | variant.outputs
33 | .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
34 | .forEach { output ->
35 | val outputFileName = "CC_${variant.versionName}.apk"
36 | output.outputFileName = outputFileName
37 | }
38 | }
39 |
40 | signingConfigs {
41 | create("release") {
42 | storeFile = file("release_key.jks")
43 | storePassword = System.getenv("SIGNING_STORE_PASSWORD")
44 | keyAlias = System.getenv("SIGNING_KEY_ALIAS")
45 | keyPassword = System.getenv("SIGNING_KEY_PASSWORD")
46 | }
47 | }
48 |
49 | buildTypes {
50 | release {
51 | isMinifyEnabled = true
52 | isShrinkResources = true
53 | isCrunchPngs = true
54 | signingConfig = signingConfigs.getByName("release")
55 | proguardFiles(
56 | getDefaultProguardFile("proguard-android-optimize.txt"),
57 | "proguard-rules.pro"
58 | )
59 | }
60 | }
61 |
62 | compileOptions {
63 | sourceCompatibility = JavaVersion.VERSION_17
64 | targetCompatibility = JavaVersion.VERSION_17
65 | }
66 |
67 | kotlin {
68 | compilerOptions {
69 | jvmTarget = JvmTarget.JVM_17
70 | }
71 | }
72 |
73 | buildFeatures {
74 | compose = true
75 | aidl = false
76 | renderScript = false
77 | shaders = false
78 | buildConfig = false
79 | resValues = false
80 | viewBinding = false
81 | }
82 |
83 | dependenciesInfo {
84 | includeInApk = false
85 | includeInBundle = false
86 | }
87 | }
88 |
89 | dependencies {
90 | implementation(platform(libs.androidx.compose.bom))
91 | implementation(libs.androidx.activity.compose)
92 | implementation(libs.androidx.lifecycle.viewmodel.compose)
93 | implementation(libs.androidx.core.splashscreen)
94 | implementation(libs.androidx.material3)
95 | implementation(libs.androidx.ui)
96 | implementation(libs.androidx.datastore.preferences)
97 | implementation(libs.keval)
98 | implementation(libs.androidx.room.ktx)
99 | implementation(libs.androidx.compose.material3)
100 | ksp(libs.androidx.room.compiler)
101 | }
102 |
--------------------------------------------------------------------------------
/app/src/main/res/values-fr-rFR/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Thème
4 | Paramètres
5 | Mode sombre
6 | Clair
7 | Mode amoled
8 | Version
9 | Mettre à jour
10 | CuteCalc par sosauce
11 | Suivre le système
12 | Police d\'écriture
13 | Système
14 | Animation des buttons
15 | Historique
16 | Utiliser l\'historique
17 | Il semblerait que l\'historique ne soit pas activé !
18 | Activé l\'historique
19 | Divers
20 | Vibrations
21 | Ascendant
22 | Descendant
23 | Format décimal
24 | Afficher le button effacer
25 | Vous pouvez toujours rester appuyer sur le button d\'effacement arrière pour effacer le champs de calcul.
26 | Retour
27 | Trier
28 | Supprimer
29 | Retour arrière
30 | Remettre dans le champ
31 | Copier dans le presse-papiers
32 | Plus d\'action
33 | Apparence
34 | Pourquoi pas calculer en style ?
35 | Défaut
36 | UI
37 | Souvenez-vous de tout !
38 | Éléments max sauvegarder dans l\'historique
39 | Pas de limite
40 | Autre paramètres qui ne méritaient pas leurs pages
41 | Formattage
42 | Montrer le button arrière
43 | Vider l\'historique
44 | Êtes-vous sûr de vouloir faire ceci ?
45 | Annuler
46 | Sauvegarder les erreurs dans l\'historique
47 | Supporter
48 | Précision décimale
49 | Ajuster le nombre de chiffres après la décimale
50 | Rendez les nombres beaux !
51 | Montrer l\'app sur l\'écran de vérouillage
52 | L\'app sera utilisable sur l\'écran de vérouillage.
53 |
--------------------------------------------------------------------------------
/app/src/main/res/values-es/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Tema
4 | AJustes
5 | Oscuro
6 | Claro
7 | Amoled
8 | Versión
9 | Buscar actualizaciones
10 | CuteCalc por sosauce
11 | Sistema
12 | Fuente
13 | Sistema
14 | Botones animados
15 | Historial
16 | Usar historial
17 | ¡Parece que el historial no está habilitado!
18 | Habilitar historial
19 | Otros
20 | Respuesta háptica
21 | Ascendente
22 | Descendente
23 | Formato decimal
24 | Mostrar botón borrar
25 | Aún puede mantener presionado el botón de retroceso para borrar el campo de entrada.
26 | Atras
27 | Ordenar
28 | Eliminar
29 | Retroceso
30 | Poner retroceso en el campo de entrada
31 | Copiar al portapapeles
32 | Más acciones
33 | Apariencia y estilo
34 | ¿Por qué no calcular con estilo?
35 | Predeterminada
36 | IU
37 | ¡Recordarlo todo!
38 | Máximo de elementos guardados en el historial
39 | Sin limite
40 | Otros ajustes que no merecen su propia página.
41 | Formato
42 | Mostrar botón de retroceso
43 | Limpiar historial
44 | ¿Realmente quieres hacer eso? esta acción no se puede revertir
45 | Cancelar
46 | Guardar errores en el historial
47 | Soporte
48 | Precisión decimal
49 | Ajustar el número de decimales.
50 | ¡Haz que los números se vean geniales!
51 | Mostrar app en la pantalla de bloqueo
52 | La aplicación seguirá siendo utilizable en la pantalla de bloqueo. Esto puede resultar útil, por ejemplo, en tiendas.
53 |
54 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Theme
4 | Settings
5 | Dark
6 | Light
7 | Amoled
8 | Version
9 | Check updates
10 | CuteCalc by sosauce
11 | System
12 | Font
13 | System
14 | Buttons animation
15 | History
16 | Use history
17 | It looks like history isn\'t enabled !
18 | Enable history
19 | Misc
20 | Haptic feedback
21 | Ascending
22 | Descending
23 | Decimal formatting
24 | Show clear button
25 | You can still long press the backspace button to clear the input field.
26 | Back
27 | Sort
28 | Delete
29 | Backspace
30 | Put back in input field
31 | Copy to clipboard
32 | More actions
33 | Look and feel
34 | Why not calculate in style ?
35 | Default
36 | UI
37 | Remember everything !
38 | Max saved items in history
39 | No limit
40 | Other settings that didn\'t deserve their own page.
41 | Formatting
42 | Show back button
43 | Clear history
44 | Are you sure you want to do that ? This action can\'t be undone !
45 | Cancel
46 | Save errors to history
47 | Support
48 | Decimal precision
49 | Adjust the number of decimal places.
50 | Make numbers look great !
51 | Show app on lock screen
52 | The app will still be usable on the lock screen. This can be useful for example, in stores.
53 | Newest first
54 | Oldest first
55 | No calculation found !
56 | Start calculating already !
57 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sosauce/cutecalc/ui/screens/calculator/components/CalculationDisplay.kt:
--------------------------------------------------------------------------------
1 | package com.sosauce.cutecalc.ui.screens.calculator.components
2 |
3 | import androidx.compose.foundation.horizontalScroll
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.rememberScrollState
7 | import androidx.compose.foundation.text.BasicTextField
8 | import androidx.compose.foundation.text.input.TextFieldLineLimits
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.LaunchedEffect
13 | import androidx.compose.runtime.getValue
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.graphics.SolidColor
16 | import androidx.compose.ui.text.font.FontWeight
17 | import androidx.compose.ui.text.style.TextAlign
18 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
19 | import com.sosauce.cutecalc.data.datastore.rememberDecimal
20 | import com.sosauce.cutecalc.data.datastore.rememberUseSystemFont
21 | import com.sosauce.cutecalc.ui.screens.calculator.CalculatorViewModel
22 | import com.sosauce.cutecalc.ui.theme.nunitoFontFamily
23 | import com.sosauce.cutecalc.utils.FormatTransformation
24 | import com.sosauce.cutecalc.utils.formatNumber
25 | import com.sosauce.cutecalc.utils.isErrorMessage
26 |
27 | @Composable
28 | fun CalculationDisplay(
29 | modifier: Modifier = Modifier,
30 | viewModel: CalculatorViewModel
31 | ) {
32 |
33 | val useSystemFont by rememberUseSystemFont()
34 | val shouldFormat by rememberDecimal()
35 | val scrollState = rememberScrollState()
36 | val previewScrollState = rememberScrollState()
37 | val previewCanShowErrors by viewModel.previewShowErrors.collectAsStateWithLifecycle()
38 |
39 |
40 | LaunchedEffect(viewModel.textFieldState.text) {
41 | scrollState.animateScrollTo(scrollState.maxValue)
42 | previewScrollState.animateScrollTo(previewScrollState.maxValue)
43 | }
44 |
45 |
46 | Column(modifier) {
47 | Text(
48 | text = viewModel.evaluatedCalculation
49 | .formatNumber(shouldFormat)
50 | .takeIf { !it.isErrorMessage() || previewCanShowErrors } ?: "",
51 | modifier = Modifier
52 | .fillMaxWidth()
53 | .horizontalScroll(previewScrollState),
54 | style = MaterialTheme.typography.displaySmall.copy(
55 | textAlign = TextAlign.End,
56 | fontWeight = FontWeight.SemiBold,
57 | color = if (!viewModel.evaluatedCalculation.isErrorMessage()) {
58 | MaterialTheme.colorScheme.onSurfaceVariant
59 | } else MaterialTheme.colorScheme.error
60 | )
61 | )
62 | DisableSoftKeyboard {
63 | BasicTextField(
64 | state = viewModel.textFieldState,
65 | lineLimits = TextFieldLineLimits.SingleLine,
66 | textStyle = MaterialTheme.typography.displayMedium.copy(
67 | textAlign = TextAlign.End,
68 | color = MaterialTheme.colorScheme.onBackground,
69 | fontFamily = if (!useSystemFont) nunitoFontFamily else null,
70 | fontWeight = FontWeight.ExtraBold
71 | ),
72 | modifier = Modifier.fillMaxWidth(),
73 | cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
74 | scrollState = scrollState,
75 | outputTransformation = if (shouldFormat) FormatTransformation else null
76 | )
77 | }
78 | }
79 |
80 |
81 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sosauce/cutecalc/ui/screens/settings/SettingsFormatting.kt:
--------------------------------------------------------------------------------
1 | package com.sosauce.cutecalc.ui.screens.settings
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.navigationBarsPadding
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.rememberScrollState
7 | import androidx.compose.foundation.verticalScroll
8 | import androidx.compose.material3.RadioButton
9 | import androidx.compose.material3.Scaffold
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.getValue
13 | import androidx.compose.runtime.setValue
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.unit.dp
17 | import androidx.compose.ui.util.fastForEach
18 | import com.sosauce.cutecalc.R
19 | import com.sosauce.cutecalc.data.datastore.rememberDecimal
20 | import com.sosauce.cutecalc.data.datastore.rememberDecimalPrecision
21 | import com.sosauce.cutecalc.ui.screens.settings.components.SettingsDropdownMenu
22 | import com.sosauce.cutecalc.ui.screens.settings.components.SettingsSwitch
23 | import com.sosauce.cutecalc.ui.screens.settings.components.SettingsWithTitle
24 | import com.sosauce.cutecalc.ui.shared_components.CuteDropdownMenuItem
25 | import com.sosauce.cutecalc.ui.shared_components.CuteNavigationButton
26 | import com.sosauce.cutecalc.utils.formatNumber
27 | import com.sosauce.cutecalc.utils.selfAlignHorizontally
28 |
29 | @Composable
30 | fun SettingsFormatting(onNavigateUp: () -> Unit) {
31 | val scrollState = rememberScrollState()
32 | var shouldFormat by rememberDecimal()
33 | var decimalPrecision by rememberDecimalPrecision()
34 | val decimalPrecisionOptions = MutableList(16) { it }.apply { add(1000) }
35 |
36 | Scaffold(
37 | bottomBar = {
38 | CuteNavigationButton(
39 | modifier = Modifier
40 | .padding(start = 15.dp)
41 | .navigationBarsPadding()
42 | .selfAlignHorizontally(Alignment.Start),
43 | onNavigateUp = onNavigateUp
44 | )
45 | }
46 | ) { pv ->
47 | Column(
48 | modifier = Modifier
49 | .verticalScroll(scrollState)
50 | .padding(pv)
51 | ) {
52 | SettingsWithTitle(
53 | title = R.string.formatting
54 | ) {
55 | SettingsSwitch(
56 | checked = shouldFormat,
57 | onCheckedChange = { shouldFormat = !shouldFormat },
58 | topDp = 24.dp,
59 | bottomDp = 4.dp,
60 | text = R.string.decimal_formatting
61 | )
62 | SettingsDropdownMenu(
63 | value = decimalPrecision.toLong(),
64 | topDp = 4.dp,
65 | bottomDp = 24.dp,
66 | text = R.string.decimal_precision,
67 | optionalDescription = R.string.decimal_precision_desc
68 | ) {
69 | decimalPrecisionOptions.fastForEach { number ->
70 | CuteDropdownMenuItem(
71 | onClick = { decimalPrecision = number },
72 | text = { Text(number.toString().formatNumber(shouldFormat)) },
73 | leadingIcon = {
74 | RadioButton(
75 | selected = number == decimalPrecision,
76 | onClick = null
77 | )
78 | }
79 | )
80 | }
81 | }
82 | }
83 | }
84 | }
85 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sosauce/cutecalc/ui/screens/history/components/HistoryActionButtons.kt:
--------------------------------------------------------------------------------
1 | package com.sosauce.cutecalc.ui.screens.history.components
2 |
3 | import androidx.compose.animation.AnimatedContent
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.shape.RoundedCornerShape
6 | import androidx.compose.material3.DropdownMenu
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.material3.IconButton
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.RadioButton
11 | import androidx.compose.material3.SmallFloatingActionButton
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.getValue
15 | import androidx.compose.runtime.mutableStateOf
16 | import androidx.compose.runtime.remember
17 | import androidx.compose.runtime.setValue
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.res.painterResource
20 | import androidx.compose.ui.res.stringResource
21 | import androidx.compose.ui.unit.dp
22 | import com.sosauce.cutecalc.R
23 | import com.sosauce.cutecalc.data.datastore.rememberHistoryNewestFirst
24 | import com.sosauce.cutecalc.ui.shared_components.CuteDropdownMenuItem
25 |
26 | @Composable
27 | fun HistoryActionButtons(
28 | modifier: Modifier = Modifier,
29 | onDeleteHistory: () -> Unit
30 | ) {
31 | var dropDownExpanded by remember { mutableStateOf(false) }
32 | var newestFirst by rememberHistoryNewestFirst()
33 |
34 | SmallFloatingActionButton(
35 | onClick = {},
36 | modifier = modifier,
37 | shape = RoundedCornerShape(14.dp),
38 | containerColor = MaterialTheme.colorScheme.surfaceContainer
39 | ) {
40 | Row {
41 | IconButton(
42 | onClick = { dropDownExpanded = true }
43 | ) {
44 | AnimatedContent(
45 | targetState = !dropDownExpanded
46 | ) {
47 | Icon(
48 | painter = if (it) painterResource(R.drawable.sort_rounded) else painterResource(
49 | R.drawable.close
50 | ),
51 | contentDescription = stringResource(R.string.sort)
52 | )
53 | }
54 | }
55 | IconButton(
56 | onClick = onDeleteHistory
57 | ) {
58 | Icon(
59 | painter = painterResource(R.drawable.trash_rounded),
60 | contentDescription = stringResource(R.string.delete),
61 | tint = MaterialTheme.colorScheme.error
62 | )
63 | }
64 |
65 | DropdownMenu(
66 | expanded = dropDownExpanded,
67 | onDismissRequest = { dropDownExpanded = false },
68 | shape = RoundedCornerShape(24.dp)
69 | ) {
70 | CuteDropdownMenuItem(
71 | onClick = { newestFirst = true },
72 | text = { Text(stringResource(R.string.newest_first)) },
73 | leadingIcon = {
74 | RadioButton(
75 | selected = newestFirst,
76 | onClick = null
77 | )
78 | }
79 | )
80 | CuteDropdownMenuItem(
81 | onClick = { newestFirst = false },
82 | text = { Text(stringResource(R.string.oldest_first)) },
83 | leadingIcon = {
84 | RadioButton(
85 | selected = !newestFirst,
86 | onClick = null
87 | )
88 | }
89 | )
90 | }
91 | }
92 | }
93 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sosauce/cutecalc/ui/screens/settings/components/AboutCard.kt:
--------------------------------------------------------------------------------
1 | package com.sosauce.cutecalc.ui.screens.settings.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.size
11 | import androidx.compose.foundation.layout.width
12 | import androidx.compose.foundation.shape.RoundedCornerShape
13 | import androidx.compose.material3.Button
14 | import androidx.compose.material3.Card
15 | import androidx.compose.material3.CardDefaults
16 | import androidx.compose.material3.Icon
17 | import androidx.compose.material3.MaterialTheme
18 | import androidx.compose.material3.Text
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.draw.clip
23 | import androidx.compose.ui.graphics.Color
24 | import androidx.compose.ui.platform.LocalContext
25 | import androidx.compose.ui.platform.LocalUriHandler
26 | import androidx.compose.ui.res.painterResource
27 | import androidx.compose.ui.res.stringResource
28 | import androidx.compose.ui.unit.dp
29 | import com.sosauce.cutecalc.R
30 | import com.sosauce.cutecalc.utils.GITHUB_RELEASES
31 | import com.sosauce.cutecalc.utils.SUPPORT_PAGE
32 |
33 | @Composable
34 | fun AboutCard() {
35 |
36 | val context = LocalContext.current
37 | val version = context.packageManager.getPackageInfo(context.packageName, 0).versionName
38 | val uriHandler = LocalUriHandler.current
39 |
40 | Card(
41 | colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainer),
42 | modifier = Modifier
43 | .fillMaxWidth()
44 | .padding(horizontal = 16.dp, vertical = 2.dp),
45 | shape = RoundedCornerShape(24.dp)
46 | ) {
47 | Row(verticalAlignment = Alignment.CenterVertically) {
48 | Box(
49 | modifier = Modifier
50 | .size(100.dp)
51 | .padding(15.dp)
52 | .clip(RoundedCornerShape(15))
53 | .background(Color(0xFFFAB3AA)),
54 | contentAlignment = Alignment.Center
55 | ) {
56 | Icon(
57 | painter = painterResource(R.drawable.calculator),
58 | contentDescription = null,
59 | modifier = Modifier.size(50.dp)
60 | )
61 | }
62 | Column {
63 | Text(stringResource(R.string.cc_by_sosauce))
64 | Text(
65 | text = "${stringResource(R.string.version)} $version",
66 | color = MaterialTheme.colorScheme.onSurfaceVariant
67 | )
68 | }
69 | }
70 | Row(
71 | modifier = Modifier.padding(8.dp)
72 | ) {
73 | Button(
74 | onClick = { uriHandler.openUri(GITHUB_RELEASES) },
75 | shape = RoundedCornerShape(
76 | topStart = 24.dp,
77 | bottomStart = 24.dp,
78 | topEnd = 4.dp,
79 | bottomEnd = 4.dp
80 | ),
81 | modifier = Modifier.weight(1f)
82 | ) { Text(stringResource(R.string.update)) }
83 | Spacer(Modifier.width(2.dp))
84 | Button(
85 | onClick = { uriHandler.openUri(SUPPORT_PAGE) },
86 | shape = RoundedCornerShape(
87 | topStart = 4.dp,
88 | bottomStart = 4.dp,
89 | topEnd = 24.dp,
90 | bottomEnd = 24.dp
91 | ),
92 | modifier = Modifier.weight(1f)
93 | ) { Text(stringResource(R.string.support)) }
94 | }
95 | }
96 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sosauce/cutecalc/data/datastore/DataStore.kt:
--------------------------------------------------------------------------------
1 | package com.sosauce.cutecalc.data.datastore
2 |
3 | import android.content.Context
4 | import androidx.compose.runtime.Composable
5 | import androidx.datastore.core.DataStore
6 | import androidx.datastore.preferences.core.Preferences
7 | import androidx.datastore.preferences.core.booleanPreferencesKey
8 | import androidx.datastore.preferences.core.intPreferencesKey
9 | import androidx.datastore.preferences.core.longPreferencesKey
10 | import androidx.datastore.preferences.core.stringPreferencesKey
11 | import androidx.datastore.preferences.preferencesDataStore
12 | import com.sosauce.cutecalc.utils.CuteTheme
13 |
14 |
15 | val Context.dataStore: DataStore by preferencesDataStore(name = "settings")
16 |
17 | data object PreferencesKeys {
18 | val THEME = stringPreferencesKey("theme")
19 | val BUTTON_VIBRATION_ENABLED = booleanPreferencesKey("button_vibration_enabled")
20 | val DECIMAL_FORMATTING = booleanPreferencesKey("decimal_formatting")
21 | val ENABLE_HISTORY = booleanPreferencesKey("enable_history")
22 | val HISTORY_MAX_ITEMS = longPreferencesKey("HISTORY_MAX_ITEMS")
23 | val SAVE_ERRORS_TO_HISTORY = booleanPreferencesKey("SAVE_ERRORS_TO_HISTORY")
24 | val USE_BUTTONS_ANIMATIONS = booleanPreferencesKey("use_buttons_animation")
25 | val USE_SYSTEM_FONT = booleanPreferencesKey("use_system_font")
26 | val SHOW_CLEAR_BUTTON = booleanPreferencesKey("show_clear_button")
27 | val DECIMAL_PRECISION = intPreferencesKey("DECIMAL_PRECISION")
28 | val SHOW_ON_LOCKSCREEN = booleanPreferencesKey("SHOW_ON_LOCKSCREEN")
29 | val HISTORY_NEWEST_FIRST = booleanPreferencesKey("HISTORY_NEWEST_FIRST")
30 | }
31 |
32 | @Composable
33 | fun rememberVibration() =
34 | rememberPreference(
35 | key = PreferencesKeys.BUTTON_VIBRATION_ENABLED,
36 | defaultValue = false
37 | )
38 |
39 | @Composable
40 | fun rememberAppTheme() =
41 | rememberPreference(
42 | key = PreferencesKeys.THEME,
43 | defaultValue = CuteTheme.SYSTEM
44 | )
45 |
46 | @Composable
47 | fun rememberDecimal() =
48 | rememberPreference(
49 | key = PreferencesKeys.DECIMAL_FORMATTING,
50 | defaultValue = false
51 | )
52 |
53 | @Composable
54 | fun rememberUseHistory() =
55 | rememberPreference(
56 | key = PreferencesKeys.ENABLE_HISTORY,
57 | defaultValue = true
58 | )
59 |
60 | @Composable
61 | fun rememberUseButtonsAnimation() =
62 | rememberPreference(
63 | key = PreferencesKeys.USE_BUTTONS_ANIMATIONS,
64 | defaultValue = true
65 | )
66 |
67 | @Composable
68 | fun rememberUseSystemFont() =
69 | rememberPreference(
70 | key = PreferencesKeys.USE_SYSTEM_FONT,
71 | defaultValue = false
72 | )
73 |
74 | @Composable
75 | fun rememberShowClearButton() =
76 | rememberPreference(
77 | key = PreferencesKeys.SHOW_CLEAR_BUTTON,
78 | defaultValue = true
79 | )
80 |
81 | @Composable
82 | fun rememberHistoryMaxItems() =
83 | rememberPreference(
84 | key = PreferencesKeys.HISTORY_MAX_ITEMS,
85 | defaultValue = Long.MAX_VALUE
86 | )
87 |
88 | @Composable
89 | fun rememberSaveErrorsToHistory() =
90 | rememberPreference(
91 | key = PreferencesKeys.SAVE_ERRORS_TO_HISTORY,
92 | defaultValue = false
93 | )
94 |
95 | @Composable
96 | fun rememberDecimalPrecision() =
97 | rememberPreference(
98 | key = PreferencesKeys.DECIMAL_PRECISION,
99 | defaultValue = 100
100 | )
101 |
102 | @Composable
103 | fun rememberShowOnLockScreen() =
104 | rememberPreference(
105 | key = PreferencesKeys.SHOW_ON_LOCKSCREEN,
106 | defaultValue = false
107 | )
108 |
109 | @Composable
110 | fun rememberHistoryNewestFirst() =
111 | rememberPreference(
112 | key = PreferencesKeys.HISTORY_NEWEST_FIRST,
113 | defaultValue = true
114 | )
115 |
116 | fun getDecimalPrecision(context: Context) = getPreference(
117 | key = PreferencesKeys.DECIMAL_PRECISION,
118 | defaultValue = 1000,
119 | context = context
120 | )
121 |
122 |
123 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sosauce/cutecalc/ui/screens/settings/SettingsHistory.kt:
--------------------------------------------------------------------------------
1 | package com.sosauce.cutecalc.ui.screens.settings
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.navigationBarsPadding
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.rememberScrollState
7 | import androidx.compose.foundation.verticalScroll
8 | import androidx.compose.material3.RadioButton
9 | import androidx.compose.material3.Scaffold
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.getValue
13 | import androidx.compose.runtime.setValue
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.res.stringResource
17 | import androidx.compose.ui.unit.dp
18 | import androidx.compose.ui.util.fastForEach
19 | import com.sosauce.cutecalc.R
20 | import com.sosauce.cutecalc.data.datastore.rememberHistoryMaxItems
21 | import com.sosauce.cutecalc.data.datastore.rememberSaveErrorsToHistory
22 | import com.sosauce.cutecalc.data.datastore.rememberUseHistory
23 | import com.sosauce.cutecalc.ui.screens.settings.components.SettingsDropdownMenu
24 | import com.sosauce.cutecalc.ui.screens.settings.components.SettingsSwitch
25 | import com.sosauce.cutecalc.ui.screens.settings.components.SettingsWithTitle
26 | import com.sosauce.cutecalc.ui.shared_components.CuteDropdownMenuItem
27 | import com.sosauce.cutecalc.ui.shared_components.CuteNavigationButton
28 | import com.sosauce.cutecalc.utils.selfAlignHorizontally
29 |
30 | @Composable
31 | fun SettingsHistory(
32 | onNavigateUp: () -> Unit,
33 | ) {
34 |
35 | val scrollState = rememberScrollState()
36 | var useHistory by rememberUseHistory()
37 | var historyMaxItems by rememberHistoryMaxItems()
38 | var saveErrorsToHistory by rememberSaveErrorsToHistory()
39 | val historyItemsChoice = listOf(
40 | 10,
41 | 20,
42 | 50,
43 | 100,
44 | 200,
45 | 500,
46 | 1000,
47 | 10000,
48 | Long.MAX_VALUE
49 | )
50 |
51 |
52 | Scaffold(
53 | bottomBar = {
54 | CuteNavigationButton(
55 | modifier = Modifier
56 | .padding(start = 15.dp)
57 | .navigationBarsPadding()
58 | .selfAlignHorizontally(Alignment.Start),
59 | onNavigateUp = onNavigateUp
60 | )
61 | }
62 | ) { pv ->
63 |
64 | Column(
65 | modifier = Modifier
66 | .verticalScroll(scrollState)
67 | .padding(pv)
68 | ) {
69 | SettingsWithTitle(
70 | title = R.string.history
71 | ) {
72 | SettingsSwitch(
73 | checked = useHistory,
74 | onCheckedChange = { useHistory = !useHistory },
75 | topDp = 24.dp,
76 | bottomDp = 4.dp,
77 | text = R.string.enable_history
78 | )
79 | SettingsSwitch(
80 | checked = saveErrorsToHistory,
81 | onCheckedChange = { saveErrorsToHistory = !saveErrorsToHistory },
82 | topDp = 4.dp,
83 | bottomDp = 4.dp,
84 | text = R.string.save_errors
85 | )
86 | SettingsDropdownMenu(
87 | value = historyMaxItems,
88 | topDp = 4.dp,
89 | bottomDp = 24.dp,
90 | text = R.string.max_history_items
91 | ) {
92 | historyItemsChoice.fastForEach {
93 | CuteDropdownMenuItem(
94 | onClick = { historyMaxItems = it },
95 | text = { Text(if (it == Long.MAX_VALUE) stringResource(R.string.no_limit) else it.toString()) },
96 | leadingIcon = {
97 | RadioButton(
98 | selected = historyMaxItems == it,
99 | onClick = null
100 | )
101 | }
102 | )
103 | }
104 | }
105 | }
106 | }
107 | }
108 | }
--------------------------------------------------------------------------------
/font_licence.txt:
--------------------------------------------------------------------------------
1 | Copyright 2014 The Nunito Project Authors (https://github.com/googlefonts/nunito)
2 |
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 | This license is copied below, and is also available with a FAQ at:
5 | http://scripts.sil.org/OFL
6 |
7 |
8 | -----------------------------------------------------------
9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 | -----------------------------------------------------------
11 |
12 | PREAMBLE
13 | The goals of the Open Font License (OFL) are to stimulate worldwide
14 | development of collaborative font projects, to support the font creation
15 | efforts of academic and linguistic communities, and to provide a free and
16 | open framework in which fonts may be shared and improved in partnership
17 | with others.
18 |
19 | The OFL allows the licensed fonts to be used, studied, modified and
20 | redistributed freely as long as they are not sold by themselves. The
21 | fonts, including any derivative works, can be bundled, embedded,
22 | redistributed and/or sold with any software provided that any reserved
23 | names are not used by derivative works. The fonts and derivatives,
24 | however, cannot be released under any other type of license. The
25 | requirement for fonts to remain under this license does not apply
26 | to any document created using the fonts or their derivatives.
27 |
28 | DEFINITIONS
29 | "Font Software" refers to the set of files released by the Copyright
30 | Holder(s) under this license and clearly marked as such. This may
31 | include source files, build scripts and documentation.
32 |
33 | "Reserved Font Name" refers to any names specified as such after the
34 | copyright statement(s).
35 |
36 | "Original Version" refers to the collection of Font Software components as
37 | distributed by the Copyright Holder(s).
38 |
39 | "Modified Version" refers to any derivative made by adding to, deleting,
40 | or substituting -- in part or in whole -- any of the components of the
41 | Original Version, by changing formats or by porting the Font Software to a
42 | new environment.
43 |
44 | "Author" refers to any designer, engineer, programmer, technical
45 | writer or other person who contributed to the Font Software.
46 |
47 | PERMISSION & CONDITIONS
48 | Permission is hereby granted, free of charge, to any person obtaining
49 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 | redistribute, and sell modified and unmodified copies of the Font
51 | Software, subject to the following conditions:
52 |
53 | 1) Neither the Font Software nor any of its individual components,
54 | in Original or Modified Versions, may be sold by itself.
55 |
56 | 2) Original or Modified Versions of the Font Software may be bundled,
57 | redistributed and/or sold with any software, provided that each copy
58 | contains the above copyright notice and this license. These can be
59 | included either as stand-alone text files, human-readable headers or
60 | in the appropriate machine-readable metadata fields within text or
61 | binary files as long as those fields can be easily viewed by the user.
62 |
63 | 3) No Modified Version of the Font Software may use the Reserved Font
64 | Name(s) unless explicit written permission is granted by the corresponding
65 | Copyright Holder. This restriction only applies to the primary font name as
66 | presented to the users.
67 |
68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 | Software shall not be used to promote, endorse or advertise any
70 | Modified Version, except to acknowledge the contribution(s) of the
71 | Copyright Holder(s) and the Author(s) or with their explicit written
72 | permission.
73 |
74 | 5) The Font Software, modified or unmodified, in part or in whole,
75 | must be distributed entirely under this license, and must not be
76 | distributed under any other license. The requirement for fonts to
77 | remain under this license does not apply to any document created
78 | using the Font Software.
79 |
80 | TERMINATION
81 | This license becomes null and void if any of the above conditions are
82 | not met.
83 |
84 | DISCLAIMER
85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 | OTHER DEALINGS IN THE FONT SOFTWARE.
94 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sosauce/cutecalc/ui/navigation/Navigation.kt:
--------------------------------------------------------------------------------
1 | package com.sosauce.cutecalc.ui.navigation
2 |
3 | import androidx.activity.compose.BackHandler
4 | import androidx.activity.compose.LocalActivity
5 | import androidx.compose.animation.AnimatedContent
6 | import androidx.compose.animation.fadeIn
7 | import androidx.compose.animation.fadeOut
8 | import androidx.compose.animation.togetherWith
9 | import androidx.compose.foundation.background
10 | import androidx.compose.foundation.pager.HorizontalPager
11 | import androidx.compose.foundation.pager.rememberPagerState
12 | import androidx.compose.material3.MaterialTheme
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.LaunchedEffect
15 | import androidx.compose.runtime.getValue
16 | import androidx.compose.runtime.mutableStateOf
17 | import androidx.compose.runtime.rememberCoroutineScope
18 | import androidx.compose.runtime.saveable.rememberSaveable
19 | import androidx.compose.runtime.setValue
20 | import androidx.compose.ui.Modifier
21 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
22 | import androidx.lifecycle.viewmodel.compose.viewModel
23 | import com.sosauce.cutecalc.data.actions.CalcAction
24 | import com.sosauce.cutecalc.ui.screens.calculator.CalculatorScreen
25 | import com.sosauce.cutecalc.ui.screens.calculator.CalculatorViewModel
26 | import com.sosauce.cutecalc.ui.screens.history.HistoryScreen
27 | import com.sosauce.cutecalc.ui.screens.history.HistoryViewModel
28 | import com.sosauce.cutecalc.ui.screens.settings.SettingsScreen
29 | import com.sosauce.cutecalc.utils.CalculatorViewModelFactory
30 | import com.sosauce.cutecalc.utils.HistoryViewModelFactory
31 | import kotlinx.coroutines.launch
32 |
33 | @Composable
34 | fun Nav() {
35 |
36 |
37 | val activity = LocalActivity.current!!
38 | val scope = rememberCoroutineScope()
39 | val viewModel =
40 | viewModel(factory = CalculatorViewModelFactory(activity.application))
41 | val historyViewModel =
42 | viewModel(factory = HistoryViewModelFactory(activity.application))
43 | var screenToDisplay by rememberSaveable { mutableStateOf(Screens.MAIN) }
44 |
45 | val pagerState = rememberPagerState { 2 }
46 |
47 | // Mimic back behavior from navigation
48 | BackHandler {
49 | if (screenToDisplay != Screens.MAIN) {
50 | screenToDisplay = Screens.MAIN
51 | } else {
52 | activity.moveTaskToBack(true)
53 | }
54 | }
55 |
56 | AnimatedContent(
57 | targetState = screenToDisplay,
58 | transitionSpec = { fadeIn() togetherWith fadeOut() },
59 | modifier = Modifier.background(MaterialTheme.colorScheme.background)
60 | ) { screen ->
61 | when (screen) {
62 | Screens.MAIN -> {
63 | HorizontalPager(
64 | state = pagerState
65 | ) { page ->
66 | when (page) {
67 | 0 -> {
68 | CalculatorScreen(
69 | viewModel = viewModel,
70 | onNavigate = { screenToDisplay = it },
71 | historyViewModel = historyViewModel,
72 | onScrollToHistory = {
73 | scope.launch {
74 | pagerState.animateScrollToPage(1)
75 | }
76 | }
77 | )
78 | }
79 |
80 | 1 -> {
81 |
82 | val calculations by historyViewModel.allCalculations.collectAsStateWithLifecycle()
83 |
84 |
85 | HistoryScreen(
86 | calculations = calculations,
87 | onEvents = historyViewModel::onEvent,
88 | onPutBackToField = { calculation ->
89 | viewModel.handleAction(CalcAction.ResetField)
90 | viewModel.handleAction(CalcAction.AddToField(calculation))
91 | },
92 | onScrollToMain = {
93 | scope.launch {
94 | pagerState.animateScrollToPage(0)
95 | }
96 | }
97 | )
98 | }
99 | }
100 | }
101 | }
102 |
103 | Screens.SETTINGS -> {
104 | SettingsScreen(
105 | onNavigate = { screenToDisplay = it }
106 | )
107 | }
108 | }
109 | }
110 |
111 | }
--------------------------------------------------------------------------------
/.kotlin/errors/errors-1728076536248.log:
--------------------------------------------------------------------------------
1 | kotlin version: 2.0.20
2 | error message: java.lang.IncompatibleClassChangeError: class com.google.devtools.ksp.common.PersistentMap cannot inherit from final class org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap
3 | at java.base/java.lang.ClassLoader.defineClass1(Native Method)
4 | at java.base/java.lang.ClassLoader.defineClass(Unknown Source)
5 | at java.base/java.security.SecureClassLoader.defineClass(Unknown Source)
6 | at java.base/java.net.URLClassLoader.defineClass(Unknown Source)
7 | at java.base/java.net.URLClassLoader$1.run(Unknown Source)
8 | at java.base/java.net.URLClassLoader$1.run(Unknown Source)
9 | at java.base/java.security.AccessController.doPrivileged(Unknown Source)
10 | at java.base/java.net.URLClassLoader.findClass(Unknown Source)
11 | at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
12 | at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
13 | at java.base/java.lang.ClassLoader.defineClass1(Native Method)
14 | at java.base/java.lang.ClassLoader.defineClass(Unknown Source)
15 | at java.base/java.security.SecureClassLoader.defineClass(Unknown Source)
16 | at java.base/java.net.URLClassLoader.defineClass(Unknown Source)
17 | at java.base/java.net.URLClassLoader$1.run(Unknown Source)
18 | at java.base/java.net.URLClassLoader$1.run(Unknown Source)
19 | at java.base/java.security.AccessController.doPrivileged(Unknown Source)
20 | at java.base/java.net.URLClassLoader.findClass(Unknown Source)
21 | at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
22 | at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
23 | at com.google.devtools.ksp.common.IncrementalContextBase.(IncrementalContextBase.kt:103)
24 | at com.google.devtools.ksp.IncrementalContext.(IncrementalContext.kt:64)
25 | at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension$doAnalysis$2.invoke(KotlinSymbolProcessingExtension.kt:192)
26 | at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension$doAnalysis$2.invoke(KotlinSymbolProcessingExtension.kt:189)
27 | at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.handleException(KotlinSymbolProcessingExtension.kt:414)
28 | at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.doAnalysis(KotlinSymbolProcessingExtension.kt:189)
29 | at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(TopDownAnalyzerFacadeForJVM.kt:112)
30 | at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(TopDownAnalyzerFacadeForJVM.kt:75)
31 | at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze$lambda$12(KotlinToJVMBytecodeCompiler.kt:373)
32 | at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport.analyzeAndReport(AnalyzerWithCompilerReport.kt:112)
33 | at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze(KotlinToJVMBytecodeCompiler.kt:364)
34 | at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.runFrontendAndGenerateIrUsingClassicFrontend(KotlinToJVMBytecodeCompiler.kt:195)
35 | at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules$cli(KotlinToJVMBytecodeCompiler.kt:106)
36 | at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:170)
37 | at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
38 | at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
39 | at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
40 | at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
41 | at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1555)
42 | at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
43 | at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
44 | at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
45 | at java.base/java.lang.reflect.Method.invoke(Unknown Source)
46 | at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
47 | at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
48 | at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
49 | at java.base/java.security.AccessController.doPrivileged(Unknown Source)
50 | at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
51 | at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
52 | at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
53 | at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
54 | at java.base/java.security.AccessController.doPrivileged(Unknown Source)
55 | at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
56 | at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
57 | at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
58 | at java.base/java.lang.Thread.run(Unknown Source)
59 |
60 |
61 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sosauce/cutecalc/ui/screens/calculator/components/CuteButton.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
2 |
3 | package com.sosauce.cutecalc.ui.screens.calculator.components
4 |
5 | import androidx.compose.animation.core.animateIntAsState
6 | import androidx.compose.foundation.background
7 | import androidx.compose.foundation.combinedClickable
8 | import androidx.compose.foundation.interaction.MutableInteractionSource
9 | import androidx.compose.foundation.interaction.collectIsPressedAsState
10 | import androidx.compose.foundation.layout.Box
11 | import androidx.compose.foundation.layout.RowScope
12 | import androidx.compose.foundation.layout.aspectRatio
13 | import androidx.compose.foundation.layout.defaultMinSize
14 | import androidx.compose.foundation.layout.size
15 | import androidx.compose.foundation.shape.RoundedCornerShape
16 | import androidx.compose.material3.ButtonDefaults
17 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
18 | import androidx.compose.material3.Icon
19 | import androidx.compose.material3.MaterialTheme
20 | import androidx.compose.material3.Text
21 | import androidx.compose.material3.contentColorFor
22 | import androidx.compose.material3.ripple
23 | import androidx.compose.runtime.Composable
24 | import androidx.compose.runtime.getValue
25 | import androidx.compose.runtime.remember
26 | import androidx.compose.ui.Alignment
27 | import androidx.compose.ui.Modifier
28 | import androidx.compose.ui.draw.clip
29 | import androidx.compose.ui.graphics.Color
30 | import androidx.compose.ui.hapticfeedback.HapticFeedbackType
31 | import androidx.compose.ui.platform.LocalHapticFeedback
32 | import androidx.compose.ui.res.painterResource
33 | import androidx.compose.ui.res.stringResource
34 | import androidx.compose.ui.semantics.Role
35 | import androidx.compose.ui.semantics.role
36 | import androidx.compose.ui.semantics.semantics
37 | import androidx.compose.ui.unit.dp
38 | import com.sosauce.cutecalc.R
39 | import com.sosauce.cutecalc.data.datastore.rememberUseButtonsAnimation
40 | import com.sosauce.cutecalc.data.datastore.rememberVibration
41 | import com.sosauce.cutecalc.utils.BACKSPACE
42 | import com.sosauce.cutecalc.utils.thenIf
43 |
44 | @Composable
45 | fun RowScope.CuteButton(
46 | modifier: Modifier = Modifier,
47 | text: String,
48 | backgroundColor: Color,
49 | onClick: () -> Unit,
50 | onLongClick: (() -> Unit)? = null,
51 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
52 | roundButton: Boolean = true
53 | ) {
54 | val haptic = LocalHapticFeedback.current
55 | val shouldVibrate by rememberVibration()
56 | val useButtonsAnimation by rememberUseButtonsAnimation()
57 | val isPressed by interactionSource.collectIsPressedAsState()
58 | val cornerRadius by animateIntAsState(
59 | targetValue = if (isPressed && useButtonsAnimation) 24 else 50
60 | )
61 |
62 |
63 |
64 | Box(
65 | modifier = modifier
66 | .semantics { role = Role.Button }
67 | .defaultMinSize(
68 | minWidth = ButtonDefaults.MinWidth,
69 | minHeight = ButtonDefaults.MinHeight
70 | )
71 | .clip(RoundedCornerShape(cornerRadius))
72 | .background(backgroundColor)
73 | .combinedClickable(
74 | interactionSource = interactionSource,
75 | indication = ripple(),
76 | onClick = {
77 | onClick()
78 | if (shouldVibrate) haptic.performHapticFeedback(HapticFeedbackType.Confirm)
79 | },
80 | onLongClick = {
81 | onLongClick?.invoke()
82 | if (shouldVibrate) haptic.performHapticFeedback(HapticFeedbackType.Confirm)
83 | }
84 | )
85 | .weight(1f)
86 | .thenIf(roundButton) {
87 | aspectRatio(1f)
88 | },
89 | contentAlignment = Alignment.Center
90 | ) {
91 | if (text == BACKSPACE) {
92 | Icon(
93 | painter = painterResource(R.drawable.backspace_filled),
94 | contentDescription = stringResource(R.string.back),
95 | tint = MaterialTheme.colorScheme.contentColorFor(backgroundColor),
96 | modifier = Modifier.size(48.dp)
97 | )
98 | } else {
99 | Text(
100 | text = text,
101 | color = contentColorFor(backgroundColor),
102 | style = MaterialTheme.typography.displaySmall
103 | )
104 | }
105 | }
106 |
107 |
108 | // Button(
109 | // onClick = {
110 | // onClick()
111 | // if (shouldVibrate) haptic.performHapticFeedback(HapticFeedbackType.Confirm)
112 | // },
113 | // colors = color,
114 | // modifier = modifier,
115 | // shape = RoundedCornerShape(cornerRadius),
116 | // interactionSource = interactionSource,
117 | // enabled = enabled
118 | // ) {
119 | // CuteText(
120 | // text = text,
121 | // color = textColor,
122 | // fontSize = 35.sp
123 | // )
124 | // }
125 | }
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sosauce/cutecalc/data/calculator/Evaluator.kt:
--------------------------------------------------------------------------------
1 | package com.sosauce.cutecalc.data.calculator
2 |
3 | import com.notkamui.keval.Keval
4 | import com.notkamui.keval.KevalInvalidArgumentException
5 | import com.notkamui.keval.KevalZeroDivisionException
6 | import java.math.RoundingMode
7 | import kotlin.math.floor
8 | import kotlin.math.pow
9 | import kotlin.math.sqrt
10 |
11 | class NegativeSquareRootException : RuntimeException("Negative square root")
12 | class ValueTooLargeException : RuntimeException("Value too large")
13 | object Evaluator {
14 |
15 | private val KEVAL = Keval.create {
16 | binaryOperator {
17 | symbol = '+'
18 | precedence = 2
19 | isLeftAssociative = true
20 | implementation = { a, b -> a + b }
21 | }
22 | unaryOperator {
23 | symbol = '+'
24 | isPrefix = true
25 | implementation = { it }
26 | }
27 | binaryOperator {
28 | symbol = '-'
29 | precedence = 2
30 | isLeftAssociative = true
31 | implementation = { a, b -> a - b }
32 | }
33 | unaryOperator {
34 | symbol = '-'
35 | isPrefix = true
36 | implementation = { -it }
37 | }
38 |
39 | binaryOperator {
40 | symbol = '×'
41 | precedence = 3
42 | isLeftAssociative = true
43 | implementation = { a, b -> a * b }
44 | }
45 |
46 | binaryOperator {
47 | symbol = '/'
48 | precedence = 3
49 | isLeftAssociative = true
50 | implementation = { a, b ->
51 | if (b == 0.0) throw KevalZeroDivisionException()
52 | a / b
53 | }
54 | }
55 |
56 | binaryOperator {
57 | symbol = '^'
58 | precedence = 4
59 | isLeftAssociative = false
60 | implementation = { a, b -> a.pow(b) }
61 | }
62 |
63 | unaryOperator {
64 | symbol = '!'
65 | isPrefix = false
66 | implementation = {
67 | if (it < 0) throw KevalInvalidArgumentException("Factorial of a negative number")
68 | if (floor(it) != it) throw KevalInvalidArgumentException("Factorial of a non-integer")
69 | (1..it.toInt()).fold(1.0) { acc, i -> acc * i }
70 | }
71 | }
72 |
73 | unaryOperator {
74 | symbol = '√'
75 | isPrefix = true
76 | implementation =
77 | { arg -> if (arg < 0) throw NegativeSquareRootException() else sqrt(arg) }
78 | }
79 |
80 | unaryOperator {
81 | symbol = '%'
82 | isPrefix = false
83 | implementation = { arg -> arg / 100 }
84 | }
85 |
86 | constant {
87 | name = "PI"
88 | value = Math.PI
89 | }
90 |
91 | }
92 |
93 | // Storing the previous result to show previous output even though expression is not complete
94 | @JvmStatic
95 | private var prevResult: String = ""
96 |
97 |
98 | @JvmStatic
99 | fun eval(
100 | formula: String,
101 | precision: Int
102 | ): String = try {
103 | val result = KEVAL
104 | .eval(formula.replace("π", "PI").handleRelativePercentage())
105 |
106 | val formattedResult = if (result > Double.MAX_VALUE) {
107 | throw ValueTooLargeException()
108 | } else {
109 | result
110 | .toBigDecimal()
111 | .setScale(precision, RoundingMode.HALF_UP)
112 | .stripTrailingZeros()
113 | .toPlainString()
114 | }
115 | prevResult = formattedResult
116 | formattedResult
117 | } catch (e: Exception) {
118 |
119 | if (e.message?.startsWith("Invalid expression at position") ?: false) {
120 | prevResult
121 | } else {
122 | e.message ?: "Undetermined error"
123 | }
124 | }
125 |
126 | // We don't call "handleRelativePercentage" here to avoid recursive call
127 | @JvmStatic
128 | private fun evalParenthesis(formula: String): String {
129 | val result = KEVAL.eval(formula)
130 | return if (result > Double.MAX_VALUE) {
131 | throw ValueTooLargeException()
132 | } else {
133 | result.toBigDecimal().stripTrailingZeros().toPlainString()
134 | }
135 | }
136 |
137 | private fun String.handleRelativePercentage(): String {
138 | val regex = Regex("""(\d+(?:\.\d+)?)\s*([+\-*])\s*(\d+(?:\.\d+)?)%""")
139 |
140 | return regex.replace(this.processParenthesisExpression()) { match ->
141 | val firstOperand = match.groupValues[1].toDouble()
142 | val operator = match.groupValues[2]
143 | val percentage = match.groupValues[3].toDouble()
144 |
145 | when (operator) {
146 | "+" -> "$firstOperand + ($firstOperand * $percentage / 100)"
147 | "-" -> "$firstOperand - ($firstOperand * $percentage / 100)"
148 | "*" -> "$firstOperand * ($percentage / 100)"
149 | else -> "$firstOperand"
150 | }
151 |
152 | }
153 |
154 | }
155 |
156 | private fun String.processParenthesisExpression(): String {
157 | val parenthesisRegex = Regex("""\(([^()]+)\)""")
158 | var expression = this
159 |
160 |
161 | parenthesisRegex.findAll(this).forEach { matchResult ->
162 | val calculated = evalParenthesis(matchResult.value)
163 | val replaceWith = if (this.contains("%")) calculated else "($calculated)"
164 | expression = expression.replace(matchResult.value, replaceWith)
165 | }
166 | return expression
167 | }
168 |
169 |
170 | }
171 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sosauce/cutecalc/ui/screens/settings/components/SettingsSwitch.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
2 |
3 | package com.sosauce.cutecalc.ui.screens.settings.components
4 |
5 | import androidx.compose.animation.AnimatedContent
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.ColumnScope
9 | import androidx.compose.foundation.layout.Row
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.shape.RoundedCornerShape
12 | import androidx.compose.material3.Card
13 | import androidx.compose.material3.CardDefaults
14 | import androidx.compose.material3.DropdownMenu
15 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
16 | import androidx.compose.material3.MaterialTheme
17 | import androidx.compose.material3.Switch
18 | import androidx.compose.material3.SwitchDefaults
19 | import androidx.compose.material3.Text
20 | import androidx.compose.material3.TextButton
21 | import androidx.compose.runtime.Composable
22 | import androidx.compose.runtime.getValue
23 | import androidx.compose.runtime.mutableStateOf
24 | import androidx.compose.runtime.remember
25 | import androidx.compose.runtime.setValue
26 | import androidx.compose.ui.Alignment
27 | import androidx.compose.ui.Modifier
28 | import androidx.compose.ui.graphics.Color
29 | import androidx.compose.ui.res.stringResource
30 | import androidx.compose.ui.unit.Dp
31 | import androidx.compose.ui.unit.dp
32 | import androidx.compose.ui.unit.sp
33 | import com.sosauce.cutecalc.R
34 | import com.sosauce.cutecalc.data.datastore.rememberDecimal
35 |
36 |
37 | @Composable
38 | fun SettingsSwitch(
39 | checked: Boolean,
40 | onCheckedChange: () -> Unit,
41 | topDp: Dp,
42 | bottomDp: Dp,
43 | text: Int,
44 | optionalDescription: Int? = null
45 | ) {
46 | Card(
47 | colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainer),
48 | modifier = Modifier
49 | .padding(horizontal = 16.dp, vertical = 2.dp),
50 | shape = RoundedCornerShape(
51 | topStart = topDp,
52 | topEnd = topDp,
53 | bottomStart = bottomDp,
54 | bottomEnd = bottomDp
55 | )
56 | ) {
57 | Row(
58 | verticalAlignment = Alignment.CenterVertically,
59 | horizontalArrangement = Arrangement.SpaceBetween,
60 | modifier = Modifier
61 | .padding(15.dp)
62 | ) {
63 | Row(
64 | verticalAlignment = Alignment.CenterVertically,
65 | modifier = Modifier
66 | .weight(1f)
67 | ) {
68 | Column {
69 | Text(stringResource(text))
70 | optionalDescription?.let {
71 | Text(
72 | text = stringResource(it),
73 | color = MaterialTheme.colorScheme.onSurfaceVariant,
74 | fontSize = 12.sp
75 | )
76 | }
77 | }
78 | }
79 | Switch(
80 | checked = checked,
81 | onCheckedChange = { onCheckedChange() },
82 | colors = SwitchDefaults.colors(
83 | uncheckedBorderColor = Color.Transparent
84 | )
85 | )
86 | }
87 | }
88 | }
89 |
90 | @Composable
91 | fun SettingsDropdownMenu(
92 | value: Long,
93 | topDp: Dp,
94 | bottomDp: Dp,
95 | text: Int,
96 | optionalDescription: Int? = null,
97 | dropdownContent: @Composable (ColumnScope.() -> Unit)
98 | ) {
99 |
100 | var expanded by remember { mutableStateOf(false) }
101 | val useDecimalFormatting by rememberDecimal()
102 |
103 |
104 | Card(
105 | colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainer),
106 | modifier = Modifier
107 | .padding(horizontal = 16.dp, vertical = 2.dp),
108 | shape = RoundedCornerShape(
109 | topStart = topDp,
110 | topEnd = topDp,
111 | bottomStart = bottomDp,
112 | bottomEnd = bottomDp
113 | )
114 | ) {
115 | Row(
116 | verticalAlignment = Alignment.CenterVertically,
117 | horizontalArrangement = Arrangement.SpaceBetween,
118 | modifier = Modifier
119 | .padding(15.dp)
120 | ) {
121 | Row(
122 | verticalAlignment = Alignment.CenterVertically,
123 | modifier = Modifier
124 | .weight(1f)
125 | ) {
126 | Column {
127 | Text(stringResource(text))
128 | optionalDescription?.let {
129 | Text(
130 | text = stringResource(it),
131 | color = MaterialTheme.colorScheme.onSurfaceVariant,
132 | fontSize = 12.sp
133 | )
134 | }
135 | }
136 | }
137 | TextButton(
138 | onClick = { expanded = true }
139 | ) {
140 | AnimatedContent(
141 | targetState = value
142 | ) {
143 | Text(
144 | text = if (it == Long.MAX_VALUE) stringResource(R.string.no_limit) else it.toString(),
145 | color = MaterialTheme.colorScheme.onSurfaceVariant,
146 | fontSize = 15.sp
147 | )
148 | }
149 |
150 | DropdownMenu(
151 | expanded = expanded,
152 | onDismissRequest = { expanded = false },
153 | shape = RoundedCornerShape(24.dp)
154 | ) { dropdownContent() }
155 | }
156 | }
157 | }
158 | }
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sosauce/cutecalc/ui/screens/settings/SettingsScreen.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalUuidApi::class)
2 |
3 | package com.sosauce.cutecalc.ui.screens.settings
4 |
5 | import androidx.activity.compose.BackHandler
6 | import androidx.compose.animation.AnimatedContent
7 | import androidx.compose.animation.fadeIn
8 | import androidx.compose.animation.fadeOut
9 | import androidx.compose.animation.togetherWith
10 | import androidx.compose.foundation.layout.Spacer
11 | import androidx.compose.foundation.layout.fillMaxSize
12 | import androidx.compose.foundation.layout.height
13 | import androidx.compose.foundation.layout.navigationBarsPadding
14 | import androidx.compose.foundation.layout.padding
15 | import androidx.compose.foundation.lazy.LazyColumn
16 | import androidx.compose.foundation.lazy.itemsIndexed
17 | import androidx.compose.foundation.lazy.rememberLazyListState
18 | import androidx.compose.material3.Scaffold
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.runtime.Immutable
21 | import androidx.compose.runtime.getValue
22 | import androidx.compose.runtime.mutableStateOf
23 | import androidx.compose.runtime.saveable.rememberSaveable
24 | import androidx.compose.runtime.setValue
25 | import androidx.compose.ui.Alignment
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.unit.dp
28 | import com.sosauce.cutecalc.R
29 | import com.sosauce.cutecalc.ui.navigation.Screens
30 | import com.sosauce.cutecalc.ui.navigation.SettingsScreen
31 | import com.sosauce.cutecalc.ui.screens.settings.components.AboutCard
32 | import com.sosauce.cutecalc.ui.screens.settings.components.SettingsCategoryCard
33 | import com.sosauce.cutecalc.ui.shared_components.CuteNavigationButton
34 | import com.sosauce.cutecalc.utils.selfAlignHorizontally
35 | import kotlin.uuid.ExperimentalUuidApi
36 | import kotlin.uuid.Uuid
37 |
38 | @Composable
39 | fun SettingsScreen(
40 | onNavigate: (Screens) -> Unit
41 | ) {
42 |
43 | var screenToDisplay by rememberSaveable { mutableStateOf(SettingsScreen.SETTINGS) }
44 |
45 |
46 | // Mimic back behavior from navigation
47 | BackHandler {
48 | if (screenToDisplay != SettingsScreen.SETTINGS) {
49 | screenToDisplay = SettingsScreen.SETTINGS
50 | } else {
51 | onNavigate(Screens.MAIN)
52 | }
53 | }
54 |
55 | AnimatedContent(
56 | targetState = screenToDisplay,
57 | transitionSpec = { fadeIn() togetherWith fadeOut() }
58 | ) { screen ->
59 | when (screen) {
60 | SettingsScreen.SETTINGS -> {
61 | SettingsPage(
62 | onNavigate = onNavigate,
63 | onNavigateSettings = { screenToDisplay = it }
64 | )
65 | }
66 |
67 | SettingsScreen.LOOK_AND_FEEL -> {
68 | SettingsLookAndFeel(
69 | onNavigateUp = { screenToDisplay = SettingsScreen.SETTINGS }
70 | )
71 | }
72 |
73 | SettingsScreen.HISTORY -> {
74 | SettingsHistory(
75 | onNavigateUp = { screenToDisplay = SettingsScreen.SETTINGS }
76 | )
77 | }
78 |
79 | SettingsScreen.FORMATTING -> {
80 | SettingsFormatting(
81 | onNavigateUp = { screenToDisplay = SettingsScreen.SETTINGS }
82 | )
83 | }
84 |
85 | SettingsScreen.MISC -> {
86 | SettingsMisc(
87 | onNavigateUp = { screenToDisplay = SettingsScreen.SETTINGS }
88 | )
89 | }
90 | }
91 | }
92 | }
93 |
94 | @Composable
95 | private fun SettingsPage(
96 | onNavigate: (Screens) -> Unit,
97 | onNavigateSettings: (SettingsScreen) -> Unit
98 | ) {
99 | val listState = rememberLazyListState()
100 | val settingsCategories = arrayOf(
101 | SettingsCategory(
102 | name = R.string.look_and_feel,
103 | description = R.string.look_and_feel_desc,
104 | icon = R.drawable.palette,
105 | onNavigate = { onNavigateSettings(SettingsScreen.LOOK_AND_FEEL) }
106 | ),
107 | SettingsCategory(
108 | name = R.string.history,
109 | description = R.string.history_desc,
110 | icon = R.drawable.history_rounded,
111 | onNavigate = { onNavigateSettings(SettingsScreen.HISTORY) }
112 | ),
113 | SettingsCategory(
114 | name = R.string.formatting,
115 | description = R.string.formatting_desc,
116 | icon = R.drawable.formatting,
117 | onNavigate = { onNavigateSettings(SettingsScreen.FORMATTING) }
118 | ),
119 | SettingsCategory(
120 | name = R.string.misc,
121 | description = R.string.misc_desc,
122 | icon = R.drawable.more_horiz,
123 | onNavigate = { onNavigateSettings(SettingsScreen.MISC) }
124 | )
125 | )
126 |
127 | Scaffold(
128 | bottomBar = {
129 | CuteNavigationButton(
130 | modifier = Modifier
131 | .padding(start = 15.dp)
132 | .navigationBarsPadding()
133 | .selfAlignHorizontally(Alignment.Start),
134 | onNavigateUp = { onNavigate(Screens.MAIN) }
135 | )
136 | }
137 | ) { pv ->
138 | LazyColumn(
139 | modifier = Modifier.fillMaxSize(),
140 | horizontalAlignment = Alignment.CenterHorizontally,
141 | contentPadding = pv,
142 | state = listState
143 | ) {
144 | item {
145 | AboutCard()
146 | Spacer(Modifier.height(20.dp))
147 | }
148 | itemsIndexed(
149 | items = settingsCategories,
150 | key = { _, category -> category.id }
151 | ) { index, category ->
152 | SettingsCategoryCard(
153 | icon = category.icon,
154 | name = category.name,
155 | description = category.description,
156 | topDp = if (index == 0) 24.dp else 4.dp,
157 | bottomDp = if (index == settingsCategories.lastIndex) 24.dp else 4.dp,
158 | onNavigate = category.onNavigate
159 | )
160 | }
161 | }
162 | }
163 | }
164 |
165 | @Immutable
166 | private data class SettingsCategory(
167 | val id: String = Uuid.random().toString(),
168 | val name: Int,
169 | val description: Int,
170 | val icon: Int,
171 | val onNavigate: () -> Unit
172 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/sosauce/cutecalc/utils/Extensions.kt:
--------------------------------------------------------------------------------
1 | package com.sosauce.cutecalc.utils
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import android.view.WindowManager
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.wrapContentWidth
8 | import androidx.compose.foundation.lazy.LazyListState
9 | import androidx.compose.foundation.text.input.OutputTransformation
10 | import androidx.compose.foundation.text.input.TextFieldBuffer
11 | import androidx.compose.foundation.text.input.TextFieldState
12 | import androidx.compose.foundation.text.input.delete
13 | import androidx.compose.foundation.text.input.insert
14 | import androidx.compose.material3.ColorScheme
15 | import androidx.compose.material3.darkColorScheme
16 | import androidx.compose.material3.dynamicDarkColorScheme
17 | import androidx.compose.material3.dynamicLightColorScheme
18 | import androidx.compose.material3.lightColorScheme
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.platform.LocalContext
23 | import com.sosauce.cutecalc.domain.model.Calculation
24 | import java.text.DecimalFormatSymbols
25 |
26 | fun Modifier.thenIf(
27 | condition: Boolean,
28 | modifier: Modifier.() -> Modifier
29 | ): Modifier {
30 | return if (condition) {
31 | this.then(modifier())
32 | } else this
33 | }
34 |
35 |
36 | fun List.sort(
37 | newestFirst: Boolean
38 | ): List {
39 | return if (newestFirst) {
40 | this.sortedByDescending { it.id }
41 | } else {
42 | this
43 | }
44 | }
45 |
46 | @Composable
47 | fun anyLightColorScheme(): ColorScheme {
48 | val context = LocalContext.current
49 |
50 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
51 | dynamicLightColorScheme(context)
52 | } else {
53 | lightColorScheme()
54 | }
55 | }
56 |
57 | @Composable
58 | fun anyDarkColorScheme(): ColorScheme {
59 | val context = LocalContext.current
60 |
61 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
62 | dynamicDarkColorScheme(context)
63 | } else {
64 | darkColorScheme()
65 | }
66 | }
67 |
68 | val LazyListState.showBottomBar
69 | get() =
70 | if (layoutInfo.totalItemsCount == 0) {
71 | true
72 | } else if (
73 | layoutInfo.visibleItemsInfo.firstOrNull()?.index == 0 &&
74 | layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1
75 | ) {
76 | true
77 | } else {
78 | layoutInfo.visibleItemsInfo.lastOrNull()?.index != layoutInfo.totalItemsCount - 1
79 | }
80 |
81 | fun TextFieldState.insertText(text: String) {
82 |
83 | val textAsChar =
84 | text.first() // We're not mean to be able to input more than one char at once anyways
85 | val expression = this.text
86 | val cursorPosition = selection.start
87 | val charInfrontCursor = expression.getOrNull(cursorPosition - 1) ?: ' '
88 | val charBehindCursor = expression.getOrNull(cursorPosition) ?: ' '
89 |
90 |
91 | when {
92 | expression.isEmpty() -> {
93 | if (textAsChar.canStartExpression()) {
94 | edit { insert(cursorPosition, text) }
95 | }
96 | }
97 |
98 | textAsChar.isOperator() -> {
99 | if (charInfrontCursor.isChainable() && charBehindCursor.isChainable()) {
100 | edit { insert(cursorPosition, text) }
101 | }
102 | }
103 |
104 | else -> edit { insert(cursorPosition, text) }
105 | }
106 | }
107 |
108 | fun TextFieldState.backspace() {
109 | val cursorPosition = selection.start
110 | if (selection.collapsed && cursorPosition > 0) {
111 | edit {
112 | delete(cursorPosition - 1, cursorPosition)
113 | }
114 | }
115 | }
116 |
117 |
118 | fun String.isErrorMessage(): Boolean {
119 | return any { char -> char.isLetter() }
120 | }
121 |
122 | fun String.whichParenthesis(): String {
123 | return if (count { it == '(' } > count { it == ')' }) {
124 | ")"
125 | } else {
126 | "("
127 | }
128 | }
129 |
130 | fun Char.isChainable(): Boolean {
131 |
132 | val unchainableOperators = listOf('×', '/', '^', '.')
133 |
134 |
135 | return this !in unchainableOperators
136 | }
137 |
138 | fun Char.canStartExpression(): Boolean {
139 | val startables = listOf('-', '+', '√', 'π', '(')
140 | return this in startables || this.isDigit()
141 | }
142 |
143 | fun Char.isOperator(): Boolean {
144 | val allOperators = listOf('×', '/', '^', '.', '√', '!', '%')
145 |
146 | return this in allOperators
147 | }
148 |
149 | /**
150 | * Formats a number not an expression !!
151 | */
152 | fun String.formatNumber(shouldFormat: Boolean): String {
153 | val number = this
154 | val localSymbols = DecimalFormatSymbols.getInstance()
155 |
156 | if (number.any { it.isLetter() } || !shouldFormat) return number
157 | var integer = number.takeWhile { it != '.' }
158 | val decimal = number.removePrefix(integer).replace('.', localSymbols.decimalSeparator)
159 | val offset = 3 - integer.length.mod(3)
160 | val formattedInteger = if (offset != 3) {
161 | integer = " ".repeat(offset) + integer
162 | integer.chunked(3).joinToString(localSymbols.groupingSeparator.toString()).drop(offset)
163 | } else {
164 | integer.chunked(3).joinToString(localSymbols.groupingSeparator.toString())
165 | }
166 |
167 | return "${formattedInteger}${decimal}"
168 | }
169 |
170 | fun String.formatExpression(shouldFormat: Boolean): String {
171 |
172 | if (!shouldFormat) return this
173 |
174 | var expression = this
175 | val numberRegex = Regex("[\\d.]+")
176 |
177 | numberRegex.findAll(expression).forEach { result ->
178 | expression = expression.replace(result.value, result.value.formatNumber(true))
179 | }
180 |
181 | return expression
182 |
183 | }
184 |
185 |
186 | object FormatTransformation : OutputTransformation {
187 | override fun TextFieldBuffer.transformOutput() {
188 | val expression = this.originalText.toString()
189 |
190 | if (expression.isEmpty()) return
191 |
192 | val localSymbols = DecimalFormatSymbols.getInstance()
193 |
194 | expression.formatExpression(true).forEachIndexed { index, char ->
195 | when (char) {
196 | localSymbols.groupingSeparator -> insert(
197 | index,
198 | localSymbols.groupingSeparator.toString()
199 | )
200 |
201 | localSymbols.decimalSeparator -> replace(
202 | index,
203 | index + 1,
204 | localSymbols.decimalSeparator.toString()
205 | )
206 | }
207 | }
208 | }
209 | }
210 |
211 | fun Activity.showOnLockScreen(show: Boolean) {
212 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
213 | setShowWhenLocked(show)
214 | } else {
215 | if (show) {
216 | window.addFlags(
217 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
218 | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
219 | )
220 | }
221 | }
222 | }
223 |
224 | fun Modifier.selfAlignHorizontally(align: Alignment.Horizontal = Alignment.CenterHorizontally): Modifier {
225 | return this.then(
226 | Modifier
227 | .fillMaxWidth()
228 | .wrapContentWidth(align)
229 | )
230 | }
231 |
232 |
233 |
234 |
235 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sosauce/cutecalc/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
2 |
3 | package com.sosauce.cutecalc.ui.theme
4 |
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
7 | import androidx.compose.material3.MaterialExpressiveTheme
8 | import androidx.compose.material3.Typography
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.getValue
11 | import androidx.compose.ui.graphics.Color
12 | import androidx.compose.ui.text.font.Font
13 | import androidx.compose.ui.text.font.FontFamily
14 | import androidx.compose.ui.text.font.FontStyle
15 | import androidx.compose.ui.text.font.FontWeight
16 | import com.sosauce.cutecalc.R
17 | import com.sosauce.cutecalc.data.datastore.rememberAppTheme
18 | import com.sosauce.cutecalc.data.datastore.rememberUseSystemFont
19 | import com.sosauce.cutecalc.utils.CuteTheme
20 | import com.sosauce.cutecalc.utils.anyDarkColorScheme
21 | import com.sosauce.cutecalc.utils.anyLightColorScheme
22 |
23 | @Composable
24 | fun CuteCalcTheme(
25 | content: @Composable () -> Unit
26 | ) {
27 |
28 | val isSystemInDarkTheme = isSystemInDarkTheme()
29 | val appTheme by rememberAppTheme()
30 | val useSystemFont by rememberUseSystemFont()
31 |
32 |
33 | val colorScheme = when (appTheme) {
34 | CuteTheme.AMOLED -> anyDarkColorScheme().copy(
35 | surface = Color.Black,
36 | inverseSurface = Color.White,
37 | background = Color.Black,
38 | )
39 |
40 | CuteTheme.SYSTEM -> if (isSystemInDarkTheme) anyDarkColorScheme() else anyLightColorScheme()
41 | CuteTheme.DARK -> anyDarkColorScheme()
42 | CuteTheme.LIGHT -> anyLightColorScheme()
43 | else -> anyDarkColorScheme()
44 | }
45 |
46 |
47 |
48 | MaterialExpressiveTheme(
49 | colorScheme = colorScheme,
50 | content = content,
51 | typography = if (useSystemFont) null else NunitoTypography
52 | )
53 |
54 | }
55 |
56 | val nunitoFontFamily = FontFamily(
57 | Font(R.font.nunito_black, FontWeight.Black, FontStyle.Normal),
58 | Font(R.font.nunito_bold, FontWeight.Bold, FontStyle.Normal),
59 | Font(R.font.nunito_extrabold, FontWeight.ExtraBold, FontStyle.Normal),
60 | Font(R.font.nunito_extralight, FontWeight.ExtraLight, FontStyle.Normal),
61 | Font(R.font.nunito_light, FontWeight.Light, FontStyle.Normal),
62 | Font(R.font.nunito_medium, FontWeight.Medium, FontStyle.Normal),
63 | Font(R.font.nunito_regular, FontWeight.Normal, FontStyle.Normal),
64 | Font(R.font.nunito_semibold, FontWeight.SemiBold, FontStyle.Normal)
65 | )
66 |
67 | val NunitoTypography = Typography().run {
68 | copy(
69 | displayLarge = displayLarge.copy(
70 | fontFamily = nunitoFontFamily,
71 | fontWeight = FontWeight.ExtraBold
72 | ),
73 | displayMedium = displayMedium.copy(
74 | fontFamily = nunitoFontFamily,
75 | fontWeight = FontWeight.ExtraBold
76 | ),
77 | displaySmall = displaySmall.copy(
78 | fontFamily = nunitoFontFamily,
79 | fontWeight = FontWeight.ExtraBold
80 | ),
81 | headlineLarge = headlineLarge.copy(
82 | fontFamily = nunitoFontFamily,
83 | fontWeight = FontWeight.ExtraBold
84 | ),
85 | headlineMedium = headlineMedium.copy(
86 | fontFamily = nunitoFontFamily,
87 | fontWeight = FontWeight.ExtraBold
88 | ),
89 | headlineSmall = headlineSmall.copy(
90 | fontFamily = nunitoFontFamily,
91 | fontWeight = FontWeight.ExtraBold
92 | ),
93 | titleLarge = titleLarge.copy(
94 | fontFamily = nunitoFontFamily,
95 | fontWeight = FontWeight.ExtraBold
96 | ),
97 | titleMedium = titleMedium.copy(
98 | fontFamily = nunitoFontFamily,
99 | fontWeight = FontWeight.ExtraBold
100 | ),
101 | titleSmall = titleSmall.copy(
102 | fontFamily = nunitoFontFamily,
103 | fontWeight = FontWeight.ExtraBold
104 | ),
105 | bodyLarge = bodyLarge.copy(
106 | fontFamily = nunitoFontFamily,
107 | fontWeight = FontWeight.ExtraBold
108 | ),
109 | bodyMedium = bodyMedium.copy(
110 | fontFamily = nunitoFontFamily,
111 | fontWeight = FontWeight.ExtraBold
112 | ),
113 | bodySmall = bodySmall.copy(
114 | fontFamily = nunitoFontFamily,
115 | fontWeight = FontWeight.ExtraBold
116 | ),
117 | labelLarge = labelLarge.copy(
118 | fontFamily = nunitoFontFamily,
119 | fontWeight = FontWeight.ExtraBold
120 | ),
121 | labelMedium = labelMedium.copy(
122 | fontFamily = nunitoFontFamily,
123 | fontWeight = FontWeight.ExtraBold
124 | ),
125 | labelSmall = labelSmall.copy(
126 | fontFamily = nunitoFontFamily,
127 | fontWeight = FontWeight.ExtraBold
128 | ),
129 | displayLargeEmphasized = displayLargeEmphasized.copy(
130 | fontFamily = nunitoFontFamily,
131 | fontWeight = FontWeight.ExtraBold
132 | ),
133 | displayMediumEmphasized = displayMediumEmphasized.copy(
134 | fontFamily = nunitoFontFamily,
135 | fontWeight = FontWeight.ExtraBold
136 | ),
137 | displaySmallEmphasized = displaySmallEmphasized.copy(
138 | fontFamily = nunitoFontFamily,
139 | fontWeight = FontWeight.ExtraBold
140 | ),
141 | headlineLargeEmphasized = headlineLargeEmphasized.copy(
142 | fontFamily = nunitoFontFamily,
143 | fontWeight = FontWeight.ExtraBold
144 | ),
145 | headlineMediumEmphasized = headlineMediumEmphasized.copy(
146 | fontFamily = nunitoFontFamily,
147 | fontWeight = FontWeight.ExtraBold
148 | ),
149 | headlineSmallEmphasized = headlineSmallEmphasized.copy(
150 | fontFamily = nunitoFontFamily,
151 | fontWeight = FontWeight.ExtraBold
152 | ),
153 | titleLargeEmphasized = titleLargeEmphasized.copy(
154 | fontFamily = nunitoFontFamily,
155 | fontWeight = FontWeight.ExtraBold
156 | ),
157 | titleMediumEmphasized = titleMediumEmphasized.copy(
158 | fontFamily = nunitoFontFamily,
159 | fontWeight = FontWeight.ExtraBold
160 | ),
161 | titleSmallEmphasized = titleSmallEmphasized.copy(
162 | fontFamily = nunitoFontFamily,
163 | fontWeight = FontWeight.ExtraBold
164 | ),
165 | bodyLargeEmphasized = bodyLargeEmphasized.copy(
166 | fontFamily = nunitoFontFamily,
167 | fontWeight = FontWeight.ExtraBold
168 | ),
169 | bodyMediumEmphasized = bodyMediumEmphasized.copy(
170 | fontFamily = nunitoFontFamily,
171 | fontWeight = FontWeight.ExtraBold
172 | ),
173 | bodySmallEmphasized = bodySmallEmphasized.copy(
174 | fontFamily = nunitoFontFamily,
175 | fontWeight = FontWeight.ExtraBold
176 | ),
177 | labelLargeEmphasized = labelLargeEmphasized.copy(
178 | fontFamily = nunitoFontFamily,
179 | fontWeight = FontWeight.ExtraBold
180 | ),
181 | labelMediumEmphasized = labelMediumEmphasized.copy(
182 | fontFamily = nunitoFontFamily,
183 | fontWeight = FontWeight.ExtraBold
184 | ),
185 | labelSmallEmphasized = labelSmallEmphasized.copy(
186 | fontFamily = nunitoFontFamily,
187 | fontWeight = FontWeight.ExtraBold
188 | )
189 | )
190 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sosauce/cutecalc/ui/screens/settings/SettingsLookAndFeel.kt:
--------------------------------------------------------------------------------
1 | package com.sosauce.cutecalc.ui.screens.settings
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.navigationBarsPadding
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.rememberScrollState
9 | import androidx.compose.foundation.verticalScroll
10 | import androidx.compose.material3.Card
11 | import androidx.compose.material3.CardDefaults
12 | import androidx.compose.material3.Icon
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.Scaffold
15 | import androidx.compose.material3.Text
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.runtime.Immutable
18 | import androidx.compose.runtime.getValue
19 | import androidx.compose.runtime.setValue
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.graphics.Color
23 | import androidx.compose.ui.res.painterResource
24 | import androidx.compose.ui.res.stringResource
25 | import androidx.compose.ui.text.font.FontWeight
26 | import androidx.compose.ui.unit.dp
27 | import com.sosauce.cutecalc.R
28 | import com.sosauce.cutecalc.data.datastore.rememberAppTheme
29 | import com.sosauce.cutecalc.data.datastore.rememberShowClearButton
30 | import com.sosauce.cutecalc.data.datastore.rememberUseButtonsAnimation
31 | import com.sosauce.cutecalc.data.datastore.rememberUseSystemFont
32 | import com.sosauce.cutecalc.data.datastore.rememberVibration
33 | import com.sosauce.cutecalc.ui.screens.settings.components.FontSelector
34 | import com.sosauce.cutecalc.ui.screens.settings.components.LazyRowWithScrollButton
35 | import com.sosauce.cutecalc.ui.screens.settings.components.SettingsSwitch
36 | import com.sosauce.cutecalc.ui.screens.settings.components.SettingsWithTitle
37 | import com.sosauce.cutecalc.ui.screens.settings.components.ThemeSelector
38 | import com.sosauce.cutecalc.ui.shared_components.CuteNavigationButton
39 | import com.sosauce.cutecalc.ui.theme.nunitoFontFamily
40 | import com.sosauce.cutecalc.utils.CuteTheme
41 | import com.sosauce.cutecalc.utils.anyDarkColorScheme
42 | import com.sosauce.cutecalc.utils.anyLightColorScheme
43 | import com.sosauce.cutecalc.utils.selfAlignHorizontally
44 |
45 | @Composable
46 | fun SettingsLookAndFeel(
47 | onNavigateUp: () -> Unit,
48 | ) {
49 | val scrollState = rememberScrollState()
50 | var theme by rememberAppTheme()
51 | var useSystemFont by rememberUseSystemFont()
52 | var useButtonsAnimation by rememberUseButtonsAnimation()
53 | var useHapticFeedback by rememberVibration()
54 | var showClearButton by rememberShowClearButton()
55 | val themeItems = listOf(
56 | _root_ide_package_.com.sosauce.cutecalc.ui.screens.settings.components.ThemeItem(
57 | onClick = { theme = CuteTheme.SYSTEM },
58 | backgroundColor = if (isSystemInDarkTheme()) anyDarkColorScheme().background else anyLightColorScheme().background,
59 | text = stringResource(R.string.follow_sys),
60 | isSelected = theme == CuteTheme.SYSTEM,
61 | iconAndTint = Pair(
62 | painterResource(R.drawable.system_theme),
63 | if (isSystemInDarkTheme()) anyDarkColorScheme().onBackground else anyLightColorScheme().onBackground
64 | )
65 | ),
66 | _root_ide_package_.com.sosauce.cutecalc.ui.screens.settings.components.ThemeItem(
67 | onClick = { theme = CuteTheme.DARK },
68 | backgroundColor = anyDarkColorScheme().background,
69 | text = stringResource(R.string.dark_mode),
70 | isSelected = theme == CuteTheme.DARK,
71 | iconAndTint = Pair(
72 | painterResource(R.drawable.dark_mode),
73 | anyDarkColorScheme().onBackground
74 | )
75 | ),
76 | _root_ide_package_.com.sosauce.cutecalc.ui.screens.settings.components.ThemeItem(
77 | onClick = { theme = CuteTheme.LIGHT },
78 | backgroundColor = anyLightColorScheme().background,
79 | text = stringResource(R.string.light_mode),
80 | isSelected = theme == CuteTheme.LIGHT,
81 | iconAndTint = Pair(
82 | painterResource(R.drawable.light_mode),
83 | anyLightColorScheme().onBackground
84 | )
85 | ),
86 | _root_ide_package_.com.sosauce.cutecalc.ui.screens.settings.components.ThemeItem(
87 | onClick = { theme = CuteTheme.AMOLED },
88 | backgroundColor = Color.Black,
89 | text = stringResource(R.string.amoled_mode),
90 | isSelected = theme == CuteTheme.AMOLED,
91 | iconAndTint = Pair(painterResource(R.drawable.amoled), Color.White)
92 | )
93 | )
94 | val fontItems = listOf(
95 | FontItem(
96 | onClick = { useSystemFont = false },
97 | fontStyle = FontStyle.DEFAULT,
98 | borderColor = if (!useSystemFont) MaterialTheme.colorScheme.primary else Color.Transparent,
99 | text = {
100 | Text(
101 | text = "Tt",
102 | fontFamily = nunitoFontFamily,
103 | fontWeight = FontWeight.ExtraBold
104 | )
105 | },
106 | ),
107 | FontItem(
108 | onClick = { useSystemFont = true },
109 | fontStyle = FontStyle.SYSTEM,
110 | borderColor = if (useSystemFont) MaterialTheme.colorScheme.primary else Color.Transparent,
111 | text = { Text("Tt") }
112 | )
113 | )
114 |
115 | Scaffold(
116 | bottomBar = {
117 | CuteNavigationButton(
118 | modifier = Modifier
119 | .padding(start = 15.dp)
120 | .navigationBarsPadding()
121 | .selfAlignHorizontally(Alignment.Start),
122 | onNavigateUp = onNavigateUp
123 | )
124 | }
125 | ) { pv ->
126 | Column(
127 | modifier = Modifier
128 | .verticalScroll(scrollState)
129 | .padding(pv)
130 | ) {
131 | SettingsWithTitle(
132 | title = R.string.theme
133 | ) {
134 | Card(
135 | colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainer),
136 | modifier = Modifier
137 | .fillMaxWidth()
138 | .padding(horizontal = 16.dp, vertical = 2.dp)
139 | ) {
140 | LazyRowWithScrollButton(
141 | items = themeItems
142 | ) { item ->
143 | ThemeSelector(
144 | onClick = item.onClick,
145 | backgroundColor = item.backgroundColor,
146 | text = item.text,
147 | isThemeSelected = item.isSelected,
148 | icon = {
149 | Icon(
150 | painter = item.iconAndTint.first,
151 | contentDescription = null,
152 | tint = item.iconAndTint.second,
153 | )
154 | }
155 | )
156 | }
157 | }
158 | }
159 | SettingsWithTitle(
160 | title = R.string.font
161 | ) {
162 | Card(
163 | colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainer),
164 | modifier = Modifier
165 | .fillMaxWidth()
166 | .padding(horizontal = 16.dp, vertical = 2.dp)
167 | ) {
168 | LazyRowWithScrollButton(
169 | items = fontItems
170 | ) { item ->
171 | FontSelector(
172 | item
173 | )
174 | }
175 | }
176 | }
177 | SettingsWithTitle(
178 | title = R.string.ui
179 | ) {
180 | SettingsSwitch(
181 | checked = useButtonsAnimation,
182 | onCheckedChange = { useButtonsAnimation = !useButtonsAnimation },
183 | topDp = 24.dp,
184 | bottomDp = 4.dp,
185 | text = R.string.buttons_anim
186 | )
187 | SettingsSwitch(
188 | checked = useHapticFeedback,
189 | onCheckedChange = { useHapticFeedback = !useHapticFeedback },
190 | topDp = 4.dp,
191 | bottomDp = 4.dp,
192 | text = R.string.haptic_feedback
193 | )
194 | SettingsSwitch(
195 | checked = showClearButton,
196 | onCheckedChange = { showClearButton = !showClearButton },
197 | topDp = 4.dp,
198 | bottomDp = 24.dp,
199 | text = R.string.show_clear_button,
200 | optionalDescription = R.string.clear_button_desc
201 | )
202 | }
203 | }
204 | }
205 | }
206 |
207 | @Immutable
208 | data class FontItem(
209 | val onClick: () -> Unit,
210 | val fontStyle: FontStyle,
211 | val borderColor: Color,
212 | val text: @Composable () -> Unit
213 | )
214 |
215 | enum class FontStyle {
216 | DEFAULT,
217 | SYSTEM
218 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sosauce/cutecalc/ui/screens/history/HistoryScreen.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
2 |
3 | package com.sosauce.cutecalc.ui.screens.history
4 |
5 | import android.content.ClipData
6 | import androidx.compose.foundation.basicMarquee
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.Row
10 | import androidx.compose.foundation.layout.Spacer
11 | import androidx.compose.foundation.layout.fillMaxSize
12 | import androidx.compose.foundation.layout.fillMaxWidth
13 | import androidx.compose.foundation.layout.height
14 | import androidx.compose.foundation.layout.navigationBarsPadding
15 | import androidx.compose.foundation.layout.padding
16 | import androidx.compose.foundation.layout.size
17 | import androidx.compose.foundation.lazy.LazyColumn
18 | import androidx.compose.foundation.lazy.itemsIndexed
19 | import androidx.compose.foundation.lazy.rememberLazyListState
20 | import androidx.compose.foundation.shape.RoundedCornerShape
21 | import androidx.compose.material3.Button
22 | import androidx.compose.material3.ButtonDefaults
23 | import androidx.compose.material3.Card
24 | import androidx.compose.material3.CardDefaults
25 | import androidx.compose.material3.DropdownMenu
26 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
27 | import androidx.compose.material3.Icon
28 | import androidx.compose.material3.IconButton
29 | import androidx.compose.material3.MaterialTheme
30 | import androidx.compose.material3.Scaffold
31 | import androidx.compose.material3.Text
32 | import androidx.compose.runtime.Composable
33 | import androidx.compose.runtime.getValue
34 | import androidx.compose.runtime.mutableStateOf
35 | import androidx.compose.runtime.remember
36 | import androidx.compose.runtime.setValue
37 | import androidx.compose.ui.Alignment
38 | import androidx.compose.ui.Modifier
39 | import androidx.compose.ui.draw.clip
40 | import androidx.compose.ui.platform.LocalClipboard
41 | import androidx.compose.ui.res.painterResource
42 | import androidx.compose.ui.res.stringResource
43 | import androidx.compose.ui.text.font.FontWeight
44 | import androidx.compose.ui.unit.Dp
45 | import androidx.compose.ui.unit.dp
46 | import com.sosauce.cutecalc.R
47 | import com.sosauce.cutecalc.data.datastore.rememberDecimal
48 | import com.sosauce.cutecalc.data.datastore.rememberHistoryNewestFirst
49 | import com.sosauce.cutecalc.data.datastore.rememberUseHistory
50 | import com.sosauce.cutecalc.domain.model.Calculation
51 | import com.sosauce.cutecalc.domain.repository.HistoryEvents
52 | import com.sosauce.cutecalc.ui.navigation.Screens
53 | import com.sosauce.cutecalc.ui.screens.history.components.DeletionConfirmationDialog
54 | import com.sosauce.cutecalc.ui.screens.history.components.HistoryActionButtons
55 | import com.sosauce.cutecalc.ui.shared_components.CuteDropdownMenuItem
56 | import com.sosauce.cutecalc.ui.shared_components.CuteNavigationButton
57 | import com.sosauce.cutecalc.utils.formatExpression
58 | import com.sosauce.cutecalc.utils.formatNumber
59 | import com.sosauce.cutecalc.utils.isErrorMessage
60 | import com.sosauce.cutecalc.utils.sort
61 |
62 | @Composable
63 | fun HistoryScreen(
64 | calculations: List,
65 | onEvents: (HistoryEvents) -> Unit,
66 | onPutBackToField: (String) -> Unit,
67 | onScrollToMain: () -> Unit
68 | ) {
69 | val lazyState = rememberLazyListState()
70 | var isHistoryEnable by rememberUseHistory()
71 | val newestFirst by rememberHistoryNewestFirst()
72 | var showDeleteConfirmation by remember { mutableStateOf(false) }
73 |
74 | if (showDeleteConfirmation) {
75 | DeletionConfirmationDialog(
76 | onDismissRequest = { showDeleteConfirmation = false },
77 | onDelete = { onEvents(HistoryEvents.DeleteAllCalculation) }
78 | )
79 | }
80 |
81 | Scaffold(
82 | bottomBar = {
83 | Row(
84 | modifier = Modifier
85 | .padding(horizontal = 15.dp)
86 | .fillMaxWidth()
87 | .navigationBarsPadding(),
88 | horizontalArrangement = Arrangement.SpaceBetween
89 | ) {
90 | CuteNavigationButton(onNavigateUp = onScrollToMain)
91 | HistoryActionButtons { showDeleteConfirmation = true }
92 | }
93 | }
94 | ) { pv ->
95 | if (!isHistoryEnable) {
96 | Column(
97 | modifier = Modifier
98 | .fillMaxSize(),
99 | verticalArrangement = Arrangement.Center,
100 | horizontalAlignment = Alignment.CenterHorizontally
101 | ) {
102 | Text(stringResource(R.string.history_not_enabled))
103 | Spacer(Modifier.height(10.dp))
104 | Button(
105 | onClick = { isHistoryEnable = !isHistoryEnable },
106 | shapes = ButtonDefaults.shapes()
107 | ) {
108 | Text(stringResource(R.string.enable_history))
109 | }
110 | }
111 | } else {
112 | LazyColumn(
113 | modifier = Modifier.fillMaxSize(),
114 | contentPadding = pv,
115 | state = lazyState
116 | ) {
117 |
118 | if (calculations.isEmpty()) {
119 | item {
120 | Column(
121 | horizontalAlignment = Alignment.CenterHorizontally,
122 | modifier = Modifier.fillMaxWidth()
123 | ) {
124 | Icon(
125 | painter = painterResource(R.drawable.history_rounded),
126 | contentDescription = null,
127 | modifier = Modifier.size(70.dp)
128 | )
129 | Spacer(Modifier.height(10.dp))
130 | Text(
131 | text = stringResource(R.string.no_calc_found),
132 | style = MaterialTheme.typography.headlineMediumEmphasized,
133 | fontWeight = FontWeight.Black
134 | )
135 | Text(
136 | text = stringResource(R.string.calc_empty),
137 | style = MaterialTheme.typography.bodyMediumEmphasized,
138 | color = MaterialTheme.colorScheme.onSurfaceVariant
139 | )
140 | }
141 | }
142 | } else {
143 | itemsIndexed(
144 | items = calculations.sort(newestFirst),
145 | key = { _, item -> item.id }
146 | ) { index, item ->
147 | CalculationItem(
148 | calculation = item,
149 | onEvents = onEvents,
150 | onPutBackToField = onPutBackToField,
151 | topDp = if (index == 0) 24.dp else 4.dp,
152 | bottomDp = if (index == calculations.lastIndex) 24.dp else 4.dp,
153 | modifier = Modifier.animateItem()
154 | )
155 | }
156 | }
157 |
158 | }
159 | }
160 | }
161 |
162 | }
163 |
164 | @Composable
165 | private fun CalculationItem(
166 | calculation: Calculation,
167 | onEvents: (HistoryEvents) -> Unit,
168 | onPutBackToField: (String) -> Unit,
169 | topDp: Dp,
170 | bottomDp: Dp,
171 | modifier: Modifier = Modifier
172 | ) {
173 | val clipboardManager = LocalClipboard.current
174 | val shouldFormat by rememberDecimal()
175 | var actionsExpanded by remember { mutableStateOf(false) }
176 | val actions = arrayOf(
177 | HistoryAction(
178 | onClick = { onPutBackToField(calculation.operation) },
179 | icon = R.drawable.undo,
180 | text = R.string.put_field
181 | ),
182 | HistoryAction(
183 | onClick = {
184 | clipboardManager.nativeClipboard.setPrimaryClip(
185 | ClipData.newPlainText(
186 | "",
187 | "${calculation.operation} = ${calculation.result}"
188 | )
189 | )
190 | },
191 | icon = R.drawable.copy,
192 | text = R.string.copy
193 | )
194 | )
195 |
196 |
197 | Card(
198 | onClick = { onPutBackToField(calculation.operation) },
199 | modifier = modifier
200 | .padding(horizontal = 16.dp, vertical = 2.dp)
201 | .clip(
202 | RoundedCornerShape(
203 | topStart = topDp,
204 | topEnd = topDp,
205 | bottomEnd = bottomDp,
206 | bottomStart = bottomDp
207 | )
208 | ),
209 | colors = CardDefaults.cardColors(
210 | containerColor = MaterialTheme.colorScheme.surfaceContainer
211 | )
212 | ) {
213 | Row(
214 | modifier = Modifier
215 | .fillMaxWidth()
216 | .padding(15.dp),
217 | verticalAlignment = Alignment.CenterVertically,
218 | horizontalArrangement = Arrangement.SpaceBetween
219 | ) {
220 | Column(
221 | modifier = Modifier
222 | .weight(1f),
223 | horizontalAlignment = Alignment.Start
224 | ) {
225 | Text(
226 | text = calculation.operation.formatExpression(shouldFormat),
227 | style = MaterialTheme.typography.titleLarge,
228 | modifier = Modifier.basicMarquee()
229 | )
230 | Text(
231 | text = calculation.result.formatNumber(shouldFormat),
232 | style = MaterialTheme.typography.titleLarge.copy(
233 | color = if (calculation.result.isErrorMessage()) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant
234 | ),
235 | modifier = Modifier.basicMarquee()
236 | )
237 | }
238 | IconButton(
239 | onClick = { actionsExpanded = true }
240 | ) {
241 | Icon(
242 | painter = painterResource(R.drawable.more_vert),
243 | contentDescription = stringResource(R.string.more_actions)
244 | )
245 |
246 | DropdownMenu(
247 | expanded = actionsExpanded,
248 | onDismissRequest = { actionsExpanded = false },
249 | shape = RoundedCornerShape(24.dp)
250 | ) {
251 | actions.forEach { action ->
252 | CuteDropdownMenuItem(
253 | onClick = {
254 | action.onClick()
255 | actionsExpanded = false
256 | },
257 | text = { Text(stringResource(action.text)) },
258 | leadingIcon = {
259 | Icon(
260 | painter = painterResource(action.icon),
261 | contentDescription = null
262 | )
263 | }
264 | )
265 | }
266 | CuteDropdownMenuItem(
267 | onClick = { onEvents(HistoryEvents.DeleteCalculation(calculation)) },
268 | text = {
269 | Text(
270 | text = stringResource(R.string.delete),
271 | color = MaterialTheme.colorScheme.error
272 | )
273 | },
274 | leadingIcon = {
275 | Icon(
276 | painter = painterResource(R.drawable.delete),
277 | contentDescription = null,
278 | tint = MaterialTheme.colorScheme.error
279 | )
280 | }
281 | )
282 | }
283 | }
284 | }
285 | }
286 | }
287 |
288 | private data class HistoryAction(
289 | val onClick: () -> Unit,
290 | val icon: Int,
291 | val text: Int
292 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/sosauce/cutecalc/ui/screens/calculator/CalculatorScreenLandscape.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
2 |
3 | package com.sosauce.cutecalc.ui.screens.calculator
4 |
5 | import android.annotation.SuppressLint
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.Row
10 | import androidx.compose.foundation.layout.WindowInsets
11 | import androidx.compose.foundation.layout.consumeWindowInsets
12 | import androidx.compose.foundation.layout.fillMaxSize
13 | import androidx.compose.foundation.layout.padding
14 | import androidx.compose.foundation.layout.safeDrawing
15 | import androidx.compose.material3.ExperimentalMaterial3Api
16 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
17 | import androidx.compose.material3.Icon
18 | import androidx.compose.material3.IconButton
19 | import androidx.compose.material3.IconButtonDefaults
20 | import androidx.compose.material3.MaterialTheme
21 | import androidx.compose.material3.NavigationRail
22 | import androidx.compose.material3.Scaffold
23 | import androidx.compose.runtime.Composable
24 | import androidx.compose.runtime.getValue
25 | import androidx.compose.runtime.remember
26 | import androidx.compose.ui.Alignment
27 | import androidx.compose.ui.Modifier
28 | import androidx.compose.ui.res.painterResource
29 | import androidx.compose.ui.res.stringResource
30 | import androidx.compose.ui.unit.dp
31 | import androidx.compose.ui.util.fastForEach
32 | import com.sosauce.cutecalc.R
33 | import com.sosauce.cutecalc.data.actions.CalcAction
34 | import com.sosauce.cutecalc.data.datastore.rememberHistoryMaxItems
35 | import com.sosauce.cutecalc.data.datastore.rememberSaveErrorsToHistory
36 | import com.sosauce.cutecalc.data.datastore.rememberShowClearButton
37 | import com.sosauce.cutecalc.data.datastore.rememberUseHistory
38 | import com.sosauce.cutecalc.domain.repository.HistoryEvents
39 | import com.sosauce.cutecalc.ui.navigation.Screens
40 | import com.sosauce.cutecalc.ui.screens.calculator.components.CalcButton
41 | import com.sosauce.cutecalc.ui.screens.calculator.components.CalculationDisplay
42 | import com.sosauce.cutecalc.ui.screens.calculator.components.CuteButton
43 | import com.sosauce.cutecalc.ui.screens.history.HistoryViewModel
44 | import com.sosauce.cutecalc.utils.BACKSPACE
45 | import com.sosauce.cutecalc.utils.whichParenthesis
46 | import java.text.DecimalFormatSymbols
47 |
48 | @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
49 | @Composable
50 | fun CalculatorScreenLandscape(
51 | viewModel: CalculatorViewModel,
52 | historyViewModel: HistoryViewModel,
53 | onNavigate: (Screens) -> Unit,
54 | onScrollToHistory: () -> Unit
55 | ) {
56 | val showClearButton by rememberShowClearButton()
57 | val localeDecimalChar =
58 | remember { DecimalFormatSymbols.getInstance().decimalSeparator.toString() }
59 | val saveErrorsToHistory by rememberSaveErrorsToHistory()
60 | val maxItemsToHistory by rememberHistoryMaxItems()
61 | val saveToHistory by rememberUseHistory()
62 |
63 | val row1 = listOf(
64 | CalcButton(
65 | text = "√",
66 | backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
67 | onClick = { viewModel.handleAction(CalcAction.AddToField("√")) }
68 | ),
69 | CalcButton(
70 | text = "π",
71 | backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
72 | onClick = { viewModel.handleAction(CalcAction.AddToField("π")) }
73 | ),
74 | CalcButton(
75 | text = "9",
76 | backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
77 | onClick = { viewModel.handleAction(CalcAction.AddToField("9")) }
78 | ),
79 | CalcButton(
80 | text = "8",
81 | backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
82 | onClick = { viewModel.handleAction(CalcAction.AddToField("8")) }
83 | ),
84 | CalcButton(
85 | text = "7",
86 | backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
87 | onClick = { viewModel.handleAction(CalcAction.AddToField("7")) }
88 | ),
89 | CalcButton(
90 | text = "^",
91 | backgroundColor = MaterialTheme.colorScheme.secondaryContainer,
92 | onClick = { viewModel.handleAction(CalcAction.AddToField("^")) }
93 | ),
94 | if (showClearButton) {
95 | CalcButton(
96 | text = viewModel.textFieldState.text.toString().whichParenthesis(),
97 | backgroundColor = MaterialTheme.colorScheme.secondaryContainer,
98 | onClick = {
99 | viewModel.handleAction(
100 | CalcAction.AddToField(
101 | viewModel.textFieldState.text.toString().whichParenthesis()
102 | )
103 | )
104 | }
105 | )
106 | } else {
107 | CalcButton(
108 | text = "(",
109 | backgroundColor = MaterialTheme.colorScheme.secondaryContainer,
110 | onClick = { viewModel.handleAction(CalcAction.AddToField("(")) }
111 | )
112 | },
113 | if (showClearButton) {
114 | CalcButton(
115 | text = "C",
116 | backgroundColor = MaterialTheme.colorScheme.inversePrimary,
117 | onClick = { viewModel.handleAction(CalcAction.ResetField) }
118 | )
119 | } else {
120 | CalcButton(
121 | text = ")",
122 | backgroundColor = MaterialTheme.colorScheme.secondaryContainer,
123 | onClick = {
124 | viewModel.handleAction(
125 | CalcAction.AddToField(
126 | viewModel.textFieldState.text.toString().whichParenthesis()
127 | )
128 | )
129 | }
130 | )
131 | }
132 | )
133 | val row2 = listOf(
134 | CalcButton(
135 | text = "%",
136 | backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
137 | onClick = { viewModel.handleAction(CalcAction.AddToField("%")) }
138 | ),
139 | CalcButton(
140 | text = "3",
141 | backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
142 | onClick = { viewModel.handleAction(CalcAction.AddToField("3")) }
143 | ),
144 | CalcButton(
145 | text = "4",
146 | backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
147 | onClick = { viewModel.handleAction(CalcAction.AddToField("4")) }
148 | ),
149 | CalcButton(
150 | text = "5",
151 | backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
152 | onClick = { viewModel.handleAction(CalcAction.AddToField("5")) }
153 | ),
154 | CalcButton(
155 | text = "6",
156 | backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
157 | onClick = { viewModel.handleAction(CalcAction.AddToField("6")) }
158 | ),
159 | CalcButton(
160 | text = "+",
161 | backgroundColor = MaterialTheme.colorScheme.secondaryContainer,
162 | onClick = { viewModel.handleAction(CalcAction.AddToField("+")) }
163 | ),
164 | CalcButton(
165 | text = "-",
166 | backgroundColor = MaterialTheme.colorScheme.secondaryContainer,
167 | onClick = { viewModel.handleAction(CalcAction.AddToField("-")) }
168 | ),
169 | CalcButton(
170 | text = BACKSPACE,
171 | backgroundColor = MaterialTheme.colorScheme.inversePrimary,
172 | onClick = { viewModel.handleAction(CalcAction.Backspace) },
173 | onLongClick = { viewModel.handleAction(CalcAction.ResetField) }
174 | )
175 | )
176 | val row3 = listOf(
177 | CalcButton(
178 | text = "!",
179 | backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
180 | onClick = { viewModel.handleAction(CalcAction.AddToField("!")) }
181 | ),
182 | CalcButton(
183 | text = "2",
184 | backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
185 | onClick = { viewModel.handleAction(CalcAction.AddToField("2")) }
186 | ),
187 | CalcButton(
188 | text = "1",
189 | backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
190 | onClick = { viewModel.handleAction(CalcAction.AddToField("1")) }
191 | ),
192 | CalcButton(
193 | text = "0",
194 | backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
195 | onClick = { viewModel.handleAction(CalcAction.AddToField("0")) }
196 | ),
197 | CalcButton(
198 | text = localeDecimalChar,
199 | backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
200 | onClick = { viewModel.handleAction(CalcAction.AddToField(".")) }
201 | ),
202 | CalcButton(
203 | text = "×",
204 | backgroundColor = MaterialTheme.colorScheme.secondaryContainer,
205 | onClick = { viewModel.handleAction(CalcAction.AddToField("×")) }
206 | ),
207 | CalcButton(
208 | text = "/",
209 | backgroundColor = MaterialTheme.colorScheme.secondaryContainer,
210 | onClick = { viewModel.handleAction(CalcAction.AddToField("/")) }
211 | ),
212 | CalcButton(
213 | text = "=",
214 | backgroundColor = MaterialTheme.colorScheme.inversePrimary,
215 | onClick = {
216 | val operation = viewModel.textFieldState.text.toString()
217 | viewModel.handleAction(CalcAction.GetResult)
218 | val result = viewModel.evaluatedCalculation
219 |
220 | if (saveToHistory && operation != result) {
221 | historyViewModel.onEvent(
222 | HistoryEvents.AddCalculation(
223 | operation = operation,
224 | result = result,
225 | maxHistoryItems = maxItemsToHistory,
226 | saveErrors = saveErrorsToHistory
227 | )
228 | )
229 | }
230 | },
231 | )
232 | )
233 |
234 | Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { pv ->
235 |
236 | Row(
237 | modifier = Modifier
238 | .fillMaxSize()
239 | .consumeWindowInsets(pv)
240 | .padding(pv)
241 | ) {
242 | NavigationRail {
243 | IconButton(
244 | onClick = { onNavigate(Screens.SETTINGS) },
245 | shapes = IconButtonDefaults.shapes()
246 | ) {
247 | Icon(
248 | painter = painterResource(R.drawable.settings_filled),
249 | contentDescription = stringResource(R.string.settings),
250 | tint = MaterialTheme.colorScheme.onBackground
251 | )
252 | }
253 | IconButton(
254 | onClick = onScrollToHistory,
255 | shapes = IconButtonDefaults.shapes()
256 | ) {
257 | Icon(
258 | painter = painterResource(R.drawable.history_rounded),
259 | contentDescription = stringResource(R.string.history),
260 | tint = MaterialTheme.colorScheme.onBackground
261 | )
262 | }
263 | }
264 | Box(
265 | modifier = Modifier.fillMaxSize(),
266 | contentAlignment = Alignment.BottomCenter
267 | ) {
268 | Column {
269 | CalculationDisplay(
270 | viewModel = viewModel
271 | )
272 | Column(
273 | modifier = Modifier.padding(horizontal = 10.dp),
274 | verticalArrangement = Arrangement.spacedBy(9.dp),
275 | ) {
276 | Row(
277 | horizontalArrangement = Arrangement.spacedBy(9.dp)
278 | ) {
279 | row1.fastForEach { button ->
280 | CuteButton(
281 | text = button.text,
282 | backgroundColor = button.backgroundColor,
283 | onClick = button.onClick,
284 | roundButton = false
285 | )
286 | }
287 |
288 | }
289 | Row(
290 | horizontalArrangement = Arrangement.spacedBy(9.dp)
291 | ) {
292 | row2.fastForEach { button ->
293 | CuteButton(
294 | text = button.text,
295 | backgroundColor = button.backgroundColor,
296 | onClick = button.onClick,
297 | onLongClick = button.onLongClick,
298 | roundButton = false
299 | )
300 | }
301 | }
302 | Row(
303 | horizontalArrangement = Arrangement.spacedBy(9.dp)
304 | ) {
305 | row3.fastForEach { button ->
306 | CuteButton(
307 | text = button.text,
308 | backgroundColor = button.backgroundColor,
309 | onClick = button.onClick,
310 | roundButton = false
311 | )
312 | }
313 | }
314 | }
315 | }
316 | }
317 | }
318 |
319 |
320 | }
321 | }
--------------------------------------------------------------------------------