├── .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 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.run/Baseline Profile Generator.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
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 | 
2 |
3 |
4 |
5 |
6 |
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 |
16 |
17 |
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 | 
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 |
--------------------------------------------------------------------------------