├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── build_and_github_release.yml ├── .gitignore ├── .idea ├── .gitignore └── copyright │ ├── Erfan_Sn.xml │ └── profiles_settings.xml ├── .run └── Baseline Profile Generator.run.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro ├── schemas │ └── ir.erfansn.siliconecalculator.data.source.local.db.SiliconeCalculatorDatabase │ │ └── 1.json └── src │ ├── androidTest │ └── java │ │ └── ir │ │ └── erfansn │ │ └── siliconecalculator │ │ ├── SiliconeCalculatorTest.kt │ │ └── data │ │ └── source │ │ └── local │ │ └── db │ │ └── dao │ │ └── HistoryDaoTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── ir │ │ │ └── erfansn │ │ │ └── siliconecalculator │ │ │ ├── SiliconeCalculator.kt │ │ │ ├── SiliconeCalculatorActivity.kt │ │ │ ├── calculator │ │ │ ├── CalculatorScreen.kt │ │ │ ├── CalculatorUiState.kt │ │ │ ├── CalculatorViewModel.kt │ │ │ └── button │ │ │ │ ├── CalculatorButton.kt │ │ │ │ ├── common │ │ │ │ ├── AllClear.kt │ │ │ │ ├── Clear.kt │ │ │ │ ├── Decimal.kt │ │ │ │ └── Digit.kt │ │ │ │ ├── function │ │ │ │ ├── Equals.kt │ │ │ │ ├── NumSign.kt │ │ │ │ └── Percent.kt │ │ │ │ └── operator │ │ │ │ ├── Add.kt │ │ │ │ ├── Div.kt │ │ │ │ ├── Mul.kt │ │ │ │ └── Sub.kt │ │ │ ├── data │ │ │ ├── model │ │ │ │ └── History.kt │ │ │ ├── repository │ │ │ │ ├── HistoryRepository.kt │ │ │ │ └── HistoryRepositoryImpl.kt │ │ │ └── source │ │ │ │ └── local │ │ │ │ └── db │ │ │ │ ├── SiliconeCalculatorDatabase.kt │ │ │ │ ├── dao │ │ │ │ └── HistoryDao.kt │ │ │ │ └── model │ │ │ │ └── HistoryEntity.kt │ │ │ ├── di │ │ │ └── AppModule.kt │ │ │ ├── history │ │ │ ├── HistoryItem.kt │ │ │ ├── HistoryScreen.kt │ │ │ ├── HistoryUiState.kt │ │ │ └── HistoryViewModel.kt │ │ │ ├── navigation │ │ │ ├── AppNavigationActions.kt │ │ │ └── SiliconeCalculatorNavHost.kt │ │ │ ├── ui │ │ │ ├── SiliconeCalculatorIcon.kt │ │ │ ├── animation │ │ │ │ └── CircularRevealAnimation.kt │ │ │ ├── component │ │ │ │ ├── CorneredFlatButton.kt │ │ │ │ └── NeuButton.kt │ │ │ ├── layout │ │ │ │ └── Grid.kt │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Shape.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ └── util │ │ │ ├── DateConverter.kt │ │ │ ├── DateFormatter.kt │ │ │ ├── Evaluator.kt │ │ │ ├── MathExpressionFormatter.kt │ │ │ └── SafeUri.kt │ └── res │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── ir │ └── erfansn │ └── siliconecalculator │ ├── calculator │ ├── CalculatorScreenTest.kt │ ├── CalculatorViewModelTest.kt │ └── button │ │ └── common │ │ └── ClearTest.kt │ ├── data │ └── repository │ │ └── FakeHistoryRepository.kt │ ├── history │ └── HistoryScreenTest.kt │ ├── rule │ └── MainDispatcherRule.kt │ └── util │ ├── DateFormatterTest.kt │ ├── EvaluatorTest.kt │ └── MathExpressionFormatterTest.kt ├── benchmark ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── ir │ └── erfansn │ └── siliconecalculator │ ├── Utils.kt │ ├── benchmark │ └── StartupBenchmark.kt │ └── profiler │ ├── BaselineProfileGenerator.kt │ └── StartupProfileGenerator.kt ├── build.gradle.kts ├── buildSrc ├── .gitignore ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── Configs.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── media ├── banner.png ├── preview0.gif ├── preview1.png ├── preview2.png └── summary.png └── settings.gradle.kts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://plisio.net/donate/drrliVek'] 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gradle" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | labels: [ "version update" ] 13 | open-pull-requests-limit: 10 14 | registries: "*" 15 | groups: 16 | kotlin-related-plugins: 17 | patterns: 18 | - "org.jetbrains.kotlin.android" 19 | - "com.google.devtools.ksp" 20 | - "org.jetbrains.kotlin.plugin.compose" 21 | -------------------------------------------------------------------------------- /.github/workflows/build_and_github_release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Github Release 2 | run-name: Cooking the next version 3 | 4 | on: 5 | push: 6 | tags: 7 | - 'v*' 8 | jobs: 9 | build: 10 | name: Build APK 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Enable KVM group perms 14 | run: | 15 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules 16 | sudo udevadm control --reload-rules 17 | sudo udevadm trigger --name-match=kvm 18 | ls /dev/kvm 19 | 20 | - name: Checkout repository 21 | uses: actions/checkout@v4.1.4 22 | 23 | - name: Set up JDK 21 24 | uses: actions/setup-java@v4.2.1 25 | with: 26 | java-version: '21' 27 | distribution: 'temurin' 28 | 29 | - name: Setup Android SDK 30 | uses: android-actions/setup-android@v3.2.1 31 | with: 32 | cmdline-tools-version: 11076708 33 | 34 | - name: Grant execute permission for gradlew 35 | run: chmod +x gradlew 36 | 37 | - name: Decode and persist temporarily Keystore 38 | run: | 39 | DEST_FILE_PATH="${RUNNER_TEMP}"/release-key.jks 40 | echo "KEYSTORE_FILE_PATH=$DEST_FILE_PATH" >> "$GITHUB_ENV" 41 | base64 -d <<< "${{ secrets.RELEASE_STORE_FILE_BASE64 }}" > $DEST_FILE_PATH 42 | 43 | - name: Assemble release variant including baseline profile generation 44 | run: ./gradlew :app:assembleRelease 45 | -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=BaselineProfile 46 | -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" 47 | -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true 48 | -Pandroid.experimental.androidTest.numManagedDeviceShards=1 49 | -Pandroid.experimental.testOptions.managedDevices.maxConcurrentDevices=1 50 | env: 51 | RELEASE_STORE_FILE_PATH: ${{ env.KEYSTORE_FILE_PATH }} 52 | RELEASE_KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }} 53 | RELEASE_KEY_PASS: ${{ secrets.RELEASE_KEY_PASS }} 54 | RELEASE_STORE_PASS: ${{ secrets.RELEASE_STORE_PASS }} 55 | 56 | - name: Upload APK 57 | uses: actions/upload-artifact@v4.3.3 58 | with: 59 | name: app-release 60 | path: app/build/outputs/apk/release/app-release.apk 61 | 62 | release: 63 | name: Release 64 | needs: build 65 | permissions: 66 | contents: write 67 | runs-on: ubuntu-latest 68 | steps: 69 | - name: Download APK from build 70 | uses: actions/download-artifact@v4.1.7 71 | with: 72 | name: app-release 73 | 74 | - name: Create release 75 | uses: softprops/action-gh-release@v2.0.4 76 | with: 77 | files: app-release.apk 78 | generate_release_notes: true 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # files for the dex VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # generated files 12 | /bin 13 | /gen 14 | /out 15 | /build 16 | /.kotlin 17 | 18 | # Local configuration file (sdk path, etc) 19 | local.properties 20 | 21 | # Eclipse project files 22 | .classpath 23 | .project 24 | 25 | # Windows thumbnail db 26 | .DS_Store 27 | 28 | # Gradle cache 29 | .gradle 30 | 31 | # Sandbox stuff 32 | _sandbox 33 | 34 | # Android Studio captures folder 35 | /captures -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /* 3 | !/copyright 4 | # Keep the code styles. 5 | !/codeStyles 6 | /codeStyles/* 7 | !/codeStyles/Project.xml 8 | !/codeStyles/codeStyleConfig.xml 9 | -------------------------------------------------------------------------------- /.idea/copyright/Erfan_Sn.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.run/Baseline Profile Generator.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![App banner](media/banner.png) 2 | 3 |

4 | License 5 | API 6 | API 7 |

8 | 9 | It's a cloned version of the [Apple calculator](https://apps.apple.com/us/app/calculator/id1069511488) with [Jetpack Compose](https://developer.android.com/jetpack/compose), but with more attractive graphics, history for calculations, and dark and light theme support. 10 | 11 | Design by [SINTHAI](https://www.behance.net/gallery/130717085/The-calculator-) with a slight change. 12 | 13 | ## 📷 Preview 14 |

15 | drawing 16 | drawing 17 | drawing 18 |

