├── app ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── font │ │ │ ├── .DS_Store │ │ │ ├── nunito_black.ttf │ │ │ ├── nunito_bold.ttf │ │ │ ├── nunito_light.ttf │ │ │ ├── nunito_medium.ttf │ │ │ ├── nunito_regular.ttf │ │ │ ├── nunito_extrabold.ttf │ │ │ ├── nunito_extralight.ttf │ │ │ └── nunito_semibold.ttf │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── values │ │ │ ├── ic_launcher_background.xml │ │ │ ├── colors.xml │ │ │ ├── themes.xml │ │ │ └── strings.xml │ │ ├── values-v31 │ │ │ └── colors.xml │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── drawable │ │ │ ├── arrow_right.xml │ │ │ ├── back_arrow.xml │ │ │ ├── close.xml │ │ │ ├── amoled.xml │ │ │ ├── copy.xml │ │ │ ├── more_horiz.xml │ │ │ ├── more_vert.xml │ │ │ ├── undo.xml │ │ │ ├── dark_mode.xml │ │ │ ├── backspace_filled.xml │ │ │ ├── formatting.xml │ │ │ ├── delete.xml │ │ │ ├── sort_rounded.xml │ │ │ ├── settings_filled.xml │ │ │ ├── calculator.xml │ │ │ ├── trash_rounded.xml │ │ │ ├── palette.xml │ │ │ ├── icon_splash.xml │ │ │ ├── history_rounded.xml │ │ │ ├── ic_launcher_foreground.xml │ │ │ ├── backspace_rounded.xml │ │ │ ├── light_mode.xml │ │ │ └── system_theme.xml │ │ ├── values-fr-rFR │ │ │ └── strings.xml │ │ └── values-es │ │ │ └── strings.xml │ │ ├── ic_launcher-playstore.png │ │ ├── java │ │ └── com │ │ │ └── sosauce │ │ │ └── cutecalc │ │ │ ├── ui │ │ │ ├── navigation │ │ │ │ ├── Screens.kt │ │ │ │ └── Navigation.kt │ │ │ ├── screens │ │ │ │ ├── calculator │ │ │ │ │ ├── components │ │ │ │ │ │ ├── CalcButton.kt │ │ │ │ │ │ ├── DisableSoftKeyboard.kt │ │ │ │ │ │ ├── CalculationDisplay.kt │ │ │ │ │ │ └── CuteButton.kt │ │ │ │ │ ├── CalculatorViewModel.kt │ │ │ │ │ └── CalculatorScreenLandscape.kt │ │ │ │ ├── settings │ │ │ │ │ ├── components │ │ │ │ │ │ ├── SettingsWithTitle.kt │ │ │ │ │ │ ├── LazyRowWithScrollButton.kt │ │ │ │ │ │ ├── SettingsCategoryCard.kt │ │ │ │ │ │ ├── FontSelector.kt │ │ │ │ │ │ ├── ThemeSelector.kt │ │ │ │ │ │ ├── AboutCard.kt │ │ │ │ │ │ └── SettingsSwitch.kt │ │ │ │ │ ├── SettingsMisc.kt │ │ │ │ │ ├── SettingsFormatting.kt │ │ │ │ │ ├── SettingsHistory.kt │ │ │ │ │ ├── SettingsScreen.kt │ │ │ │ │ └── SettingsLookAndFeel.kt │ │ │ │ └── history │ │ │ │ │ ├── components │ │ │ │ │ ├── DeletionConfirmationDialog.kt │ │ │ │ │ └── HistoryActionButtons.kt │ │ │ │ │ ├── HistoryViewModel.kt │ │ │ │ │ └── HistoryScreen.kt │ │ │ ├── shared_components │ │ │ │ ├── CuteDropdownMenuItem.kt │ │ │ │ └── CuteNavigationButton.kt │ │ │ └── theme │ │ │ │ └── Theme.kt │ │ │ ├── domain │ │ │ ├── model │ │ │ │ └── Calculation.kt │ │ │ └── repository │ │ │ │ ├── HistoryDatabase.kt │ │ │ │ ├── HistoryState.kt │ │ │ │ ├── HistoryEvents.kt │ │ │ │ └── HistoryDao.kt │ │ │ ├── data │ │ │ ├── actions │ │ │ │ └── CalcAction.kt │ │ │ ├── sysui │ │ │ │ └── QSTile.kt │ │ │ ├── datastore │ │ │ │ ├── SettingsExt.kt │ │ │ │ └── DataStore.kt │ │ │ └── calculator │ │ │ │ └── Evaluator.kt │ │ │ ├── utils │ │ │ ├── Constants.kt │ │ │ ├── ViewModelFactories.kt │ │ │ └── Extensions.kt │ │ │ └── MainActivity.kt │ │ └── AndroidManifest.xml ├── proguard-rules.pro └── build.gradle.kts ├── fastlane └── metadata │ └── android │ ├── en-US │ ├── short_description.txt │ ├── full_description.txt │ └── images │ │ ├── icon.png │ │ └── phoneScreenshots │ │ ├── 01.png │ │ ├── 02.png │ │ └── 03.png │ └── de │ ├── short_description.txt │ └── full_description.txt ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── settings.gradle.kts ├── .github ├── ISSUE_TEMPLATE │ ├── ui-bug.md │ ├── logic-bug.md │ └── feature_request.md ├── FUNDING.yml └── workflows │ └── release_stable.yml ├── .gitignore ├── gradle.properties ├── gradlew.bat ├── README.md ├── font_licence.txt ├── .kotlin └── errors │ └── errors-1728076536248.log └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | A cute an elegant calculator app for Android -------------------------------------------------------------------------------- /fastlane/metadata/android/de/short_description.txt: -------------------------------------------------------------------------------- 1 | Eine simple, kleine, Open-Source Taschenrechner-App 2 | -------------------------------------------------------------------------------- /app/src/main/res/font/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/app/src/main/res/font/.DS_Store -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 |