19 | 20 | ## 🏛️ Architecture 21 | The architecture used in this project is [Android recommended architecture](https://developer.android.com/courses/pathways/android-architecture), 22 | which I personally believe is a combination of some layers and principles of three architectures MVVM, MVI and Clean, which makes it great. 23 | 24 | ## 🧪 Testing 25 | For this project, **unit**, **integration**, **end-to-end** tests have been written based on the pyramid test in the Android test [codelab](https://developer.android.com/codelabs/advanced-android-kotlin-training-testing-test-doubles#2), and in addition, 26 | a **benchmark** test has been written for the app startup states to measure the effectiveness of using the Baseline Profile. 27 | 28 | ## 🛠️ Tech Stack 29 | - **UI layer** 30 | - Jetpack Compose 31 | - ViewModel component 32 | - Constraint Layout 33 | - Compose Navigation 34 | - Material 2 35 | - **Data layer** 36 | - Room database 37 | - Dagger Hilt 38 | - Kotlinx Coroutine 39 | - Mxparser 40 | - Kotlinx datatime 41 | - **Testing** 42 | - Truth 43 | - Turbine 44 | - Mockk 45 | - Robolectric 46 | - Androidx Benchmark 47 | - **Other** 48 | - Profiler 49 | - Desugar jdk libs 50 | 51 | ## 💯 MAD Score 52 | ![summary](media/summary.png) 53 | 54 | ## License 55 | ``` 56 | Copyright 2022 Erfan Sn 57 | Licensed under the Apache License, Version 2.0 (the "License"); 58 | you may not use this file except in compliance with the License. 59 | You may obtain a copy of the License at 60 | 61 | http://www.apache.org/licenses/LICENSE-2.0 62 | 63 | Unless required by applicable law or agreed to in writing, software 64 | distributed under the License is distributed on an "AS IS" BASIS, 65 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 66 | See the License for the specific language governing permissions and 67 | limitations under the License. 68 | ``` 69 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | alias(libs.plugins.android.application) 19 | alias(libs.plugins.kotlin.android) 20 | alias(libs.plugins.ksp) 21 | alias(libs.plugins.dagger.hilt) 22 | alias(libs.plugins.kotlin.compose) 23 | alias(libs.plugins.androidx.baselineprofile) 24 | alias(libs.plugins.androidx.room) 25 | } 26 | 27 | kotlin { 28 | jvmToolchain(Configs.JVM_TOOLCHAIN_VERSION) 29 | } 30 | 31 | android { 32 | compileSdk = Configs.COMPILE_SDK_VERSION 33 | namespace = Configs.PACKAGE_NAME 34 | 35 | defaultConfig { 36 | applicationId = Configs.PACKAGE_NAME 37 | minSdk = Configs.MIN_SDK_VERSION 38 | targetSdk = Configs.TARGET_SDK_VERSION 39 | versionCode = 6 40 | versionName = "2.2.0" 41 | 42 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 43 | vectorDrawables { 44 | useSupportLibrary = true 45 | } 46 | } 47 | 48 | signingConfigs { 49 | create("release") { 50 | storeFile = file(System.getenv("RELEASE_STORE_FILE_PATH")) 51 | storePassword = System.getenv("RELEASE_STORE_PASS") 52 | keyAlias = System.getenv("RELEASE_KEY_ALIAS") 53 | keyPassword = System.getenv("RELEASE_KEY_PASS") 54 | } 55 | } 56 | buildTypes { 57 | release { 58 | isMinifyEnabled = true 59 | isShrinkResources = true 60 | proguardFiles( 61 | getDefaultProguardFile("proguard-android-optimize.txt"), 62 | "proguard-rules.pro" 63 | ) 64 | signingConfig = signingConfigs.getByName("release") 65 | } 66 | } 67 | compileOptions { 68 | isCoreLibraryDesugaringEnabled = true 69 | } 70 | 71 | buildFeatures { 72 | compose = true 73 | } 74 | packaging { 75 | resources { 76 | excludes += listOf("/META-INF/{AL2.0,LGPL2.1}", "DebugProbesKt.bin") 77 | } 78 | } 79 | 80 | testOptions { 81 | unitTests { 82 | isIncludeAndroidResources = true 83 | } 84 | } 85 | } 86 | 87 | baselineProfile { 88 | dexLayoutOptimization = true 89 | automaticGenerationDuringBuild = true 90 | saveInSrc = false 91 | } 92 | 93 | composeCompiler { 94 | enableStrongSkippingMode = true 95 | } 96 | 97 | room { 98 | schemaDirectory("$projectDir/schemas") 99 | } 100 | 101 | dependencies { 102 | baselineProfile(project(":benchmark")) 103 | 104 | coreLibraryDesugaring(libs.desugar.jdk.libs) 105 | 106 | implementation(libs.core.ktx) 107 | implementation(libs.appcompat) 108 | implementation(libs.profileinstaller) 109 | implementation(libs.room.runtime) 110 | implementation(libs.room.ktx) 111 | 112 | implementation(libs.hilt.navigation.compose) 113 | implementation(libs.hilt.android) 114 | ksp(libs.hilt.compiler) 115 | 116 | implementation(libs.mathparser.org.mxparser) 117 | implementation(libs.kotlinx.datetime) 118 | implementation(libs.kotlinx.coroutines.android) 119 | 120 | val composeBom = platform(libs.androidx.compose.bom) 121 | implementation(composeBom) 122 | implementation(libs.material) 123 | implementation(libs.material.icons.extended) 124 | implementation(libs.ui.tooling.preview) 125 | 126 | implementation(libs.activity.compose) 127 | implementation(libs.constraintlayout.compose) 128 | implementation(libs.navigation.compose) 129 | implementation(libs.lifecycle.viewmodel.compose) 130 | implementation(libs.lifecycle.runtime.compose) 131 | 132 | testImplementation(composeBom) 133 | testImplementation(libs.junit) 134 | testImplementation(libs.truth) 135 | testImplementation(libs.kotlinx.coroutines.test) 136 | testImplementation(libs.turbine) 137 | testImplementation(libs.mockk.android) 138 | testImplementation(libs.mockk.agent) 139 | testImplementation(libs.robolectric) 140 | testImplementation(libs.ext.junit) 141 | testImplementation(libs.ui.test.junit4) 142 | 143 | androidTestImplementation(composeBom) 144 | androidTestImplementation(libs.ext.junit) 145 | androidTestImplementation(libs.androidx.ui.test.junit4) 146 | 147 | ksp(libs.room.compiler) 148 | 149 | debugImplementation(libs.androidx.ui.tooling) 150 | debugImplementation(libs.androidx.ui.test.manifest) 151 | } 152 | -------------------------------------------------------------------------------- /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.kts. 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 22 | -------------------------------------------------------------------------------- /app/schemas/ir.erfansn.siliconecalculator.data.source.local.db.SiliconeCalculatorDatabase/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 1, 5 | "identityHash": "8371c7ca03afa6640c1b034b87386d83", 6 | "entities": [ 7 | { 8 | "tableName": "History", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `epoch_day` INTEGER NOT NULL, `expression` TEXT NOT NULL, `result` TEXT NOT NULL)", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "INTEGER", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "date", 19 | "columnName": "epoch_day", 20 | "affinity": "INTEGER", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "expression", 25 | "columnName": "expression", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "result", 31 | "columnName": "result", 32 | "affinity": "TEXT", 33 | "notNull": true 34 | } 35 | ], 36 | "primaryKey": { 37 | "columnNames": [ 38 | "id" 39 | ], 40 | "autoGenerate": true 41 | }, 42 | "indices": [], 43 | "foreignKeys": [] 44 | } 45 | ], 46 | "views": [], 47 | "setupQueries": [ 48 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 49 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8371c7ca03afa6640c1b034b87386d83')" 50 | ] 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/ir/erfansn/siliconecalculator/SiliconeCalculatorTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator 18 | 19 | import androidx.compose.ui.ExperimentalComposeUiApi 20 | import androidx.compose.ui.test.assertTextEquals 21 | import androidx.compose.ui.test.junit4.createAndroidComposeRule 22 | import androidx.compose.ui.test.onChildren 23 | import androidx.compose.ui.test.onFirst 24 | import androidx.compose.ui.test.onNodeWithContentDescription 25 | import androidx.compose.ui.test.onNodeWithTag 26 | import androidx.compose.ui.test.performClick 27 | import ir.erfansn.siliconecalculator.calculator.button.common.AllClear 28 | import ir.erfansn.siliconecalculator.calculator.button.function.Equals 29 | import ir.erfansn.siliconecalculator.calculator.button.operator.Add 30 | import org.junit.Rule 31 | import org.junit.Test 32 | 33 | @ExperimentalComposeUiApi 34 | class SiliconeCalculatorTest { 35 | 36 | @get:Rule 37 | val composeRule = createAndroidComposeRule() 38 | 39 | @Test 40 | fun savedCalculation_whenRetrieveItFromHistory_showItAsCurrentCalculation() { 41 | with(composeRule) { 42 | onNodeWithContentDescription(activity.getString(R.string.calculations_history)) 43 | .performClick() 44 | onNodeWithContentDescription(activity.getString(R.string.clear_history)) 45 | .performClick() 46 | onNodeWithTag("history:clear") 47 | .performClick() 48 | 49 | listOf("1", "2", Add.symbol, "3", "4", Equals.symbol).forEach { 50 | onNodeWithTag("calculator:$it") 51 | .performClick() 52 | } 53 | onNodeWithTag("calculator:expression") 54 | .assertTextEquals("12 + 34") 55 | onNodeWithTag("calculator:result") 56 | .assertTextEquals("46.0") 57 | 58 | onNodeWithTag("calculator:${AllClear.symbol}") 59 | .performClick() 60 | onNodeWithTag("calculator:expression") 61 | .assertTextEquals("") 62 | onNodeWithTag("calculator:result") 63 | .assertTextEquals("0") 64 | 65 | onNodeWithContentDescription(activity.getString(R.string.calculations_history)) 66 | .performClick() 67 | onNodeWithTag("history:items") 68 | .onChildren() 69 | .onFirst() 70 | .performClick() 71 | 72 | onNodeWithTag("calculator:expression") 73 | .assertTextEquals("12 + 34") 74 | onNodeWithTag("calculator:result") 75 | .assertTextEquals("46.0") 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/androidTest/java/ir/erfansn/siliconecalculator/data/source/local/db/dao/HistoryDaoTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.data.source.local.db.dao 18 | 19 | import android.content.Context 20 | import androidx.room.Room 21 | import androidx.test.core.app.ApplicationProvider 22 | import androidx.test.ext.junit.runners.AndroidJUnit4 23 | import ir.erfansn.siliconecalculator.data.source.local.db.SiliconeCalculatorDatabase 24 | import ir.erfansn.siliconecalculator.data.source.local.db.model.HistoryEntity 25 | import kotlinx.coroutines.ExperimentalCoroutinesApi 26 | import kotlinx.coroutines.flow.first 27 | import kotlinx.coroutines.test.runTest 28 | import org.junit.After 29 | import org.junit.Assert.assertEquals 30 | import org.junit.Before 31 | import org.junit.Test 32 | import org.junit.runner.RunWith 33 | 34 | @ExperimentalCoroutinesApi 35 | @RunWith(AndroidJUnit4::class) 36 | class HistoryDaoTest { 37 | 38 | private lateinit var database: SiliconeCalculatorDatabase 39 | private lateinit var historyDao: HistoryDao 40 | 41 | @Before 42 | fun createDatabase() { 43 | val context = ApplicationProvider.getApplicationContext() 44 | database = Room.inMemoryDatabaseBuilder( 45 | context, 46 | SiliconeCalculatorDatabase::class.java 47 | ).build() 48 | 49 | historyDao = database.historyDao() 50 | } 51 | 52 | @After 53 | fun closeDatabase() { 54 | database.close() 55 | } 56 | 57 | @Test 58 | fun oneEntity_whenInserts_retrievesCorrectly() = runTest { 59 | val historyEntity = HistoryEntity( 60 | id = 1, 61 | expression = "1 + 0", 62 | result = "1" 63 | ) 64 | 65 | historyDao.insertHistoryEntity(historyEntity) 66 | 67 | assertEquals(historyDao.getHistoryEntitiesStream().first(), listOf(historyEntity)) 68 | } 69 | 70 | @Test 71 | fun autoGenerateIdEntities_whenInsertsAndDeletes_worksCorrectly() = runTest { 72 | val historyEntities = testHistoryEntities 73 | 74 | historyEntities.forEach { historyDao.insertHistoryEntity(it) } 75 | assertEquals(historyDao.getHistoryEntitiesStream().first().size, historyEntities.size) 76 | 77 | historyDao.deleteAllHistoryEntities() 78 | assertEquals(historyDao.getHistoryEntitiesStream().first(), emptyList()) 79 | } 80 | } 81 | 82 | private val testHistoryEntities = listOf( 83 | HistoryEntity( 84 | expression = "0 + 0", 85 | result = "0" 86 | ), 87 | HistoryEntity( 88 | expression = "1 + 0", 89 | result = "1" 90 | ), 91 | HistoryEntity( 92 | expression = "0 + 1", 93 | result = "1" 94 | ), 95 | HistoryEntity( 96 | expression = "1 + 1", 97 | result = "2" 98 | ) 99 | ) 100 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 33 | 36 | 37 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/SiliconeCalculator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator 18 | 19 | import android.app.Application 20 | import dagger.hilt.android.AndroidEntryPoint 21 | import dagger.hilt.android.HiltAndroidApp 22 | 23 | @HiltAndroidApp 24 | class SiliconeCalculator : Application() 25 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/SiliconeCalculatorActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator 18 | 19 | import android.graphics.Color 20 | import android.os.Build 21 | import android.os.Bundle 22 | import androidx.activity.ComponentActivity 23 | import androidx.activity.SystemBarStyle 24 | import androidx.activity.compose.setContent 25 | import androidx.activity.enableEdgeToEdge 26 | import androidx.compose.animation.core.tween 27 | import androidx.compose.foundation.isSystemInDarkTheme 28 | import androidx.compose.material.MaterialTheme 29 | import androidx.compose.material.Surface 30 | import androidx.compose.runtime.DisposableEffect 31 | import androidx.compose.runtime.getValue 32 | import androidx.compose.runtime.mutableStateOf 33 | import androidx.compose.runtime.remember 34 | import androidx.compose.runtime.setValue 35 | import androidx.compose.ui.ExperimentalComposeUiApi 36 | import androidx.compose.ui.Modifier 37 | import androidx.compose.ui.semantics.semantics 38 | import androidx.compose.ui.semantics.testTagsAsResourceId 39 | import androidx.navigation.compose.rememberNavController 40 | import dagger.hilt.android.AndroidEntryPoint 41 | import ir.erfansn.siliconecalculator.navigation.SiliconeCalculatorNavHost 42 | import ir.erfansn.siliconecalculator.ui.animation.CircularReveal 43 | import ir.erfansn.siliconecalculator.ui.theme.SiliconeCalculatorTheme 44 | 45 | @ExperimentalComposeUiApi 46 | @AndroidEntryPoint 47 | class SiliconeCalculatorActivity : ComponentActivity() { 48 | 49 | override fun onCreate(savedInstanceState: Bundle?) { 50 | super.onCreate(savedInstanceState) 51 | 52 | setContent { 53 | val isSystemDark = isSystemInDarkTheme() 54 | var darkTheme by remember { mutableStateOf(isSystemDark) } 55 | DisposableEffect(darkTheme) { 56 | val transparentStyle = SystemBarStyle.auto( 57 | lightScrim = Color.TRANSPARENT, 58 | darkScrim = Color.TRANSPARENT, 59 | detectDarkMode = { darkTheme } 60 | ) 61 | enableEdgeToEdge( 62 | navigationBarStyle = transparentStyle, 63 | statusBarStyle = transparentStyle 64 | ) 65 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 66 | window.isNavigationBarContrastEnforced = false 67 | } 68 | onDispose { } 69 | } 70 | 71 | val navController = rememberNavController() 72 | CircularReveal( 73 | expanded = darkTheme, 74 | animationSpec = tween(500) 75 | ) { isDark -> 76 | SiliconeCalculatorTheme(darkTheme = isDark) { 77 | Surface( 78 | modifier = Modifier.semantics { testTagsAsResourceId = true }, 79 | color = MaterialTheme.colors.background 80 | ) { 81 | SiliconeCalculatorNavHost( 82 | navController = navController, 83 | onThemeToggle = { 84 | darkTheme = !darkTheme 85 | } 86 | ) 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/calculator/CalculatorUiState.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.calculator 18 | 19 | import androidx.compose.runtime.Immutable 20 | import ir.erfansn.siliconecalculator.data.model.Calculation 21 | 22 | @Immutable 23 | data class CalculatorUiState( 24 | val calculation: Calculation = Calculation() 25 | ) 26 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/calculator/CalculatorViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.calculator 18 | 19 | import androidx.lifecycle.SavedStateHandle 20 | import androidx.lifecycle.ViewModel 21 | import androidx.lifecycle.viewModelScope 22 | import dagger.hilt.android.lifecycle.HiltViewModel 23 | import ir.erfansn.siliconecalculator.calculator.button.CalculatorButton 24 | import ir.erfansn.siliconecalculator.calculator.button.calculatorButtonsInOrderAllClear 25 | import ir.erfansn.siliconecalculator.calculator.button.calculatorButtonsInOrderClear 26 | import ir.erfansn.siliconecalculator.calculator.button.common.AllClear 27 | import ir.erfansn.siliconecalculator.calculator.button.common.Decimal 28 | import ir.erfansn.siliconecalculator.calculator.button.common.Digit 29 | import ir.erfansn.siliconecalculator.calculator.button.function.Equals 30 | import ir.erfansn.siliconecalculator.calculator.button.function.NumSign 31 | import ir.erfansn.siliconecalculator.calculator.button.function.Percent 32 | import ir.erfansn.siliconecalculator.data.model.Calculation 33 | import ir.erfansn.siliconecalculator.data.repository.HistoryRepository 34 | import ir.erfansn.siliconecalculator.navigation.SiliconeCalculatorDestinationsArg.EXPRESSION_ARG 35 | import ir.erfansn.siliconecalculator.navigation.SiliconeCalculatorDestinationsArg.RESULT_ARG 36 | import kotlinx.coroutines.CoroutineDispatcher 37 | import kotlinx.coroutines.flow.MutableStateFlow 38 | import kotlinx.coroutines.flow.SharingStarted 39 | import kotlinx.coroutines.flow.asStateFlow 40 | import kotlinx.coroutines.flow.launchIn 41 | import kotlinx.coroutines.flow.map 42 | import kotlinx.coroutines.flow.onEach 43 | import kotlinx.coroutines.flow.stateIn 44 | import kotlinx.coroutines.flow.update 45 | import kotlinx.coroutines.launch 46 | import javax.inject.Inject 47 | 48 | @HiltViewModel 49 | class CalculatorViewModel @Inject constructor( 50 | savedStateHandle: SavedStateHandle, 51 | private val historyRepository: HistoryRepository, 52 | private val defaultDispatcher: CoroutineDispatcher, 53 | ) : ViewModel() { 54 | 55 | private var _calculation = MutableStateFlow(Calculation()) 56 | private val currentCalculation get() = _calculation.value 57 | 58 | private var previousExpression = currentCalculation.expression 59 | 60 | private val _calculatorButtons = MutableStateFlow(listOf()) 61 | val calculatorButtons = _calculatorButtons.asStateFlow() 62 | 63 | val uiState = _calculation 64 | .map(::CalculatorUiState) 65 | .stateIn( 66 | scope = viewModelScope, 67 | started = SharingStarted.WhileSubscribed(), 68 | initialValue = CalculatorUiState() 69 | ) 70 | 71 | init { 72 | updateCalculatorDisplay( 73 | expression = savedStateHandle[EXPRESSION_ARG], 74 | result = savedStateHandle[RESULT_ARG] 75 | ) 76 | 77 | _calculation 78 | .onEach { calculation -> 79 | _calculatorButtons.update { 80 | if (!calculation.isNotEvaluated || calculation.resultIsInvalid) { 81 | calculatorButtonsInOrderAllClear 82 | } else { 83 | calculatorButtonsInOrderClear 84 | } 85 | } 86 | } 87 | .launchIn(viewModelScope) 88 | } 89 | 90 | private fun updateCalculatorDisplay(expression: String?, result: String?) { 91 | if (expression == null || result == null) return 92 | 93 | _calculation.update { 94 | it.copy(expression = expression, result = result) 95 | } 96 | 97 | previousExpression = expression 98 | } 99 | 100 | fun performCalculatorButton(calculatorButton: CalculatorButton): Boolean { 101 | if (currentCalculation.resultIsInvalid && calculatorButton != AllClear) return false 102 | if (!currentCalculation.isNotEvaluated && (calculatorButton is Digit || calculatorButton in listOf(Decimal, NumSign, Percent))) return false 103 | 104 | viewModelScope.launch(defaultDispatcher) { 105 | _calculation.update { 106 | calculatorButton.perform(it).also { result -> 107 | if (calculatorButton == Equals) saveCalculationInHistory(result) 108 | } 109 | } 110 | } 111 | return true 112 | } 113 | 114 | private suspend fun saveCalculationInHistory(calculation: Calculation) { 115 | if (calculation.expression == previousExpression || calculation.isNotEvaluated || calculation.resultIsInvalid) return 116 | 117 | historyRepository.saveCalculation(calculation) 118 | 119 | previousExpression = calculation.expression 120 | } 121 | 122 | private val Calculation.isNotEvaluated 123 | get() = expression.endsWith(lastOperator) || expression.isEmpty() 124 | } 125 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/calculator/button/CalculatorButton.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.calculator.button 18 | 19 | import ir.erfansn.siliconecalculator.calculator.button.common.AllClear 20 | import ir.erfansn.siliconecalculator.calculator.button.common.Clear 21 | import ir.erfansn.siliconecalculator.calculator.button.common.Decimal 22 | import ir.erfansn.siliconecalculator.calculator.button.common.Digit 23 | import ir.erfansn.siliconecalculator.calculator.button.function.Equals 24 | import ir.erfansn.siliconecalculator.calculator.button.function.NumSign 25 | import ir.erfansn.siliconecalculator.calculator.button.function.Percent 26 | import ir.erfansn.siliconecalculator.calculator.button.operator.Add 27 | import ir.erfansn.siliconecalculator.calculator.button.operator.Div 28 | import ir.erfansn.siliconecalculator.calculator.button.operator.Mul 29 | import ir.erfansn.siliconecalculator.calculator.button.operator.Sub 30 | import ir.erfansn.siliconecalculator.data.model.Calculation 31 | import ir.erfansn.siliconecalculator.util.Evaluator 32 | 33 | abstract class CalculatorButton(val symbol: String) { 34 | open val applier = { n: String -> "$n $symbol " } 35 | abstract fun perform(calculation: Calculation): Calculation 36 | } 37 | 38 | open class FunctionButton(symbol: String) : CalculatorButton(symbol) { 39 | 40 | protected val evaluator = Evaluator() 41 | 42 | override fun perform(calculation: Calculation): Calculation { 43 | if (calculation.result == "0") return calculation 44 | 45 | evaluator.expression = applier(calculation.result) 46 | 47 | return calculation.copy(result = evaluator.eval()) 48 | } 49 | } 50 | 51 | open class OperatorButton(symbol: String) : CalculatorButton(symbol) { 52 | 53 | override fun perform(calculation: Calculation): Calculation { 54 | if (calculation.expression.isEmpty() && calculation.result == "0") return calculation 55 | 56 | val amendedExpression = when { 57 | calculation.result == "0" -> calculation.expression.substringBeforeLast(calculation.lastOperator) 58 | calculation.expression.endsWith(calculation.lastOperator) -> calculation.expression.plus(calculation.result) 59 | else -> calculation.result 60 | } 61 | 62 | return calculation.copy( 63 | expression = applier(amendedExpression), 64 | result = "0" 65 | ) 66 | } 67 | } 68 | 69 | val calculatorButtonsInOrderClear = listOf( 70 | Clear, 71 | NumSign, 72 | Percent, 73 | Div, 74 | Digit('7'), 75 | Digit('8'), 76 | Digit('9'), 77 | Mul, 78 | Digit('4'), 79 | Digit('5'), 80 | Digit('6'), 81 | Sub, 82 | Digit('1'), 83 | Digit('2'), 84 | Digit('3'), 85 | Add, 86 | Digit('0'), 87 | Decimal, 88 | Equals, 89 | ) 90 | 91 | val calculatorButtonsInOrderAllClear = calculatorButtonsInOrderClear.toMutableList() 92 | .also { 93 | it[0] = AllClear 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/calculator/button/common/AllClear.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.calculator.button.common 18 | 19 | import ir.erfansn.siliconecalculator.calculator.button.CalculatorButton 20 | import ir.erfansn.siliconecalculator.data.model.Calculation 21 | 22 | data object AllClear : CalculatorButton("AC") { 23 | 24 | override val applier: (String) -> String = { "" } 25 | 26 | override fun perform(calculation: Calculation): Calculation { 27 | return calculation.copy( 28 | expression = applier(calculation.expression), 29 | result = "0" 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/calculator/button/common/Clear.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.calculator.button.common 18 | 19 | import ir.erfansn.siliconecalculator.calculator.button.CalculatorButton 20 | import ir.erfansn.siliconecalculator.data.model.Calculation 21 | import ir.erfansn.siliconecalculator.util.DECIMAL_REGEX 22 | 23 | data object Clear : CalculatorButton("C") { 24 | 25 | override val applier: (String) -> String = { 26 | it.substring(0, it.length - 1).let { reducedString -> 27 | if (reducedString.isEmpty() || reducedString == "-") { 28 | "0" 29 | } else { 30 | reducedString 31 | } 32 | } 33 | } 34 | 35 | override fun perform(calculation: Calculation): Calculation { 36 | val (amendExpression, amendResult) = if (calculation.expression.isEmpty() || applier(calculation.result) != "0") { 37 | calculation.expression to applier(calculation.result) 38 | } else { 39 | calculation.expression.substringBeforeLast(calculation.lastNumber) to calculation.lastNumber 40 | } 41 | 42 | return calculation.copy( 43 | expression = amendExpression, 44 | result = amendResult 45 | ) 46 | } 47 | 48 | private val Calculation.lastNumber 49 | get() = DECIMAL_REGEX.toRegex().findAll(expression).last().value 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/calculator/button/common/Decimal.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.calculator.button.common 18 | 19 | import ir.erfansn.siliconecalculator.calculator.button.CalculatorButton 20 | import ir.erfansn.siliconecalculator.data.model.Calculation 21 | 22 | data object Decimal : CalculatorButton(".") { 23 | 24 | override val applier: (String) -> String = { n -> "$n$symbol" } 25 | 26 | override fun perform(calculation: Calculation): Calculation { 27 | if (symbol in calculation.result) return calculation 28 | 29 | return calculation.copy(result = applier(calculation.result)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/calculator/button/common/Digit.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.calculator.button.common 18 | 19 | import ir.erfansn.siliconecalculator.calculator.button.CalculatorButton 20 | import ir.erfansn.siliconecalculator.data.model.Calculation 21 | 22 | data class Digit(val digit: Char) : CalculatorButton("$digit") { 23 | 24 | override val applier: (String) -> String = { n -> "$n$digit" } 25 | 26 | override fun perform(calculation: Calculation): Calculation { 27 | val amendedResult = calculation.result.takeUnless { it == "0" }.orEmpty() 28 | 29 | return calculation.copy(result = applier(amendedResult)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/calculator/button/function/Equals.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.calculator.button.function 18 | 19 | import ir.erfansn.siliconecalculator.calculator.button.FunctionButton 20 | import ir.erfansn.siliconecalculator.data.model.Calculation 21 | 22 | data object Equals : FunctionButton("=") { 23 | 24 | override val applier: (String) -> String = { it } 25 | 26 | override fun perform(calculation: Calculation): Calculation { 27 | if (!calculation.isComplete) return calculation 28 | 29 | val amendedExpression = if (calculation.result == "0") 30 | calculation.expression.substringBeforeLast(calculation.lastOperator) 31 | else 32 | calculation.expression.plus(calculation.result) 33 | 34 | evaluator.expression = applier(amendedExpression) 35 | 36 | return calculation.copy(expression = evaluator.expression, result = evaluator.eval()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/calculator/button/function/NumSign.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.calculator.button.function 18 | 19 | import ir.erfansn.siliconecalculator.calculator.button.FunctionButton 20 | 21 | data object NumSign : FunctionButton("±") { 22 | 23 | override val applier: (String) -> String = { n -> "-$n" } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/calculator/button/function/Percent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.calculator.button.function 18 | 19 | import ir.erfansn.siliconecalculator.calculator.button.FunctionButton 20 | 21 | data object Percent : FunctionButton("%") { 22 | 23 | override val applier: (String) -> String = { n -> "$n$symbol" } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/calculator/button/operator/Add.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.calculator.button.operator 18 | 19 | import ir.erfansn.siliconecalculator.calculator.button.OperatorButton 20 | 21 | data object Add : OperatorButton("+") 22 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/calculator/button/operator/Div.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.calculator.button.operator 18 | 19 | import ir.erfansn.siliconecalculator.calculator.button.OperatorButton 20 | 21 | data object Div : OperatorButton("÷") 22 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/calculator/button/operator/Mul.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.calculator.button.operator 18 | 19 | import ir.erfansn.siliconecalculator.calculator.button.OperatorButton 20 | 21 | data object Mul : OperatorButton("×") 22 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/calculator/button/operator/Sub.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.calculator.button.operator 18 | 19 | import ir.erfansn.siliconecalculator.calculator.button.OperatorButton 20 | 21 | data object Sub : OperatorButton("-") 22 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/data/model/History.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.data.model 18 | 19 | import ir.erfansn.siliconecalculator.data.source.local.db.model.HistoryEntity 20 | import ir.erfansn.siliconecalculator.util.OPERATORS_REGEX 21 | 22 | data class History( 23 | val id: Int, 24 | val date: String, 25 | val calculation: Calculation, 26 | ) 27 | 28 | data class Calculation( 29 | val expression: String = "", 30 | val result: String = "0", 31 | ) { 32 | val isComplete: Boolean 33 | get() = expression.isNotEmpty() && ((result != "0" || operators.count() > 1) && expression.endsWith(lastOperator)) 34 | 35 | val resultIsInvalid: Boolean 36 | get() = result.matches("-?Infinity|NaN".toRegex()) 37 | 38 | val lastOperator 39 | get() = operators.lastOrNull()?.value ?: "$" 40 | 41 | private val operators 42 | get() = "\\s$OPERATORS_REGEX\\s".toRegex().findAll(expression) 43 | } 44 | 45 | fun Calculation.asHistoryEntity() = HistoryEntity( 46 | expression = expression, 47 | result = result 48 | ) 49 | 50 | val previewHistoryItems = listOf( 51 | History( 52 | id = 0, 53 | date = "12 April", 54 | calculation = Calculation( 55 | expression = "1 + 788 * 875", 56 | result = "10" 57 | ) 58 | ), 59 | History( 60 | id = 1, 61 | date = "12 April", 62 | calculation = Calculation( 63 | expression = "68774 + 9888 * 4763 / 9847", 64 | result = "2675.09" 65 | ) 66 | ), 67 | History( 68 | id = 2, 69 | date = "15 March", 70 | calculation = Calculation( 71 | expression = "458867 / 76", 72 | result = "0.002" 73 | ) 74 | ), 75 | History( 76 | id = 3, 77 | date = "15 April", 78 | calculation = Calculation( 79 | expression = "9475 * 0.88888", 80 | result = "4755.2" 81 | ) 82 | ), 83 | History( 84 | id = 4, 85 | date = "19 April", 86 | calculation = Calculation( 87 | expression = "47362 / 1 / 98585", 88 | result = "12345" 89 | ) 90 | ), 91 | History( 92 | id = 5, 93 | date = "19 April", 94 | calculation = Calculation( 95 | expression = "5452 - 97584 + 9573 / 848 * 764", 96 | result = "14795" 97 | ) 98 | ), 99 | History( 100 | id = 6, 101 | date = "19 April", 102 | calculation = Calculation( 103 | expression = "12 - 957 + 857 - 9588 / 4388 * 8746", 104 | result = "25874333" 105 | ) 106 | ), 107 | History( 108 | id = 7, 109 | date = "Yesterday", 110 | calculation = Calculation( 111 | expression = "23857 - 979400 + 9488 / 8858", 112 | result = "234555" 113 | ) 114 | ), 115 | History( 116 | id = 8, 117 | date = "Yesterday", 118 | calculation = Calculation( 119 | expression = "1 * 2 * 3 * 6", 120 | result = "56776" 121 | ) 122 | ), 123 | History( 124 | id = 9, 125 | date = "Yesterday", 126 | calculation = Calculation( 127 | expression = "999 * 4678", 128 | result = "2" 129 | ) 130 | ), 131 | History( 132 | id = 10, 133 | date = "Today", 134 | calculation = Calculation( 135 | expression = "1 + 1", 136 | result = "2" 137 | ) 138 | ), 139 | ) 140 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/data/repository/HistoryRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.data.repository 18 | 19 | import ir.erfansn.siliconecalculator.data.model.Calculation 20 | import ir.erfansn.siliconecalculator.data.model.History 21 | import kotlinx.coroutines.flow.Flow 22 | 23 | interface HistoryRepository { 24 | val historyItemsStream: Flow> 25 | suspend fun clearAllHistory() 26 | suspend fun saveCalculation(calculation: Calculation) 27 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/data/repository/HistoryRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.data.repository 18 | 19 | import ir.erfansn.siliconecalculator.data.model.Calculation 20 | import ir.erfansn.siliconecalculator.data.model.asHistoryEntity 21 | import ir.erfansn.siliconecalculator.data.source.local.db.dao.HistoryDao 22 | import ir.erfansn.siliconecalculator.data.source.local.db.model.HistoryEntity 23 | import ir.erfansn.siliconecalculator.data.source.local.db.model.asHistory 24 | import kotlinx.coroutines.flow.map 25 | import javax.inject.Inject 26 | 27 | class HistoryRepositoryImpl @Inject constructor( 28 | private val historyDao: HistoryDao, 29 | ) : HistoryRepository { 30 | 31 | override val historyItemsStream = historyDao.getHistoryEntitiesStream() 32 | .map { 33 | it.map(HistoryEntity::asHistory) 34 | } 35 | 36 | override suspend fun clearAllHistory() { 37 | historyDao.deleteAllHistoryEntities() 38 | } 39 | 40 | override suspend fun saveCalculation(calculation: Calculation) { 41 | historyDao.insertHistoryEntity(calculation.asHistoryEntity()) 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/data/source/local/db/SiliconeCalculatorDatabase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.data.source.local.db 18 | 19 | import androidx.room.Database 20 | import androidx.room.RoomDatabase 21 | import androidx.room.TypeConverters 22 | import ir.erfansn.siliconecalculator.data.source.local.db.dao.HistoryDao 23 | import ir.erfansn.siliconecalculator.data.source.local.db.model.HistoryEntity 24 | import ir.erfansn.siliconecalculator.util.DateConverter 25 | 26 | @Database(entities = [HistoryEntity::class], version = 1) 27 | @TypeConverters(DateConverter::class) 28 | abstract class SiliconeCalculatorDatabase : RoomDatabase() { 29 | abstract fun historyDao(): HistoryDao 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/data/source/local/db/dao/HistoryDao.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.data.source.local.db.dao 18 | 19 | import androidx.room.* 20 | import ir.erfansn.siliconecalculator.data.source.local.db.model.HistoryEntity 21 | import kotlinx.coroutines.flow.Flow 22 | 23 | @Dao 24 | interface HistoryDao { 25 | @Insert 26 | suspend fun insertHistoryEntity(historyEntity: HistoryEntity) 27 | 28 | @Query("SELECT * FROM History") 29 | fun getHistoryEntitiesStream(): Flow> 30 | 31 | @Query("DELETE FROM History") 32 | suspend fun deleteAllHistoryEntities() 33 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/data/source/local/db/model/HistoryEntity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.data.source.local.db.model 18 | 19 | import androidx.room.ColumnInfo 20 | import androidx.room.Entity 21 | import androidx.room.PrimaryKey 22 | import ir.erfansn.siliconecalculator.data.model.Calculation 23 | import ir.erfansn.siliconecalculator.data.model.History 24 | import ir.erfansn.siliconecalculator.util.format 25 | import kotlinx.datetime.Clock 26 | import kotlinx.datetime.LocalDate 27 | import kotlinx.datetime.TimeZone 28 | import kotlinx.datetime.todayIn 29 | 30 | @Entity(tableName = "History") 31 | data class HistoryEntity( 32 | @PrimaryKey(autoGenerate = true) val id: Int = 0, 33 | @ColumnInfo(name = "epoch_day") val date: LocalDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), 34 | val expression: String, 35 | val result: String, 36 | ) 37 | 38 | fun HistoryEntity.asHistory() = History( 39 | id = id, 40 | date = date.format(), 41 | calculation = Calculation( 42 | expression = expression, 43 | result = result 44 | ) 45 | ) 46 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.di 18 | 19 | import android.content.Context 20 | import androidx.room.Room 21 | import dagger.Binds 22 | import dagger.Module 23 | import dagger.Provides 24 | import dagger.hilt.InstallIn 25 | import dagger.hilt.android.qualifiers.ApplicationContext 26 | import dagger.hilt.components.SingletonComponent 27 | import ir.erfansn.siliconecalculator.data.repository.HistoryRepository 28 | import ir.erfansn.siliconecalculator.data.repository.HistoryRepositoryImpl 29 | import ir.erfansn.siliconecalculator.data.source.local.db.SiliconeCalculatorDatabase 30 | import kotlinx.coroutines.Dispatchers 31 | import javax.inject.Singleton 32 | 33 | @Module 34 | @InstallIn(SingletonComponent::class) 35 | interface AppModule { 36 | 37 | @Binds 38 | fun bindsHistoryRepository( 39 | historyRepositoryImpl: HistoryRepositoryImpl, 40 | ): HistoryRepository 41 | 42 | companion object { 43 | 44 | @[Provides Singleton] 45 | fun providesRoomDatabase(@ApplicationContext context: Context) = Room.databaseBuilder( 46 | context, 47 | SiliconeCalculatorDatabase::class.java, 48 | "silicone_calculator" 49 | ).build() 50 | 51 | @Provides 52 | fun providesHistoryDao(siliconeCalculatorDatabase: SiliconeCalculatorDatabase) = 53 | siliconeCalculatorDatabase.historyDao() 54 | 55 | @Provides 56 | fun providesDefaultDispatcher() = Dispatchers.Default 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/history/HistoryItem.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.history 18 | 19 | import androidx.compose.foundation.clickable 20 | import androidx.compose.foundation.horizontalScroll 21 | import androidx.compose.foundation.layout.Column 22 | import androidx.compose.foundation.layout.fillMaxWidth 23 | import androidx.compose.foundation.layout.padding 24 | import androidx.compose.foundation.rememberScrollState 25 | import androidx.compose.material.ContentAlpha 26 | import androidx.compose.material.MaterialTheme 27 | import androidx.compose.material.Text 28 | import androidx.compose.runtime.Composable 29 | import androidx.compose.runtime.key 30 | import androidx.compose.ui.Alignment 31 | import androidx.compose.ui.Modifier 32 | import androidx.compose.ui.draw.alpha 33 | import androidx.compose.ui.text.font.FontWeight 34 | import androidx.compose.ui.tooling.preview.Preview 35 | import androidx.compose.ui.unit.dp 36 | import androidx.lifecycle.compose.dropUnlessResumed 37 | import ir.erfansn.siliconecalculator.data.model.Calculation 38 | import ir.erfansn.siliconecalculator.data.model.History 39 | import ir.erfansn.siliconecalculator.data.model.previewHistoryItems 40 | import ir.erfansn.siliconecalculator.util.formatNumbers 41 | 42 | @Composable 43 | fun HistoryItem( 44 | calculations: List, 45 | onCalculationClick: (Calculation) -> Unit, 46 | date: String, 47 | ) { 48 | for (calculation in calculations) { 49 | key(calculation.hashCode()) { 50 | CalculationItem( 51 | calculation = calculation, 52 | onCalculationClick = onCalculationClick 53 | ) 54 | } 55 | } 56 | 57 | Text( 58 | modifier = Modifier.padding( 59 | vertical = 12.dp, 60 | horizontal = 28.dp 61 | ), 62 | text = date, 63 | style = MaterialTheme.typography.subtitle1.copy( 64 | fontWeight = FontWeight.Medium, 65 | ) 66 | ) 67 | } 68 | 69 | @Composable 70 | fun CalculationItem( 71 | calculation: Calculation, 72 | onCalculationClick: (Calculation) -> Unit, 73 | ) { 74 | Column( 75 | modifier = Modifier 76 | .fillMaxWidth() 77 | .clickable(onClick = dropUnlessResumed { onCalculationClick(calculation) }) 78 | .padding(vertical = 8.dp), 79 | horizontalAlignment = Alignment.End 80 | ) { 81 | Text( 82 | modifier = Modifier 83 | .alpha(ContentAlpha.medium) 84 | .horizontalScroll(rememberScrollState(), reverseScrolling = true) 85 | .padding(horizontal = 16.dp), 86 | text = calculation.expression.formatNumbers(), 87 | style = MaterialTheme.typography.h5.copy( 88 | fontWeight = FontWeight.Light 89 | ) 90 | ) 91 | Text( 92 | modifier = Modifier 93 | .horizontalScroll(rememberScrollState(), reverseScrolling = true) 94 | .padding(horizontal = 16.dp), 95 | text = calculation.result.formatNumbers(), 96 | style = MaterialTheme.typography.h5, 97 | ) 98 | } 99 | } 100 | 101 | @Preview(showBackground = true) 102 | @Composable 103 | fun HistoryItemPreview() { 104 | MaterialTheme { 105 | HistoryItem( 106 | calculations = previewHistoryItems.map(History::calculation).take(3), 107 | onCalculationClick = { }, 108 | date = "Today" 109 | ) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/history/HistoryUiState.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.history 18 | 19 | import androidx.compose.runtime.Immutable 20 | import ir.erfansn.siliconecalculator.data.model.History 21 | 22 | @Immutable 23 | data class HistoryUiState( 24 | val historyItems: List = emptyList() 25 | ) -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/history/HistoryViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.history 18 | 19 | import androidx.lifecycle.ViewModel 20 | import androidx.lifecycle.viewModelScope 21 | import dagger.hilt.android.lifecycle.HiltViewModel 22 | import ir.erfansn.siliconecalculator.data.repository.HistoryRepository 23 | import kotlinx.coroutines.flow.SharingStarted 24 | import kotlinx.coroutines.flow.map 25 | import kotlinx.coroutines.flow.stateIn 26 | import kotlinx.coroutines.launch 27 | import javax.inject.Inject 28 | 29 | @HiltViewModel 30 | class HistoryViewModel @Inject constructor( 31 | private val historyRepository: HistoryRepository, 32 | ) : ViewModel() { 33 | 34 | val uiState = historyRepository.historyItemsStream 35 | .map(::HistoryUiState) 36 | .stateIn( 37 | scope = viewModelScope, 38 | started = SharingStarted.WhileSubscribed(), 39 | initialValue = HistoryUiState() 40 | ) 41 | 42 | fun onHistoryClear() { 43 | viewModelScope.launch { 44 | historyRepository.clearAllHistory() 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/navigation/AppNavigationActions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.navigation 18 | 19 | import androidx.navigation.NavController 20 | import ir.erfansn.siliconecalculator.data.model.Calculation 21 | import ir.erfansn.siliconecalculator.navigation.SiliconeCalculatorDestinationsArg.EXPRESSION_ARG 22 | import ir.erfansn.siliconecalculator.navigation.SiliconeCalculatorDestinationsArg.RESULT_ARG 23 | import ir.erfansn.siliconecalculator.navigation.SiliconeCalculatorScreens.CALCULATOR 24 | import ir.erfansn.siliconecalculator.navigation.SiliconeCalculatorScreens.HISTORY 25 | import ir.erfansn.siliconecalculator.util.encodeReservedChars 26 | 27 | object SiliconeCalculatorScreens { 28 | const val CALCULATOR = "calculator" 29 | const val HISTORY = "history" 30 | } 31 | 32 | object SiliconeCalculatorDestinationsArg { 33 | const val EXPRESSION_ARG = "expression" 34 | const val RESULT_ARG = "result" 35 | } 36 | 37 | object SiliconeCalculatorDestinations { 38 | const val CALCULATOR_ROUTE = 39 | "$CALCULATOR?$EXPRESSION_ARG={$EXPRESSION_ARG}&$RESULT_ARG={$RESULT_ARG}" 40 | const val HISTORY_ROUTE = HISTORY 41 | } 42 | 43 | class AppNavigationActions(private val navController: NavController) { 44 | 45 | fun navigateToCalculator(calculation: Calculation) { 46 | val (expression, result) = calculation 47 | 48 | navController.navigate( 49 | route = "$CALCULATOR?$EXPRESSION_ARG=${expression.encodeReservedChars}&$RESULT_ARG=${result}" 50 | ) { 51 | popUpTo(CALCULATOR) { 52 | inclusive = true 53 | } 54 | } 55 | } 56 | 57 | fun navigateToHistory() { 58 | navController.navigate(HISTORY) 59 | } 60 | 61 | fun onBackPress() { 62 | navController.popBackStack() 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/navigation/SiliconeCalculatorNavHost.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.navigation 18 | 19 | import androidx.compose.animation.AnimatedContentTransitionScope 20 | import androidx.compose.animation.core.tween 21 | import androidx.compose.animation.fadeIn 22 | import androidx.compose.animation.fadeOut 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.runtime.getValue 25 | import androidx.compose.runtime.remember 26 | import androidx.hilt.navigation.compose.hiltViewModel 27 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 28 | import androidx.navigation.NavHostController 29 | import androidx.navigation.compose.NavHost 30 | import androidx.navigation.compose.composable 31 | import androidx.navigation.compose.rememberNavController 32 | import androidx.navigation.navArgument 33 | import ir.erfansn.siliconecalculator.calculator.CalculatorScreen 34 | import ir.erfansn.siliconecalculator.calculator.CalculatorViewModel 35 | import ir.erfansn.siliconecalculator.history.HistoryScreen 36 | import ir.erfansn.siliconecalculator.history.HistoryViewModel 37 | import ir.erfansn.siliconecalculator.navigation.SiliconeCalculatorDestinations.CALCULATOR_ROUTE 38 | import ir.erfansn.siliconecalculator.navigation.SiliconeCalculatorDestinations.HISTORY_ROUTE 39 | 40 | @Composable 41 | fun SiliconeCalculatorNavHost( 42 | navController: NavHostController = rememberNavController(), 43 | navActions: AppNavigationActions = remember(navController) { 44 | AppNavigationActions(navController) 45 | }, 46 | onThemeToggle: () -> Unit, 47 | ) { 48 | NavHost( 49 | navController = navController, 50 | startDestination = CALCULATOR_ROUTE, 51 | popEnterTransition = { 52 | fadeIn(animationSpec = tween(durationMillis = 700)) 53 | }, 54 | popExitTransition = { 55 | fadeOut( 56 | animationSpec = tween(durationMillis = 700) 57 | ) + slideOutOfContainer( 58 | towards = AnimatedContentTransitionScope.SlideDirection.End, 59 | animationSpec = tween(durationMillis = 700) 60 | ) 61 | }, 62 | ) { 63 | composable( 64 | CALCULATOR_ROUTE, 65 | arguments = listOf( 66 | navArgument(SiliconeCalculatorDestinationsArg.EXPRESSION_ARG) { defaultValue = "" }, 67 | navArgument(SiliconeCalculatorDestinationsArg.RESULT_ARG) { defaultValue = "0" }, 68 | ) 69 | ) { 70 | val calculatorViewModel = hiltViewModel() 71 | val uiState by calculatorViewModel.uiState.collectAsStateWithLifecycle() 72 | val calculatorButtons by calculatorViewModel.calculatorButtons.collectAsStateWithLifecycle() 73 | 74 | CalculatorScreen( 75 | uiState = uiState, 76 | onCalculatorButtonClick = calculatorViewModel::performCalculatorButton, 77 | onHistoryNav = { navActions.navigateToHistory() }, 78 | onThemeToggle = onThemeToggle, 79 | calculatorButtons = calculatorButtons 80 | ) 81 | } 82 | composable(HISTORY_ROUTE) { backStackEntry -> 83 | val historyViewModel = hiltViewModel() 84 | val uiState by historyViewModel.uiState.collectAsStateWithLifecycle() 85 | 86 | HistoryScreen( 87 | uiState = uiState, 88 | onHistoryClear = historyViewModel::onHistoryClear, 89 | onBackPress = { navActions.onBackPress() }, 90 | onCalculationClick = { navActions.navigateToCalculator(it) } 91 | ) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/ui/SiliconeCalculatorIcon.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.ui 18 | 19 | import android.graphics.BlurMaskFilter 20 | import androidx.compose.foundation.Canvas 21 | import androidx.compose.foundation.background 22 | import androidx.compose.foundation.layout.* 23 | import androidx.compose.foundation.shape.CircleShape 24 | import androidx.compose.foundation.shape.RoundedCornerShape 25 | import androidx.compose.material.ButtonDefaults 26 | import androidx.compose.material.Surface 27 | import androidx.compose.runtime.Composable 28 | import androidx.compose.ui.Alignment 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.geometry.Offset 31 | import androidx.compose.ui.geometry.boundingRect 32 | import androidx.compose.ui.graphics.* 33 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 34 | import androidx.compose.ui.tooling.preview.Preview 35 | import androidx.compose.ui.unit.Dp 36 | import androidx.compose.ui.unit.dp 37 | import ir.erfansn.siliconecalculator.ui.theme.BlueGrey300 38 | import ir.erfansn.siliconecalculator.ui.theme.DeepOrange800 39 | 40 | @Preview( 41 | backgroundColor = 0xFFECECEC, 42 | showBackground = true, 43 | widthDp = 108, 44 | heightDp = 108 45 | ) 46 | @Composable 47 | fun SiliconeCalculatorIconPreview() { 48 | Column( 49 | horizontalAlignment = Alignment.CenterHorizontally, 50 | verticalArrangement = Arrangement.Center 51 | ) { 52 | val baseModifier = Modifier 53 | .width(58.dp) 54 | .height(16.dp) 55 | 56 | NeuShape( 57 | modifier = baseModifier, 58 | lightColor = BlueGrey300, 59 | shape = CircleShape, 60 | borderWidthPercent = 25, 61 | elevation = 24.dp 62 | ) 63 | Spacer(modifier = Modifier.height(8.dp)) 64 | NeuShape( 65 | modifier = baseModifier, 66 | lightColor = DeepOrange800, 67 | shape = CircleShape, 68 | borderWidthPercent = 25, 69 | elevation = 24.dp 70 | ) 71 | } 72 | } 73 | 74 | @Composable 75 | private fun NeuShape( 76 | modifier: Modifier = Modifier, 77 | shape: Shape = RoundedCornerShape(36), 78 | lightColor: Color, 79 | darkColor: Color = lightColor.copy( 80 | red = (lightColor.red + 0.125f).coerceAtMost(1.0f), 81 | green = (lightColor.green + 0.125f).coerceAtMost(1.0f), 82 | blue = (lightColor.blue + 0.125f).coerceAtMost(1.0f), 83 | ), 84 | borderWidthPercent: Int = 12, 85 | elevation: Dp = 18.dp, 86 | ) { 87 | Surface( 88 | modifier = modifier, 89 | shape = shape, 90 | elevation = elevation 91 | ) { 92 | Canvas( 93 | modifier = Modifier 94 | .defaultMinSize( 95 | minWidth = ButtonDefaults.MinWidth, 96 | minHeight = ButtonDefaults.MinWidth 97 | ) 98 | .background( 99 | brush = Brush.linearGradient( 100 | 0.0f to lightColor, 101 | 1.0f to darkColor, 102 | ) 103 | ) 104 | ) { 105 | val paint = Paint() 106 | .asFrameworkPaint() 107 | .apply { 108 | isAntiAlias = true 109 | style = android.graphics.Paint.Style.STROKE 110 | strokeWidth = size.minDimension * (borderWidthPercent / 100.0f) 111 | shader = LinearGradientShader( 112 | from = Offset.Zero, 113 | to = Offset(x = size.width, y = size.height), 114 | colors = listOf(darkColor, lightColor) 115 | ) 116 | maskFilter = 117 | BlurMaskFilter(strokeWidth / 2, 118 | BlurMaskFilter.Blur.NORMAL) 119 | } 120 | 121 | drawIntoCanvas { 122 | when (val outline = shape.createOutline(size, layoutDirection, this)) { 123 | is Outline.Rectangle -> { 124 | val rect = outline.rect 125 | it.nativeCanvas.drawRect( 126 | rect.toAndroidRect(), 127 | paint 128 | ) 129 | } 130 | is Outline.Rounded -> { 131 | val roundRect = outline.roundRect 132 | it.nativeCanvas.drawRoundRect( 133 | roundRect.boundingRect.toAndroidRectF(), 134 | roundRect.topLeftCornerRadius.x, 135 | roundRect.topLeftCornerRadius.y, 136 | paint 137 | ) 138 | } 139 | is Outline.Generic -> { 140 | val path = outline.path 141 | it.nativeCanvas.drawPath( 142 | path.asAndroidPath(), 143 | paint 144 | ) 145 | } 146 | } 147 | } 148 | } 149 | } 150 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/ui/animation/CircularRevealAnimation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.ui.animation 18 | 19 | import android.graphics.Path 20 | import android.view.MotionEvent 21 | import androidx.annotation.FloatRange 22 | import androidx.compose.animation.core.FiniteAnimationSpec 23 | import androidx.compose.animation.core.Transition 24 | import androidx.compose.animation.core.animateFloat 25 | import androidx.compose.animation.core.tween 26 | import androidx.compose.animation.core.updateTransition 27 | import androidx.compose.foundation.focusable 28 | import androidx.compose.foundation.isSystemInDarkTheme 29 | import androidx.compose.foundation.layout.Box 30 | import androidx.compose.foundation.layout.fillMaxSize 31 | import androidx.compose.foundation.layout.size 32 | import androidx.compose.material.ExperimentalMaterialApi 33 | import androidx.compose.material.Icon 34 | import androidx.compose.material.MaterialTheme 35 | import androidx.compose.material.Surface 36 | import androidx.compose.material.icons.Icons 37 | import androidx.compose.material.icons.filled.DarkMode 38 | import androidx.compose.material.icons.filled.LightMode 39 | import androidx.compose.runtime.Composable 40 | import androidx.compose.runtime.LaunchedEffect 41 | import androidx.compose.runtime.getValue 42 | import androidx.compose.runtime.key 43 | import androidx.compose.runtime.mutableStateListOf 44 | import androidx.compose.runtime.mutableStateOf 45 | import androidx.compose.runtime.remember 46 | import androidx.compose.runtime.setValue 47 | import androidx.compose.ui.Alignment 48 | import androidx.compose.ui.ExperimentalComposeUiApi 49 | import androidx.compose.ui.Modifier 50 | import androidx.compose.ui.draw.clip 51 | import androidx.compose.ui.focus.FocusRequester 52 | import androidx.compose.ui.focus.focusRequester 53 | import androidx.compose.ui.geometry.Offset 54 | import androidx.compose.ui.geometry.Size 55 | import androidx.compose.ui.graphics.Outline 56 | import androidx.compose.ui.graphics.Shape 57 | import androidx.compose.ui.graphics.asComposePath 58 | import androidx.compose.ui.input.pointer.pointerInteropFilter 59 | import androidx.compose.ui.tooling.preview.Preview 60 | import androidx.compose.ui.unit.Density 61 | import androidx.compose.ui.unit.LayoutDirection 62 | import androidx.compose.ui.unit.dp 63 | import androidx.compose.ui.util.fastForEach 64 | import ir.erfansn.siliconecalculator.ui.theme.SiliconeCalculatorTheme 65 | import kotlin.math.hypot 66 | 67 | @Composable 68 | fun CircularReveal( 69 | expanded: Boolean, 70 | modifier: Modifier = Modifier, 71 | animationSpec: FiniteAnimationSpec = tween(), 72 | content: @Composable (Boolean) -> Unit, 73 | ) { 74 | val transition = updateTransition(expanded, label = "Circular reveal") 75 | transition.CircularReveal(modifier, animationSpec, content = content) 76 | } 77 | 78 | @OptIn(ExperimentalComposeUiApi::class) 79 | @Composable 80 | private fun Transition.CircularReveal( 81 | modifier: Modifier = Modifier, 82 | animationSpec: FiniteAnimationSpec = tween(), 83 | content: @Composable (targetState: Boolean) -> Unit, 84 | ) { 85 | var offset: Offset? by remember { mutableStateOf(null) } 86 | val currentlyVisible = remember { mutableStateListOf().apply { add(false) } } 87 | val contentMap = remember { 88 | mutableMapOf Unit>() 89 | } 90 | if (currentState == targetState) { 91 | // If not animating, just display the current state 92 | if (currentlyVisible.size != 1 || currentlyVisible[0] != targetState) { 93 | contentMap.clear() 94 | } 95 | } 96 | if (!contentMap.contains(targetState)) { 97 | // Replace target with the same key if any 98 | val replacementId = currentlyVisible.indexOfFirst { 99 | it == targetState 100 | } 101 | if (replacementId == -1) { 102 | currentlyVisible.add(targetState) 103 | } else { 104 | currentlyVisible[replacementId] = targetState 105 | } 106 | contentMap.clear() 107 | currentlyVisible.fastForEach { stateForContent -> 108 | contentMap[stateForContent] = { 109 | val progress by animateFloat( 110 | label = "Progress", 111 | transitionSpec = { animationSpec } 112 | ) { 113 | val targetedContent = stateForContent != currentlyVisible.last() || it == stateForContent 114 | if (targetedContent) 1f else 0f 115 | } 116 | 117 | val focusRequester = remember { FocusRequester() } 118 | LaunchedEffect(targetState) { 119 | focusRequester.requestFocus() 120 | } 121 | Box( 122 | modifier = Modifier 123 | .focusable() 124 | .focusRequester(focusRequester) 125 | .circularReveal(progress = progress, offset = offset) 126 | ) { 127 | content(stateForContent) 128 | } 129 | } 130 | } 131 | } 132 | Box( 133 | modifier = modifier.pointerInteropFilter { 134 | if (it.action == MotionEvent.ACTION_DOWN) { 135 | if (!started) offset = Offset(it.x, it.y) 136 | } 137 | started 138 | } 139 | ) { 140 | currentlyVisible.fastForEach { 141 | key(it) { 142 | contentMap[it]?.invoke() 143 | } 144 | } 145 | } 146 | } 147 | 148 | private val Transition.started get() = 149 | currentState != targetState || isRunning 150 | 151 | private fun Modifier.circularReveal( 152 | @FloatRange(from = 0.0, to = 1.0) progress: Float, 153 | offset: Offset? = null, 154 | ) = clip(CircularRevealShape(progress, offset)) 155 | 156 | private class CircularRevealShape( 157 | @FloatRange(from = 0.0, to = 1.0) private val progress: Float, 158 | private val offset: Offset? = null, 159 | ) : Shape { 160 | override fun createOutline( 161 | size: Size, 162 | layoutDirection: LayoutDirection, 163 | density: Density, 164 | ): Outline { 165 | return Outline.Generic(Path().apply { 166 | addCircle( 167 | offset?.x ?: (size.width / 2f), 168 | offset?.y ?: (size.height / 2f), 169 | longestDistanceToACorner(size, offset) * progress, 170 | Path.Direction.CW 171 | ) 172 | }.asComposePath()) 173 | } 174 | 175 | private fun longestDistanceToACorner(size: Size, offset: Offset?): Float { 176 | if (offset == null) { 177 | return hypot(size.width / 2f, size.height / 2f) 178 | } 179 | 180 | val topLeft = hypot(offset.x, offset.y) 181 | val topRight = hypot(size.width - offset.x, offset.y) 182 | val bottomLeft = hypot(offset.x, size.height - offset.y) 183 | val bottomRight = hypot(size.width - offset.x, size.height - offset.y) 184 | 185 | return maxOf(topLeft, topRight, bottomLeft, bottomRight) 186 | } 187 | } 188 | 189 | @OptIn(ExperimentalMaterialApi::class) 190 | @Preview 191 | @Composable 192 | private fun CircularRevealAnimationPreview() { 193 | val isSystemDark = isSystemInDarkTheme() 194 | var darkTheme by remember { mutableStateOf(isSystemDark) } 195 | val onThemeToggle = { darkTheme = !darkTheme } 196 | 197 | CircularReveal( 198 | expanded = darkTheme, 199 | animationSpec = tween(1500) 200 | ) { isDark -> 201 | SiliconeCalculatorTheme(darkTheme = isDark) { 202 | Surface( 203 | modifier = Modifier.fillMaxSize(), 204 | color = MaterialTheme.colors.background, 205 | onClick = onThemeToggle 206 | ) { 207 | Box( 208 | contentAlignment = Alignment.Center 209 | ) { 210 | Icon( 211 | modifier = Modifier.size(120.dp), 212 | imageVector = if (!isDark) Icons.Default.DarkMode else Icons.Default.LightMode, 213 | contentDescription = "Toggle", 214 | ) 215 | } 216 | } 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/ui/component/CorneredFlatButton.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.ui.component 18 | 19 | import androidx.compose.foundation.BorderStroke 20 | import androidx.compose.foundation.layout.PaddingValues 21 | import androidx.compose.foundation.layout.RowScope 22 | import androidx.compose.foundation.layout.fillMaxSize 23 | import androidx.compose.foundation.shape.CornerSize 24 | import androidx.compose.material.* 25 | import androidx.compose.material.icons.Icons 26 | import androidx.compose.material.icons.filled.Adb 27 | import androidx.compose.runtime.Composable 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.graphics.Color 30 | import androidx.compose.ui.graphics.Shape 31 | import androidx.compose.ui.graphics.compositeOver 32 | import androidx.compose.ui.graphics.vector.ImageVector 33 | import androidx.compose.ui.tooling.preview.Preview 34 | import androidx.compose.ui.unit.dp 35 | import ir.erfansn.siliconecalculator.ui.theme.BlueGrey100 36 | 37 | @Composable 38 | fun CorneredFlatButton( 39 | modifier: Modifier = Modifier, 40 | onClick: () -> Unit, 41 | backgroundColor: Color = MaterialTheme.colors.primary.copy(alpha = 0.7f).compositeOver(BlueGrey100), 42 | border: BorderStroke? = null, 43 | shape: Shape = MaterialTheme.shapes.small.copy(bottomStart = CornerSize(percent = 0)), 44 | contentPadding: PaddingValues = ButtonDefaults.ContentPadding, 45 | content: @Composable RowScope.() -> Unit, 46 | ) = Button( 47 | modifier = modifier, 48 | shape = shape, 49 | elevation = null, 50 | onClick = onClick, 51 | colors = ButtonDefaults.buttonColors( 52 | backgroundColor = backgroundColor 53 | ), 54 | contentPadding = contentPadding, 55 | border = border, 56 | content = content 57 | ) 58 | 59 | @Composable 60 | fun OutlinedCorneredFlatButton( 61 | modifier: Modifier = Modifier, 62 | shape: Shape = MaterialTheme.shapes.small.copy(bottomStart = CornerSize(percent = 0)), 63 | onClick: () -> Unit, 64 | border: BorderStroke = ButtonDefaults.outlinedBorder, 65 | contentPadding: PaddingValues = ButtonDefaults.ContentPadding, 66 | content: @Composable RowScope.() -> Unit, 67 | ) = CorneredFlatButton( 68 | modifier = modifier, 69 | border = border, 70 | backgroundColor = Color.Transparent, 71 | shape = shape, 72 | contentPadding = contentPadding, 73 | onClick = onClick, 74 | content = content 75 | ) 76 | 77 | @Composable 78 | fun CorneredFlatIconButton( 79 | modifier: Modifier = Modifier, 80 | shape: Shape = MaterialTheme.shapes.small.copy(bottomStart = CornerSize(percent = 0)), 81 | onClick: () -> Unit, 82 | icon: ImageVector, 83 | contentDescription: String, 84 | ) { 85 | CorneredFlatButton( 86 | modifier = modifier, 87 | shape = shape, 88 | contentPadding = PaddingValues(0.dp), 89 | onClick = onClick 90 | ) { 91 | Icon( 92 | modifier = Modifier 93 | .fillMaxSize(0.60f), 94 | imageVector = icon, 95 | contentDescription = contentDescription, 96 | ) 97 | } 98 | } 99 | 100 | @Preview 101 | @Composable 102 | fun CorneredFlatButtonPreview() { 103 | MaterialTheme { 104 | CorneredFlatButton( 105 | onClick = { } 106 | ) { 107 | Text("Flat button") 108 | } 109 | } 110 | } 111 | 112 | @Preview 113 | @Composable 114 | fun OutlinedCorneredFlatButtonPreview() { 115 | MaterialTheme { 116 | OutlinedCorneredFlatButton( 117 | onClick = { } 118 | ) { 119 | Text("Flat button") 120 | } 121 | } 122 | } 123 | 124 | @Preview 125 | @Composable 126 | fun CorneredFlatIconButtonPreview() { 127 | MaterialTheme { 128 | CorneredFlatIconButton( 129 | onClick = { }, 130 | icon = Icons.Default.Adb, 131 | contentDescription = "Adb", 132 | ) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/ui/component/NeuButton.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.ui.component 18 | 19 | import android.graphics.BlurMaskFilter 20 | import androidx.compose.foundation.BorderStroke 21 | import androidx.compose.foundation.ExperimentalFoundationApi 22 | import androidx.compose.foundation.background 23 | import androidx.compose.foundation.border 24 | import androidx.compose.foundation.combinedClickable 25 | import androidx.compose.foundation.interaction.MutableInteractionSource 26 | import androidx.compose.foundation.layout.Box 27 | import androidx.compose.foundation.layout.BoxWithConstraints 28 | import androidx.compose.foundation.layout.BoxWithConstraintsScope 29 | import androidx.compose.foundation.layout.defaultMinSize 30 | import androidx.compose.foundation.layout.padding 31 | import androidx.compose.foundation.shape.RoundedCornerShape 32 | import androidx.compose.material.ButtonDefaults 33 | import androidx.compose.material.ButtonElevation 34 | import androidx.compose.material.ElevationOverlay 35 | import androidx.compose.material.LocalAbsoluteElevation 36 | import androidx.compose.material.LocalContentAlpha 37 | import androidx.compose.material.LocalContentColor 38 | import androidx.compose.material.LocalElevationOverlay 39 | import androidx.compose.material.MaterialTheme 40 | import androidx.compose.material.ProvideTextStyle 41 | import androidx.compose.material.Text 42 | import androidx.compose.material.contentColorFor 43 | import androidx.compose.material.minimumInteractiveComponentSize 44 | import androidx.compose.material.ripple.rememberRipple 45 | import androidx.compose.runtime.Composable 46 | import androidx.compose.runtime.CompositionLocalProvider 47 | import androidx.compose.runtime.getValue 48 | import androidx.compose.runtime.remember 49 | import androidx.compose.ui.Alignment 50 | import androidx.compose.ui.Modifier 51 | import androidx.compose.ui.draw.clip 52 | import androidx.compose.ui.draw.drawWithCache 53 | import androidx.compose.ui.draw.shadow 54 | import androidx.compose.ui.geometry.Offset 55 | import androidx.compose.ui.geometry.boundingRect 56 | import androidx.compose.ui.graphics.Brush 57 | import androidx.compose.ui.graphics.Color 58 | import androidx.compose.ui.graphics.LinearGradientShader 59 | import androidx.compose.ui.graphics.Outline 60 | import androidx.compose.ui.graphics.Paint 61 | import androidx.compose.ui.graphics.RectangleShape 62 | import androidx.compose.ui.graphics.Shape 63 | import androidx.compose.ui.graphics.asAndroidPath 64 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 65 | import androidx.compose.ui.graphics.nativeCanvas 66 | import androidx.compose.ui.graphics.toAndroidRect 67 | import androidx.compose.ui.graphics.toAndroidRectF 68 | import androidx.compose.ui.tooling.preview.Preview 69 | import androidx.compose.ui.unit.Dp 70 | import androidx.compose.ui.unit.dp 71 | 72 | @Composable 73 | fun NeuButton( 74 | modifier: Modifier = Modifier, 75 | shape: Shape = RoundedCornerShape(36), 76 | lightColor: Color, 77 | darkColor: Color = lightColor.copy( 78 | red = (lightColor.red + 0.125f).coerceAtMost(1.0f), 79 | green = (lightColor.green + 0.125f).coerceAtMost(1.0f), 80 | blue = (lightColor.blue + 0.125f).coerceAtMost(1.0f), 81 | ), 82 | borderWidthPercent: Int = 12, 83 | elevation: ButtonElevation = ButtonDefaults.elevation( 84 | defaultElevation = 18.dp, 85 | pressedElevation = 8.dp 86 | ), 87 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 88 | onClick: () -> Unit, 89 | onLongClick: () -> Unit = { }, 90 | content: @Composable BoxWithConstraintsScope.() -> Unit, 91 | ) { 92 | check(borderWidthPercent in 0..100) { "The border width percent should be in the range of [0, 100]" } 93 | 94 | val colors = ButtonDefaults.buttonColors() 95 | val contentColor by colors.contentColor(true) 96 | 97 | Surface( 98 | onLongClick = onLongClick, 99 | onClick = onClick, 100 | modifier = modifier, 101 | shape = shape, 102 | color = colors.backgroundColor(true).value, 103 | contentColor = contentColor.copy(alpha = 1f), 104 | elevation = elevation.elevation( 105 | enabled = true, 106 | interactionSource = interactionSource 107 | ).value, 108 | interactionSource = interactionSource, 109 | ) { 110 | CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) { 111 | ProvideTextStyle( 112 | value = MaterialTheme.typography.button 113 | ) { 114 | BoxWithConstraints( 115 | modifier = Modifier 116 | .defaultMinSize( 117 | minWidth = ButtonDefaults.MinWidth, 118 | minHeight = ButtonDefaults.MinWidth 119 | ) 120 | .background( 121 | brush = Brush.linearGradient( 122 | 0.0f to lightColor, 123 | 1.0f to darkColor, 124 | ) 125 | ) 126 | .drawWithCache { 127 | val paint = Paint() 128 | .asFrameworkPaint() 129 | .apply { 130 | isAntiAlias = true 131 | style = android.graphics.Paint.Style.STROKE 132 | strokeWidth = size.minDimension * (borderWidthPercent / 100.0f) 133 | shader = LinearGradientShader( 134 | from = Offset.Zero, 135 | to = Offset(x = size.width, y = size.height), 136 | colors = listOf(darkColor, lightColor) 137 | ) 138 | maskFilter = 139 | BlurMaskFilter(strokeWidth / 2, 140 | BlurMaskFilter.Blur.NORMAL) 141 | } 142 | 143 | onDrawBehind { 144 | drawIntoCanvas { 145 | when (val outline = 146 | shape.createOutline(size, layoutDirection, this)) { 147 | is Outline.Rectangle -> { 148 | val rect = outline.rect 149 | it.nativeCanvas.drawRect( 150 | rect.toAndroidRect(), 151 | paint 152 | ) 153 | } 154 | is Outline.Rounded -> { 155 | val roundRect = outline.roundRect 156 | it.nativeCanvas.drawRoundRect( 157 | roundRect.boundingRect.toAndroidRectF(), 158 | roundRect.topLeftCornerRadius.x, 159 | roundRect.topLeftCornerRadius.y, 160 | paint 161 | ) 162 | } 163 | is Outline.Generic -> { 164 | val path = outline.path 165 | it.nativeCanvas.drawPath( 166 | path.asAndroidPath(), 167 | paint 168 | ) 169 | } 170 | } 171 | } 172 | } 173 | }, 174 | contentAlignment = Alignment.Center, 175 | content = content 176 | ) 177 | } 178 | } 179 | } 180 | } 181 | 182 | @OptIn(ExperimentalFoundationApi::class) 183 | @Composable 184 | private fun Surface( 185 | onClick: () -> Unit, 186 | onLongClick: () -> Unit, 187 | modifier: Modifier = Modifier, 188 | enabled: Boolean = true, 189 | shape: Shape = RectangleShape, 190 | color: Color = MaterialTheme.colors.surface, 191 | contentColor: Color = contentColorFor(color), 192 | border: BorderStroke? = null, 193 | elevation: Dp = 0.dp, 194 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 195 | content: @Composable () -> Unit 196 | ) { 197 | val absoluteElevation = LocalAbsoluteElevation.current + elevation 198 | CompositionLocalProvider( 199 | LocalContentColor provides contentColor, 200 | LocalAbsoluteElevation provides absoluteElevation 201 | ) { 202 | Box( 203 | modifier = modifier 204 | .minimumInteractiveComponentSize() 205 | .surface( 206 | shape = shape, 207 | backgroundColor = surfaceColorAtElevation( 208 | color = color, 209 | elevationOverlay = LocalElevationOverlay.current, 210 | absoluteElevation = absoluteElevation 211 | ), 212 | border = border, 213 | elevation = elevation 214 | ) 215 | .combinedClickable( 216 | interactionSource = interactionSource, 217 | indication = rememberRipple(), 218 | enabled = enabled, 219 | onClick = onClick, 220 | onLongClick = onLongClick 221 | ), 222 | propagateMinConstraints = true 223 | ) { 224 | content() 225 | } 226 | } 227 | } 228 | 229 | private fun Modifier.surface( 230 | shape: Shape, 231 | backgroundColor: Color, 232 | border: BorderStroke?, 233 | elevation: Dp 234 | ) = this.shadow(elevation, shape, clip = false) 235 | .then(if (border != null) Modifier.border(border, shape) else Modifier) 236 | .background(color = backgroundColor, shape = shape) 237 | .clip(shape) 238 | 239 | @Composable 240 | private fun surfaceColorAtElevation( 241 | color: Color, 242 | elevationOverlay: ElevationOverlay?, 243 | absoluteElevation: Dp 244 | ): Color { 245 | return if (color == MaterialTheme.colors.surface && elevationOverlay != null) { 246 | elevationOverlay.apply(color, absoluteElevation) 247 | } else { 248 | color 249 | } 250 | } 251 | 252 | @Preview(showBackground = true) 253 | @Composable 254 | fun NeuButtonPreview() { 255 | MaterialTheme { 256 | NeuButton( 257 | modifier = Modifier.padding(10.dp), 258 | lightColor = MaterialTheme.colors.primary, 259 | onClick = { }, 260 | ) { 261 | Text(text = "1") 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/ui/layout/Grid.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.ui.layout 18 | 19 | import androidx.compose.foundation.background 20 | import androidx.compose.foundation.layout.Box 21 | import androidx.compose.foundation.layout.fillMaxSize 22 | import androidx.compose.foundation.layout.fillMaxWidth 23 | import androidx.compose.material.MaterialTheme 24 | import androidx.compose.material.Surface 25 | import androidx.compose.material.Text 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.runtime.Stable 28 | import androidx.compose.ui.Alignment 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.graphics.Color 31 | import androidx.compose.ui.layout.Layout 32 | import androidx.compose.ui.layout.Measurable 33 | import androidx.compose.ui.layout.ParentDataModifier 34 | import androidx.compose.ui.tooling.preview.Preview 35 | import androidx.compose.ui.unit.Constraints 36 | import androidx.compose.ui.unit.Density 37 | import ir.erfansn.siliconecalculator.ui.theme.SiliconeCalculatorTheme 38 | import kotlin.math.max 39 | 40 | interface GridScope { 41 | @Stable 42 | fun Modifier.span(columns: Int = 1, rows: Int = 1) = this.then( 43 | GridData(columns, rows) 44 | ) 45 | 46 | companion object : GridScope 47 | } 48 | 49 | private class GridData( 50 | val columnSpan: Int, 51 | val rowSpan: Int, 52 | ) : ParentDataModifier { 53 | 54 | override fun Density.modifyParentData(parentData: Any?): Any = this@GridData 55 | 56 | override fun equals(other: Any?): Boolean { 57 | if (this === other) return true 58 | if (javaClass != other?.javaClass) return false 59 | 60 | other as GridData 61 | 62 | if (columnSpan != other.columnSpan) return false 63 | if (rowSpan != other.rowSpan) return false 64 | 65 | return true 66 | } 67 | 68 | override fun hashCode(): Int { 69 | var result = columnSpan 70 | result = 31 * result + rowSpan 71 | return result 72 | } 73 | } 74 | 75 | private val Measurable.gridData: GridData? 76 | get() = parentData as? GridData 77 | 78 | private val Measurable.columnSpan: Int 79 | get() = gridData?.columnSpan ?: 1 80 | 81 | private val Measurable.rowSpan: Int 82 | get() = gridData?.rowSpan ?: 1 83 | 84 | data class GridInfo( 85 | val numChildren: Int, 86 | val columnSpan: Int, 87 | val rowSpan: Int, 88 | ) 89 | 90 | @Composable 91 | fun Grid( 92 | columns: Int, 93 | modifier: Modifier = Modifier, 94 | content: @Composable GridScope.() -> Unit, 95 | ) { 96 | check(columns > 0) { "Columns must be greater than 0" } 97 | Layout( 98 | content = { with(GridScope) { content() } }, 99 | modifier = modifier, 100 | ) { measurables, constraints -> 101 | // calculate how many rows we need 102 | val standardGrid = GridData(1, 1) 103 | val spans = measurables.map { measurable -> measurable.gridData ?: standardGrid } 104 | val gridInfo = calculateGridInfo(spans, columns) 105 | val rows = gridInfo.sumOf { it.rowSpan } 106 | 107 | // build constraints 108 | val baseConstraints = Constraints.fixed( 109 | width = constraints.maxWidth / columns, 110 | height = constraints.maxHeight / rows, 111 | ) 112 | val cellConstraints = measurables.map { measurable -> 113 | val columnSpan = measurable.columnSpan 114 | val rowSpan = measurable.rowSpan 115 | Constraints.fixed( 116 | width = baseConstraints.maxWidth * columnSpan, 117 | height = baseConstraints.maxHeight * rowSpan 118 | ) 119 | } 120 | 121 | // measure children 122 | val placeables = measurables.mapIndexed { index, measurable -> 123 | measurable.measure(cellConstraints[index]) 124 | } 125 | 126 | // place children 127 | layout( 128 | width = constraints.maxWidth, 129 | height = constraints.maxHeight, 130 | ) { 131 | var x = 0 132 | var y = 0 133 | var childIndex = 0 134 | gridInfo.forEach { info -> 135 | repeat(info.numChildren) { 136 | val placeable = placeables[childIndex++] 137 | placeable.placeRelative( 138 | x = x, 139 | y = y, 140 | ) 141 | x += placeable.width 142 | } 143 | x = 0 144 | y += info.rowSpan * baseConstraints.maxHeight 145 | } 146 | } 147 | } 148 | } 149 | 150 | private fun calculateGridInfo( 151 | spans: List, 152 | columns: Int, 153 | ): List { 154 | var currentColumnSpan = 0 155 | var currentRowSpan = 0 156 | var numChildren = 0 157 | return buildList { 158 | spans.forEach { span -> 159 | val columnSpan = span.columnSpan.coerceAtMost(columns) 160 | val rowSpan = span.rowSpan 161 | if (currentColumnSpan + columnSpan <= columns) { 162 | currentColumnSpan += columnSpan 163 | currentRowSpan = max(currentRowSpan, rowSpan) 164 | ++numChildren 165 | } else { 166 | add( 167 | GridInfo( 168 | numChildren = numChildren, 169 | columnSpan = currentColumnSpan, 170 | rowSpan = currentRowSpan 171 | ) 172 | ) 173 | currentColumnSpan = columnSpan 174 | currentRowSpan = rowSpan 175 | numChildren = 1 176 | } 177 | } 178 | add( 179 | GridInfo( 180 | numChildren = numChildren, 181 | columnSpan = currentColumnSpan, 182 | rowSpan = currentRowSpan, 183 | ) 184 | ) 185 | } 186 | } 187 | 188 | @Preview 189 | @Composable 190 | fun PreviewGrid() { 191 | SiliconeCalculatorTheme { 192 | Surface( 193 | modifier = Modifier 194 | .fillMaxWidth() 195 | .background(MaterialTheme.colors.background), 196 | ) { 197 | Grid( 198 | columns = 3, 199 | modifier = Modifier.fillMaxSize(), 200 | ) { 201 | Box( 202 | modifier = Modifier 203 | .background(Color.Red) 204 | .span( 205 | columns = 1, 206 | rows = 1, 207 | ) 208 | ) { 209 | Text(text = "1x1", modifier = Modifier.align(Alignment.Center)) 210 | } 211 | Box( 212 | modifier = Modifier 213 | .background(Color.Cyan) 214 | .span( 215 | columns = 2, 216 | rows = 1, 217 | ) 218 | ) { 219 | Text(text = "2x1", modifier = Modifier.align(Alignment.Center)) 220 | } 221 | Box( 222 | modifier = Modifier 223 | .background(Color.Green) 224 | .span( 225 | columns = 2, 226 | rows = 1, 227 | ) 228 | ) { 229 | Text(text = "2x1", modifier = Modifier.align(Alignment.Center)) 230 | } 231 | Box( 232 | modifier = Modifier 233 | .background(Color.Red) 234 | .span( 235 | columns = 2, 236 | rows = 3, 237 | ) 238 | ) { 239 | Text(text = "2x3", modifier = Modifier.align(Alignment.Center)) 240 | } 241 | Box( 242 | modifier = Modifier 243 | .background(Color.Cyan) 244 | .span( 245 | columns = 1, 246 | rows = 2, 247 | ) 248 | ) { 249 | Text(text = "1x2", modifier = Modifier.align(Alignment.Center)) 250 | } 251 | Box( 252 | modifier = Modifier 253 | .background(Color.Magenta) 254 | .span( 255 | columns = 3, 256 | rows = 1, 257 | ) 258 | ) { 259 | Text(text = "3x1", modifier = Modifier.align(Alignment.Center)) 260 | } 261 | } 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.ui.theme 18 | 19 | import androidx.compose.ui.graphics.Color 20 | 21 | val BlueGrey50 = Color(0xFFECECEC) 22 | val BlueGrey100 = Color(0xFFCFD3DE) 23 | val BlueGrey300 = Color(0xFFA0A8BB) 24 | val BlueGrey600 = Color(0xFF5B5F6A) 25 | val BlueGrey700 = Color(0xFF474D5B) 26 | val BlueGrey800 = Color(0xFF333D49) 27 | val BlueGrey900 = Color(0xFF2A2E39) 28 | val DeepOrange800 = Color(0xFFC25A11) 29 | val DeepOrange900 = Color(0xFFC05F1C) 30 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.ui.theme 18 | 19 | import androidx.compose.foundation.shape.CornerSize 20 | import androidx.compose.foundation.shape.RoundedCornerShape 21 | import androidx.compose.material.Shapes 22 | import androidx.compose.ui.unit.dp 23 | 24 | val Shapes = Shapes( 25 | small = RoundedCornerShape(50), 26 | medium = RoundedCornerShape(24), 27 | large = RoundedCornerShape(12) 28 | ) -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.ui.theme 18 | 19 | import androidx.compose.foundation.isSystemInDarkTheme 20 | import androidx.compose.material.MaterialTheme 21 | import androidx.compose.material.darkColors 22 | import androidx.compose.material.lightColors 23 | import androidx.compose.runtime.Composable 24 | 25 | private val DarkColorPalette = darkColors( 26 | primary = BlueGrey900, 27 | primaryVariant = BlueGrey700, 28 | secondary = DeepOrange800, 29 | background = BlueGrey900, 30 | surface = BlueGrey600, 31 | onPrimary = BlueGrey50, 32 | onSecondary = BlueGrey50, 33 | onBackground = BlueGrey50, 34 | onSurface = BlueGrey100, 35 | ) 36 | 37 | private val LightColorPalette = lightColors( 38 | primary = BlueGrey100, 39 | primaryVariant = BlueGrey300, 40 | secondary = DeepOrange900, 41 | background = BlueGrey50, 42 | surface = BlueGrey100, 43 | onPrimary = BlueGrey800, 44 | onSecondary = BlueGrey50, 45 | onBackground = BlueGrey800, 46 | onSurface = BlueGrey800, 47 | ) 48 | 49 | @Composable 50 | fun SiliconeCalculatorTheme( 51 | darkTheme: Boolean = isSystemInDarkTheme(), 52 | content: @Composable () -> Unit 53 | ) { 54 | val colors = if (darkTheme) { 55 | DarkColorPalette 56 | } else { 57 | LightColorPalette 58 | } 59 | MaterialTheme( 60 | colors = colors, 61 | typography = Typography, 62 | shapes = Shapes, 63 | content = content 64 | ) 65 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.ui.theme 18 | 19 | import androidx.compose.material.Typography 20 | import androidx.compose.ui.text.TextStyle 21 | import androidx.compose.ui.text.font.FontFamily 22 | import androidx.compose.ui.text.font.FontWeight 23 | import androidx.compose.ui.unit.sp 24 | 25 | // Set of Material typography styles to start with 26 | val Typography = Typography( 27 | body1 = TextStyle( 28 | fontFamily = FontFamily.Default, 29 | fontWeight = FontWeight.Normal, 30 | fontSize = 16.sp 31 | ) 32 | /* Other default text styles to override 33 | button = TextStyle( 34 | fontFamily = FontFamily.Default, 35 | fontWeight = FontWeight.W500, 36 | fontSize = 14.sp 37 | ), 38 | caption = TextStyle( 39 | fontFamily = FontFamily.Default, 40 | fontWeight = FontWeight.Normal, 41 | fontSize = 12.sp 42 | ) 43 | */ 44 | ) -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/util/DateConverter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.util 18 | 19 | import androidx.room.TypeConverter 20 | import kotlinx.datetime.LocalDate 21 | 22 | class DateConverter { 23 | 24 | @TypeConverter 25 | fun Int.toLocalDate() = LocalDate.fromEpochDays(this) 26 | 27 | @TypeConverter 28 | fun LocalDate.toEpochDay() = toEpochDays() 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/util/DateFormatter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.util 18 | 19 | import kotlinx.datetime.* 20 | import kotlinx.datetime.TimeZone 21 | import java.time.format.DateTimeFormatter 22 | import java.util.* 23 | 24 | fun LocalDate.format(pattern: String = "MMM d"): String = 25 | when (daysUntil(Clock.System.todayIn(TimeZone.currentSystemDefault()))) { 26 | 0 -> "Today" 27 | 1 -> "Yesterday" 28 | else -> { 29 | val formatter = DateTimeFormatter.ofPattern(pattern, Locale.ENGLISH) 30 | formatter.format(toJavaLocalDate()) 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/util/Evaluator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.util 18 | 19 | import org.mariuszgromada.math.mxparser.Expression 20 | import org.mariuszgromada.math.mxparser.License 21 | 22 | class Evaluator { 23 | 24 | init { 25 | License.iConfirmNonCommercialUse("Erfan Sn") 26 | } 27 | 28 | private val _expression = Expression() 29 | 30 | var expression: String = "" 31 | set(value) { 32 | field = value.amendExpression().also(_expression::setExpressionString) 33 | } 34 | 35 | fun eval(): String = _expression.calculate().let { 36 | if (it.isFinite()) { 37 | it.toBigDecimal().toPlainString() 38 | } else { 39 | it.toString() 40 | } 41 | } 42 | 43 | private fun String.amendExpression(): String { 44 | return replace( 45 | regex = """(\d+)\.(\d*)""".toRegex(), 46 | ) { result -> 47 | val (integer, fraction) = result.destructured 48 | "$integer.${fraction.ifEmpty { "0" }}" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/util/MathExpressionFormatter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.util 18 | 19 | const val OPERATORS_REGEX = """[+\-×÷]""" 20 | const val DECIMAL_REGEX = """-?\d+\.?(?:\d+(?:E-?\d+)?)?""" 21 | 22 | private const val SPECIFIC_NUMBER_REGEX = """(-)?(0|\d*)(\.)?(\d*)(E-?)?(\d+)?""" 23 | 24 | fun String.formatNumbers(): String { 25 | fun String.separateByComma() = reversed().chunked(3).joinToString(",").reversed() 26 | 27 | return replace(SPECIFIC_NUMBER_REGEX.toRegex()) { 28 | val (integerSign, integer, point, fraction, exponentSign, exponent) = it.destructured 29 | 30 | "$integerSign${integer.separateByComma()}$point$fraction$exponentSign${exponent.separateByComma()}" 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/erfansn/siliconecalculator/util/SafeUri.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.util 18 | 19 | val String.encodeReservedChars 20 | get() = replace( 21 | """[!*'();:@&=+$,/?%#\[\]]""".toRegex() 22 | ) { "%${it.value.single().code.toString(16)}" } 23 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | #FFECECEC 20 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | Silicone Calculator 19 | 20 | Theme changer 21 | Calculations history 22 | Back to calculator 23 | Clear history 24 | Nothing to show! 25 | Clear 26 | Clear history now? 27 | Cancel 28 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | 25 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 23 | 24 | 28 | 29 | 35 | -------------------------------------------------------------------------------- /app/src/test/java/ir/erfansn/siliconecalculator/calculator/CalculatorScreenTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.calculator 18 | 19 | import androidx.compose.runtime.collectAsState 20 | import androidx.compose.runtime.getValue 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.ui.test.assertTextEquals 23 | import androidx.compose.ui.test.junit4.createComposeRule 24 | import androidx.compose.ui.test.onNodeWithTag 25 | import androidx.compose.ui.test.performClick 26 | import androidx.lifecycle.SavedStateHandle 27 | import ir.erfansn.siliconecalculator.calculator.button.calculatorButtonsInOrderClear 28 | import ir.erfansn.siliconecalculator.data.repository.FakeHistoryRepository 29 | import kotlinx.coroutines.Dispatchers 30 | import org.junit.Before 31 | import org.junit.Rule 32 | import org.junit.Test 33 | import org.junit.runner.RunWith 34 | import org.robolectric.RobolectricTestRunner 35 | 36 | @RunWith(RobolectricTestRunner::class) 37 | class CalculatorScreenTest { 38 | 39 | @get:Rule 40 | val composeTestRule = createComposeRule() 41 | 42 | @Before 43 | fun setUp() { 44 | setContent() 45 | } 46 | 47 | @Test 48 | fun initialState_whenButtonsOfKeyLayoutClicked_showsExpressionAndResultCorrectly() { 49 | with(composeTestRule) { 50 | val nodeTexts = listOf("1", ".", "2", "+", "2", "%", "±") 51 | nodeTexts.forEach { 52 | onNodeWithTag("calculator:$it").performClick() 53 | } 54 | 55 | onNodeWithTag("calculator:expression").assertTextEquals("1.2 + ") 56 | onNodeWithTag("calculator:result").assertTextEquals("-0.02") 57 | } 58 | } 59 | 60 | private fun setContent() { 61 | composeTestRule.setContent { 62 | val viewModel = 63 | remember { CalculatorViewModel(SavedStateHandle(), FakeHistoryRepository(), Dispatchers.Main.immediate) } 64 | val uiState by viewModel.uiState.collectAsState() 65 | 66 | CalculatorScreen( 67 | uiState = uiState, 68 | onCalculatorButtonClick = viewModel::performCalculatorButton, 69 | onHistoryNav = { }, 70 | onThemeToggle = { }, 71 | calculatorButtons = calculatorButtonsInOrderClear 72 | ) 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /app/src/test/java/ir/erfansn/siliconecalculator/calculator/button/common/ClearTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.calculator.button.common 18 | 19 | import com.google.common.truth.Truth.assertThat 20 | import ir.erfansn.siliconecalculator.data.model.Calculation 21 | import org.junit.Test 22 | 23 | class ClearTest { 24 | 25 | @Test 26 | fun `Clears one digit when a number is in result`() { 27 | val calculation = Calculation( 28 | result = "1234" 29 | ) 30 | 31 | val result = Clear.perform(calculation) 32 | 33 | assertThat(result.result).isEqualTo("123") 34 | } 35 | 36 | @Test 37 | fun `Sets zero as result when clearing the last digit with sign`() { 38 | val calculation = Calculation( 39 | result = "-1" 40 | ) 41 | 42 | val result = Clear.perform(calculation) 43 | 44 | assertThat(result.result).isEqualTo("0") 45 | } 46 | 47 | @Test 48 | fun `Moves last number from expression to result when clearing last digit in result`() { 49 | val calculation = Calculation( 50 | expression = "12 + -23 -", 51 | result = "-1" 52 | ) 53 | 54 | val result = Clear.perform(calculation) 55 | 56 | assertThat(result.result).isEqualTo("-23") 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/test/java/ir/erfansn/siliconecalculator/data/repository/FakeHistoryRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.data.repository 18 | 19 | import ir.erfansn.siliconecalculator.data.model.Calculation 20 | import ir.erfansn.siliconecalculator.data.model.History 21 | import ir.erfansn.siliconecalculator.data.model.asHistoryEntity 22 | import ir.erfansn.siliconecalculator.data.source.local.db.model.HistoryEntity 23 | import ir.erfansn.siliconecalculator.data.source.local.db.model.asHistory 24 | import kotlinx.coroutines.flow.MutableStateFlow 25 | import javax.inject.Inject 26 | 27 | class FakeHistoryRepository : HistoryRepository { 28 | 29 | private val historyEntities = mutableListOf() 30 | 31 | override val historyItemsStream = MutableStateFlow(listOf()) 32 | 33 | override suspend fun clearAllHistory() { 34 | historyEntities.clear() 35 | historyItemsStream.value = emptyList() 36 | } 37 | 38 | override suspend fun saveCalculation(calculation: Calculation) { 39 | historyEntities += calculation.asHistoryEntity() 40 | historyItemsStream.value = historyEntities.map(HistoryEntity::asHistory) 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/test/java/ir/erfansn/siliconecalculator/history/HistoryScreenTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.history 18 | 19 | import androidx.activity.ComponentActivity 20 | import androidx.compose.runtime.collectAsState 21 | import androidx.compose.runtime.getValue 22 | import androidx.compose.runtime.remember 23 | import androidx.compose.ui.test.* 24 | import androidx.compose.ui.test.junit4.createAndroidComposeRule 25 | import ir.erfansn.siliconecalculator.R 26 | import ir.erfansn.siliconecalculator.data.model.Calculation 27 | import ir.erfansn.siliconecalculator.data.repository.FakeHistoryRepository 28 | import ir.erfansn.siliconecalculator.data.repository.HistoryRepository 29 | import kotlinx.coroutines.runBlocking 30 | import org.junit.Before 31 | import org.junit.Rule 32 | import org.junit.Test 33 | import org.junit.runner.RunWith 34 | import org.robolectric.RobolectricTestRunner 35 | 36 | @RunWith(RobolectricTestRunner::class) 37 | class HistoryScreenTest { 38 | 39 | @get:Rule 40 | val composeTestRule = createAndroidComposeRule() 41 | 42 | private lateinit var nothingToShow: String 43 | private lateinit var clearHistory: String 44 | private lateinit var historyItems: String 45 | 46 | private val fakeHistoryRepository = FakeHistoryRepository() 47 | 48 | @Before 49 | fun setUp() { 50 | composeTestRule.activity.apply { 51 | nothingToShow = getString(R.string.nothing_to_show) 52 | clearHistory = getString(R.string.clear_history) 53 | historyItems = "history:items" 54 | } 55 | } 56 | 57 | @Test 58 | fun noItem_whenNoItemForShowing_showAppropriateMessage() { 59 | setContent() 60 | 61 | with(composeTestRule) { 62 | onNodeWithTag(historyItems) 63 | .assertDoesNotExist() 64 | 65 | onNodeWithText(nothingToShow) 66 | .assertExists() 67 | .assertIsDisplayed() 68 | } 69 | } 70 | 71 | @Test 72 | fun items_whenHistoryIsNotEmpty_showItems() { 73 | fakeHistoryRepository.saveCalculationBlocking( 74 | calculation = Calculation( 75 | expression = "1 + 1", 76 | result = "2" 77 | ) 78 | ) 79 | 80 | setContent() 81 | 82 | with(composeTestRule) { 83 | onNodeWithText(nothingToShow) 84 | .assertDoesNotExist() 85 | 86 | onNodeWithTag(historyItems) 87 | .assertExists() 88 | .assertIsDisplayed() 89 | } 90 | } 91 | 92 | @Test 93 | fun items_whenClickOnClearHistory_clearItems() { 94 | fakeHistoryRepository.saveCalculationBlocking( 95 | calculation = Calculation( 96 | expression = "2 + 1", 97 | result = "2" 98 | ) 99 | ) 100 | 101 | setContent() 102 | 103 | with(composeTestRule) { 104 | onNodeWithContentDescription(clearHistory) 105 | .performClick() 106 | 107 | onNodeWithTag("history:clear") 108 | .performClick() 109 | 110 | onNodeWithTag(historyItems) 111 | .assertDoesNotExist() 112 | 113 | onNodeWithText(nothingToShow) 114 | .assertExists() 115 | .assertIsDisplayed() 116 | } 117 | } 118 | 119 | private fun setContent() { 120 | composeTestRule.setContent { 121 | val viewModel = remember { HistoryViewModel(fakeHistoryRepository) } 122 | val uiState by viewModel.uiState.collectAsState() 123 | 124 | HistoryScreen( 125 | uiState = uiState, 126 | onBackPress = { }, 127 | onHistoryClear = viewModel::onHistoryClear, 128 | onCalculationClick = { } 129 | ) 130 | } 131 | } 132 | } 133 | 134 | private fun HistoryRepository.saveCalculationBlocking(calculation: Calculation) { 135 | runBlocking { saveCalculation(calculation) } 136 | } -------------------------------------------------------------------------------- /app/src/test/java/ir/erfansn/siliconecalculator/rule/MainDispatcherRule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.rule 18 | 19 | import kotlinx.coroutines.Dispatchers 20 | import kotlinx.coroutines.ExperimentalCoroutinesApi 21 | import kotlinx.coroutines.test.* 22 | import org.junit.rules.TestWatcher 23 | import org.junit.runner.Description 24 | 25 | @ExperimentalCoroutinesApi 26 | class MainDispatcherRule( 27 | val testDispatcher: TestDispatcher = UnconfinedTestDispatcher() 28 | ) : TestWatcher() { 29 | 30 | override fun starting(description: Description) { 31 | Dispatchers.setMain(testDispatcher) 32 | } 33 | 34 | override fun finished(description: Description) { 35 | Dispatchers.resetMain() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/test/java/ir/erfansn/siliconecalculator/util/DateFormatterTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.util 18 | 19 | import com.google.common.truth.Truth.assertThat 20 | import kotlinx.datetime.* 21 | import org.junit.Test 22 | 23 | class DateFormatterTest { 24 | 25 | @Test 26 | fun `Returns 'Today' when given Instant is now`() { 27 | val today = Clock.System.todayIn(TimeZone.currentSystemDefault()) 28 | 29 | val formattedDate = today.format() 30 | 31 | assertThat(formattedDate).isEqualTo("Today") 32 | } 33 | 34 | @Test 35 | fun `Returns 'Yesterday' when given Instant was yesterday`() { 36 | val yesterday = 37 | Clock.System.todayIn(TimeZone.currentSystemDefault()).minus(1, DateTimeUnit.DAY) 38 | 39 | val formattedDate = yesterday.format() 40 | 41 | assertThat(formattedDate).isEqualTo("Yesterday") 42 | } 43 | 44 | @Test 45 | fun `Returns corresponding date when given Instant was a few days ago`() { 46 | val fewDayAgo = 47 | Clock.System.todayIn(TimeZone.currentSystemDefault()).minus(2, DateTimeUnit.DAY) 48 | 49 | val formattedDate = fewDayAgo.format() 50 | 51 | assertThat(formattedDate).isNotEqualTo("Today") 52 | assertThat(formattedDate).isNotEqualTo("Yesterday") 53 | assertThat(formattedDate).matches("""^\w+ \d+$""".toRegex().pattern) 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/test/java/ir/erfansn/siliconecalculator/util/EvaluatorTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.util 18 | 19 | import com.google.common.truth.Truth.assertThat 20 | import org.junit.Test 21 | 22 | class EvaluatorTest { 23 | 24 | private var evaluator = Evaluator() 25 | 26 | @Test 27 | fun `Returns 'NaN' when expression '1 div 0' is evaluated`() { 28 | val result = eval("1 ÷ 0") 29 | 30 | assertThat(result.toDouble()).isEqualTo(Double.NaN) 31 | } 32 | 33 | @Test 34 | fun `Returns '121,0' when expression '11 mul 11' is evaluated`() { 35 | val result = eval("2 × 2") 36 | 37 | assertThat(result.toDouble()).isEqualTo(4.0) 38 | } 39 | 40 | @Test 41 | fun `Returns '1' when expression '1 + 0,' is evaluated`() { 42 | val result = eval("1 + 0.") 43 | 44 | assertThat(result.toDouble()).isEqualTo(1.0) 45 | } 46 | 47 | @Test 48 | fun `Returns '1' when expression '1 + 9r1000' is evaluated`() { 49 | val result = eval("1 + ${"9".repeat(1000)}") 50 | 51 | assertThat(result.toDouble()).isEqualTo(Double.POSITIVE_INFINITY) 52 | } 53 | 54 | @Test 55 | fun `Returns '1' when expression '1 - 9r1000' is evaluated`() { 56 | val result = eval("1 - ${"9".repeat(1000)}") 57 | 58 | assertThat(result.toDouble()).isEqualTo(Double.NEGATIVE_INFINITY) 59 | } 60 | 61 | private fun eval(expression: String): String { 62 | evaluator.expression = expression 63 | return evaluator.eval() 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/test/java/ir/erfansn/siliconecalculator/util/MathExpressionFormatterTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.util 18 | 19 | import com.google.common.truth.Truth.assertThat 20 | import org.junit.Test 21 | 22 | class MathExpressionFormatterTest { 23 | 24 | @Test 25 | fun `Returns empty when given input is empty`() { 26 | assertThat("".formatNumbers()).isEmpty() 27 | } 28 | 29 | @Test 30 | fun `Returns '100,000' when given input is '100000'`() { 31 | assertThat("100000".formatNumbers()).isEqualTo("100,000") 32 | } 33 | 34 | @Test 35 | fun `Returns '1,000,000' when given input is '1000000'`() { 36 | assertThat("1000000".formatNumbers()).isEqualTo("1,000,000") 37 | } 38 | 39 | @Test 40 | fun `Returns 'dot' when given input is 'dot'`() { 41 | assertThat(".".formatNumbers()).isEqualTo(".") 42 | } 43 | 44 | @Test 45 | fun `Returns 'dot0000' when given input is 'dot0000'`() { 46 | assertThat(".0000".formatNumbers()).isEqualTo(".0000") 47 | } 48 | 49 | @Test 50 | fun `Returns 'dot0000E0,000' when given input is 'dot0000E0000'`() { 51 | assertThat(".0000E0000".formatNumbers()).isEqualTo(".0000E0,000") 52 | } 53 | 54 | @Test 55 | fun `Returns '1,000dot' when given input is '1000dot'`() { 56 | assertThat("1000.".formatNumbers()).isEqualTo("1,000.") 57 | } 58 | 59 | @Test 60 | fun `Returns '1,000dot0000' when given input is '1000dot0000'`() { 61 | assertThat("1000.0000".formatNumbers()).isEqualTo("1,000.0000") 62 | } 63 | 64 | @Test 65 | fun `Returns '1,000dot0000E0,000' when given input is '1000dot0000E0000'`() { 66 | assertThat("1000.0000E0000".formatNumbers()).isEqualTo("1,000.0000E0,000") 67 | } 68 | 69 | @Test 70 | fun `Returns '-100dot0000E-000' when given input is '-100dot0000E-000'`() { 71 | assertThat("-100.0000E-000".formatNumbers()).isEqualTo("-100.0000E-000") 72 | } 73 | 74 | @Test 75 | fun `Returns '1,000dot0E0,000 + 1,000' when given input is '1000dot0000E0,000 + 1000'`() { 76 | assertThat("1000dot0E0,000 + 1000".formatNumbers()).isEqualTo("1,000dot0E0,000 + 1,000") 77 | } 78 | } -------------------------------------------------------------------------------- /benchmark/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /benchmark/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | alias(libs.plugins.android.test) 19 | alias(libs.plugins.kotlin.android) 20 | alias(libs.plugins.androidx.baselineprofile) 21 | } 22 | 23 | kotlin { 24 | jvmToolchain(Configs.JVM_TOOLCHAIN_VERSION) 25 | } 26 | 27 | android { 28 | compileSdk = Configs.COMPILE_SDK_VERSION 29 | namespace = "${Configs.PACKAGE_NAME}.benchmark" 30 | 31 | defaultConfig { 32 | minSdk = 28 33 | // Used only for Manifest merging purpose 34 | targetSdk = Configs.TARGET_SDK_VERSION 35 | 36 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 37 | } 38 | 39 | targetProjectPath = ":app" 40 | experimentalProperties["android.experimental.self-instrumenting"] = true 41 | 42 | testOptions.managedDevices.localDevices { 43 | create("pixel8Api34") { 44 | device = "Pixel 8" 45 | apiLevel = 34 46 | systemImageSource = "aosp" 47 | } 48 | } 49 | } 50 | 51 | baselineProfile { 52 | managedDevices += "pixel8Api34" 53 | useConnectedDevices = false 54 | } 55 | 56 | dependencies { 57 | implementation(libs.androidx.core.ktx) 58 | implementation(libs.androidx.runner) 59 | implementation(libs.ext.junit) 60 | implementation(libs.androidx.uiautomator) 61 | implementation(libs.androidx.benchmark.macro.junit4) 62 | } 63 | -------------------------------------------------------------------------------- /benchmark/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | -------------------------------------------------------------------------------- /benchmark/src/main/java/ir/erfansn/siliconecalculator/Utils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator 18 | 19 | const val TARGET_PACKAGE = "ir.erfansn.siliconecalculator" 20 | -------------------------------------------------------------------------------- /benchmark/src/main/java/ir/erfansn/siliconecalculator/benchmark/StartupBenchmark.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.benchmark 18 | 19 | import androidx.benchmark.macro.BaselineProfileMode 20 | import androidx.benchmark.macro.CompilationMode 21 | import androidx.benchmark.macro.StartupMode 22 | import androidx.benchmark.macro.StartupTimingMetric 23 | import androidx.benchmark.macro.junit4.MacrobenchmarkRule 24 | import androidx.test.ext.junit.runners.AndroidJUnit4 25 | import ir.erfansn.siliconecalculator.TARGET_PACKAGE 26 | import org.junit.Rule 27 | import org.junit.Test 28 | import org.junit.runner.RunWith 29 | 30 | /** 31 | * Run this benchmark from Studio to see startup measurements, and captured system traces 32 | * for investigating your app's performance from a cold state. 33 | */ 34 | @RunWith(AndroidJUnit4::class) 35 | class ColdStartupBenchmark : AbstractStartupBenchmark(StartupMode.COLD) 36 | 37 | /** 38 | * Run this benchmark from Studio to see startup measurements, and captured system traces 39 | * for investigating your app's performance from a warm state. 40 | */ 41 | @RunWith(AndroidJUnit4::class) 42 | class WarmStartupBenchmark : AbstractStartupBenchmark(StartupMode.WARM) 43 | 44 | /** 45 | * Run this benchmark from Studio to see startup measurements, and captured system traces 46 | * for investigating your app's performance from a hot state. 47 | */ 48 | @RunWith(AndroidJUnit4::class) 49 | class HotStartupBenchmark : AbstractStartupBenchmark(StartupMode.HOT) 50 | 51 | /** 52 | * Base class for benchmarks with different startup modes. 53 | * Enables app startups from various states of baseline profile or [CompilationMode]s. 54 | */ 55 | abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) { 56 | 57 | @get:Rule 58 | val benchmarkRule = MacrobenchmarkRule() 59 | 60 | @Test 61 | fun startupNoCompilation() = startup(CompilationMode.None()) 62 | 63 | @Test 64 | fun startupPartialCompilation() = startup( 65 | CompilationMode.Partial( 66 | baselineProfileMode = BaselineProfileMode.Disable, 67 | warmupIterations = 3 68 | ) 69 | ) 70 | 71 | @Test 72 | fun startupPartialWithBaselineProfiles() = 73 | startup(CompilationMode.Partial(baselineProfileMode = BaselineProfileMode.Require)) 74 | 75 | @Test 76 | fun startupFullCompilation() = startup(CompilationMode.Full()) 77 | 78 | private fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated( 79 | packageName = TARGET_PACKAGE, 80 | metrics = listOf(StartupTimingMetric()), 81 | compilationMode = compilationMode, 82 | iterations = 10, 83 | startupMode = startupMode, 84 | setupBlock = { pressHome() } 85 | ) { 86 | startActivityAndWait() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /benchmark/src/main/java/ir/erfansn/siliconecalculator/profiler/BaselineProfileGenerator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.profiler 18 | 19 | import androidx.benchmark.macro.junit4.BaselineProfileRule 20 | import androidx.test.ext.junit.runners.AndroidJUnit4 21 | import androidx.test.uiautomator.By 22 | import androidx.test.uiautomator.Direction 23 | import ir.erfansn.siliconecalculator.TARGET_PACKAGE 24 | import org.junit.Rule 25 | import org.junit.Test 26 | import org.junit.runner.RunWith 27 | 28 | @RunWith(AndroidJUnit4::class) 29 | class BaselineProfileGenerator { 30 | 31 | @get:Rule 32 | val baselineProfileRule = BaselineProfileRule() 33 | 34 | @Test 35 | fun generate() = baselineProfileRule.collect(packageName = TARGET_PACKAGE) { 36 | pressHome() 37 | startActivityAndWait() 38 | 39 | repeat(2) { 40 | for (digit in '1'..'9') { 41 | device.findObject(By.res("calculator:$digit")).click() 42 | } 43 | } 44 | device.waitForIdle() 45 | device.findObject(By.res("calculator:result")).swipe(Direction.RIGHT, 1.0f) 46 | device.waitForIdle() 47 | 48 | device.findObject(By.res("calculator:+")).click() 49 | device.findObject(By.res("calculator:1")).click() 50 | device.findObject(By.res("calculator:=")).click() 51 | device.waitForIdle() 52 | 53 | device.findObject(By.descContains("Theme changer")).click() 54 | device.waitForWindowUpdate(packageName, 1000) 55 | 56 | device.findObject(By.res("calculator:AC")).click() 57 | device.findObject(By.res("calculator:3")).click() 58 | repeat(10) { 59 | device.findObject(By.res("calculator:-")).click() 60 | device.findObject(By.res("calculator:2")).click() 61 | device.findObject(By.res("calculator:=")).click() 62 | device.waitForIdle() 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /benchmark/src/main/java/ir/erfansn/siliconecalculator/profiler/StartupProfileGenerator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ir.erfansn.siliconecalculator.profiler 18 | 19 | import androidx.benchmark.macro.junit4.BaselineProfileRule 20 | import androidx.test.ext.junit.runners.AndroidJUnit4 21 | import ir.erfansn.siliconecalculator.TARGET_PACKAGE 22 | import org.junit.Rule 23 | import org.junit.Test 24 | import org.junit.runner.RunWith 25 | 26 | @RunWith(AndroidJUnit4::class) 27 | class StartupProfileGenerator { 28 | 29 | @get:Rule 30 | val baselineProfileRule = BaselineProfileRule() 31 | 32 | @Test 33 | fun generate() = baselineProfileRule.collect( 34 | packageName = TARGET_PACKAGE, 35 | includeInStartupProfile = true 36 | ) { 37 | pressHome() 38 | startActivityAndWait() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | alias(libs.plugins.android.application) apply false 19 | alias(libs.plugins.android.test) apply false 20 | alias(libs.plugins.android.library) apply false 21 | alias(libs.plugins.kotlin.android) apply false 22 | alias(libs.plugins.ksp) apply false 23 | alias(libs.plugins.dagger.hilt) apply false 24 | alias(libs.plugins.kotlin.compose) apply false 25 | alias(libs.plugins.androidx.baselineprofile) apply false 26 | alias(libs.plugins.androidx.room) apply false 27 | } 28 | -------------------------------------------------------------------------------- /buildSrc/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | `kotlin-dsl` 19 | } 20 | 21 | repositories { 22 | mavenCentral() 23 | } 24 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/Configs.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | object Configs { 18 | const val PACKAGE_NAME = "ir.erfansn.siliconecalculator" 19 | const val COMPILE_SDK_VERSION = 34 20 | const val MIN_SDK_VERSION = 21 21 | const val TARGET_SDK_VERSION = 34 22 | const val JVM_TOOLCHAIN_VERSION = 17 23 | } -------------------------------------------------------------------------------- /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 | org.gradle.unsafe.configuration-cache=true 15 | org.gradle.unsafe.configuration-cache-problems=warn 16 | org.gradle.caching=true 17 | org.gradle.console=verbose 18 | # AndroidX package structure to make it clearer which packages are bundled with the 19 | # Android operating system, and which are packaged with your app"s APK 20 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 21 | android.useAndroidX=true 22 | # Kotlin code style for this project: "official" or "obsolete": 23 | kotlin.code.style=official 24 | # Enables namespacing of each library's R class so that its R class includes only the 25 | # resources declared in the library itself and none from the library's dependencies, 26 | # thereby reducing the size of the R class for that library 27 | android.nonTransitiveRClass=true 28 | android.nonFinalResIds=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | activityComposeVersion = "1.9.1" 3 | agpVersion = "8.6.0" 4 | benchmarkMacroJunit4Version = "1.3.0" 5 | constraintlayoutComposeVersion = "1.0.1" 6 | androidxTestVersion = "1.6.1" 7 | coreKtxVersion = "1.13.1" 8 | appcompatVersion = "1.7.0" 9 | desugarJdkLibsVersion = "2.1.1" 10 | hiltNavigationComposeVersion = "1.2.0" 11 | androidxJunitVersion = "1.2.1" 12 | junitVersion = "4.13.2" 13 | kotlinVersion = "2.0.20" 14 | kotlinxCoroutinesVersion = "1.9.0-RC" 15 | kotlinxDatetimeVersion = "0.6.1" 16 | kspVersion = "2.0.20-1.0.24" 17 | daggerHiltVersion = "2.52" 18 | androidxLifecycleVersion = "2.8.4" 19 | mathparserOrgMxparserVersion = "6.0.0" 20 | mockkVersion = "1.13.12" 21 | navigationComposeVersion = "2.7.7" 22 | profileinstallerVersion = "1.3.1" 23 | robolectricVersion = "4.13" 24 | roomVersion = "2.6.1" 25 | truthVersion = "1.4.4" 26 | turbineVersion = "1.1.0" 27 | composeBomVersion = "2024.08.00" 28 | uiautomatorVersion = "2.3.0" 29 | androidxBaselineProfileVersion = "1.3.0" 30 | 31 | [libraries] 32 | activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityComposeVersion" } 33 | androidx-benchmark-macro-junit4 = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4Version" } 34 | androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBomVersion" } 35 | androidx-runner = { module = "androidx.test:runner", version.ref = "androidxTestVersion" } 36 | androidx-core-ktx = { module = "androidx.test:core-ktx", version.ref = "androidxTestVersion" } 37 | androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } 38 | androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } 39 | androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } 40 | androidx-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "uiautomatorVersion" } 41 | appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompatVersion" } 42 | constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayoutComposeVersion" } 43 | core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtxVersion" } 44 | desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugarJdkLibsVersion" } 45 | ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidxJunitVersion" } 46 | hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "daggerHiltVersion" } 47 | hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "daggerHiltVersion" } 48 | hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationComposeVersion" } 49 | kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesVersion" } 50 | kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesVersion" } 51 | junit = { module = "junit:junit", version.ref = "junitVersion" } 52 | kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetimeVersion" } 53 | lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidxLifecycleVersion" } 54 | material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } 55 | material = { module = "androidx.compose.material:material" } 56 | mathparser-org-mxparser = { module = "org.mariuszgromada.math:MathParser.org-mXparser", version.ref = "mathparserOrgMxparserVersion" } 57 | mockk-agent = { module = "io.mockk:mockk-agent", version.ref = "mockkVersion" } 58 | mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockkVersion" } 59 | navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationComposeVersion" } 60 | profileinstaller = { module = "androidx.profileinstaller:profileinstaller", version.ref = "profileinstallerVersion" } 61 | robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectricVersion" } 62 | room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomVersion" } 63 | room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomVersion" } 64 | truth = { module = "com.google.truth:truth", version.ref = "truthVersion" } 65 | turbine = { module = "app.cash.turbine:turbine", version.ref = "turbineVersion" } 66 | ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } 67 | ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } 68 | room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomVersion" } 69 | lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidxLifecycleVersion" } 70 | 71 | [plugins] 72 | android-application = { id = "com.android.application", version.ref = "agpVersion" } 73 | android-test = { id = "com.android.test", version.ref = "agpVersion" } 74 | android-library = { id = "com.android.library", version.ref = "agpVersion" } 75 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinVersion" } 76 | ksp = { id = "com.google.devtools.ksp", version.ref = "kspVersion" } 77 | dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "daggerHiltVersion" } 78 | kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlinVersion" } 79 | androidx-baselineprofile = { id = "androidx.baselineprofile", version.ref = "androidxBaselineProfileVersion" } 80 | androidx-room = { id = "androidx.room", version.ref = "roomVersion" } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Aug 04 16:10:00 IRDT 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /media/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/media/banner.png -------------------------------------------------------------------------------- /media/preview0.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/media/preview0.gif -------------------------------------------------------------------------------- /media/preview1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/media/preview1.png -------------------------------------------------------------------------------- /media/preview2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/media/preview2.png -------------------------------------------------------------------------------- /media/summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfansn/SiliconeCalculator/97ff4a23af575bf70aa581522b55b5527017caf7/media/summary.png -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Erfan Sn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | pluginManagement { 18 | repositories { 19 | gradlePluginPortal() 20 | google() 21 | mavenCentral() 22 | } 23 | } 24 | dependencyResolutionManagement { 25 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 26 | repositories { 27 | google() 28 | mavenCentral() 29 | } 30 | } 31 | rootProject.name = "SiliconeCalculator" 32 | include(":app") 33 | include(":benchmark") 34 | --------------------------------------------------------------------------------