CuteCalc is a cute an elegant calculator app for Android!

-------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/res/font/nunito_black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/app/src/main/res/font/nunito_black.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/nunito_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/app/src/main/res/font/nunito_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/nunito_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/app/src/main/res/font/nunito_light.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/nunito_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/app/src/main/res/font/nunito_medium.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/nunito_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/app/src/main/res/font/nunito_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/nunito_extrabold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/app/src/main/res/font/nunito_extrabold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/nunito_extralight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/app/src/main/res/font/nunito_extralight.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/nunito_semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/app/src/main/res/font/nunito_semibold.ttf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/01.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/02.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteCalc/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/03.png -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FAB3AA 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de/full_description.txt: -------------------------------------------------------------------------------- 1 |

CuteCalc ist eine kleine, schnelle, Open-Source Android Taschenrechner-App im Material 3 Design, die keine Berechtigungen braucht.

2 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #201A1A 4 | #f0d2ce 5 | -------------------------------------------------------------------------------- /app/src/main/res/values-v31/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @android:color/system_neutral2_900 4 | @android:color/system_accent1_100 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Dec 15 16:55:56 CET 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/ui/navigation/Screens.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutecalc.ui.navigation 2 | 3 | enum class Screens { 4 | MAIN, 5 | SETTINGS 6 | } 7 | 8 | enum class SettingsScreen { 9 | SETTINGS, 10 | LOOK_AND_FEEL, 11 | HISTORY, 12 | FORMATTING, 13 | MISC 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/domain/model/Calculation.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutecalc.domain.model 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | @Entity 7 | data class Calculation( 8 | val operation: String, 9 | val result: String, 10 | @PrimaryKey(autoGenerate = true) 11 | val id: Int = 0 12 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/data/actions/CalcAction.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutecalc.data.actions 2 | 3 | sealed interface CalcAction { 4 | data object GetResult : CalcAction 5 | data object ResetField : CalcAction 6 | data object Backspace : CalcAction 7 | data class AddToField( 8 | val value: String 9 | ) : CalcAction 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/ui/screens/calculator/components/CalcButton.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutecalc.ui.screens.calculator.components 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | data class CalcButton( 6 | val text: String, 7 | val backgroundColor: Color, 8 | val onClick: () -> Unit, 9 | val onLongClick: (() -> Unit)? = null, 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/domain/repository/HistoryDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutecalc.domain.repository 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.sosauce.cutecalc.domain.model.Calculation 6 | 7 | @Database( 8 | entities = [Calculation::class], 9 | version = 1 10 | ) 11 | abstract class HistoryDatabase : RoomDatabase() { 12 | abstract val dao: HistoryDao 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/domain/repository/HistoryState.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutecalc.domain.repository 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.mutableStateOf 5 | import com.sosauce.cutecalc.domain.model.Calculation 6 | 7 | data class HistoryState( 8 | val calculations: List = emptyList(), 9 | val operation: MutableState = mutableStateOf(""), 10 | val result: MutableState = mutableStateOf("") 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/utils/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutecalc.utils 2 | 3 | const val CUTE_MUSIC = "com.sosauce.cutemusic" 4 | const val GITHUB_RELEASES = "https://github.com/sosauce/CuteCalc/releases" 5 | const val SUPPORT_PAGE = "https://sosauce.github.io/support/" 6 | const val BACKSPACE = "backspace" 7 | 8 | object CuteTheme { 9 | const val SYSTEM = "SYSTEM" 10 | const val DARK = "DARK" 11 | const val LIGHT = "LIGHT" 12 | const val AMOLED = "AMOLED" 13 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/arrow_right.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | pluginManagement { 4 | repositories { 5 | google() 6 | mavenCentral() 7 | gradlePluginPortal() 8 | } 9 | } 10 | dependencyResolutionManagement { 11 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 12 | repositories { 13 | google() 14 | mavenCentral() 15 | maven { url = uri("https://jitpack.io") } 16 | } 17 | } 18 | 19 | rootProject.name = "CuteCalc" 20 | include(":app") 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ui-bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: UI Bug 3 | about: Use this to report an UI bug 4 | title: UI Bug 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Screenshots** 14 | If applicable, add screenshots to help explain your problem. 15 | 16 | **Smartphone (please complete the following information):** 17 | - Device: 18 | - OS: 19 | - Version: 20 | 21 | **Additional context** 22 | Add any other context about the problem here. 23 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/domain/repository/HistoryEvents.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutecalc.domain.repository 2 | 3 | import com.sosauce.cutecalc.domain.model.Calculation 4 | 5 | sealed interface HistoryEvents { 6 | 7 | data class DeleteCalculation(val calculation: Calculation) : HistoryEvents 8 | data object DeleteAllCalculation : HistoryEvents 9 | data class AddCalculation( 10 | val operation: String, 11 | val result: String, 12 | val maxHistoryItems: Long, 13 | val saveErrors: Boolean 14 | ) : HistoryEvents 15 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/back_arrow.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/close.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/amoled.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/copy.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/more_horiz.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/more_vert.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle/ 3 | build/ 4 | 5 | # Local configuration file (sdk path, etc) 6 | local.properties 7 | 8 | # Log/OS Files 9 | *.log 10 | 11 | # Android Studio generated files and folders 12 | captures/ 13 | .externalNativeBuild/ 14 | .cxx/ 15 | *.apk 16 | output.json 17 | 18 | # IntelliJ 19 | *.iml 20 | .idea/ 21 | misc.xml 22 | deploymentTargetDropDown.xml 23 | render.experimental.xml 24 | 25 | # Keystore files 26 | *.jks 27 | *.keystore 28 | 29 | # Google Services (e.g. APIs or Firebase) 30 | google-services.json 31 | 32 | # Android Profiling 33 | *.hprof 34 | .DS_Store 35 | .kotlin/sessions/kotlin-compiler-9804472835993706459.salive -------------------------------------------------------------------------------- /app/src/main/res/drawable/undo.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/logic-bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Logic Bug 3 | about: Use this to report a logic bug 4 | title: Logic Bug 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Indicate the steps to reproduce. 15 | 16 | **Expected behavior** 17 | What behavior did you expect ? 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Smartphone (please complete the following information):** 23 | - Device: 24 | - OS: 25 | - Version: 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a feature 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/dark_mode.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/ui/screens/calculator/components/DisableSoftKeyboard.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutecalc.ui.screens.calculator.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.ExperimentalComposeUiApi 5 | import androidx.compose.ui.platform.InterceptPlatformTextInput 6 | import kotlinx.coroutines.awaitCancellation 7 | 8 | // https://stackoverflow.com/a/78720287 9 | @OptIn(ExperimentalComposeUiApi::class) 10 | @Composable 11 | fun DisableSoftKeyboard( 12 | content: @Composable () -> Unit 13 | ) { 14 | InterceptPlatformTextInput( 15 | interceptor = { _, _ -> 16 | awaitCancellation() 17 | }, 18 | content = content 19 | ) 20 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/backspace_filled.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/domain/repository/HistoryDao.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutecalc.domain.repository 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.Query 7 | import com.sosauce.cutecalc.domain.model.Calculation 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | @Dao 11 | interface HistoryDao { 12 | 13 | @Insert 14 | suspend fun insertCalculation(calculation: Calculation) 15 | 16 | @Delete 17 | suspend fun deleteCalculation(calculation: Calculation) 18 | 19 | @Query("DELETE FROM calculation") 20 | suspend fun deleteAllCalculations() 21 | 22 | @Query("SELECT * FROM calculation ORDER BY id ASC") 23 | fun getAllCalculations(): Flow> 24 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/formatting.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: sosauce 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | buy_me_a_coffee: sosauce 14 | custom: https://bit.ly/sosaucePayPal 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/delete.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/ui/screens/settings/components/SettingsWithTitle.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutecalc.ui.screens.settings.components 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.res.stringResource 10 | import androidx.compose.ui.unit.dp 11 | 12 | @Composable 13 | fun SettingsWithTitle( 14 | title: Int, 15 | content: @Composable () -> Unit 16 | ) { 17 | Column { 18 | Text( 19 | text = stringResource(id = title), 20 | color = MaterialTheme.colorScheme.primary, 21 | modifier = Modifier.padding(horizontal = 34.dp, vertical = 8.dp) 22 | ) 23 | content() 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/sort_rounded.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/settings_filled.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/calculator.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/trash_rounded.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/data/sysui/QSTile.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutecalc.data.sysui 2 | 3 | import android.app.PendingIntent 4 | import android.content.Intent 5 | import android.os.Build 6 | import android.service.quicksettings.TileService 7 | import androidx.annotation.RequiresApi 8 | import com.sosauce.cutecalc.MainActivity 9 | 10 | @RequiresApi(Build.VERSION_CODES.N) 11 | class QSTile : TileService() { 12 | 13 | override fun onClick() { 14 | super.onClick() 15 | 16 | val intent = Intent(this, MainActivity::class.java) 17 | val pendingIntent = PendingIntent.getActivity( 18 | this, 19 | 0, 20 | intent, 21 | PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT 22 | ) 23 | 24 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 25 | startActivityAndCollapse(pendingIntent) 26 | } else { 27 | val newIntent = Intent(intent).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 28 | startActivity(newIntent) 29 | } 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/palette.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_splash.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/utils/ViewModelFactories.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutecalc.utils 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.ViewModelProvider 6 | import androidx.room.Room 7 | import com.sosauce.cutecalc.domain.repository.HistoryDatabase 8 | import com.sosauce.cutecalc.ui.screens.calculator.CalculatorViewModel 9 | import com.sosauce.cutecalc.ui.screens.history.HistoryViewModel 10 | 11 | 12 | class HistoryViewModelFactory(val application: Application) : ViewModelProvider.Factory { 13 | private val historyDb by lazy { 14 | Room.databaseBuilder( 15 | context = application, 16 | klass = HistoryDatabase::class.java, 17 | name = "history.db" 18 | ).build() 19 | } 20 | 21 | override fun create(modelClass: Class): T { 22 | return HistoryViewModel(historyDb.dao) as T 23 | } 24 | } 25 | 26 | class CalculatorViewModelFactory(val application: Application) : ViewModelProvider.Factory { 27 | override fun create(modelClass: Class): T { 28 | return CalculatorViewModel(application) as T 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/history_rounded.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/ui/shared_components/CuteDropdownMenuItem.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutecalc.ui.shared_components 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.foundation.shape.RoundedCornerShape 5 | import androidx.compose.material3.DropdownMenuItem 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.draw.clip 9 | import androidx.compose.ui.unit.dp 10 | 11 | /** 12 | * A dropdown menu item with some padding and clipped corners, 13 | * also adds a visible parameter, if needed. 14 | */ 15 | @Composable 16 | fun CuteDropdownMenuItem( 17 | text: @Composable () -> Unit, 18 | onClick: () -> Unit, 19 | modifier: Modifier = Modifier, 20 | leadingIcon: @Composable (() -> Unit)? = null, 21 | trailingIcon: @Composable (() -> Unit)? = null, 22 | visible: Boolean = true 23 | ) { 24 | 25 | if (visible) { 26 | DropdownMenuItem( 27 | text = text, 28 | onClick = onClick, 29 | modifier = modifier 30 | .padding(horizontal = 2.dp) 31 | .clip(RoundedCornerShape(12.dp)), 32 | leadingIcon = leadingIcon, 33 | trailingIcon = trailingIcon, 34 | ) 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/ui/shared_components/CuteNavigationButton.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalMaterial3ExpressiveApi::class) 2 | 3 | package com.sosauce.cutecalc.ui.shared_components 4 | 5 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 6 | import androidx.compose.material3.FloatingActionButton 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.MaterialShapes 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.toShape 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.res.painterResource 14 | import androidx.compose.ui.res.stringResource 15 | import com.sosauce.cutecalc.R 16 | 17 | @Composable 18 | fun CuteNavigationButton( 19 | modifier: Modifier = Modifier, 20 | onNavigateUp: () -> Unit 21 | ) { 22 | FloatingActionButton( 23 | onClick = onNavigateUp, 24 | shape = MaterialShapes.Cookie9Sided.toShape(), 25 | modifier = modifier, 26 | containerColor = MaterialTheme.colorScheme.surfaceContainer 27 | ) { 28 | Icon( 29 | painter = painterResource(R.drawable.back_arrow), 30 | contentDescription = stringResource(R.string.back) 31 | ) 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/backspace_rounded.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/light_mode.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 9 | 10 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/system_theme.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/ui/screens/history/components/DeletionConfirmationDialog.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutecalc.ui.screens.history.components 2 | 3 | import androidx.compose.material3.AlertDialog 4 | import androidx.compose.material3.ButtonDefaults 5 | import androidx.compose.material3.Icon 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Text 8 | import androidx.compose.material3.TextButton 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.res.painterResource 11 | import androidx.compose.ui.res.stringResource 12 | import com.sosauce.cutecalc.R 13 | 14 | @Composable 15 | fun DeletionConfirmationDialog( 16 | onDismissRequest: () -> Unit, 17 | onDelete: () -> Unit 18 | ) { 19 | AlertDialog( 20 | onDismissRequest = onDismissRequest, 21 | title = { Text(stringResource(R.string.clear_history)) }, 22 | text = { Text(stringResource(R.string.cant_be_undone)) }, 23 | confirmButton = { 24 | TextButton( 25 | onClick = { 26 | onDelete() 27 | onDismissRequest() 28 | }, 29 | colors = ButtonDefaults.textButtonColors( 30 | contentColor = MaterialTheme.colorScheme.error 31 | ) 32 | ) { 33 | Text(stringResource(R.string.delete)) 34 | } 35 | }, 36 | dismissButton = { 37 | TextButton( 38 | onClick = onDismissRequest 39 | ) { 40 | Text(stringResource(R.string.cancel)) 41 | } 42 | }, 43 | icon = { 44 | Icon( 45 | painter = painterResource(R.drawable.delete), 46 | contentDescription = null 47 | ) 48 | } 49 | ) 50 | } -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.13.1" 3 | composeBom = "2025.12.00" 4 | coreSplashscreen = "1.2.0" 5 | datastorePreferences = "1.2.0" 6 | keval = "1.1.1" 7 | kotlin = "2.2.21" 8 | ksp = "2.3.3" 9 | lifecycleViewmodelCompose = "2.10.0" 10 | roomCompiler = "2.8.4" 11 | roomKtx = "2.8.4" 12 | activityCompose = "1.12.1" 13 | material3 = "1.5.0-alpha10" 14 | material3Version = "1.4.0" 15 | 16 | 17 | [libraries] 18 | androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } 19 | androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" } 20 | androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } 21 | androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } 22 | androidx-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } 23 | androidx-ui = { module = "androidx.compose.ui:ui" } 24 | keval = { module = "com.notkamui.libs:keval", version.ref = "keval" } 25 | androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomCompiler" } 26 | androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomKtx" } 27 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } 28 | androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3Version" } 29 | 30 | [plugins] 31 | androidApplication = { id = "com.android.application", version.ref = "agp" } 32 | kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 33 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 34 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutecalc 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.enableEdgeToEdge 7 | import androidx.compose.foundation.isSystemInDarkTheme 8 | import androidx.compose.runtime.getValue 9 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 10 | import androidx.core.view.WindowCompat 11 | import com.sosauce.cutecalc.data.datastore.rememberAppTheme 12 | import com.sosauce.cutecalc.data.datastore.rememberShowOnLockScreen 13 | import com.sosauce.cutecalc.ui.navigation.Nav 14 | import com.sosauce.cutecalc.ui.theme.CuteCalcTheme 15 | import com.sosauce.cutecalc.utils.CuteTheme 16 | import com.sosauce.cutecalc.utils.showOnLockScreen 17 | 18 | class MainActivity : ComponentActivity() { 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | 22 | 23 | installSplashScreen() 24 | enableEdgeToEdge() 25 | setContent { 26 | val isSystemInDarkTheme = isSystemInDarkTheme() 27 | val theme by rememberAppTheme() 28 | val showOnLockScreen by rememberShowOnLockScreen() 29 | 30 | showOnLockScreen(showOnLockScreen) 31 | 32 | CuteCalcTheme { 33 | WindowCompat 34 | .getInsetsController(window, window.decorView) 35 | .apply { 36 | 37 | val isLight = 38 | if (theme == CuteTheme.SYSTEM) !isSystemInDarkTheme else theme == CuteTheme.LIGHT 39 | 40 | isAppearanceLightStatusBars = isLight 41 | isAppearanceLightNavigationBars = isLight 42 | } 43 | 44 | Nav() 45 | } 46 | } 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/ui/screens/history/HistoryViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutecalc.ui.screens.history 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.sosauce.cutecalc.domain.model.Calculation 6 | import com.sosauce.cutecalc.domain.repository.HistoryDao 7 | import com.sosauce.cutecalc.domain.repository.HistoryEvents 8 | import com.sosauce.cutecalc.utils.isErrorMessage 9 | import kotlinx.coroutines.flow.SharingStarted 10 | import kotlinx.coroutines.flow.stateIn 11 | import kotlinx.coroutines.launch 12 | 13 | class HistoryViewModel( 14 | private val dao: HistoryDao, 15 | ) : ViewModel() { 16 | 17 | val allCalculations = dao.getAllCalculations() 18 | .stateIn( 19 | viewModelScope, 20 | SharingStarted.WhileSubscribed(5000), 21 | emptyList() 22 | ) 23 | 24 | fun onEvent(event: HistoryEvents) { 25 | when (event) { 26 | is HistoryEvents.AddCalculation -> { 27 | 28 | val calculation = Calculation( 29 | operation = event.operation, 30 | result = event.result 31 | ) 32 | 33 | if (event.saveErrors || !event.result.isErrorMessage()) { 34 | viewModelScope.launch { 35 | if (allCalculations.value.size.toLong() == event.maxHistoryItems) { 36 | dao.deleteCalculation(allCalculations.value.first()) 37 | } 38 | dao.insertCalculation(calculation) 39 | } 40 | } else { 41 | return 42 | } 43 | 44 | } 45 | 46 | is HistoryEvents.DeleteCalculation -> { 47 | viewModelScope.launch { dao.deleteCalculation(event.calculation) } 48 | } 49 | 50 | is HistoryEvents.DeleteAllCalculation -> { 51 | viewModelScope.launch { dao.deleteAllCalculations() } 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/ui/screens/settings/components/LazyRowWithScrollButton.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutecalc.ui.screens.settings.components 2 | 3 | import androidx.compose.animation.slideInHorizontally 4 | import androidx.compose.animation.slideOutHorizontally 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.lazy.LazyRow 7 | import androidx.compose.foundation.lazy.items 8 | import androidx.compose.foundation.lazy.rememberLazyListState 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.IconButton 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.rememberCoroutineScope 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.res.painterResource 16 | import com.sosauce.cutecalc.R 17 | import kotlinx.coroutines.launch 18 | 19 | @Composable 20 | fun LazyRowWithScrollButton( 21 | items: List, 22 | content: @Composable (T) -> Unit 23 | ) { 24 | val state = rememberLazyListState() 25 | val scope = rememberCoroutineScope() 26 | 27 | Box { 28 | LazyRow( 29 | state = state 30 | ) { 31 | items( 32 | items = items 33 | ) { type -> 34 | content(type) 35 | } 36 | } 37 | androidx.compose.animation.AnimatedVisibility( 38 | visible = state.canScrollForward, 39 | modifier = Modifier.align(Alignment.CenterEnd), 40 | enter = slideInHorizontally { it }, 41 | exit = slideOutHorizontally { it } 42 | ) { 43 | IconButton( 44 | onClick = { 45 | scope.launch { 46 | state.animateScrollToItem(items.lastIndex) 47 | } 48 | } 49 | ) { 50 | Icon( 51 | painter = painterResource(R.drawable.arrow_right), 52 | contentDescription = null 53 | ) 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/data/datastore/SettingsExt.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutecalc.data.datastore 2 | 3 | import android.content.Context 4 | import android.content.res.Configuration 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.MutableState 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.runtime.rememberCoroutineScope 10 | import androidx.compose.ui.platform.LocalConfiguration 11 | import androidx.compose.ui.platform.LocalContext 12 | import androidx.datastore.preferences.core.Preferences 13 | import androidx.datastore.preferences.core.edit 14 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 15 | import kotlinx.coroutines.flow.Flow 16 | import kotlinx.coroutines.flow.map 17 | import kotlinx.coroutines.launch 18 | 19 | @Composable 20 | fun rememberPreference( 21 | key: Preferences.Key, 22 | defaultValue: T, 23 | ): MutableState { 24 | val coroutineScope = rememberCoroutineScope() 25 | val context = LocalContext.current 26 | val state by remember { 27 | context.dataStore.data 28 | .map { it[key] ?: defaultValue } 29 | }.collectAsStateWithLifecycle(initialValue = defaultValue) 30 | 31 | return remember(state) { 32 | object : MutableState { 33 | override var value: T 34 | get() = state 35 | set(value) { 36 | coroutineScope.launch { 37 | context.dataStore.edit { 38 | it[key] = value 39 | } 40 | } 41 | } 42 | 43 | override fun component1() = value 44 | override fun component2(): (T) -> Unit = { value = it } 45 | } 46 | } 47 | } 48 | 49 | fun getPreference( 50 | key: Preferences.Key, 51 | defaultValue: T, 52 | context: Context 53 | ): Flow = 54 | context.dataStore.data 55 | .map { preference -> 56 | preference[key] ?: defaultValue 57 | } 58 | 59 | @Composable 60 | fun rememberIsLandscape(): Boolean { 61 | val config = LocalConfiguration.current 62 | 63 | return remember(config.orientation) { 64 | config.orientation == Configuration.ORIENTATION_LANDSCAPE 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/ui/screens/settings/SettingsMisc.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.Scaffold 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.unit.dp 15 | import com.sosauce.cutecalc.R 16 | import com.sosauce.cutecalc.data.datastore.rememberShowOnLockScreen 17 | import com.sosauce.cutecalc.ui.screens.settings.components.SettingsSwitch 18 | import com.sosauce.cutecalc.ui.screens.settings.components.SettingsWithTitle 19 | import com.sosauce.cutecalc.ui.shared_components.CuteNavigationButton 20 | import com.sosauce.cutecalc.utils.selfAlignHorizontally 21 | 22 | @Composable 23 | fun SettingsMisc( 24 | onNavigateUp: () -> Unit 25 | ) { 26 | val scrollState = rememberScrollState() 27 | var showOnLockScreen by rememberShowOnLockScreen() 28 | 29 | Scaffold( 30 | bottomBar = { 31 | CuteNavigationButton( 32 | modifier = Modifier 33 | .padding(start = 15.dp) 34 | .navigationBarsPadding() 35 | .selfAlignHorizontally(Alignment.Start), 36 | onNavigateUp = onNavigateUp 37 | ) 38 | } 39 | ) { pv -> 40 | Column( 41 | modifier = Modifier 42 | .verticalScroll(scrollState) 43 | .padding(pv) 44 | ) { 45 | SettingsWithTitle( 46 | title = R.string.misc 47 | ) { 48 | SettingsSwitch( 49 | checked = showOnLockScreen, 50 | onCheckedChange = { showOnLockScreen = !showOnLockScreen }, 51 | topDp = 24.dp, 52 | bottomDp = 24.dp, 53 | text = R.string.show_ls, 54 | optionalDescription = R.string.show_ls_desc 55 | ) 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/ui/screens/settings/components/SettingsCategoryCard.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutecalc.ui.screens.settings.components 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.foundation.shape.RoundedCornerShape 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.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.res.painterResource 19 | import androidx.compose.ui.res.stringResource 20 | import androidx.compose.ui.unit.Dp 21 | import androidx.compose.ui.unit.dp 22 | 23 | @Composable 24 | fun SettingsCategoryCard( 25 | icon: Int, 26 | name: Int, 27 | description: Int, 28 | topDp: Dp, 29 | bottomDp: Dp, 30 | onNavigate: () -> Unit 31 | ) { 32 | Card( 33 | onClick = onNavigate, 34 | colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainer), 35 | modifier = Modifier 36 | .fillMaxWidth() 37 | .padding(horizontal = 16.dp, vertical = 2.dp), 38 | shape = RoundedCornerShape( 39 | topStart = topDp, 40 | topEnd = topDp, 41 | bottomStart = bottomDp, 42 | bottomEnd = bottomDp 43 | ) 44 | ) { 45 | Row( 46 | modifier = Modifier 47 | .padding(16.dp), 48 | verticalAlignment = Alignment.CenterVertically 49 | ) { 50 | Icon( 51 | painter = painterResource(icon), 52 | contentDescription = null 53 | ) 54 | Spacer(Modifier.width(15.dp)) 55 | Column { 56 | Text(stringResource(name)) 57 | Text( 58 | text = stringResource(description), 59 | color = MaterialTheme.colorScheme.onSurfaceVariant, 60 | style = MaterialTheme.typography.bodyMedium 61 | ) 62 | } 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /.github/workflows/release_stable.yml: -------------------------------------------------------------------------------- 1 | name: Release Stable CI 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | outputs: 10 | versionName: ${{ steps.versionname.outputs.versionName }} 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: set up JDK 17 15 | uses: actions/setup-java@v4 16 | with: 17 | java-version: '17' 18 | distribution: 'temurin' 19 | cache: gradle 20 | 21 | - name: Get versionName 22 | id: versionname 23 | run: echo "versionName=$(grep 'versionName' app/build.gradle.kts | head -1 | awk -F\" '{ print $2 }')" >> $GITHUB_OUTPUT 24 | 25 | - name: Grant execute permission for gradlew 26 | run: chmod +x gradlew 27 | 28 | - name: Decode Keystore 29 | id: decode_keystore 30 | uses: timheuer/base64-to-file@v1 31 | with: 32 | fileName: 'release_key.jks' 33 | fileDir: 'app/' 34 | encodedString: ${{ secrets.SIGNING_KEY }} 35 | 36 | 37 | - name: Build Release APK 38 | run: ./gradlew assembleRelease 39 | env: 40 | SIGNING_KEY_ALIAS: ${{ secrets.KEY_ALIAS }} 41 | SIGNING_KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} 42 | SIGNING_STORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} 43 | - uses: actions/upload-artifact@v4 44 | with: 45 | name: CuteCalc Release 46 | path: app/build/**/*.apk 47 | - name: Generate Changelog Content 48 | id: generate_changelog 49 | uses: release-drafter/release-drafter@v5 50 | with: 51 | disable-prerelease: true 52 | publish: false 53 | - name: Write Changelog to File 54 | run: echo "${{ steps.generate_changelog.outputs.body }}" > ${{ needs.build.outputs.versionName }}-changelog.txt 55 | 56 | release: 57 | needs: [build] 58 | runs-on: ubuntu-latest 59 | steps: 60 | - uses: actions/download-artifact@v4 61 | with: 62 | name: CuteCalc Release 63 | - name: Create Release 64 | id: create_release 65 | uses: softprops/action-gh-release@v2 66 | with: 67 | name: Vesion ${{ needs.build.outputs.versionName }} 68 | body_path: ${{ needs.build.outputs.versionName }}-changelog.txt 69 | prerelease: false 70 | tag_name: v${{ needs.build.outputs.versionName }} 71 | files: | 72 | ./**/*.apk 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/ui/screens/settings/components/FontSelector.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalMaterial3ExpressiveApi::class) 2 | 3 | package com.sosauce.cutecalc.ui.screens.settings.components 4 | 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.border 7 | import androidx.compose.foundation.clickable 8 | import androidx.compose.foundation.layout.Arrangement 9 | import androidx.compose.foundation.layout.Box 10 | import androidx.compose.foundation.layout.Column 11 | import androidx.compose.foundation.layout.Spacer 12 | import androidx.compose.foundation.layout.height 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.foundation.layout.size 15 | import androidx.compose.foundation.shape.RoundedCornerShape 16 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 17 | import androidx.compose.material3.MaterialShapes 18 | import androidx.compose.material3.MaterialTheme 19 | import androidx.compose.material3.Text 20 | import androidx.compose.material3.toShape 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.ui.Alignment 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.draw.clip 25 | import androidx.compose.ui.res.stringResource 26 | import androidx.compose.ui.unit.dp 27 | import com.sosauce.cutecalc.R 28 | import com.sosauce.cutecalc.ui.screens.settings.FontStyle 29 | 30 | @Composable 31 | fun FontSelector( 32 | item: com.sosauce.cutecalc.ui.screens.settings.FontItem 33 | ) { 34 | Column( 35 | verticalArrangement = Arrangement.Center, 36 | horizontalAlignment = Alignment.CenterHorizontally, 37 | modifier = Modifier 38 | .padding(10.dp) 39 | .height(100.dp) 40 | .clip(RoundedCornerShape(12.dp)) 41 | .clickable { item.onClick() } 42 | ) { 43 | Box( 44 | modifier = Modifier 45 | .padding(10.dp) 46 | .size(50.dp) 47 | .clip(MaterialShapes.Cookie9Sided.toShape()) 48 | .border( 49 | width = 2.dp, 50 | color = item.borderColor, 51 | shape = MaterialShapes.Cookie9Sided.toShape() 52 | ) 53 | .background(MaterialTheme.colorScheme.surfaceContainerHighest), 54 | contentAlignment = Alignment.Center 55 | ) { item.text() } 56 | Spacer(Modifier.weight(1f)) 57 | Text( 58 | text = if (item.fontStyle == FontStyle.SYSTEM) stringResource(R.string.system) else "Default" 59 | ) 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutecalc/ui/screens/settings/components/ThemeSelector.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalMaterial3ExpressiveApi::class) 2 | 3 | package com.sosauce.cutecalc.ui.screens.settings.components 4 | 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.border 7 | import androidx.compose.foundation.clickable 8 | import androidx.compose.foundation.layout.Arrangement 9 | import androidx.compose.foundation.layout.Box 10 | import androidx.compose.foundation.layout.Column 11 | import androidx.compose.foundation.layout.Spacer 12 | import androidx.compose.foundation.layout.height 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.foundation.layout.size 15 | import androidx.compose.foundation.shape.RoundedCornerShape 16 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 17 | import androidx.compose.material3.MaterialShapes 18 | import androidx.compose.material3.MaterialTheme 19 | import androidx.compose.material3.Text 20 | import androidx.compose.material3.toShape 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.Immutable 23 | import androidx.compose.ui.Alignment 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.draw.clip 26 | import androidx.compose.ui.graphics.Color 27 | import androidx.compose.ui.graphics.painter.Painter 28 | import androidx.compose.ui.unit.dp 29 | 30 | 31 | @Composable 32 | fun ThemeSelector( 33 | onClick: () -> Unit, 34 | backgroundColor: Color, 35 | icon: @Composable () -> Unit, 36 | text: String, 37 | isThemeSelected: Boolean 38 | ) { 39 | Column( 40 | verticalArrangement = Arrangement.Center, 41 | horizontalAlignment = Alignment.CenterHorizontally, 42 | modifier = Modifier 43 | .padding(10.dp) 44 | .height(100.dp) 45 | .clip(RoundedCornerShape(12.dp)) 46 | .clickable { onClick() } 47 | ) { 48 | Box( 49 | modifier = Modifier 50 | .padding(10.dp) 51 | .size(50.dp) 52 | .clip(MaterialShapes.Cookie9Sided.toShape()) 53 | .border( 54 | width = 2.dp, 55 | color = if (isThemeSelected) MaterialTheme.colorScheme.secondary else Color.Transparent, 56 | shape = MaterialShapes.Cookie9Sided.toShape() 57 | ) 58 | .background(backgroundColor), 59 | contentAlignment = Alignment.Center 60 | ) { 61 | icon() 62 | } 63 | Spacer(Modifier.weight(1f)) 64 | Text(text) 65 | } 66 | } 67 | 68 | @Immutable 69 | data class ThemeItem( 70 | val onClick: () -> Unit, 71 | val backgroundColor: Color, 72 | val text: String, 73 | val isSelected: Boolean, 74 | val iconAndTint: Pair 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 | } --------------------------------------------------------------------------------