diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 60e370c..53a2e05 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,3 +1,2 @@
github: D4rK7355608
-patreon: patreon.com/D4rK7355608
-custom: ['https://www.paypal.me/d4rkmichaeltutorials', 'https://bit.ly/3p8bpjj']
\ No newline at end of file
+patreon: patreon.com/D4rK7355608
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..20da5f3
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,46 @@
+name: Bug Report
+description: File a bug report
+title: "[Bug]: "
+labels: [ "bug", "triage me" ]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill out this bug report!
+ - type: checkboxes
+ attributes:
+ label: Is there an existing issue for this?
+ description: Please search to see if an issue already exists for the bug you encountered.
+ options:
+ - label: I have searched the existing issues
+ required: true
+ - type: checkboxes
+ attributes:
+ label: Is there a StackOverflow question about this issue?
+ description: Please search StackOverflow if an issue with an answer already exists for the bug you encountered.
+ options:
+ - label: I have searched StackOverflow
+ required: false
+ - type: textarea
+ id: what-happened
+ attributes:
+ label: What happened?
+ description: Also tell us, what did you expect to happen?
+ placeholder: Tell us what you see!
+ value: "A bug happened!"
+ validations:
+ required: true
+ - type: textarea
+ id: logs
+ attributes:
+ label: Relevant logcat output
+ description: Please copy and paste any relevant logcat output. This will be automatically formatted into code, so no need for backticks.
+ render: shell
+ - type: checkboxes
+ id: terms
+ attributes:
+ label: Code of Conduct
+ description: By submitting this issue, you agree to follow our Code of Conduct
+ options:
+ - label: I agree to follow this project's Code of Conduct
+ required: true
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..8775570
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,46 @@
+name: Feature request
+description: File a feature request
+title: "[FR]: "
+labels: [ "enhancement", "triage me" ]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill out this bug report!
+ - type: checkboxes
+ attributes:
+ label: Is there an existing issue for this?
+ description: Please search to see if an issue already exists for this feature request.
+ options:
+ - label: I have searched the existing issues
+ required: true
+ - type: textarea
+ id: describe-problem
+ attributes:
+ label: Describe the problem
+ description: Is your feature request related to a problem? Please describe.
+ placeholder: I'm always frustrated when...
+ validations:
+ required: true
+ - type: textarea
+ id: solution
+ attributes:
+ label: Describe the solution
+ description: Please describe the solution you'd like. A clear and concise description of what you want to happen.
+ validations:
+ required: true
+ - type: textarea
+ id: context
+ attributes:
+ label: Additional context
+ description: Add any other context or screenshots about the feature request here.
+ validations:
+ required: false
+ - type: checkboxes
+ id: terms
+ attributes:
+ label: Code of Conduct
+ description: By submitting this issue, you agree to follow our [Code of Conduct](CODE_OF_CONDUCT.md)
+ options:
+ - label: I agree to follow this project's Code of Conduct
+ required: true
\ No newline at end of file
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index bcf259e..2897296 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,8 +1,8 @@
version: 2
updates:
-- package-ecosystem: gradle
- directory: "/"
- schedule:
- interval: weekly
- time: "11:00"
- open-pull-requests-limit: 10
+ - package-ecosystem: gradle
+ directory: "/"
+ schedule:
+ interval: weekly
+ time: "11:00"
+ open-pull-requests-limit: 10
diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml
index 9edee6e..a92893f 100644
--- a/.idea/appInsightsSettings.xml
+++ b/.idea/appInsightsSettings.xml
@@ -22,6 +22,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..b268ef3
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..f11c30b
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 8d81632..fe63bb6 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 5c87e53..cbf60c8 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,7 +1,9 @@
+
-
+
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c7e55c6..f203636 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,42 +1,54 @@
# Version 3.1_r1:
+
- Made various under-the-hood improvements for a better overall app experience.
# Version 3.0_r3:
+
- Added tooltips for all buttons.
- Made minor under-the-hood improvements for a better overall app experience.
# Version 3.0_r2:
+
- Fixed some typos.
# Version 3.0_r1:
+
- Added a new GDPR message to comply with Google Play policy.
- Added a new help center, with more comprehensive documentation and support options.
- Added snack bars instead of toasts for all notifications.
- Added tooltips to help users understand what is happening in the app.
- Improved translations for all supported languages.
- Improved user interface.
-- Fixed various bugs and improved readability, consistency, app performance, and responsiveness by optimizing and styling the outdated code.
+- Fixed various bugs and improved readability, consistency, app performance, and responsiveness by
+ optimizing and styling the outdated code.
- Made various project structure enhancements for better readability and maintainability.
- Made various under-the-hood improvements for a better overall app experience.
# Version 2.0_r1:
+
- Added language support for Hungarian.
- Reworked language system, making it more accurate and user-friendly.
-- Fixed various bugs and improved readability, consistency, app performance, and responsiveness by optimizing and styling the outdated code.
+- Fixed various bugs and improved readability, consistency, app performance, and responsiveness by
+ optimizing and styling the outdated code.
- Made various under-the-hood improvements for a better overall app experience.
# Version 1.2_r1:
+
- Made various under-the-hood improvements for a better overall app experience.
# Version 1.1_r1:
+
- Added ability to remove items from cart.
- Made minor under-the-hood improvements for a better overall app experience.
# Version 1.0_r3:
+
- Added functionality to "Music" chip.
# Version 1.0_r2:
+
- Made minor under-the-hood improvements for a better overall app experience.
# Version 1.0_r1:
+
- Initial stable version.
\ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index be00d95..d7ee0d5 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
-standards, including sustained inappropriate behavior, harassment of an
+standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
diff --git a/README.md b/README.md
index 3006f5e..f4840a9 100644
--- a/README.md
+++ b/README.md
@@ -1,73 +1,87 @@
-
-
-
+![Cart Calculator](/app/src/main/play/listings/en-US/graphics/feature-graphic/play_store_feature_graphic.png "Cart Calculator")
-![Works with Android](https://img.shields.io/badge/Made%20for-Android-lime?style=for-the-badge&logo=android)
-![GitHub Downloads](https://img.shields.io/github/downloads/D4rK7355608/com.d4rk.cartcalculator/total?color=green&style=for-the-badge&logo=github)
-![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/D4rK7355608/com.d4rk.cartcalculator/android.yml?style=for-the-badge)
-![GitHub Issues](https://img.shields.io/github/issues/D4rK7355608/com.d4rk.cartcalculator?style=for-the-badge&logo=github)
-![GitHub Pull Requests](https://img.shields.io/github/issues-pr/D4rK7355608/com.d4rk.cartcalculator?style=for-the-badge&logo=github)
-![GitHub License](https://img.shields.io/github/license/D4rK7355608/com.d4rk.cartcalculator?style=for-the-badge&logo=github)
+
-## 🛒 Cart Calculator 🛒
+Cart Calculator
+==================
-╔╦╦╦═╦╗╔═╦═╦══╦═╗ \
-║║║║╩╣╚╣═╣║║║║║╩╣ \
-╚══╩═╩═╩═╩═╩╩╩╩═╝
+**Cart Calculator helps you to manage your shopping cart in an easy and fast.**
-## Let's go to shopping! 🛒
+The Cart Calculator app is a convenient and simple tool that helps you to manage your shopping cart
+easily and efficiently. Whether you are grocery shopping or just buying household items, this app
+helps you to keep track of everything in one place.
-The Cart Calculator app is a convenient and simple tool that helps you to manage your shopping cart easily and efficiently. Whether you are grocery shopping or just buying household items, this app helps you to keep track of everything in one place.
+With the Cart Calculator app, you can add items to the cart, set the quantity, and calculate the
+total cost of your purchase. You can also update the quantity of each item and see the updated total
+cost in real-time. The app is user-friendly and easy to navigate, making it a perfect choice for
+people of all ages.
-With the Cart Calculator app, you can add items to the cart, set the quantity, and calculate the total cost of your purchase. You can also update the quantity of each item and see the updated total cost in real-time. The app is user-friendly and easy to navigate, making it a perfect choice for people of all ages.
+The Cart Calculator app features a simple and intuitive interface that allows you to add and manage
+items quickly and easily. It also provides a detailed list of your purchases, including the name,
+price, and quantity of each item. You can customize the list according to your preferences, and the
+app will calculate the total cost for you automatically.
-The Cart Calculator app features a simple and intuitive interface that allows you to add and manage items quickly and easily. It also provides a detailed list of your purchases, including the name, price, and quantity of each item. You can customize the list according to your preferences, and the app will calculate the total cost for you automatically.
+In addition to managing your shopping cart, the Cart Calculator app also allows you to track your
+expenses and monitor your spending. This feature helps you to stick to your budget and avoid
+overspending.
-In addition to managing your shopping cart, the Cart Calculator app also allows you to track your expenses and monitor your spending. This feature helps you to stick to your budget and avoid overspending.
+Overall, the Cart Calculator app is an essential tool for anyone who wants to manage their shopping
+cart efficiently and stay on top of their expenses. Whether you are a busy mom or a student on a
+budget, this app is perfect for you. Download it now and start managing your shopping cart like a
+pro!
-Overall, the Cart Calculator app is an essential tool for anyone who wants to manage their shopping cart efficiently and stay on top of their expenses. Whether you are a busy mom or a student on a budget, this app is perfect for you. Download it now and start managing your shopping cart like a pro!
+Our app is designed to be simple and easy to use, while also being fast and lightweight. Plus, it's
+free and open-source software!
-Our app is designed to be simple and easy to use, while also being fast and lightweight. Plus, it's free and open-source software!
+# Features
-## ⚠ Opening Issues!
-Bugs can be reported [here](https://github.com/D4rK7355608/com.d4rk.cartcalculator/issues).
+- Calculate the total amount of your cart
+- Add or remove items from cart
+- Create multiple carts
-- Create a calculator/currency/general bug. 🐞
+# Benefits
-## 🛠️ Features!
-⭐️ No internet required. \
-⭐️ Adaptive themes + Material-You. \
-⭐️ Simple and easy to use. \
-⭐️ Fast and lightweight. \
-⭐️ Free Open source & secure.
+- Efficient shopping
+- Budget management
+- Expense tracking
-## 📝 Changelog [here](https://raw.githubusercontent.com/D4rK7355608/com.d4rk.cartcalculator/master/CHANGELOG.md)!
+# Screenshots
-## 🖼️ App preview:
+
-
+# How it works
-## 🛑 Disclaimer!
-- Only use the GitHub Issues section if you discover issues with the code itself. Do not mistake the Issues page as a help desk. For support, information and requests, please contact d4rk7355608@gmail.com.
+Cart Calculator streamlines your shopping experience by allowing you to effortlessly manage your
+shopping cart. It provides a real-time calculation of your total purchase cost as you add or update
+item quantities. This user-friendly app is designed for shoppers of all ages, ensuring that managing
+your cart is a breeze.
-## 💬 Feedback!
-We are constantly updating and improving Cart Calculator to give you the best possible experience. If you have any suggested features or improvements, please leave a review. In case something is not working correctly please let me know. When posting a low rating please describe what is wrong to give the possibility to fix that issue.
+# Get started today
-Thank you for choosing Cart Calculator. We hope you enjoy using our app as much as we enjoyed creating it for you! Rate us 5 stars ⭐⭐⭐⭐⭐ if you are happy with the app! ❤
+Embark on a smarter shopping journey with Cart Calculator. Download it from the Google Play Store
+now and take the first step towards efficient shopping management. It’s free, open-source, and
+incredibly easy to use—perfect for enhancing your shopping efficiency. Enjoy a seamless shopping
+experience today!
-## 👨🏻💻 More About Me:
-
-
-
-
-
-
-
-
-
-
+# Feedback
-[ ](https://play.google.com/store/apps/details?id=com.d4rk.cartcalculator)
+We are constantly updating and improving Cart Calculator to give you the best possible experience.
+If you
+have any suggested features or improvements, please leave a review. In case something is not working
+correctly please let me know. When posting a low rating please describe what is wrong to give the
+possibility to fix that issue.
+
+Thank you for choosing Cart Calculator! We hope you enjoy using our app as much as we enjoyed
+creating it
+for you!
+
+# License
__Privacy Policy__ [here](https://sites.google.com/view/d4rk7355608/more/apps/privacy-policy).
__Terms of Service__ [here](https://sites.google.com/view/d4rk7355608/more/apps/terms-of-service).
diff --git a/app/build.gradle b/app/build.gradle
deleted file mode 100644
index 7079a96..0000000
--- a/app/build.gradle
+++ /dev/null
@@ -1,81 +0,0 @@
-plugins {
- id 'com.android.application'
- id 'org.jetbrains.kotlin.android'
- id 'com.google.gms.google-services'
- id 'com.google.firebase.crashlytics'
- id 'com.google.android.gms.oss-licenses-plugin'
-}
-android {
- namespace 'com.d4rk.cartcalculator'
- compileSdk 33
- defaultConfig {
- applicationId 'com.d4rk.cartcalculator'
- minSdk 26
- targetSdk 33
- versionCode 21
- versionName '4.0.0'
- archivesBaseName = "${applicationId}-v${versionName}"
- testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
- resourceConfigurations += ['en', 'de', 'es', 'fr', 'hi', 'hu', 'in', 'it', 'ja', 'ro', 'ru', 'tr', 'sv', 'bg', 'pl', 'uk']
- }
- buildTypes {
- release {
- multiDexEnabled true
- minifyEnabled true
- shrinkResources true
- debuggable false
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- }
- debug {
- minifyEnabled true
- shrinkResources true
- multiDexEnabled true
- debuggable true
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- }
- }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_17
- targetCompatibility JavaVersion.VERSION_17
- }
- kotlinOptions {
- jvmTarget = '17'
- }
- buildFeatures {
- viewBinding = true
- buildConfig = true
- }
- bundle {
- storeArchive {
- enable = true
- }
- }
-}
-dependencies {
- implementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.2'
- implementation 'com.google.firebase:firebase-analytics-ktx:21.5.1'
- implementation 'com.google.firebase:firebase-perf:20.5.2'
- implementation 'com.google.android.gms:play-services-ads:22.2.0'
- implementation 'com.google.android.material:material:1.11.0'
- implementation 'com.google.android.gms:play-services-oss-licenses:17.0.1'
- implementation 'com.google.android.play:review-ktx:2.0.1'
- implementation 'com.google.android.play:app-update-ktx:2.1.0'
- implementation 'androidx.core:core-ktx:1.12.0'
- implementation 'androidx.appcompat:appcompat:1.6.1'
- implementation 'androidx.core:core-splashscreen:1.0.1'
- implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
- implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
- implementation 'androidx.lifecycle:lifecycle-process:2.7.0'
- implementation 'androidx.lifecycle:lifecycle-common-java8:2.7.0'
- implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
- implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
- implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
- implementation 'androidx.preference:preference-ktx:1.2.1'
- implementation 'androidx.multidex:multidex:2.0.1'
- implementation 'com.airbnb.android:lottie:6.1.0'
- implementation 'me.zhanghai.android.fastscroll:library:1.3.0'
- testImplementation 'junit:junit:4.13.2'
- androidTestImplementation 'androidx.test.ext:junit:1.1.5'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
-}
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..84ce31b
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,144 @@
+import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.archivesName
+
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+ id("com.google.gms.google-services")
+ id("com.google.firebase.crashlytics")
+ id("com.google.android.gms.oss-licenses-plugin")
+ id("com.google.devtools.ksp")
+}
+android {
+ compileSdk = 34
+ namespace = "com.d4rk.cartcalculator"
+ defaultConfig {
+ applicationId = "com.d4rk.cartcalculator"
+ minSdk = 26
+ targetSdk = 34
+ versionCode = 24
+ versionName = "0.0.1"
+ archivesName = "${applicationId}-v${versionName}"
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ resourceConfigurations += listOf(
+ "en",
+ "de",
+ "es",
+ "fr",
+ "hi",
+ "hu",
+ "in",
+ "it",
+ "ja",
+ "ro",
+ "ru",
+ "tr",
+ "sv",
+ "bg",
+ "pl",
+ "uk"
+ )
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+ buildTypes {
+ release {
+ multiDexEnabled = true
+ isMinifyEnabled = true
+ isShrinkResources = true
+ isDebuggable = false
+ versionNameSuffix = null
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
+ )
+ }
+ debug {
+ multiDexEnabled = true
+ isDebuggable = true
+ versionNameSuffix = null
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+ buildFeatures {
+ viewBinding = true
+ buildConfig = true
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.13"
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+ bundle {
+ storeArchive {
+ enable = true
+ }
+ }
+}
+dependencies {
+ // Firebase
+ implementation(platform(libs.firebase.bom))
+ implementation(libs.firebase.crashlytics.ktx)
+ implementation(libs.firebase.analytics.ktx)
+ implementation(libs.firebase.perf)
+
+ // Google
+ implementation(libs.play.services.ads)
+ implementation(libs.billing)
+ implementation("com.google.android.material:material:1.12.0")
+ implementation(libs.play.services.oss.licenses)
+ implementation(libs.review.ktx)
+ implementation(libs.app.update.ktx)
+
+ // Compose
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.animation.core)
+ implementation(libs.androidx.foundation)
+ implementation(libs.androidx.material.icons.extended)
+ implementation(libs.androidx.material3)
+ implementation(libs.androidx.runtime)
+ implementation(libs.androidx.runtime.livedata)
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.constraintlayout.compose)
+ implementation(libs.ui.tooling)
+ implementation(libs.androidx.datastore.core)
+ implementation(libs.androidx.navigation.compose)
+
+ // AndroidX
+ implementation(libs.androidx.core.ktx)
+ implementation("androidx.appcompat:appcompat:1.6.1")
+ implementation(libs.androidx.core.splashscreen)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.lifecycle.process)
+ implementation(libs.androidx.lifecycle.common.java8)
+ implementation(libs.androidx.lifecycle.livedata.ktx)
+ implementation(libs.androidx.lifecycle.viewmodel.ktx)
+ implementation(libs.androidx.multidex)
+ implementation(libs.androidx.work.runtime.ktx)
+
+ // KSP
+ ksp(libs.androidx.room.compiler)
+ implementation(libs.androidx.room.ktx)
+ implementation(libs.androidx.room.runtime)
+
+ // Test
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(libs.ui.test.junit4)
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+}
\ No newline at end of file
diff --git a/app/release/com.d4rk.cartcalculator-v3.0_r4-release.aab b/app/release/com.d4rk.cartcalculator-v3.0_r4-release.aab
deleted file mode 100644
index 739d84f..0000000
Binary files a/app/release/com.d4rk.cartcalculator-v3.0_r4-release.aab and /dev/null differ
diff --git a/app/src/androidTest/kotlin/com/d4rk/cartcalculator/ExampleInstrumentedTest.kt b/app/src/androidTest/kotlin/com/d4rk/cartcalculator/ExampleInstrumentedTest.kt
index cff6d1f..4f996d6 100644
--- a/app/src/androidTest/kotlin/com/d4rk/cartcalculator/ExampleInstrumentedTest.kt
+++ b/app/src/androidTest/kotlin/com/d4rk/cartcalculator/ExampleInstrumentedTest.kt
@@ -1,13 +1,11 @@
package com.d4rk.cartcalculator
-import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
-
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
-import org.junit.Assert.*
-
/**
* Instrumented test, which will execute on an Android device.
*
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e776a25..c42bd44 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,85 +1,145 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
+
+
+
-
+
+
+
+ android:exported="true">
-
-
-
+
+
+
+
+ android:resource="@xml/shortcuts" />
+
+
+
+
+
+ android:parentActivityName=".MainActivity">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+ android:name=".ui.settings.about.AboutSettingsActivity"
+ android:parentActivityName=".ui.settings.SettingsActivity" />
+
+ android:parentActivityName=".MainActivity" />
+
+ android:value="true" />
+
+ android:value="true" />
+
+ android:value="@integer/google_play_services_version" />
+
+ android:value="ca-app-pub-5294151573817700~1050558256" />
+
+
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/MainActivity.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/MainActivity.kt
index 0b6e57c..27df5d7 100644
--- a/app/src/main/kotlin/com/d4rk/cartcalculator/MainActivity.kt
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/MainActivity.kt
@@ -1,209 +1,203 @@
package com.d4rk.cartcalculator
+
import android.content.Intent
import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.Menu
-import android.view.MenuItem
-import android.view.View
-import androidx.appcompat.app.AppCompatActivity
-import androidx.appcompat.app.AppCompatDelegate
-import androidx.core.os.LocaleListCompat
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.ui.Modifier
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
-import androidx.core.view.GravityCompat
-import androidx.drawerlayout.widget.DrawerLayout
-import androidx.navigation.fragment.NavHostFragment
-import androidx.navigation.ui.AppBarConfiguration
-import androidx.navigation.ui.navigateUp
-import androidx.navigation.ui.setupActionBarWithNavController
-import androidx.navigation.ui.setupWithNavController
-import androidx.preference.PreferenceManager
-import androidx.recyclerview.widget.RecyclerView
-import com.d4rk.cartcalculator.adapters.CartItemAdapter
-import com.d4rk.cartcalculator.data.CartItem
-import com.d4rk.cartcalculator.databinding.ActivityMainBinding
-import com.d4rk.cartcalculator.notifications.AppUpdateNotificationsManager
-import com.d4rk.cartcalculator.notifications.AppUsageNotificationsManager
+import androidx.lifecycle.lifecycleScope
+import com.d4rk.cartcalculator.data.store.DataStore
+import com.d4rk.cartcalculator.notifications.managers.AppUpdateNotificationsManager
+import com.d4rk.cartcalculator.notifications.managers.AppUsageNotificationsManager
+import com.d4rk.cartcalculator.ui.settings.display.theme.AppTheme
import com.d4rk.cartcalculator.ui.startup.StartupActivity
-import com.google.android.gms.ads.AdRequest
-import com.google.android.gms.ads.AdView
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.navigation.NavigationView
-import com.google.android.material.textfield.TextInputEditText
-import com.google.android.material.textview.MaterialTextView
+import com.google.android.material.snackbar.Snackbar
import com.google.android.play.core.appupdate.AppUpdateManager
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
import com.google.android.play.core.install.model.ActivityResult
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.install.model.UpdateAvailability
import com.google.firebase.analytics.FirebaseAnalytics
-class MainActivity : AppCompatActivity() {
- private lateinit var appBarConfiguration: AppBarConfiguration
- private var cartListener: CartListener? = null
- private lateinit var binding: ActivityMainBinding
- private val cartItems = mutableListOf()
- private lateinit var cartItemAdapter: CartItemAdapter
- private var total: Double = 0.0
- private lateinit var appUpdateManager: AppUpdateManager
- private val requestUpdateCode = 1
- private lateinit var appUpdateNotificationsManager: AppUpdateNotificationsManager
- override fun onCreate(savedInstanceState: Bundle?) {
+import com.google.firebase.crashlytics.FirebaseCrashlytics
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.tasks.await
+
+class MainActivity : ComponentActivity() {
+ private lateinit var dataStore : DataStore
+ private lateinit var appUpdateManager : AppUpdateManager
+ private var appUpdateNotificationsManager : AppUpdateNotificationsManager =
+ AppUpdateNotificationsManager(this)
+
+ override fun onCreate(savedInstanceState : Bundle?) {
super.onCreate(savedInstanceState)
installSplashScreen()
- cartItemAdapter = CartItemAdapter(cartItems)
- binding = ActivityMainBinding.inflate(layoutInflater)
- appUpdateManager = AppUpdateManagerFactory.create(this)
- appUpdateNotificationsManager = AppUpdateNotificationsManager(this)
- setContentView(binding.root)
- applyAppSettings()
- setSupportActionBar(binding.appBarMain.toolbar)
- val drawerLayout: DrawerLayout = binding.drawerLayout
- val navView: NavigationView = binding.navView
- val navController by lazy {
- val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment_content_main) as NavHostFragment
- navHostFragment.navController
- }
- appBarConfiguration = AppBarConfiguration(setOf(R.id.nav_home, R.id.nav_settings, R.id.nav_help, R.id.nav_about), drawerLayout)
- setupActionBarWithNavController(navController, appBarConfiguration)
- navView.setupWithNavController(navController)
- navController.setGraph(R.navigation.mobile_navigation)
- drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END)
- }
- private fun applyAppSettings() {
- val themeValues = resources.getStringArray(R.array.preference_theme_values)
- when (PreferenceManager.getDefaultSharedPreferences(this).getString(getString(R.string.key_theme), getString(R.string.default_value_theme))) {
- themeValues[0] -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
- themeValues[1] -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
- themeValues[2] -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
- themeValues[3] -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY)
+ enableEdgeToEdge()
+ dataStore = DataStore.getInstance(this@MainActivity)
+ startupScreen()
+ setupUpdateNotifications()
+ setContent {
+ AppTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize() , color = MaterialTheme.colorScheme.background
+ ) {
+ MainComposable()
+ }
+ }
}
- val languageCode = PreferenceManager.getDefaultSharedPreferences(this)?.getString(getString(R.string.key_language), getString(R.string.default_value_language))
- AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(languageCode))
+ setupSettings()
}
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.menu_main, menu)
- return true
+
+ override fun onResume() {
+ super.onResume()
+ val appUsageNotificationsManager = AppUsageNotificationsManager(this)
+ appUsageNotificationsManager.scheduleAppUsageCheck()
+ appUpdateNotificationsManager.checkAndSendUpdateNotification()
+
+ // TODO: Test on release
+ //checkForFlexibleUpdate()
}
- override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
- R.id.add_to_cart -> {
- val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_add_to_cart, null)
- val adView = dialogView.findViewById(R.id.ad_view)
- adView.loadAd(AdRequest.Builder().build())
- MaterialAlertDialogBuilder(this)
- .setView(dialogView)
- .setPositiveButton(android.R.string.ok) { _, _ ->
- val textView = findViewById(R.id.text_view_empty)
- textView.visibility = View.GONE
- val listView = findViewById(R.id.recycler_view_cart)
- listView.visibility = View.VISIBLE
- val itemNameEditText = dialogView.findViewById(R.id.edit_text_name)
- val itemPriceEditText = dialogView.findViewById(R.id.edit_text_price)
- val itemQuantityEditText = dialogView.findViewById(R.id.edit_text_quantity)
- val itemName = itemNameEditText.text.toString()
- val itemQuantity = itemQuantityEditText.text.toString().toIntOrNull() ?: 0
- val itemPrice = itemPriceEditText.text.toString().toDoubleOrNull()
- if (itemPrice != null) {
- val newItem = CartItem(itemName, itemPrice, itemQuantity)
- cartItems.add(newItem)
- total += newItem.unitPrice
- cartListener?.onCartUpdated(cartItems)
- }
+
+ /**
+ * Overrides the `onActivityResult` method to handle the result of an activity launched for result.
+ *
+ * This function is specifically designed to handle the result of a request code (1)
+ * which is used for in-app updates. It checks the `resultCode` to determine the outcome of the update process.
+ * Depending on the `resultCode`, it either displays a Snackbar message indicating a successful update or
+ * calls a function to show a Snackbar message indicating that the update failed.
+ *
+ * @param requestCode The integer request code originally supplied to startActivityForResult(),
+ * allowing you to identify who this result came from.
+ * @param resultCode The integer result code returned by the child activity through its setResult().
+ * @param data An Intent, which can return result data to the caller (various data can be attached to Intent "extras").
+ */
+ @Suppress("DEPRECATION")
+ @Deprecated("Deprecated in Java")
+ override fun onActivityResult(requestCode : Int , resultCode : Int , data : Intent?) {
+ super.onActivityResult(requestCode , resultCode , data)
+ if (requestCode == 1) {
+ when (resultCode) {
+ RESULT_OK -> {
+ val snackbar = Snackbar.make(
+ findViewById(android.R.id.content) ,
+ R.string.snack_app_updated ,
+ Snackbar.LENGTH_LONG
+ ).setAction(android.R.string.ok , null)
+ snackbar.show()
}
- .setNegativeButton(android.R.string.cancel) { dialog, _ ->
- dialog.dismiss()
+
+ ActivityResult.RESULT_IN_APP_UPDATE_FAILED -> {
+ showUpdateFailedSnackbar()
}
- .show()
- true
- }
- R.id.refresh -> {
- cartItems.clear()
- total = 0.00
- val textViewTotal = findViewById(R.id.text_view_total)
- textViewTotal.text = getString(R.string.total_default_value)
- updateCartList()
- val listView = findViewById(R.id.recycler_view_cart)
- listView.visibility = View.GONE
- val textView = findViewById(R.id.text_view_empty)
- textView.visibility = View.VISIBLE
- supportFragmentManager.popBackStack()
- true
- }
- else -> {
- super.onOptionsItemSelected(item)
+ }
}
}
- interface CartListener {
- fun onCartUpdated(cartItems: List)
- }
- fun setCartListener(listener: CartListener) {
- cartListener = listener
- }
- private fun updateCartList() {
- if (cartItems.isEmpty()) {
- findViewById(R.id.text_view_empty).visibility = View.VISIBLE
- } else {
- findViewById(R.id.text_view_empty).visibility = View.GONE
- cartItemAdapter.notifyItemInserted(cartItems.size - 1)
+
+ /**
+ * Checks for the availability of updates and triggers the appropriate update flow if conditions are met.
+ *
+ * This function uses the lifecycle scope to asynchronously check for available updates using the
+ * Google Play Core library. If an update is available and meets certain conditions, it triggers
+ * the update flow. The update can be of two types: IMMEDIATE or FLEXIBLE.
+ *
+ * For an IMMEDIATE update, it checks if the client version is more than 90 days old. If so, it triggers the update.
+ * For a FLEXIBLE update, it checks if the client version is less than 90 days old. If so, it triggers the update.
+ *
+ * The function also ensures that no developer-triggered update is in progress before triggering a new update.
+ *
+ * @param lifecycleScope The lifecycle scope used for launching coroutines, obtained from the hosting activity.
+ */
+ private fun checkForFlexibleUpdate() {
+ lifecycleScope.launch {
+ val appUpdateInfo = appUpdateManager.appUpdateInfo.await()
+ if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && appUpdateInfo.isUpdateTypeAllowed(
+ AppUpdateType.IMMEDIATE
+ ) && appUpdateInfo.updateAvailability() != UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS
+ ) {
+ @Suppress("DEPRECATION") appUpdateManager.appUpdateInfo.addOnSuccessListener { info ->
+ when {
+ info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && info.isUpdateTypeAllowed(
+ AppUpdateType.IMMEDIATE
+ ) -> {
+ info.clientVersionStalenessDays()?.let {
+ if (it > 90) {
+ appUpdateManager.startUpdateFlowForResult(
+ info , AppUpdateType.IMMEDIATE , this@MainActivity , 1
+ )
+ }
+ }
+ }
+
+ info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && info.isUpdateTypeAllowed(
+ AppUpdateType.FLEXIBLE
+ ) -> {
+ info.clientVersionStalenessDays()?.let {
+ if (it < 90) {
+ appUpdateManager.startUpdateFlowForResult(
+ info , AppUpdateType.FLEXIBLE , this@MainActivity , 1
+ )
+ }
+ }
+ }
+ }
+ }
+ }
}
}
- override fun onSupportNavigateUp(): Boolean {
- val navController by lazy {
- val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment_content_main) as NavHostFragment
- navHostFragment.navController
+
+ /**
+ * Displays a Snackbar message indicating that the update process has failed.
+ *
+ * This function creates a Snackbar with a message indicating that the update process has failed.
+ * The Snackbar includes a "Try Again" action which, when clicked, triggers the `checkForFlexibleUpdate` function
+ * to check for updates and initiate the appropriate update flow if conditions are met.
+ */
+ private fun showUpdateFailedSnackbar() {
+ val snackbar = Snackbar.make(
+ findViewById(android.R.id.content) , R.string.snack_update_failed , Snackbar.LENGTH_LONG
+ ).setAction(R.string.try_again) {
+ checkForFlexibleUpdate()
}
- return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
+ snackbar.show()
}
- @Deprecated("Deprecated in Java")
- override fun onBackPressed() {
- MaterialAlertDialogBuilder(this)
- .setTitle(R.string.close)
- .setMessage(R.string.summary_close)
- .setPositiveButton(android.R.string.ok) { _, _ ->
- super.onBackPressed()
- moveTaskToBack(true)
- }
- .setNegativeButton(android.R.string.cancel, null)
- .create()
- .show()
+
+ private fun setupUpdateNotifications() {
+ appUpdateManager = AppUpdateManagerFactory.create(this)
+ appUpdateNotificationsManager = AppUpdateNotificationsManager(this)
}
- override fun onResume() {
- super.onResume()
- val appUsageNotificationsManager = AppUsageNotificationsManager(this)
- appUsageNotificationsManager.checkAndSendAppUsageNotification()
- appUpdateNotificationsManager.checkAndSendUpdateNotification()
- val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
- val preferenceFirebaseKey = getString(R.string.key_firebase)
- val preferenceFirebase = sharedPreferences.getBoolean(preferenceFirebaseKey, true)
- if (!preferenceFirebase) {
- FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
- } else {
- FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(true)
- }
- appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo ->
- if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
- @Suppress("DEPRECATION")
- appUpdateManager.startUpdateFlowForResult(appUpdateInfo, AppUpdateType.FLEXIBLE, this, requestUpdateCode)
- }
+
+ /**
+ * Configures application settings based on data stored in a DataStore.
+ *
+ * This function uses a lifecycle coroutine scope to asynchronously retrieve the value of `usageAndDiagnostics`
+ * from the DataStore. It then adjusts the Firebase Analytics and Crashlytics collection settings based on the retrieved value.
+ *
+ * If `usageAndDiagnostics` is enabled, both Firebase Analytics and Crashlytics data collection will be enabled. If it's not, data collection will be disabled.
+ *
+ * @see androidx.lifecycle.lifecycleScope
+ * @see androidx.datastore.preferences.core.DataStore
+ * @see com.google.firebase.analytics.FirebaseAnalytics
+ * @see com.google.firebase.crashlytics.FirebaseCrashlytics
+ */
+ private fun setupSettings() {
+ lifecycleScope.launch {
+ val isEnabled = dataStore.usageAndDiagnostics.first()
+ FirebaseAnalytics.getInstance(this@MainActivity)
+ .setAnalyticsCollectionEnabled(isEnabled)
+ FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(isEnabled)
}
- startupScreen()
}
+
private fun startupScreen() {
- val startupPreference = getSharedPreferences("startup", MODE_PRIVATE)
- if (startupPreference.getBoolean("value", true)) {
- startupPreference.edit().putBoolean("value", false).apply()
- startActivity(Intent(this, StartupActivity::class.java))
- }
- }
- @Deprecated("Deprecated in Java")
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- super.onActivityResult(requestCode, resultCode, data)
- if (requestCode == requestUpdateCode) {
- when (resultCode) {
- RESULT_OK -> {
- }
- RESULT_CANCELED -> {
- }
- ActivityResult.RESULT_IN_APP_UPDATE_FAILED -> {
- }
+ lifecycleScope.launch {
+ if (dataStore.startup.first()) {
+ dataStore.saveStartup(false)
+ startActivity(Intent(this@MainActivity , StartupActivity::class.java))
}
}
}
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/MainComposable.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/MainComposable.kt
new file mode 100644
index 0000000..a0cef4c
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/MainComposable.kt
@@ -0,0 +1,153 @@
+package com.d4rk.cartcalculator
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.EventNote
+import androidx.compose.material.icons.automirrored.outlined.HelpOutline
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material.icons.outlined.Settings
+import androidx.compose.material.icons.outlined.Share
+import androidx.compose.material.icons.outlined.VolunteerActivism
+import androidx.compose.material3.DrawerValue
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.ModalDrawerSheet
+import androidx.compose.material3.ModalNavigationDrawer
+import androidx.compose.material3.NavigationDrawerItem
+import androidx.compose.material3.NavigationDrawerItemDefaults
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.rememberDrawerState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.d4rk.cartcalculator.data.navigation.NavigationItem
+import com.d4rk.cartcalculator.ui.help.HelpActivity
+import com.d4rk.cartcalculator.ui.home.HomeComposable
+import com.d4rk.cartcalculator.ui.settings.SettingsActivity
+import com.d4rk.cartcalculator.ui.support.SupportActivity
+import com.d4rk.cartcalculator.utils.Utils
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MainComposable() {
+ val drawerItems = listOf(
+ NavigationItem(
+ title = R.string.settings,
+ selectedIcon = Icons.Outlined.Settings,
+ ),
+ NavigationItem(
+ title = R.string.help_and_feedback,
+ selectedIcon = Icons.AutoMirrored.Outlined.HelpOutline,
+ ),
+ NavigationItem(
+ title = R.string.updates,
+ selectedIcon = Icons.AutoMirrored.Outlined.EventNote,
+ ),
+ NavigationItem(
+ title = R.string.share, selectedIcon = Icons.Outlined.Share
+ ),
+ )
+ val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
+ val scope = rememberCoroutineScope()
+ val context = LocalContext.current
+ val selectedItemIndex by rememberSaveable { mutableIntStateOf(-1) }
+ ModalNavigationDrawer(drawerState = drawerState, drawerContent = {
+ ModalDrawerSheet {
+ Spacer(modifier = Modifier.height(16.dp))
+ drawerItems.forEachIndexed { index, item ->
+ val title = stringResource(item.title)
+ NavigationDrawerItem(
+ label = { Text(text = title) },
+ selected = index == selectedItemIndex,
+ onClick = {
+ when (item.title) {
+
+ R.string.settings -> {
+ Utils.openActivity(
+ context, SettingsActivity::class.java
+ )
+ }
+
+ R.string.help_and_feedback -> {
+ Utils.openActivity(
+ context, HelpActivity::class.java
+ )
+ }
+
+ R.string.updates -> {
+ Utils.openUrl(
+ context,
+ "https://github.com/D4rK7355608/${context.packageName}/blob/master/CHANGELOG.md"
+ )
+ }
+
+ R.string.share -> {
+ Utils.shareApp(context)
+ }
+ }
+ scope.launch {
+ drawerState.close()
+ }
+ },
+ icon = {
+ Icon(
+ item.selectedIcon, contentDescription = title
+ )
+ },
+ badge = {
+ item.badgeCount?.let {
+ Text(text = item.badgeCount.toString())
+ }
+ },
+ modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
+ )
+ }
+ }
+
+ }, content = {
+ Scaffold(
+ topBar = {
+ TopAppBar(title = {
+ Text(text = stringResource(R.string.app_name))
+ }, navigationIcon = {
+ IconButton(onClick = {
+ scope.launch {
+ drawerState.apply {
+ if (isClosed) open() else close()
+ }
+ }
+ }) {
+ Icon(
+ imageVector = Icons.Default.Menu, contentDescription = "Menu"
+ )
+ }
+ }, actions = {
+ IconButton(onClick = {
+ Utils.openActivity(context, SupportActivity::class.java)
+ }) {
+ Icon(
+ Icons.Outlined.VolunteerActivism, contentDescription = "Support"
+ )
+ }
+ })
+ },
+ ) { innerPadding ->
+ Box(modifier = Modifier.padding(innerPadding)) {
+ HomeComposable()
+ }
+ }
+ })
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/MyApp.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/MyApp.kt
new file mode 100644
index 0000000..562650d
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/MyApp.kt
@@ -0,0 +1,154 @@
+@file:Suppress("DEPRECATION")
+
+package com.d4rk.cartcalculator
+
+import android.app.Activity
+import android.app.Application
+import android.content.Context
+import android.os.Bundle
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.OnLifecycleEvent
+import androidx.lifecycle.ProcessLifecycleOwner
+import androidx.multidex.MultiDexApplication
+import androidx.room.Room
+import com.d4rk.cartcalculator.data.db.AppDatabase
+import com.d4rk.cartcalculator.data.store.DataStore
+import com.google.android.gms.ads.AdError
+import com.google.android.gms.ads.AdRequest
+import com.google.android.gms.ads.FullScreenContentCallback
+import com.google.android.gms.ads.LoadAdError
+import com.google.android.gms.ads.MobileAds
+import com.google.android.gms.ads.appopen.AppOpenAd
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+import java.util.Date
+
+private const val AD_UNIT_ID = "ca-app-pub-5294151573817700/9208287867"
+
+@Suppress("SameParameterValue")
+class MyApp : MultiDexApplication(), Application.ActivityLifecycleCallbacks, LifecycleObserver {
+ companion object {
+ lateinit var database: AppDatabase
+ }
+
+ private lateinit var appOpenAdManager: AppOpenAdManager
+ private var currentActivity: Activity? = null
+
+ private lateinit var dataStore: DataStore
+
+ override fun onCreate() {
+ super.onCreate()
+ database = Room.databaseBuilder(this, AppDatabase::class.java, "my_database").build()
+ registerActivityLifecycleCallbacks(this)
+ MobileAds.initialize(this)
+ ProcessLifecycleOwner.get().lifecycle.addObserver(this)
+ dataStore = DataStore.getInstance(this@MyApp)
+ appOpenAdManager = AppOpenAdManager()
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_START)
+ fun onMoveToForeground() {
+ currentActivity?.let { appOpenAdManager.showAdIfAvailable(it) }
+ }
+
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
+ override fun onActivityStarted(activity: Activity) {
+ if (!appOpenAdManager.isShowingAd) {
+ currentActivity = activity
+ }
+ }
+
+ override fun onActivityResumed(activity: Activity) {}
+ override fun onActivityPaused(activity: Activity) {}
+ override fun onActivityStopped(activity: Activity) {}
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
+ override fun onActivityDestroyed(activity: Activity) {}
+
+ interface OnShowAdCompleteListener {
+ @Suppress("EmptyMethod")
+ fun onShowAdComplete()
+ }
+
+ private inner class AppOpenAdManager {
+ private var appOpenAd: AppOpenAd? = null
+ private var isLoadingAd = false
+ var isShowingAd = false
+ private var loadTime: Long = 0
+
+ fun loadAd(context: Context) {
+ if (isLoadingAd || isAdAvailable()) {
+ return
+ }
+ isLoadingAd = true
+ val request = AdRequest.Builder().build()
+ AppOpenAd.load(context,
+ AD_UNIT_ID,
+ request,
+ AppOpenAd.APP_OPEN_AD_ORIENTATION_PORTRAIT,
+ object : AppOpenAd.AppOpenAdLoadCallback() {
+ override fun onAdLoaded(ad: AppOpenAd) {
+ appOpenAd = ad
+ isLoadingAd = false
+ loadTime = Date().time
+ }
+
+ override fun onAdFailedToLoad(loadAdError: LoadAdError) {
+ isLoadingAd = false
+ }
+ })
+ }
+
+ private fun wasLoadTimeLessThanNHoursAgo(numHours: Long): Boolean {
+ val dateDifference: Long = Date().time - loadTime
+ val numMilliSecondsPerHour: Long = 3600000
+ return dateDifference < numMilliSecondsPerHour * numHours
+ }
+
+ @Suppress("BooleanMethodIsAlwaysInverted")
+ private fun isAdAvailable(): Boolean {
+ return appOpenAd != null && wasLoadTimeLessThanNHoursAgo(4)
+ }
+
+ fun showAdIfAvailable(activity: Activity) {
+ showAdIfAvailable(activity, object : OnShowAdCompleteListener {
+ override fun onShowAdComplete() {
+ }
+ })
+ }
+
+ fun showAdIfAvailable(
+ activity: Activity, onShowAdCompleteListener: OnShowAdCompleteListener
+ ) {
+ val isAdsChecked = runBlocking { dataStore.ads.first() }
+ if (isShowingAd || !isAdsChecked) {
+ return
+ }
+ if (!isAdAvailable()) {
+ onShowAdCompleteListener.onShowAdComplete()
+ loadAd(activity)
+ return
+ }
+ appOpenAd!!.fullScreenContentCallback = object : FullScreenContentCallback() {
+ override fun onAdDismissedFullScreenContent() {
+ appOpenAd = null
+ isShowingAd = false
+ onShowAdCompleteListener.onShowAdComplete()
+ loadAd(activity)
+ }
+
+ override fun onAdFailedToShowFullScreenContent(adError: AdError) {
+ appOpenAd = null
+ isShowingAd = false
+ onShowAdCompleteListener.onShowAdComplete()
+ loadAd(activity)
+ }
+
+ override fun onAdShowedFullScreenContent() {
+ }
+ }
+ isShowingAd = true
+ appOpenAd!!.show(activity)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/adapters/CartItemAdapter.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/adapters/CartItemAdapter.kt
deleted file mode 100644
index cbc2711..0000000
--- a/app/src/main/kotlin/com/d4rk/cartcalculator/adapters/CartItemAdapter.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-package com.d4rk.cartcalculator.adapters
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.recyclerview.widget.RecyclerView
-import com.d4rk.cartcalculator.data.CartItem
-import com.d4rk.cartcalculator.databinding.ItemListCartBinding
-class CartItemAdapter(private var cartItems: List, private val listener: OnQuantityChangeListener? = null) : RecyclerView.Adapter() {
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CartItemViewHolder {
- val inflater = LayoutInflater.from(parent.context)
- val binding = ItemListCartBinding.inflate(inflater, parent, false)
- return CartItemViewHolder(binding)
- }
- override fun onBindViewHolder(holder: CartItemViewHolder, position: Int) {
- val cartItem = cartItems[position]
- if (cartItem.quantity > 0) {
- holder.binding.textViewListCartItemsName.text = cartItem.name
- holder.binding.textViewListCartItemsPrice.text = cartItem.totalPrice().toString()
- holder.binding.textViewListCartItemsQuantity.text = cartItem.quantity.toString()
- holder.binding.buttonListCartItemsPlusItem.setOnClickListener {
- cartItem.quantity++
- listener?.onQuantityChanged(cartItems)
- notifyItemChanged(position)
- }
- holder.binding.buttonListCartItemsMinusItem.setOnClickListener {
- cartItem.quantity--
- if (cartItem.quantity <= 0) {
- cartItems = cartItems.filter { it != cartItem }
- listener?.onQuantityChanged(cartItems)
- notifyItemRemoved(position)
- } else {
- listener?.onQuantityChanged(cartItems)
- notifyItemChanged(position)
- }
- }
- } else {
- cartItems = cartItems.filter { it != cartItem }
- listener?.onQuantityChanged(cartItems)
- notifyItemRemoved(position)
- }
- }
- override fun getItemCount() = cartItems.size
- fun setItems(items: List) {
- cartItems = items.filter { it.quantity > 0 }
- notifyItemRangeChanged(0, cartItems.size)
- }
- interface OnQuantityChangeListener {
- fun onQuantityChanged(cartItems: List)
- }
- inner class CartItemViewHolder(val binding: ItemListCartBinding) : RecyclerView.ViewHolder(binding.root)
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ads/managers/AppOpenAdManager.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ads/managers/AppOpenAdManager.kt
deleted file mode 100644
index 17b9fe7..0000000
--- a/app/src/main/kotlin/com/d4rk/cartcalculator/ads/managers/AppOpenAdManager.kt
+++ /dev/null
@@ -1,118 +0,0 @@
-@file:Suppress("DEPRECATION")
-package com.d4rk.cartcalculator.ads.managers
-import android.app.Activity
-import android.app.Application
-import android.content.Context
-import android.os.Bundle
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleObserver
-import androidx.lifecycle.OnLifecycleEvent
-import androidx.lifecycle.ProcessLifecycleOwner
-import androidx.multidex.MultiDexApplication
-import com.google.android.gms.ads.AdError
-import com.google.android.gms.ads.AdRequest
-import com.google.android.gms.ads.FullScreenContentCallback
-import com.google.android.gms.ads.LoadAdError
-import com.google.android.gms.ads.MobileAds
-import com.google.android.gms.ads.appopen.AppOpenAd
-import java.util.Date
-private const val AD_UNIT_ID = "ca-app-pub-5294151573817700/9208287867"
-
-class AppOpenAdManager : MultiDexApplication(), Application.ActivityLifecycleCallbacks,
- LifecycleObserver {
- private lateinit var appOpenAdManager: AppOpenAdManager
- private var currentActivity: Activity? = null
- override fun onCreate() {
- super.onCreate()
- registerActivityLifecycleCallbacks(this)
- MobileAds.initialize(this)
- ProcessLifecycleOwner.get().lifecycle.addObserver(this)
- appOpenAdManager = AppOpenAdManager()
- }
- @OnLifecycleEvent(Lifecycle.Event.ON_START)
- fun onMoveToForeground() {
- currentActivity?.let { appOpenAdManager.showAdIfAvailable(it) }
- }
- override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
- override fun onActivityStarted(activity: Activity) {
- if (!appOpenAdManager.isShowingAd) {
- currentActivity = activity
- }
- }
- override fun onActivityResumed(activity: Activity) {}
- override fun onActivityPaused(activity: Activity) {}
- override fun onActivityStopped(activity: Activity) {}
- override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
- override fun onActivityDestroyed(activity: Activity) {}
- interface OnShowAdCompleteListener {
- fun onShowAdComplete()
- }
- private inner class AppOpenAdManager {
- private var appOpenAd: AppOpenAd? = null
- private var isLoadingAd = false
- var isShowingAd = false
- private var loadTime: Long = 0
- fun loadAd(context: Context) {
- if (isLoadingAd || isAdAvailable()) {
- return
- }
- isLoadingAd = true
- val request = AdRequest.Builder().build()
- AppOpenAd.load(context, AD_UNIT_ID, request, AppOpenAd.APP_OPEN_AD_ORIENTATION_PORTRAIT, object : AppOpenAd.AppOpenAdLoadCallback() {
- override fun onAdLoaded(ad: AppOpenAd) {
- appOpenAd = ad
- isLoadingAd = false
- loadTime = Date().time
- }
- override fun onAdFailedToLoad(loadAdError: LoadAdError) {
- isLoadingAd = false
- }
- })
- }
- @Suppress("SameParameterValue")
- private fun wasLoadTimeLessThanNHoursAgo(numHours: Long): Boolean {
- val dateDifference: Long = Date().time - loadTime
- val numMilliSecondsPerHour: Long = 3600000
- return dateDifference < numMilliSecondsPerHour * numHours
- }
- @Suppress("BooleanMethodIsAlwaysInverted")
- private fun isAdAvailable(): Boolean {
- return appOpenAd != null && wasLoadTimeLessThanNHoursAgo(4)
- }
- fun showAdIfAvailable(activity: Activity) {
- showAdIfAvailable(activity, object : OnShowAdCompleteListener {
- override fun onShowAdComplete() {
- }
- }
- )
- }
- fun showAdIfAvailable(activity: Activity, onShowAdCompleteListener: OnShowAdCompleteListener) {
- if (isShowingAd) {
- return
- }
- if (!isAdAvailable()) {
- onShowAdCompleteListener.onShowAdComplete()
- loadAd(activity)
- return
- }
- appOpenAd!!.fullScreenContentCallback = object : FullScreenContentCallback() {
- override fun onAdDismissedFullScreenContent() {
- appOpenAd = null
- isShowingAd = false
- onShowAdCompleteListener.onShowAdComplete()
- loadAd(activity)
- }
- override fun onAdFailedToShowFullScreenContent(adError: AdError) {
- appOpenAd = null
- isShowingAd = false
- onShowAdCompleteListener.onShowAdComplete()
- loadAd(activity)
- }
- override fun onAdShowedFullScreenContent() {
- }
- }
- isShowingAd = true
- appOpenAd!!.show(activity)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/data/CartItem.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/data/CartItem.kt
deleted file mode 100644
index 1d0e27c..0000000
--- a/app/src/main/kotlin/com/d4rk/cartcalculator/data/CartItem.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.d4rk.cartcalculator.data
-data class CartItem(val name: String, val unitPrice: Double, var quantity: Int) {
- fun totalPrice(): Double {
- return unitPrice * quantity
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/data/db/AppDatabase.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/data/db/AppDatabase.kt
new file mode 100644
index 0000000..59f24b5
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/data/db/AppDatabase.kt
@@ -0,0 +1,17 @@
+package com.d4rk.cartcalculator.data.db
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import com.d4rk.cartcalculator.data.db.dao.NewCartDao
+import com.d4rk.cartcalculator.data.db.dao.ShoppingCartItemsDao
+import com.d4rk.cartcalculator.data.db.table.ShoppingCartItemsTable
+import com.d4rk.cartcalculator.data.db.table.ShoppingCartTable
+import com.d4rk.cartcalculator.data.repository.DateConverter
+
+@Database(entities = [ShoppingCartTable::class, ShoppingCartItemsTable::class], version = 1)
+@TypeConverters(DateConverter::class)
+abstract class AppDatabase : RoomDatabase() {
+ abstract fun newCartDao(): NewCartDao
+ abstract fun shoppingCartItemsDao(): ShoppingCartItemsDao
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/data/db/dao/NewCartDao.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/data/db/dao/NewCartDao.kt
new file mode 100644
index 0000000..3e3812f
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/data/db/dao/NewCartDao.kt
@@ -0,0 +1,22 @@
+package com.d4rk.cartcalculator.data.db.dao
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import com.d4rk.cartcalculator.data.db.table.ShoppingCartTable
+
+@Dao
+interface NewCartDao {
+ @Insert
+ suspend fun insert(cart : ShoppingCartTable) : Long
+
+ @Delete
+ suspend fun delete(cart: ShoppingCartTable)
+
+ @Query("SELECT * FROM ShoppingCartTable")
+ suspend fun getAll(): List
+
+ @Query("SELECT * FROM ShoppingCartTable WHERE cartId = :cartId")
+ suspend fun getCartById(cartId: Int): ShoppingCartTable
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/data/db/dao/ShoppingCartItemsDao.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/data/db/dao/ShoppingCartItemsDao.kt
new file mode 100644
index 0000000..645cb65
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/data/db/dao/ShoppingCartItemsDao.kt
@@ -0,0 +1,29 @@
+package com.d4rk.cartcalculator.data.db.dao
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Update
+import com.d4rk.cartcalculator.data.db.table.ShoppingCartItemsTable
+
+@Dao
+interface ShoppingCartItemsDao {
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insert(item: ShoppingCartItemsTable)
+
+ @Update
+ suspend fun update(item: ShoppingCartItemsTable)
+
+ @Delete
+ suspend fun delete(item: ShoppingCartItemsTable)
+
+ @Query("SELECT * FROM ShoppingCartItemsTable WHERE cartId = :cartId")
+ suspend fun getItemsByCartId(cartId: Int): List
+
+ @Query("DELETE FROM ShoppingCartItemsTable WHERE cartId = :cartId")
+ suspend fun deleteItemsFromCart(cartId: Int)
+
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/data/db/table/ShoppingCartItemsTable.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/data/db/table/ShoppingCartItemsTable.kt
new file mode 100644
index 0000000..1fa5b35
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/data/db/table/ShoppingCartItemsTable.kt
@@ -0,0 +1,13 @@
+package com.d4rk.cartcalculator.data.db.table
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity
+data class ShoppingCartItemsTable(
+ @PrimaryKey(autoGenerate = true) val id: Int = 0,
+ var cartId: Int,
+ val name: String,
+ val price: String,
+ var quantity: Int
+)
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/data/db/table/ShoppingCartTable.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/data/db/table/ShoppingCartTable.kt
new file mode 100644
index 0000000..3b7e7f7
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/data/db/table/ShoppingCartTable.kt
@@ -0,0 +1,14 @@
+package com.d4rk.cartcalculator.data.db.table
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import androidx.room.TypeConverters
+import com.d4rk.cartcalculator.data.repository.DateConverter
+import java.util.Date
+
+@Entity
+@TypeConverters(DateConverter::class)
+data class ShoppingCartTable(
+ @PrimaryKey(autoGenerate = true) val cartId: Int = 0, val name: String,
+ val date: Date
+)
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/data/navigation/NavigationItem.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/data/navigation/NavigationItem.kt
new file mode 100644
index 0000000..8b7e6e2
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/data/navigation/NavigationItem.kt
@@ -0,0 +1,7 @@
+package com.d4rk.cartcalculator.data.navigation
+
+import androidx.compose.ui.graphics.vector.ImageVector
+
+data class NavigationItem(
+ val title: Int, val selectedIcon: ImageVector, val badgeCount: Int? = null
+)
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/data/repository/DateConverter.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/data/repository/DateConverter.kt
new file mode 100644
index 0000000..e35f600
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/data/repository/DateConverter.kt
@@ -0,0 +1,16 @@
+package com.d4rk.cartcalculator.data.repository
+
+import androidx.room.TypeConverter
+import java.util.Date
+
+class DateConverter {
+ @TypeConverter
+ fun toDate(dateLong: Long?): Date? {
+ return dateLong?.let { Date(it) }
+ }
+
+ @TypeConverter
+ fun fromDate(date: Date?): Long? {
+ return date?.time
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/data/store/DataStore.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/data/store/DataStore.kt
new file mode 100644
index 0000000..23d84cd
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/data/store/DataStore.kt
@@ -0,0 +1,136 @@
+package com.d4rk.cartcalculator.data.store
+
+import android.content.Context
+import androidx.compose.runtime.mutableStateOf
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.longPreferencesKey
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.preferencesDataStore
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+val Context.dataStore by preferencesDataStore("settings")
+
+class DataStore(context: Context) {
+ private val dataStore = context.dataStore
+
+ companion object {
+ @Volatile
+ private var instance: DataStore? = null
+
+ fun getInstance(context: Context): DataStore {
+ return instance ?: synchronized(this) {
+ instance ?: DataStore(context).also { instance = it }
+ }
+ }
+ }
+
+ // Last used app notifications
+ private val lastUsedKey = longPreferencesKey("last_used")
+ val lastUsed: Flow = dataStore.data.map { preferences ->
+ preferences[lastUsedKey] ?: 0
+ }
+
+ suspend fun saveLastUsed(timestamp: Long) {
+ dataStore.edit { preferences ->
+ preferences[lastUsedKey] = timestamp
+ }
+ }
+
+
+ // Startup
+ private val startupKey = booleanPreferencesKey("value")
+ val startup: Flow = dataStore.data.map { preferences ->
+ preferences[startupKey] ?: true
+ }
+
+ suspend fun saveStartup(isFirstTime: Boolean) {
+ dataStore.edit { preferences ->
+ preferences[startupKey] = isFirstTime
+ }
+ }
+
+ // Display
+ val themeModeState = mutableStateOf("follow_system")
+ private val themeModeKey = stringPreferencesKey("theme_mode")
+ val themeMode: Flow = dataStore.data.map { preferences ->
+ preferences[themeModeKey] ?: "follow_system"
+ }
+
+ suspend fun saveThemeMode(mode: String) {
+ dataStore.edit { preferences ->
+ preferences[themeModeKey] = mode
+ }
+ }
+
+ private val amoledModeKey = booleanPreferencesKey("amoled_mode")
+ val amoledMode: Flow = dataStore.data.map { preferences ->
+ preferences[amoledModeKey] ?: false
+ }
+
+ suspend fun saveAmoledMode(isChecked: Boolean) {
+ dataStore.edit { preferences ->
+ preferences[amoledModeKey] = isChecked
+ }
+ }
+
+ private val dynamicColorsKey = booleanPreferencesKey("dynamic_colors")
+ val dynamicColors: Flow = dataStore.data.map { preferences ->
+ preferences[dynamicColorsKey] ?: true
+ }
+
+ suspend fun saveDynamicColors(isChecked: Boolean) {
+ dataStore.edit { preferences ->
+ preferences[dynamicColorsKey] = isChecked
+ }
+ }
+
+ /* private val languageKey = stringPreferencesKey("language")
+ val language: Flow = dataStore.data.map { preferences ->
+ preferences[languageKey] ?: getString(R.string.default_value_language)
+ }
+ suspend fun saveLanguage(language: String) {
+ dataStore.edit { preferences ->
+ preferences[languageKey] = language
+ }
+ }*/
+
+ private val currencyKey = stringPreferencesKey("preferred_currency")
+
+ suspend fun getCurrency() : Flow {
+ return dataStore.data.map { preferences ->
+ preferences[currencyKey] ?: ""
+ }
+ }
+
+ suspend fun saveCurrency(currency : String) {
+ dataStore.edit { preferences ->
+ preferences[currencyKey] = currency
+ }
+ }
+
+ // Usage and Diagnostics
+ private val usageAndDiagnosticsKey = booleanPreferencesKey("usage_and_diagnostics")
+ val usageAndDiagnostics: Flow = dataStore.data.map { preferences ->
+ preferences[usageAndDiagnosticsKey] ?: true
+ }
+
+ suspend fun saveUsageAndDiagnostics(isChecked: Boolean) {
+ dataStore.edit { preferences ->
+ preferences[usageAndDiagnosticsKey] = isChecked
+ }
+ }
+
+ // Ads
+ private val adsKey = booleanPreferencesKey("ads")
+ val ads: Flow = dataStore.data.map { preferences ->
+ preferences[adsKey] ?: true
+ }
+
+ suspend fun saveAds(isChecked: Boolean) {
+ dataStore.edit { preferences ->
+ preferences[adsKey] = isChecked
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/dialogs/CurrencyDialog.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/dialogs/CurrencyDialog.kt
new file mode 100644
index 0000000..bdacc16
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/dialogs/CurrencyDialog.kt
@@ -0,0 +1,94 @@
+package com.d4rk.cartcalculator.dialogs
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Info
+import androidx.compose.material.icons.outlined.Money
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringArrayResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.d4rk.cartcalculator.R
+import com.d4rk.cartcalculator.data.store.DataStore
+import kotlinx.coroutines.flow.firstOrNull
+
+@Composable
+fun CurrencyDialog(
+ dataStore : DataStore , onDismiss : () -> Unit , onCurrencySelected : (String) -> Unit
+) {
+ val selectedCurrency = remember { mutableStateOf("") }
+ val currencies = stringArrayResource(R.array.currency).toList()
+
+ AlertDialog(onDismissRequest = onDismiss ,
+ text = { CurrencyDialogContent(selectedCurrency , dataStore , currencies) } ,
+ icon = {
+ Icon(Icons.Outlined.Money , contentDescription = null)
+ } ,
+ confirmButton = {
+ TextButton(onClick = {
+ onCurrencySelected(selectedCurrency.value)
+ }) {
+ Text(stringResource(android.R.string.ok))
+ }
+ } ,
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text(stringResource(android.R.string.cancel))
+ }
+ })
+}
+
+@Composable
+fun CurrencyDialogContent(
+ selectedCurrency : MutableState , dataStore : DataStore , currencies : List
+) {
+ LaunchedEffect(Unit) {
+ selectedCurrency.value = dataStore.getCurrency().firstOrNull() ?: ""
+ }
+
+ Column {
+ Text(stringResource(id = R.string.dialog_currency_subtitle))
+ Column(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ currencies.forEach { currency ->
+ Row(
+ Modifier.fillMaxWidth() ,
+ verticalAlignment = Alignment.CenterVertically ,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ RadioButton(selected = selectedCurrency.value == currency ,
+ onClick = { selectedCurrency.value = currency })
+ Text(
+ text = currency , style = MaterialTheme.typography.bodyMedium.merge()
+ )
+ }
+ }
+ }
+ Spacer(modifier = Modifier.height(24.dp))
+ Icon(imageVector = Icons.Outlined.Info , contentDescription = null)
+ Spacer(modifier = Modifier.height(12.dp))
+ Text(stringResource(id = R.string.dialog_info_currency))
+ }
+
+ LaunchedEffect(selectedCurrency.value) {
+ dataStore.saveCurrency(selectedCurrency.value)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/dialogs/NewCartDialogComposable.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/dialogs/NewCartDialogComposable.kt
new file mode 100644
index 0000000..e6eb374
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/dialogs/NewCartDialogComposable.kt
@@ -0,0 +1,82 @@
+package com.d4rk.cartcalculator.dialogs
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Info
+import androidx.compose.material.icons.outlined.ShoppingCartCheckout
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.d4rk.cartcalculator.MyApp
+import com.d4rk.cartcalculator.R
+import com.d4rk.cartcalculator.data.db.table.ShoppingCartTable
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import java.util.Date
+
+@Composable
+fun NewCartDialog(onDismiss : () -> Unit , onCartCreated : (Long , String) -> Unit) {
+ val newCart = remember { mutableStateOf(null) }
+ val lifecycleScope = rememberCoroutineScope()
+ val nameText = remember { mutableStateOf("") }
+
+ AlertDialog(onDismissRequest = onDismiss ,
+ text = { NewCartDialogContent(newCart , nameText) } ,
+ icon = {
+ Icon(
+ Icons.Outlined.ShoppingCartCheckout , contentDescription = null
+ )
+ } ,
+ confirmButton = {
+ TextButton(onClick = {
+ newCart.value?.let { cart ->
+ lifecycleScope.launch(Dispatchers.IO) {
+ val cartId = MyApp.database.newCartDao().insert(cart)
+ onCartCreated(cartId , cart.name)
+ }
+ }
+
+ }) {
+ Text(stringResource(android.R.string.ok))
+ }
+ } ,
+ dismissButton = {
+ TextButton(onClick = {
+ onDismiss()
+ }) {
+ Text(stringResource(android.R.string.cancel))
+ }
+ })
+}
+
+@Composable
+fun NewCartDialogContent(
+ newCart : MutableState , nameText : MutableState
+) {
+ val currentDate = Date()
+ val defaultName = stringResource(R.string.shopping_cart)
+ Column {
+ OutlinedTextField(value = nameText.value ,
+ onValueChange = { nameText.value = it } ,
+ label = { Text("Name") } ,
+ placeholder = { Text(stringResource(R.string.shopping_cart)) })
+ Spacer(modifier = Modifier.height(24.dp))
+ Icon(imageVector = Icons.Outlined.Info , contentDescription = null)
+ Spacer(modifier = Modifier.height(12.dp))
+ Text(stringResource(R.string.summary_cart_dialog))
+ }
+ newCart.value =
+ ShoppingCartTable(name = nameText.value.ifEmpty { defaultName } , date = currentDate)
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/dialogs/NewCartItemDialogComposable.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/dialogs/NewCartItemDialogComposable.kt
new file mode 100644
index 0000000..d1afd51
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/dialogs/NewCartItemDialogComposable.kt
@@ -0,0 +1,86 @@
+package com.d4rk.cartcalculator.dialogs
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Info
+import androidx.compose.material.icons.outlined.ShoppingBag
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import com.d4rk.cartcalculator.R
+import com.d4rk.cartcalculator.data.db.table.ShoppingCartItemsTable
+
+@Composable
+fun NewCartItemDialog(
+ cartId : Int , onDismiss : () -> Unit , onCartCreated : (ShoppingCartItemsTable) -> Unit
+) {
+ val newCartItem = remember { mutableStateOf(null) }
+ AlertDialog(onDismissRequest = onDismiss , text = {
+ NewCartItemDialogContent(cartId , newCartItem)
+ } , icon = {
+ Icon(
+ Icons.Outlined.ShoppingBag , contentDescription = null
+ )
+ } , confirmButton = {
+ TextButton(onClick = {
+ newCartItem.value?.let { cartItem ->
+ onCartCreated(cartItem)
+ }
+ }) {
+ Text(stringResource(android.R.string.ok))
+ }
+ } , dismissButton = {
+ TextButton(onClick = {
+ onDismiss()
+ }) {
+ Text(stringResource(android.R.string.cancel))
+ }
+ })
+}
+
+@Composable
+fun NewCartItemDialogContent(cartId : Int , newCartItem : MutableState) {
+ val nameText = remember { mutableStateOf("") }
+ val priceText = remember { mutableStateOf("") }
+ val quantityText = remember { mutableStateOf("") }
+
+ Column {
+ OutlinedTextField(value = nameText.value ,
+ onValueChange = { nameText.value = it } ,
+ label = { Text(stringResource(id = R.string.item_name)) } ,
+ placeholder = { Text(stringResource(id = R.string.enter_item_name)) })
+ OutlinedTextField(value = priceText.value ,
+ onValueChange = { priceText.value = it } ,
+ label = { Text(stringResource(id = R.string.item_price)) } ,
+ placeholder = { Text(stringResource(id = R.string.enter_item_price)) } ,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number))
+ OutlinedTextField(value = quantityText.value ,
+ onValueChange = { quantityText.value = it } ,
+ label = { Text(stringResource(id = R.string.quantity)) } ,
+ placeholder = { Text(stringResource(id = R.string.hint_quantity)) } ,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number))
+ Spacer(modifier = Modifier.height(24.dp))
+ Icon(imageVector = Icons.Outlined.Info , contentDescription = null)
+ Spacer(modifier = Modifier.height(12.dp))
+ Text(stringResource(id = R.string.dialog_info_cart_item))
+ }
+ newCartItem.value = ShoppingCartItemsTable(
+ cartId = cartId ,
+ name = nameText.value ,
+ price = priceText.value.replace(',' , '.') ,
+ quantity = if (quantityText.value.isNotEmpty()) quantityText.value.toInt() else 1
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/notifications/AppUpdateNotificationsManager.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/notifications/AppUpdateNotificationsManager.kt
deleted file mode 100644
index 80e99ad..0000000
--- a/app/src/main/kotlin/com/d4rk/cartcalculator/notifications/AppUpdateNotificationsManager.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.d4rk.cartcalculator.notifications
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.app.PendingIntent
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import androidx.core.app.NotificationCompat
-import com.d4rk.cartcalculator.R
-import com.google.android.play.core.appupdate.AppUpdateManagerFactory
-import com.google.android.play.core.install.model.AppUpdateType
-import com.google.android.play.core.install.model.UpdateAvailability
-class AppUpdateNotificationsManager(private val context: Context) {
- private val updateChannelId = "update_channel"
- private val updateNotificationId = 0
- fun checkAndSendUpdateNotification() {
- val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- val appUpdateInfoTask = AppUpdateManagerFactory.create(context).appUpdateInfo
- appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
- if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
- && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
- val updateChannel = NotificationChannel(updateChannelId, context.getString(R.string.update_notifications), NotificationManager.IMPORTANCE_HIGH)
- notificationManager.createNotificationChannel(updateChannel)
- val updateBuilder = NotificationCompat.Builder(context, updateChannelId)
- .setSmallIcon(R.drawable.ic_notification_update)
- .setContentTitle(context.getString(R.string.notification_update_title))
- .setContentText(context.getString(R.string.summary_notification_update))
- .setAutoCancel(true)
- .setContentIntent(PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${context.packageName}")), PendingIntent.FLAG_IMMUTABLE))
- notificationManager.notify(updateNotificationId, updateBuilder.build())
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/notifications/AppUsageNotificationsManager.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/notifications/AppUsageNotificationsManager.kt
deleted file mode 100644
index bf84fe4..0000000
--- a/app/src/main/kotlin/com/d4rk/cartcalculator/notifications/AppUsageNotificationsManager.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.d4rk.cartcalculator.notifications
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.content.Context
-import androidx.core.app.NotificationCompat
-import com.d4rk.cartcalculator.R
-class AppUsageNotificationsManager(private val context: Context) {
- private val appUsageChannelId = "app_usage_channel"
- private val appUsageNotificationId = 0
- fun checkAndSendAppUsageNotification() {
- val prefs = context.getSharedPreferences("app_usage", Context.MODE_PRIVATE)
- val lastUsedTimestamp = prefs.getLong("last_used", 0)
- val currentTimestamp = System.currentTimeMillis()
- val notificationThreshold = 3 * 24 * 60 * 60 * 1000
- if (currentTimestamp - lastUsedTimestamp > notificationThreshold) {
- val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- val appUsageChannel = NotificationChannel(appUsageChannelId, context.getString(R.string.app_usage_notifications),NotificationManager.IMPORTANCE_HIGH)
- notificationManager.createNotificationChannel(appUsageChannel)
- val notificationBuilder = NotificationCompat.Builder(context, appUsageChannelId)
- .setSmallIcon(R.drawable.ic_notification_important)
- .setContentTitle(context.getString(R.string.notification_last_time_used_title))
- .setContentText(context.getString(R.string.summary_notification_last_time_used))
- .setAutoCancel(true)
- notificationManager.notify(appUsageNotificationId, notificationBuilder.build())
- }
- prefs.edit().putLong("last_used", currentTimestamp).apply()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/notifications/managers/AppUpdateNotificationsManager.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/notifications/managers/AppUpdateNotificationsManager.kt
new file mode 100644
index 0000000..61f67a3
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/notifications/managers/AppUpdateNotificationsManager.kt
@@ -0,0 +1,64 @@
+package com.d4rk.cartcalculator.notifications.managers
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import androidx.core.app.NotificationCompat
+import com.d4rk.cartcalculator.R
+import com.google.android.play.core.appupdate.AppUpdateManagerFactory
+import com.google.android.play.core.install.model.AppUpdateType
+import com.google.android.play.core.install.model.UpdateAvailability
+
+/**
+ * Utility class for managing app update notifications.
+ *
+ * This class provides functionality to check for available app updates and send update notifications
+ * with a deep link to the Play Store for user interaction.
+ *
+ * @property context The application context used for notification management.
+ */
+class AppUpdateNotificationsManager(private val context: Context) {
+ private val updateChannelId = "update_channel"
+ private val updateNotificationId = 0
+
+ /**
+ * Checks for available app updates and sends a notification if an update is available.
+ *
+ * This function checks the availability of app updates using the AppUpdateManager and sends
+ * a notification with a deep link to the Play Store if a flexible update is available.
+ */
+ fun checkAndSendUpdateNotification() {
+ val notificationManager =
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val appUpdateInfoTask = AppUpdateManagerFactory.create(context).appUpdateInfo
+ appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
+ if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && appUpdateInfo.isUpdateTypeAllowed(
+ AppUpdateType.FLEXIBLE
+ )
+ ) {
+ val updateChannel = NotificationChannel(
+ updateChannelId,
+ context.getString(R.string.update_notifications),
+ NotificationManager.IMPORTANCE_HIGH
+ )
+ notificationManager.createNotificationChannel(updateChannel)
+ val updateBuilder = NotificationCompat.Builder(context, updateChannelId)
+ .setSmallIcon(R.drawable.ic_notification_update)
+ .setContentTitle(context.getString(R.string.notification_update_title))
+ .setContentText(context.getString(R.string.summary_notification_update))
+ .setAutoCancel(true).setContentIntent(
+ PendingIntent.getActivity(
+ context, 0, Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse("market://details?id=${context.packageName}")
+ ), PendingIntent.FLAG_IMMUTABLE
+ )
+ )
+ notificationManager.notify(updateNotificationId, updateBuilder.build())
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/notifications/managers/AppUsageNotificationsManager.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/notifications/managers/AppUsageNotificationsManager.kt
new file mode 100644
index 0000000..2590a69
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/notifications/managers/AppUsageNotificationsManager.kt
@@ -0,0 +1,32 @@
+package com.d4rk.cartcalculator.notifications.managers
+
+import android.content.Context
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
+import com.d4rk.cartcalculator.notifications.workers.AppUsageNotificationWorker
+import java.util.concurrent.TimeUnit
+
+/**
+ * Utility class for managing app usage notifications.
+ *
+ * This class provides functionality to schedule periodic checks for app usage notifications
+ * using WorkManager and a custom worker class.
+ *
+ * @property context The application context used for scheduling app usage checks.
+ */
+class AppUsageNotificationsManager(private val context: Context) {
+
+ /**
+ * Schedules a periodic check for app usage notifications.
+ *
+ * This function schedules a recurring task using WorkManager to perform app usage checks
+ * every 3 days. It enqueues a PeriodicWorkRequest with a specified interval and triggers
+ * an instance of the AppUsageNotificationWorker to handle the app usage check.
+ */
+ fun scheduleAppUsageCheck() {
+ val workRequest = PeriodicWorkRequestBuilder(
+ repeatInterval = 3, repeatIntervalTimeUnit = TimeUnit.DAYS
+ ).build()
+ WorkManager.getInstance(context).enqueue(workRequest)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/notifications/workers/AppUsageNotificationWorker.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/notifications/workers/AppUsageNotificationWorker.kt
new file mode 100644
index 0000000..29db227
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/notifications/workers/AppUsageNotificationWorker.kt
@@ -0,0 +1,63 @@
+package com.d4rk.cartcalculator.notifications.workers
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import androidx.core.app.NotificationCompat
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import com.d4rk.cartcalculator.R
+import com.d4rk.cartcalculator.data.store.DataStore
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+
+/**
+ * Worker class responsible for app usage notifications.
+ *
+ * This worker class extends the WorkManager's Worker class to perform background tasks for
+ * app usage notifications. It checks the last app usage timestamp stored in preferences
+ * and triggers a notification if the threshold for notification has been exceeded.
+ *
+ * @property context The application context used for accessing system services and resources.
+ * @property workerParams The parameters for this worker instance.
+ */
+class AppUsageNotificationWorker(context: Context, workerParams: WorkerParameters) :
+ Worker(context, workerParams) {
+ private val dataStore = DataStore.getInstance(context)
+ private val appUsageChannelId = "app_usage_channel"
+ private val appUsageNotificationId = 0
+
+ /**
+ * Performs the background work for app usage notification checks.
+ *
+ * This function checks the last app usage timestamp stored in preferences and compares
+ * it against the current timestamp. If the elapsed time exceeds a predefined notification
+ * threshold (3 days), it triggers a notification to remind the user about app usage.
+ *
+ * @return The result of the worker operation, indicating success or failure.
+ */
+ override fun doWork(): Result {
+ val currentTimestamp = System.currentTimeMillis()
+ val notificationThreshold = 3 * 24 * 60 * 60 * 1000
+ val lastUsedTimestamp = runBlocking { dataStore.lastUsed.first() }
+ if (currentTimestamp - lastUsedTimestamp > notificationThreshold) {
+ val notificationManager =
+ applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val appUsageChannel = NotificationChannel(
+ appUsageChannelId,
+ applicationContext.getString(R.string.app_usage_notifications),
+ NotificationManager.IMPORTANCE_HIGH
+ )
+ notificationManager.createNotificationChannel(appUsageChannel)
+ val notificationBuilder =
+ NotificationCompat.Builder(applicationContext, appUsageChannelId)
+ .setSmallIcon(R.drawable.ic_notification_important)
+ .setContentTitle(applicationContext.getString(R.string.notification_last_time_used_title))
+ .setContentText(applicationContext.getString(R.string.summary_notification_last_time_used))
+ .setAutoCancel(true)
+ notificationManager.notify(appUsageNotificationId, notificationBuilder.build())
+ }
+ runBlocking { dataStore.saveLastUsed(currentTimestamp) }
+ return Result.success()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/about/AboutFragment.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/about/AboutFragment.kt
deleted file mode 100644
index 3a254cb..0000000
--- a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/about/AboutFragment.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-package com.d4rk.cartcalculator.ui.about
-import android.content.ClipData
-import android.content.Intent
-import android.content.Context
-import android.content.ClipboardManager
-import android.net.Uri
-import android.os.Build
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.fragment.app.Fragment
-import androidx.lifecycle.ViewModelProvider
-import com.d4rk.cartcalculator.BuildConfig
-import com.d4rk.cartcalculator.R
-import com.d4rk.cartcalculator.databinding.FragmentAboutBinding
-import com.d4rk.cartcalculator.ui.viewmodel.ViewModel
-import com.google.android.gms.ads.AdRequest
-import com.google.android.gms.ads.MobileAds
-import com.google.android.material.snackbar.Snackbar
-import me.zhanghai.android.fastscroll.FastScrollerBuilder
-import java.text.SimpleDateFormat
-import java.util.Locale
-import java.util.Calendar
-class AboutFragment : Fragment() {
- private lateinit var binding: FragmentAboutBinding
- private val calendar: Calendar = Calendar.getInstance()
- private var originalNavBarColor: Int? = null
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
- ViewModelProvider(this)[ViewModel::class.java]
- binding = FragmentAboutBinding.inflate(inflater, container, false)
- originalNavBarColor = activity?.window?.navigationBarColor
- setOriginalNavigationBarColor()
- FastScrollerBuilder(binding.scrollView).useMd2Style().build()
- MobileAds.initialize(requireContext())
- binding.adView.loadAd(AdRequest.Builder().build())
- val version = String.format(resources.getString(R.string.app_version), BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)
- binding.textViewAppVersion.text = version
- val simpleDateFormat = SimpleDateFormat("yyyy", Locale.getDefault())
- val dateText = simpleDateFormat.format(calendar.time)
- val copyright = requireContext().getString(R.string.copyright, dateText)
- binding.textViewCopyright.text = copyright
- binding.textViewAppVersion.setOnLongClickListener {
- val clipboardManager = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
- val clipData: ClipData = ClipData.newPlainText("Label", binding.textViewAppVersion.text)
- clipboardManager.setPrimaryClip(clipData)
- if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2)
- Snackbar.make(requireView(), R.string.copied_to_clipboard, Snackbar.LENGTH_SHORT).show()
- true
- }
- binding.imageViewAppIcon.setOnClickListener {
- startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://sites.google.com/view/d4rk7355608")))
- }
- binding.chipGoogleDev.setOnClickListener {
- startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://g.dev/D4rK7355608")))
- }
- binding.chipYoutube.setOnClickListener {
- startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://www.youtube.com/c/D4rK7355608")))
- }
- binding.chipGithub.setOnClickListener {
- startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/D4rK7355608/" + BuildConfig.APPLICATION_ID)))
- }
- binding.chipTwitter.setOnClickListener {
- startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://twitter.com/D4rK7355608")))
- }
- binding.chipXda.setOnClickListener {
- startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://forum.xda-developers.com/m/d4rk7355608.10095012")))
- }
- binding.chipMusic.setOnClickListener {
- startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://sites.google.com/view/d4rk7355608/tracks")))
- }
- return binding.root
- }
- override fun onDestroyView() {
- super.onDestroyView()
- activity?.window?.navigationBarColor = originalNavBarColor!!
- }
-
- private fun setOriginalNavigationBarColor() {
- activity?.window?.navigationBarColor = originalNavBarColor!!
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/cart/CartActivity.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/cart/CartActivity.kt
new file mode 100644
index 0000000..a06f032
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/cart/CartActivity.kt
@@ -0,0 +1,41 @@
+package com.d4rk.cartcalculator.ui.cart
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.ui.Modifier
+import androidx.lifecycle.ViewModelProvider
+import com.d4rk.cartcalculator.data.store.DataStore
+import com.d4rk.cartcalculator.ui.settings.display.theme.AppTheme
+
+class CartActivity : ComponentActivity() {
+ private lateinit var viewModel : CartViewModel
+ val dataStore = DataStore.getInstance(this)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ val cartId = intent.getIntExtra("cartId" , 0)
+ viewModel = ViewModelProvider(
+ this , CartViewModelFactory(cartId , dataStore)
+ )[CartViewModel::class.java]
+ setContent {
+ AppTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
+ ) {
+ CartActivityComposable(activity = this@CartActivity , viewModel = viewModel)
+ }
+ }
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ viewModel.saveCartItems()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/cart/CartActivityComposable.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/cart/CartActivityComposable.kt
new file mode 100644
index 0000000..df2bc87
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/cart/CartActivityComposable.kt
@@ -0,0 +1,234 @@
+package com.d4rk.cartcalculator.ui.cart
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.outlined.AddCircleOutline
+import androidx.compose.material.icons.outlined.AddShoppingCart
+import androidx.compose.material.icons.outlined.RemoveCircleOutline
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.d4rk.cartcalculator.R
+import com.d4rk.cartcalculator.data.db.table.ShoppingCartItemsTable
+import com.d4rk.cartcalculator.dialogs.NewCartItemDialog
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CartActivityComposable(activity : CartActivity , viewModel : CartViewModel) {
+ val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
+ val cartId = activity.intent.getIntExtra("cartId", 0)
+ val primaryColor = MaterialTheme.colorScheme.primary
+
+ LaunchedEffect(viewModel.cartItems) {
+ if (viewModel.cartItems.isEmpty()) {
+ activity.window.navigationBarColor = Color.Transparent.toArgb()
+ } else {
+ activity.window.navigationBarColor = primaryColor.toArgb()
+ }
+ }
+
+ Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {
+ LargeTopAppBar(title = {
+ Text(
+ viewModel.cart.value?.name ?: stringResource(R.string.shopping_cart)
+ )
+ }, navigationIcon = {
+ IconButton(onClick = {
+ activity.finish()
+ }) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
+ }
+ }, actions = {
+ IconButton(onClick = {
+ viewModel.openDialog.value = true
+ }) {
+ Icon(
+ Icons.Outlined.AddShoppingCart, contentDescription = null,
+ )
+ }
+ }, scrollBehavior = scrollBehavior)
+ }) { paddingValues ->
+ Box(
+ modifier = Modifier
+ .padding(paddingValues)
+ .fillMaxSize()
+ ) {
+ if (viewModel.isLoading.value) {
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+ } else if (viewModel.cartItems.isEmpty()) {
+ Text(
+ text = stringResource(id = R.string.summary_empty),
+ modifier = Modifier.align(Alignment.Center)
+ )
+ } else {
+ Box {
+ Column(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ LazyColumn(
+ modifier = Modifier.weight(1f)
+ ) {
+ items(viewModel.cartItems) { cartItem ->
+ CartItemComposable(
+ viewModel = viewModel ,
+ cartItem = cartItem,
+ onMinusClick = { viewModel.decreaseQuantity(cartItem) },
+ onPlusClick = { viewModel.increaseQuantity(cartItem) },
+ quantityState = viewModel.getQuantityStateForItem(cartItem)
+ )
+ }
+ }
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(144.dp)
+ .clip(
+ RoundedCornerShape(
+ topStart = 16.dp ,
+ topEnd = 16.dp ,
+ bottomEnd = 0.dp ,
+ bottomStart = 0.dp
+ )
+ ) ,
+ shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp, bottomEnd = 0.dp, bottomStart = 0.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.primary,
+ ) ,
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = "Total" ,
+ style = MaterialTheme.typography.titleMedium
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Row {
+ Text(
+ text = viewModel.totalPrice.doubleValue.toString() ,
+ style = MaterialTheme.typography.bodyLarge
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ Text(
+ text = viewModel.selectedCurrency.value ,
+ style = MaterialTheme.typography.bodyLarge
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (viewModel.openDialog.value) {
+ NewCartItemDialog(cartId ,
+ onDismiss = { viewModel.openDialog.value = false } ,
+ onCartCreated = { cartItem ->
+ cartItem.cartId = cartId
+ viewModel.addCartItem(cartItem)
+ viewModel.openDialog.value = false
+ })
+
+ }
+ }
+ }
+}
+
+/**
+ * This Composable function displays a cart item with the ability to modify its quantity.
+ * It shows the item's name, price, and a numeric representation of quantity, which can be adjusted via plus and minus buttons.
+ *
+ * @param cartItem The ShoppingCartItemsTable object representing the cart item.
+ * @param onMinusClick Lambda function invoked when the minus button is clicked, decreasing the quantity.
+ * @param onPlusClick Lambda function invoked when the plus button is clicked, increasing the quantity.
+ * @param quantityState A MutableState that holds and updates the quantity of the cart item.
+ */
+@Composable
+fun CartItemComposable(
+ viewModel : CartViewModel ,
+ cartItem: ShoppingCartItemsTable,
+ onMinusClick: (ShoppingCartItemsTable) -> Unit,
+ onPlusClick: (ShoppingCartItemsTable) -> Unit,
+ quantityState : MutableState ,
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(24.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Column {
+ Text(text = cartItem.name, style = MaterialTheme.typography.bodyLarge)
+ Row {
+ Text(text = cartItem.price , style = MaterialTheme.typography.bodyMedium)
+ Spacer(modifier = Modifier.width(4.dp))
+ Text(
+ text = viewModel.selectedCurrency.value ,
+ style = MaterialTheme.typography.bodyLarge
+ )
+ }
+ }
+
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ IconButton(onClick = { onMinusClick(cartItem) }) {
+ Icon(
+ imageVector = Icons.Outlined.RemoveCircleOutline,
+ contentDescription = "Decrease Quantity"
+ )
+ }
+ Text(
+ text = quantityState.value.toString(),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(horizontal = 16.dp)
+ )
+
+ IconButton(onClick = { onPlusClick(cartItem) }) {
+ Icon(
+ imageVector = Icons.Outlined.AddCircleOutline,
+ contentDescription = "Increase Quantity"
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/cart/CartViewModel.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/cart/CartViewModel.kt
new file mode 100644
index 0000000..9c22cbd
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/cart/CartViewModel.kt
@@ -0,0 +1,223 @@
+package com.d4rk.cartcalculator.ui.cart
+
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableDoubleStateOf
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.d4rk.cartcalculator.MyApp
+import com.d4rk.cartcalculator.data.db.table.ShoppingCartItemsTable
+import com.d4rk.cartcalculator.data.db.table.ShoppingCartTable
+import com.d4rk.cartcalculator.data.store.DataStore
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * This ViewModel is responsible for managing the state and interactions of the shopping cart within the CartActivity.
+ * It handles the loading, addition, deletion, and updating of cart items in the database.
+ * It also manages the selected currency and total price of the cart.
+ *
+ * @property cartId The unique identifier for the cart being managed.
+ * @property selectedCurrency A mutable state holding the selected currency.
+ * @property totalPrice A mutable state holding the total price of the cart.
+ * @property cartItems A mutable list of cart items, represented by [ShoppingCartItemsTable] objects.
+ * @property cart A mutable state holding the [ShoppingCartTable] object of the current cart, if any.
+ * @property isLoading A mutable state indicating whether the cart data is being loaded.
+ * @property openDialog A mutable state that determines if a dialog related to cart operations is displayed.
+ * @property itemQuantities A mutable map that holds the quantity state of each cart item.
+ */
+class CartViewModel(private val cartId : Int , private val dataStore : DataStore) : ViewModel() {
+
+ val selectedCurrency = mutableStateOf("")
+ val totalPrice = mutableDoubleStateOf(0.0)
+ val cartItems = mutableStateListOf()
+ val cart = mutableStateOf(null)
+ val isLoading = mutableStateOf(true)
+ var openDialog = mutableStateOf(false)
+ private val itemQuantities = mutableMapOf>()
+
+ init {
+ loadCartItems()
+ loadSelectedCurrency()
+ }
+
+ /**
+ * Loads the cart items from the database and updates the UI state.
+ *
+ * This function is called during the initialization of the ViewModel. It launches a coroutine in the ViewModel's scope.
+ * In the coroutine, it performs the following operations:
+ * 1. Fetches the cart items associated with the current `cartId` from the database and adds them to the `cartItems` list.
+ * 2. Fetches the cart associated with the current `cartId` from the database and sets it as the value of the `cart` state.
+ * 3. Sets the `isLoading` state to `false` to indicate that the loading operation is complete.
+ * 4. Calls the `calculateTotalPrice` function to update the total price of the cart.
+ *
+ * Note: All database operations are performed asynchronously to avoid blocking the main thread.
+ *
+ * @throws Exception If there is an error while fetching data from the database.
+ */
+ private fun loadCartItems() {
+ viewModelScope.launch {
+ cartItems.addAll(MyApp.database.shoppingCartItemsDao().getItemsByCartId(cartId))
+ cart.value = MyApp.database.newCartDao().getCartById(cartId)
+ isLoading.value = false
+ calculateTotalPrice()
+ }
+ }
+
+ /**
+ * Loads the selected currency from the data store and updates the UI state.
+ *
+ * This function is called during the initialization of the ViewModel. It launches a coroutine in the ViewModel's scope.
+ * In the coroutine, it performs the following operations:
+ * 1. Fetches the selected currency from the data store.
+ * 2. If a currency is found, it sets the `selectedCurrency` state to the fetched currency.
+ * 3. If no currency is found, it sets the `selectedCurrency` state to an empty string.
+ *
+ * Note: The data store operation is performed asynchronously to avoid blocking the main thread.
+ *
+ * @throws Exception If there is an error while fetching the selected currency from the data store.
+ */
+ private fun loadSelectedCurrency() {
+ viewModelScope.launch {
+ selectedCurrency.value = dataStore.getCurrency().firstOrNull() ?: ""
+ }
+ }
+
+ /**
+ * Adds a new item to the cart, updates the UI state, and saves the new item to the database.
+ *
+ * This function performs the following operations:
+ * 1. Sets the `cartId` of the new cart item to the current `cartId`.
+ * 2. Adds the new cart item to the `cartItems` list.
+ * 3. Calls the `calculateTotalPrice` function to update the total price of the cart.
+ * 4. Launches a coroutine in the IO dispatcher to insert the new cart item into the database.
+ *
+ * Note: The database operation is performed asynchronously to avoid blocking the main thread.
+ *
+ * @param cartItem The new cart item to be added. This should be a [ShoppingCartItemsTable] object.
+ * @throws Exception If there is an error while inserting the new cart item into the database.
+ */
+ fun addCartItem(cartItem : ShoppingCartItemsTable) {
+ cartItem.cartId = this.cartId
+ cartItems.add(cartItem)
+ calculateTotalPrice()
+ viewModelScope.launch(Dispatchers.IO) {
+ MyApp.database.shoppingCartItemsDao().insert(cartItem)
+ }
+ }
+
+ /**
+ * Retrieves or creates a MutableState for the quantity of a specific cart item.
+ * If the quantity state for the item already exists in the `itemQuantities` map, it is returned.
+ * If it does not exist, a new MutableState is created with the initial value set to the current quantity of the cart item.
+ * This new state is then added to the `itemQuantities` map and returned.
+ *
+ * @param cartItem The cart item for which the quantity state is needed.
+ * @return The MutableState representing the quantity of the cart item.
+ */
+ fun getQuantityStateForItem(cartItem : ShoppingCartItemsTable) : MutableState {
+ return itemQuantities.getOrPut(cartItem.id) { mutableIntStateOf(cartItem.quantity) }
+ }
+
+ /**
+ * Increases the quantity of a cart item by one, updates the UI state, and saves the updated quantity to the database.
+ *
+ * This function performs the following operations:
+ * 1. Retrieves the MutableState for the quantity of the cart item using the `getQuantityStateForItem` function.
+ * 2. Increases the value of the quantity state by one.
+ * 3. Updates the quantity of the cart item in the `cartItems` list.
+ * 4. Launches a coroutine in the IO dispatcher to update the quantity of the cart item in the database.
+ * 5. Calls the `calculateTotalPrice` function to update the total price of the cart.
+ *
+ * Note: The database operation is performed asynchronously to avoid blocking the main thread.
+ *
+ * @param cartItem The cart item whose quantity is to be increased. This should be a [ShoppingCartItemsTable] object.
+ * @throws Exception If there is an error while updating the quantity of the cart item in the database.
+ */
+ fun increaseQuantity(cartItem : ShoppingCartItemsTable) {
+ val quantityState = getQuantityStateForItem(cartItem)
+ viewModelScope.launch(Dispatchers.IO) {
+ val newQuantity = quantityState.value + 1
+ quantityState.value = newQuantity
+ cartItem.quantity = newQuantity
+ MyApp.database.shoppingCartItemsDao().update(cartItem)
+ calculateTotalPrice()
+ }
+ }
+
+ /**
+ * Decreases the quantity of a cart item by one, updates the UI state, and saves the updated quantity to the database.
+ * If the quantity reaches zero, the item is removed from the cart and the database.
+ *
+ * This function performs the following operations:
+ * 1. Retrieves the MutableState for the quantity of the cart item using the `getQuantityStateForItem` function.
+ * 2. Decreases the value of the quantity state by one, but not less than zero.
+ * 3. If the new quantity is greater than zero, it updates the quantity of the cart item in the `cartItems` list and in the database.
+ * 4. If the new quantity is zero, it removes the cart item from the `cartItems` list and from the database.
+ * 5. Calls the `calculateTotalPrice` function to update the total price of the cart.
+ *
+ * Note: The database operations are performed asynchronously to avoid blocking the main thread.
+ *
+ * @param cartItem The cart item whose quantity is to be decreased. This should be a [ShoppingCartItemsTable] object.
+ * @throws Exception If there is an error while updating or deleting the cart item in the database.
+ */
+ fun decreaseQuantity(cartItem : ShoppingCartItemsTable) {
+ val quantityState = getQuantityStateForItem(cartItem)
+ viewModelScope.launch(Dispatchers.IO) {
+ val newQuantity = maxOf(quantityState.value - 1 , 0)
+ if (newQuantity > 0) {
+ quantityState.value = newQuantity
+ cartItem.quantity = newQuantity
+ MyApp.database.shoppingCartItemsDao().update(cartItem)
+ }
+ else {
+ MyApp.database.shoppingCartItemsDao().delete(cartItem)
+ withContext(Dispatchers.Main) {
+ cartItems.remove(cartItem)
+ }
+ }
+ calculateTotalPrice()
+ }
+ }
+
+ /**
+ * Calculates the total price of the cart items and updates the UI state.
+ *
+ * This function is called whenever a cart item is added, removed, or its quantity is changed.
+ * It performs the following operations:
+ * 1. Iterates over each cart item in the `cartItems` list.
+ * 2. For each cart item, it multiplies the item's price by its quantity and adds the result to the total price.
+ * 3. Sets the `totalPrice` state to the calculated total price.
+ *
+ * Note: The item's price is converted to a double before multiplication to ensure accurate calculations.
+ */
+ private fun calculateTotalPrice() {
+ val total = cartItems.sumOf { item -> item.price.toDouble() * item.quantity }
+ totalPrice.doubleValue = total
+ }
+
+ /**
+ * Saves the current state of cart items to the database. This includes any changes made to the quantity of the cart items.
+ * This function is called when the user decides to save their changes and exit the cart.
+ *
+ * This function performs the following operations:
+ * 1. Launches a coroutine in the IO dispatcher to perform database operations asynchronously.
+ * 2. Iterates over each cart item in the `cartItems` list.
+ * 3. For each cart item, it updates the corresponding record in the database with the current state of the cart item.
+ *
+ * Note: The database operation is performed asynchronously to avoid blocking the main thread.
+ *
+ * @throws Exception If there is an error while updating the cart items in the database.
+ */
+ fun saveCartItems() {
+ viewModelScope.launch(Dispatchers.IO) {
+ cartItems.forEach { cartItem ->
+ MyApp.database.shoppingCartItemsDao().update(cartItem)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/cart/CartViewModelFactory.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/cart/CartViewModelFactory.kt
new file mode 100644
index 0000000..15c2f68
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/cart/CartViewModelFactory.kt
@@ -0,0 +1,38 @@
+package com.d4rk.cartcalculator.ui.cart
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.d4rk.cartcalculator.data.store.DataStore
+
+/**
+ * Factory class for creating instances of [CartViewModel].
+ *
+ * This factory ensures that the created [CartViewModel] is initialized with the provided
+ * cart ID and a DataStore instance. The DataStore instance is used for managing data persistence
+ * in the created ViewModel.
+ *
+ * @property cartId The ID of the cart to be managed by the created ViewModel.
+ * @property dataStore The DataStore instance for managing data persistence in the created ViewModel.
+ */
+class CartViewModelFactory(private val cartId : Int , private val dataStore : DataStore) :
+ ViewModelProvider.Factory {
+
+ /**
+ * Creates a new instance of the requested ViewModel class.
+ *
+ * This method checks if the requested ViewModel class is [CartViewModel]. If it is,
+ * a new instance of [CartViewModel] is created with the provided cart ID and DataStore instance,
+ * and returned. If the requested ViewModel class is not [CartViewModel], an IllegalArgumentException is thrown.
+ *
+ * @param modelClass The class of the ViewModel to create.
+ * @throws IllegalArgumentException if the requested class is not [CartViewModel].
+ * @return A newly created ViewModel instance.
+ */
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T {
+ if (modelClass.isAssignableFrom(CartViewModel::class.java)) {
+ return CartViewModel(cartId , dataStore) as T
+ }
+ throw IllegalArgumentException("Unknown ViewModel class")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/help/HelpActivity.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/help/HelpActivity.kt
index cee821b..39a5c06 100644
--- a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/help/HelpActivity.kt
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/help/HelpActivity.kt
@@ -1,76 +1,56 @@
package com.d4rk.cartcalculator.ui.help
-import android.content.ActivityNotFoundException
-import android.content.Intent
-import android.net.Uri
+
import android.os.Bundle
-import android.view.Menu
-import android.view.MenuItem
-import androidx.appcompat.app.AppCompatActivity
-import androidx.preference.Preference
-import androidx.preference.PreferenceFragmentCompat
-import com.d4rk.cartcalculator.R
-import com.d4rk.cartcalculator.databinding.ActivityHelpBinding
-import com.google.android.material.snackbar.Snackbar
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.ui.Modifier
+import com.d4rk.cartcalculator.ui.settings.display.theme.AppTheme
+import com.d4rk.cartcalculator.utils.Utils
import com.google.android.play.core.review.ReviewManager
import com.google.android.play.core.review.ReviewManagerFactory
-class HelpActivity : AppCompatActivity() {
- private lateinit var binding: ActivityHelpBinding
+
+class HelpActivity : ComponentActivity() {
+ private lateinit var reviewManager: ReviewManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- binding = ActivityHelpBinding.inflate(layoutInflater)
- setContentView(binding.root)
- supportFragmentManager.beginTransaction().replace(R.id.frame_layout_faq, FaqFragment()).commit()
- supportFragmentManager.beginTransaction().replace(R.id.frame_layout_feedback, FeedbackFragment()).commit()
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
- }
- class FaqFragment : PreferenceFragmentCompat() {
- override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
- setPreferencesFromResource(R.xml.preferences_faq, rootKey)
- }
- }
- class FeedbackFragment : PreferenceFragmentCompat() {
- private lateinit var reviewManager: ReviewManager
- override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
- setPreferencesFromResource(R.xml.preferences_feedback, rootKey)
- reviewManager = ReviewManagerFactory.create(requireContext())
- val feedbackPreference: Preference? = findPreference(getString(R.string.key_feedback))
- feedbackPreference?.setOnPreferenceClickListener {
- reviewManager.requestReviewFlow().addOnSuccessListener { reviewInfo ->
- reviewManager.launchReviewFlow(requireActivity(), reviewInfo)
- }
- .addOnFailureListener {
- val uri = Uri.parse("https://play.google.com/store/apps/details?id=${requireContext().packageName}&showAllReviews=true")
- val intent = Intent(Intent.ACTION_VIEW, uri)
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- try {
- startActivity(intent)
- } catch (e: ActivityNotFoundException) {
- Snackbar.make(requireView(), R.string.snack_unable_to_open_google_play_store, Snackbar.LENGTH_SHORT).show()
- }
+ enableEdgeToEdge()
+ setContent {
+ AppTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
+ ) {
+ HelpComposable(this@HelpActivity)
}
- .also {
- Snackbar.make(requireView(), R.string.snack_feedback, Snackbar.LENGTH_SHORT).show()
- }
- true
}
}
+
}
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.menu_feedback, menu)
- return true
- }
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- return when (item.itemId) {
- R.id.dev_mail -> {
- val emailIntent = Intent(Intent.ACTION_SEND)
- emailIntent.type = "text/email"
- emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf("d4rk7355608@gmail.com"))
- emailIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.feedback_for) + getString(R.string.app_name))
- emailIntent.putExtra(Intent.EXTRA_TEXT, getString(R.string.dear_developer))
- startActivity(Intent.createChooser(emailIntent, getString(R.string.send_email_using)))
- true
- }
- else -> super.onOptionsItemSelected(item)
+
+ /**
+ * Initiates the feedback process for the app.
+ *
+ * This function uses the Google Play In-App Review API to prompt the user for feedback.
+ * If the request to launch the in-app review flow is successful, the review dialog is displayed.
+ * If the request fails, it opens the Google Play Store page for the app's reviews.
+ *
+ * @see com.google.android.play.core.review.ReviewManagerFactory
+ * @see com.google.android.play.core.review.ReviewManager
+ * @param context The context used to create the ReviewManager instance and launch review flows.
+ */
+ fun feedback() {
+ reviewManager = ReviewManagerFactory.create(this)
+ val task = reviewManager.requestReviewFlow()
+ task.addOnSuccessListener { reviewInfo ->
+ reviewManager.launchReviewFlow(this, reviewInfo)
+ }.addOnFailureListener {
+ Utils.openUrl(
+ this,
+ "https://play.google.com/store/apps/details?id=${this.packageName}&showAllReviews=true"
+ )
}
}
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/help/HelpComposable.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/help/HelpComposable.kt
new file mode 100644
index 0000000..22d89b6
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/help/HelpComposable.kt
@@ -0,0 +1,303 @@
+package com.d4rk.cartcalculator.ui.help
+
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.drawable.AdaptiveIconDrawable
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.MailOutline
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Card
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExtendedFloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.constraintlayout.compose.ConstraintLayout
+import com.d4rk.cartcalculator.BuildConfig
+import com.d4rk.cartcalculator.R
+import com.d4rk.cartcalculator.utils.Utils
+import com.d4rk.cartcalculator.utils.bounceClick
+import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun HelpComposable(activity: HelpActivity) {
+ val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
+ var showMenu by remember { mutableStateOf(false) }
+ val context = LocalContext.current
+ val showDialog = remember { mutableStateOf(false) }
+ Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {
+ LargeTopAppBar(title = { Text(stringResource(R.string.help)) }, navigationIcon = {
+ IconButton(onClick = {
+ activity.finish()
+ }) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
+ }
+ }, actions = {
+ IconButton(onClick = { showMenu = true }) {
+ Icon(Icons.Default.MoreVert, contentDescription = "Localized description")
+ }
+ DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
+ DropdownMenuItem(text = { Text(stringResource(R.string.view_in_google_play_store)) },
+ onClick = {
+ Utils.openUrl(
+ context,
+ "https://play.google.com/store/apps/details?id=${activity.packageName}"
+ )
+ })
+ DropdownMenuItem(text = { Text(stringResource(R.string.version_info)) },
+ onClick = { showDialog.value = true })
+ DropdownMenuItem(text = { Text(stringResource(R.string.beta_program)) },
+ onClick = {
+ Utils.openUrl(
+ context,
+ "https://play.google.com/apps/testing/${activity.packageName}"
+ )
+ })
+ DropdownMenuItem(text = { Text(stringResource(R.string.terms_of_service)) },
+ onClick = {
+ Utils.openUrl(
+ context,
+ "https://sites.google.com/view/d4rk7355608/more/apps/terms-of-service"
+ )
+ })
+ DropdownMenuItem(text = { Text(stringResource(R.string.privacy_policy)) },
+ onClick = {
+ Utils.openUrl(
+ context,
+ "https://sites.google.com/view/d4rk7355608/more/apps/privacy-policy"
+ )
+ })
+ DropdownMenuItem(text = { Text(stringResource(com.google.android.gms.oss.licenses.R.string.oss_license_title)) },
+ onClick = {
+ Utils.openActivity(
+ context,
+ OssLicensesMenuActivity::class.java
+ )
+ })
+ }
+ if (showDialog.value) {
+ VersionInfoDialog(onDismiss = { showDialog.value = false })
+ }
+ }, scrollBehavior = scrollBehavior)
+ }) { paddingValues ->
+ Box(
+ modifier = Modifier
+ .padding(16.dp)
+ .fillMaxSize()
+ ) {
+ ConstraintLayout(modifier = Modifier.padding(paddingValues)) {
+ val (faqTitle, faqCard, fabButton) = createRefs()
+ Text(text = stringResource(R.string.faq),
+ modifier = Modifier
+ .padding(bottom = 24.dp)
+ .constrainAs(faqTitle) {
+ top.linkTo(parent.top)
+ start.linkTo(parent.start)
+ })
+ Card(modifier = Modifier
+ .fillMaxWidth()
+ .constrainAs(faqCard) {
+ top.linkTo(faqTitle.bottom)
+ bottom.linkTo(parent.bottom)
+ }) {
+ FAQComposable()
+ }
+ ExtendedFloatingActionButton(
+ text = { Text("Feedback") },
+ onClick = {
+ activity.feedback()
+ },
+ icon = {
+ Icon(
+ Icons.Default.MailOutline, contentDescription = null
+ )
+ },
+ modifier = Modifier
+ .padding(6.dp)
+ .bounceClick()
+ .constrainAs(fabButton) {
+ bottom.linkTo(parent.bottom)
+ end.linkTo(parent.end)
+ },
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun FAQComposable() {
+ LazyColumn {
+ item {
+ QuestionComposable(
+ title = stringResource(R.string.question_1),
+ summary = stringResource(R.string.summary_preference_faq_1)
+ )
+ }
+ item {
+ QuestionComposable(
+ title = stringResource(R.string.question_2),
+ summary = stringResource(R.string.summary_preference_faq_2)
+ )
+ }
+ item {
+ QuestionComposable(
+ title = stringResource(R.string.question_3),
+ summary = stringResource(R.string.summary_preference_faq_3)
+ )
+ }
+ item {
+ QuestionComposable(
+ title = stringResource(R.string.question_4),
+ summary = stringResource(R.string.summary_preference_faq_5)
+ )
+ }
+ item {
+ QuestionComposable(
+ title = stringResource(R.string.question_5),
+ summary = stringResource(R.string.summary_preference_faq_5)
+ )
+ }
+ item {
+ QuestionComposable(
+ title = stringResource(R.string.question_6),
+ summary = stringResource(R.string.summary_preference_faq_6)
+ )
+ }
+ item {
+ QuestionComposable(
+ title = stringResource(R.string.question_7),
+ summary = stringResource(R.string.summary_preference_faq_7)
+ )
+ }
+ item {
+ QuestionComposable(
+ title = stringResource(R.string.question_8),
+ summary = stringResource(R.string.summary_preference_faq_8)
+ )
+ }
+ item {
+ QuestionComposable(
+ title = stringResource(R.string.question_9),
+ summary = stringResource(R.string.summary_preference_faq_9)
+ )
+ }
+ }
+}
+
+@Composable
+fun QuestionComposable(title: String, summary: String) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ ) {
+ Text(text = title, style = MaterialTheme.typography.titleMedium)
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(text = summary)
+ }
+}
+
+@Composable
+fun VersionInfoDialog(onDismiss: () -> Unit) {
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ text = { VersionInfoContent() },
+ confirmButton = {},
+ dismissButton = {})
+}
+
+@Composable
+fun VersionInfoContent() {
+ val context = LocalContext.current
+ val appName = context.getString(R.string.app_name)
+ val version = String.format(context.getString(R.string.version), BuildConfig.VERSION_NAME)
+ val copyright = context.getString(R.string.copyright)
+
+ val appIcon = context.packageManager.getApplicationIcon(context.packageName)
+ val bitmapDrawable = convertAdaptiveIconDrawableToBitmap(appIcon)
+
+ Row(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Image(
+ bitmap = bitmapDrawable.bitmap.asImageBitmap(),
+ contentDescription = null,
+ modifier = Modifier.size(48.dp)
+ )
+ Spacer(modifier = Modifier.width(24.dp))
+ Column {
+ Text(
+ text = appName,
+ style = MaterialTheme.typography.titleLarge,
+ )
+ Text(
+ text = version, style = MaterialTheme.typography.bodyMedium
+ )
+ Spacer(modifier = Modifier.height(24.dp))
+ Text(
+ text = copyright, style = MaterialTheme.typography.bodyMedium
+ )
+ }
+ }
+}
+
+fun convertAdaptiveIconDrawableToBitmap(drawable: Drawable): BitmapDrawable {
+ return when (drawable) {
+ is BitmapDrawable -> {
+ drawable
+ }
+
+ is AdaptiveIconDrawable -> {
+ val bitmap = Bitmap.createBitmap(
+ drawable.intrinsicWidth,
+ drawable.intrinsicHeight,
+ Bitmap.Config.ARGB_8888
+ )
+ val canvas = Canvas(bitmap)
+ drawable.setBounds(0, 0, canvas.width, canvas.height)
+ drawable.draw(canvas)
+ BitmapDrawable(Resources.getSystem(), bitmap)
+ }
+
+ else -> {
+ throw IllegalArgumentException("Unsupported drawable type")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/home/HomeComposable.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/home/HomeComposable.kt
new file mode 100644
index 0000000..7163da8
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/home/HomeComposable.kt
@@ -0,0 +1,146 @@
+package com.d4rk.cartcalculator.ui.home
+
+import android.content.Intent
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.AddShoppingCart
+import androidx.compose.material.icons.outlined.DeleteForever
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExtendedFloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedCard
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.d4rk.cartcalculator.R
+import com.d4rk.cartcalculator.data.db.table.ShoppingCartTable
+import com.d4rk.cartcalculator.dialogs.NewCartDialog
+import com.d4rk.cartcalculator.ui.cart.CartActivity
+import com.d4rk.cartcalculator.utils.bounceClick
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+@Composable
+fun HomeComposable() {
+ val context = LocalContext.current
+ val viewModel : HomeViewModel = viewModel()
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(24.dp)
+ ) {
+
+ if (viewModel.isLoading.value) {
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+ }
+ else if (viewModel.carts.isEmpty()) {
+ Text(
+ text = "No carts available" , modifier = Modifier.align(Alignment.Center)
+ )
+ }
+ else {
+ LazyColumn {
+ items(viewModel.carts) { cart ->
+ CartItemComposable(cart , onDelete = { cartToDelete ->
+ viewModel.deleteCart(cartToDelete)
+ } , onCardClick = {
+ val intent = Intent(context , CartActivity::class.java)
+ intent.putExtra("cartId" , cart.cartId)
+ context.startActivity(intent)
+ })
+ }
+ }
+ }
+
+ if (viewModel.openDialog.value) {
+ NewCartDialog(onDismiss = { viewModel.openDialog.value = false } ,
+ onCartCreated = { cartId , cartName ->
+ val cart = ShoppingCartTable(
+ cartId = cartId.toInt() , name = cartName , date = Date()
+ )
+ viewModel.addCart(cart)
+ })
+ }
+
+ ExtendedFloatingActionButton(modifier = Modifier
+ .bounceClick()
+ .align(Alignment.BottomEnd) ,
+ text = { Text(stringResource(R.string.add_new_cart)) } ,
+ onClick = {
+ viewModel.openDialog.value = true
+ } ,
+ icon = {
+ Icon(
+ Icons.Outlined.AddShoppingCart ,
+ contentDescription = null
+ )
+ })
+ }
+}
+
+@Composable
+fun CartItemComposable(
+ cart : ShoppingCartTable , onDelete : (ShoppingCartTable) -> Unit , onCardClick : () -> Unit
+) {
+ val dateFormat = SimpleDateFormat("dd-MM-yyyy" , Locale.getDefault())
+ val dateString = dateFormat.format(cart.date)
+
+ OutlinedCard(shape = RoundedCornerShape(12.dp) ,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp) ,
+ onClick = {
+ onCardClick()
+ }) {
+ Box(modifier = Modifier.clip(MaterialTheme.shapes.medium)) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween ,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(
+ text = cart.name , style = MaterialTheme.typography.titleMedium ,
+ modifier = Modifier.weight(1f)
+ )
+
+ IconButton(onClick = { onDelete(cart) }) {
+ Icon(
+ imageVector = Icons.Outlined.DeleteForever ,
+ contentDescription = "Delete cart" ,
+ tint = MaterialTheme.colorScheme.error
+ )
+ }
+ }
+
+ Text(
+ text = "Created on $dateString" , style = MaterialTheme.typography.bodyMedium ,
+ textAlign = TextAlign.End
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/home/HomeFragment.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/home/HomeFragment.kt
deleted file mode 100644
index 8860fe9..0000000
--- a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/home/HomeFragment.kt
+++ /dev/null
@@ -1,113 +0,0 @@
-package com.d4rk.cartcalculator.ui.home
-import android.os.Bundle
-import android.util.TypedValue
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.drawerlayout.widget.DrawerLayout
-import androidx.fragment.app.Fragment
-import androidx.lifecycle.ViewModelProvider
-import androidx.navigation.fragment.findNavController
-import androidx.recyclerview.widget.LinearLayoutManager
-import com.d4rk.cartcalculator.MainActivity
-import com.d4rk.cartcalculator.R
-import com.d4rk.cartcalculator.adapters.CartItemAdapter
-import com.d4rk.cartcalculator.data.CartItem
-import com.d4rk.cartcalculator.databinding.FragmentHomeBinding
-import com.d4rk.cartcalculator.ui.viewmodel.ViewModel
-import com.google.android.gms.ads.AdRequest
-import com.google.android.gms.ads.MobileAds
-class HomeFragment : Fragment(), MainActivity.CartListener, CartItemAdapter.OnQuantityChangeListener {
- private lateinit var viewModel: ViewModel
- private lateinit var cartItemAdapter: CartItemAdapter
- private var _binding: FragmentHomeBinding? = null
- private val binding get() = _binding!!
- private var originalNavBarColor: Int? = null
- private var totalCost: Double = 0.00
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
- viewModel = ViewModelProvider(this)[ViewModel::class.java]
- _binding = FragmentHomeBinding.inflate(inflater, container, false)
- MobileAds.initialize(requireContext())
- binding.adView.loadAd(AdRequest.Builder().build())
- val recyclerView = binding.recyclerViewCart
- recyclerView.layoutManager = LinearLayoutManager(requireContext())
- cartItemAdapter = CartItemAdapter(emptyList(), this)
- recyclerView.adapter = cartItemAdapter
- originalNavBarColor = activity?.window?.navigationBarColor
- binding.textViewTotal.text = getString(R.string.total_default_value)
- setNavigationBarColor()
- return binding.root
- }
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- val drawerLayout: DrawerLayout? = activity?.findViewById(R.id.drawer_layout)
- drawerLayout?.addDrawerListener(drawerListener)
- setNavigationBarColor()
- }
- override fun onDestroy() {
- super.onDestroy()
- val drawerLayout: DrawerLayout? = activity?.findViewById(R.id.drawer_layout)
- drawerLayout?.removeDrawerListener(drawerListener)
- }
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
- activity?.window?.navigationBarColor = originalNavBarColor!!
- }
- override fun onPause() {
- super.onPause()
- activity?.window?.navigationBarColor = originalNavBarColor!!
- setNavigationBarColor()
- }
- override fun onResume() {
- super.onResume()
- activity?.window?.navigationBarColor = originalNavBarColor!!
- setNavigationBarColor()
- }
- @Deprecated("Deprecated in Java")
- override fun onActivityCreated(savedInstanceState: Bundle?) {
- @Suppress("DEPRECATION")
- super.onActivityCreated(savedInstanceState)
- (activity as? MainActivity)?.setCartListener(this)
- }
- override fun onCartUpdated(cartItems: List) {
- cartItemAdapter.setItems(cartItems)
- cartItemAdapter.notifyItemRangeChanged(0, cartItems.size)
- val textView = binding.textViewEmpty
- if (cartItems.isEmpty()) {
- textView.visibility = View.VISIBLE
- } else {
- textView.visibility = View.GONE
- }
- updateTotalCost(cartItems)
- }
- override fun onQuantityChanged(cartItems: List) {
- updateTotalCost(cartItems)
- }
- private fun updateTotalCost(cartItems: List) {
- totalCost = cartItems.sumOf { it.totalPrice() }
- binding.textViewTotal.text = String.format("%.2f $", totalCost).replace(",", ".")
- }
- private fun setNavigationBarColor() {
- when (findNavController().currentDestination?.id) {
- R.id.nav_home -> {
- val typedValue = TypedValue()
- val theme = requireContext().theme
- theme.resolveAttribute(R.attr.colorPrimaryContainer, typedValue, true)
- val color = typedValue.data
- activity?.window?.navigationBarColor = color
- }
- else -> activity?.window?.navigationBarColor = originalNavBarColor!!
- }
- }
- private val drawerListener = object : DrawerLayout.DrawerListener {
- override fun onDrawerSlide(drawerView: View, slideOffset: Float) {}
- override fun onDrawerOpened(drawerView: View) {
- activity?.window?.navigationBarColor = originalNavBarColor!!
- }
- override fun onDrawerClosed(drawerView: View) {
- setNavigationBarColor()
- }
- override fun onDrawerStateChanged(newState: Int) {}
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/home/HomeViewModel.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/home/HomeViewModel.kt
new file mode 100644
index 0000000..d6aeb31
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/home/HomeViewModel.kt
@@ -0,0 +1,51 @@
+package com.d4rk.cartcalculator.ui.home
+
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.d4rk.cartcalculator.MyApp
+import com.d4rk.cartcalculator.data.db.table.ShoppingCartTable
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+/**
+ * ViewModel for the Home Activity, responsible for managing the list of available shopping carts.
+ * Handles data loading, deletion, and addition operations.
+ */
+class HomeViewModel : ViewModel() {
+ val openDialog = mutableStateOf(false)
+ val carts = mutableStateListOf()
+ val isLoading = mutableStateOf(true)
+
+ init {
+ viewModelScope.launch {
+ val loadedCarts = MyApp.database.newCartDao().getAll()
+ carts.addAll(loadedCarts)
+ isLoading.value = false
+ }
+ }
+
+ /**
+ * Deletes a shopping cart from the list and persists the deletion to the database.
+ *
+ * @param cartToDelete The [ShoppingCartTable] object representing the cart to be deleted.
+ */
+ fun deleteCart(cartToDelete: ShoppingCartTable) {
+ carts.remove(cartToDelete)
+ viewModelScope.launch(Dispatchers.IO) {
+ MyApp.database.newCartDao().delete(cartToDelete)
+ MyApp.database.shoppingCartItemsDao().deleteItemsFromCart(cartToDelete.cartId)
+ }
+ }
+
+ /**
+ * Adds a new shopping cart to the list and potentially closes any open cart-related dialogs.
+ *
+ * @param cart The [ShoppingCartTable] object representing the new cart to be added.
+ */
+ fun addCart(cart: ShoppingCartTable) {
+ carts.add(cart)
+ openDialog.value = false
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/SettingsActivity.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/SettingsActivity.kt
index fa4a769..6698642 100644
--- a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/SettingsActivity.kt
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/SettingsActivity.kt
@@ -1,104 +1,26 @@
package com.d4rk.cartcalculator.ui.settings
-import android.content.SharedPreferences
-import android.content.Intent
-import android.content.Context
-import android.content.ClipboardManager
-import android.content.ClipData
-import android.os.Build
+
import android.os.Bundle
-import android.provider.Settings
-import androidx.appcompat.app.AppCompatActivity
-import androidx.appcompat.app.AppCompatDelegate
-import androidx.core.os.LocaleListCompat
-import androidx.preference.Preference
-import androidx.preference.PreferenceFragmentCompat
-import androidx.preference.PreferenceManager
-import com.d4rk.cartcalculator.BuildConfig
-import com.d4rk.cartcalculator.R
-import com.d4rk.cartcalculator.databinding.ActivitySettingsBinding
-import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.snackbar.Snackbar
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.ui.Modifier
+import com.d4rk.cartcalculator.ui.settings.display.theme.AppTheme
-class SettingsActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
- private lateinit var binding: ActivitySettingsBinding
+class SettingsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- binding = ActivitySettingsBinding.inflate(layoutInflater)
- setContentView(binding.root)
- supportFragmentManager.beginTransaction().replace(R.id.frame_layout_settings, SettingsFragment()).commit()
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
- PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this)
- }
- override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
- val themeKey = getString(R.string.key_theme)
- key?.let {
- if (it == themeKey) sharedPreferences?.let { pref ->
- val themeValues = resources.getStringArray(R.array.preference_theme_values)
- when (pref.getString(themeKey, themeValues[0])) {
- themeValues[0] -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
- themeValues[1] -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
- themeValues[2] -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
- themeValues[3] -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY)
- }
- }
- }
- val languageCode = sharedPreferences?.getString(getString(R.string.key_language), getString(R.string.default_value_language))
- AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(languageCode))
- }
- class SettingsFragment : PreferenceFragmentCompat() {
- override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
- setPreferencesFromResource(R.xml.preferences_settings, rootKey)
- val sharePreference = findPreference(getString(R.string.key_share))
- sharePreference?.setOnPreferenceClickListener {
- val sharingIntent = Intent(Intent.ACTION_SEND).apply {
- type = "text/plain"
- putExtra(Intent.EXTRA_TEXT, "https://play.google.com/store/apps/details?id=" + BuildConfig.APPLICATION_ID)
- putExtra(Intent.EXTRA_SUBJECT, R.string.share_subject)
- }
- startActivity(Intent.createChooser(sharingIntent, getString(R.string.share_using)))
- true
- }
- val ossPreference = findPreference(getString(R.string.key_open_source_licenses))
- ossPreference?.setOnPreferenceClickListener {
- startActivity(Intent(activity, OssLicensesMenuActivity::class.java))
- true
- }
- val changelogPreference: Preference? = findPreference(getString(R.string.key_changelog))
- changelogPreference?.setOnPreferenceClickListener {
- MaterialAlertDialogBuilder(requireContext())
- .setTitle(requireContext().getString(R.string.changelog_title, BuildConfig.VERSION_NAME))
- .setIcon(R.drawable.ic_changelog)
- .setMessage(R.string.changes)
- .setNegativeButton(android.R.string.cancel, null)
- .show()
- true
- }
- val notificationsSettings = findPreference(getString(R.string.key_notifications_settings))
- notificationsSettings?.setOnPreferenceClickListener {
- val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
- intent.putExtra(Settings.EXTRA_APP_PACKAGE, context?.packageName)
- startActivity(intent)
- true
- }
- val deviceInfoPreference = findPreference(getString(R.string.key_device_info))
- val version = String.format(
- resources.getString(R.string.app_build),
- "${resources.getString(R.string.manufacturer)} ${Build.MANUFACTURER}",
- "${resources.getString(R.string.device_model)} ${Build.MODEL}",
- "${resources.getString(R.string.android_version)} ${Build.VERSION.RELEASE}",
- "${resources.getString(R.string.api_level)} ${Build.VERSION.SDK_INT}",
- "${resources.getString(R.string.arch)} ${Build.SUPPORTED_ABIS.joinToString()}"
- )
- deviceInfoPreference?.summary = version
- deviceInfoPreference?.setOnPreferenceClickListener {
- val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
- val clip = ClipData.newPlainText("text", version)
- clipboard.setPrimaryClip(clip)
- if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
- Snackbar.make(requireView(), R.string.copied_to_clipboard, Snackbar.LENGTH_SHORT).show()
+ enableEdgeToEdge()
+ setContent {
+ AppTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
+ ) {
+ SettingsComposable(this@SettingsActivity)
}
- true
}
}
}
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/SettingsComposable.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/SettingsComposable.kt
new file mode 100644
index 0000000..332d1e4
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/SettingsComposable.kt
@@ -0,0 +1,107 @@
+package com.d4rk.cartcalculator.ui.settings
+
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.outlined.Build
+import androidx.compose.material.icons.outlined.Info
+import androidx.compose.material.icons.outlined.Notifications
+import androidx.compose.material.icons.outlined.Palette
+import androidx.compose.material.icons.outlined.SafetyCheck
+import androidx.compose.material.icons.outlined.ShoppingCart
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import com.d4rk.cartcalculator.R
+import com.d4rk.cartcalculator.ui.settings.about.AboutSettingsActivity
+import com.d4rk.cartcalculator.ui.settings.advanced.AdvancedSettingsActivity
+import com.d4rk.cartcalculator.ui.settings.cart.CartSettingsActivity
+import com.d4rk.cartcalculator.ui.settings.display.DisplaySettingsActivity
+import com.d4rk.cartcalculator.ui.settings.privacy.PrivacySettingsActivity
+import com.d4rk.cartcalculator.utils.PreferenceItem
+import com.d4rk.cartcalculator.utils.Utils
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SettingsComposable(activity: SettingsActivity) {
+ val context = LocalContext.current
+ val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
+ Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {
+ LargeTopAppBar(title = { Text(stringResource(R.string.settings)) }, navigationIcon = {
+ IconButton(onClick = {
+ activity.finish()
+ }) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
+ }
+ }, scrollBehavior = scrollBehavior)
+ }) { paddingValues ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxHeight()
+ .padding(paddingValues) ,
+ ) {
+ item {
+ PreferenceItem(Icons.Outlined.Palette,
+ title = stringResource(R.string.display),
+ summary = stringResource(R.string.summary_preference_settings_display),
+ onClick = {
+ Utils.openActivity(context, DisplaySettingsActivity::class.java)
+ })
+ }
+ item {
+ PreferenceItem(Icons.Outlined.ShoppingCart ,
+ title = stringResource(R.string.cart_settings) ,
+ summary = stringResource(R.string.summary_preference_settings_cart) ,
+ onClick = {
+ Utils.openActivity(context , CartSettingsActivity::class.java)
+ })
+ }
+ item {
+ PreferenceItem(Icons.Outlined.Notifications,
+ title = stringResource(R.string.notifications),
+ summary = stringResource(R.string.summary_preference_settings_notifications),
+ onClick = {
+ Utils.openAppNotificationSettings(context)
+ })
+ }
+ item {
+ PreferenceItem(Icons.Outlined.Build,
+ title = stringResource(R.string.advanced),
+ summary = stringResource(R.string.summary_preference_settings_advanced),
+ onClick = {
+ Utils.openActivity(
+ context, AdvancedSettingsActivity::class.java
+ )
+ })
+ }
+ item {
+ PreferenceItem(Icons.Outlined.SafetyCheck,
+ title = stringResource(R.string.security_and_privacy),
+ summary = stringResource(R.string.summary_preference_settings_privacy_and_security),
+ onClick = {
+ Utils.openActivity(context, PrivacySettingsActivity::class.java)
+ })
+ }
+ item {
+ PreferenceItem(Icons.Outlined.Info,
+ title = stringResource(R.string.about),
+ summary = stringResource(R.string.summary_preference_settings_about),
+ onClick = {
+ Utils.openActivity(context, AboutSettingsActivity::class.java)
+ })
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/about/AboutSettingsActivity.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/about/AboutSettingsActivity.kt
new file mode 100644
index 0000000..5714cfd
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/about/AboutSettingsActivity.kt
@@ -0,0 +1,28 @@
+package com.d4rk.cartcalculator.ui.settings.about
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.ui.Modifier
+import com.d4rk.cartcalculator.ui.settings.display.theme.AppTheme
+
+class AboutSettingsActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ AppTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ AboutSettingsComposable(this@AboutSettingsActivity)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/about/AboutSettingsComposable.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/about/AboutSettingsComposable.kt
new file mode 100644
index 0000000..67a77b8
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/about/AboutSettingsComposable.kt
@@ -0,0 +1,89 @@
+package com.d4rk.cartcalculator.ui.settings.about
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.os.Build
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import com.d4rk.cartcalculator.BuildConfig
+import com.d4rk.cartcalculator.R
+import com.d4rk.cartcalculator.utils.PreferenceCategoryItem
+import com.d4rk.cartcalculator.utils.PreferenceItem
+import com.d4rk.cartcalculator.utils.Utils
+import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AboutSettingsComposable(activity: AboutSettingsActivity) {
+ val context = LocalContext.current
+ val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
+ Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {
+ LargeTopAppBar(title = { Text(stringResource(R.string.about)) }, navigationIcon = {
+ IconButton(onClick = {
+ activity.finish()
+ }) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
+ }
+ }, scrollBehavior = scrollBehavior)
+ }) { paddingValues ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxHeight()
+ .padding(paddingValues),
+ ) {
+ item {
+ PreferenceCategoryItem(title = stringResource(R.string.app_info))
+ PreferenceItem(
+ title = stringResource(R.string.app_name),
+ summary = stringResource(R.string.copyright),
+ )
+ PreferenceItem(
+ title = stringResource(R.string.app_build_version),
+ summary = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
+ )
+ PreferenceItem(title = stringResource(com.google.android.gms.oss.licenses.R.string.oss_license_title),
+ summary = stringResource(R.string.summary_preference_settings_oss),
+ onClick = {
+ Utils.openActivity(context, OssLicensesMenuActivity::class.java)
+ })
+ }
+ item {
+ PreferenceCategoryItem(title = stringResource(R.string.device_info))
+ val version = stringResource(
+ id = R.string.app_build,
+ "${stringResource(R.string.manufacturer)} ${Build.MANUFACTURER}",
+ "${stringResource(R.string.device_model)} ${Build.MODEL}",
+ "${stringResource(R.string.android_version)} ${Build.VERSION.RELEASE}",
+ "${stringResource(R.string.api_level)} ${Build.VERSION.SDK_INT}",
+ "${stringResource(R.string.arch)} ${Build.SUPPORTED_ABIS.joinToString()}"
+ )
+
+ PreferenceItem(title = stringResource(id = R.string.device_info),
+ summary = version,
+ onClick = {
+ val clip = ClipData.newPlainText("text", version)
+ clipboardManager.setPrimaryClip(clip)
+ // Show snackbar
+ })
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/advanced/AdvancedSettingsActivity.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/advanced/AdvancedSettingsActivity.kt
new file mode 100644
index 0000000..d661b67
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/advanced/AdvancedSettingsActivity.kt
@@ -0,0 +1,27 @@
+package com.d4rk.cartcalculator.ui.settings.advanced
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.ui.Modifier
+import com.d4rk.cartcalculator.ui.settings.display.theme.AppTheme
+
+class AdvancedSettingsActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ AppTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
+ ) {
+ AdvancedSettingsComposable(this@AdvancedSettingsActivity)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/advanced/AdvancedSettingsComposable.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/advanced/AdvancedSettingsComposable.kt
new file mode 100644
index 0000000..0816143
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/advanced/AdvancedSettingsComposable.kt
@@ -0,0 +1,60 @@
+package com.d4rk.cartcalculator.ui.settings.advanced
+
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import com.d4rk.cartcalculator.R
+import com.d4rk.cartcalculator.utils.PreferenceCategoryItem
+import com.d4rk.cartcalculator.utils.PreferenceItem
+import com.d4rk.cartcalculator.utils.Utils
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AdvancedSettingsComposable(activity: AdvancedSettingsActivity) {
+ val context = LocalContext.current
+ val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
+ Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {
+ LargeTopAppBar(title = { Text(stringResource(R.string.advanced)) }, navigationIcon = {
+ IconButton(onClick = {
+ activity.finish()
+ }) {
+ Icon(
+ Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null
+ )
+ }
+ }, scrollBehavior = scrollBehavior)
+ }) { paddingValues ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxHeight()
+ .padding(paddingValues),
+ ) {
+ item {
+ PreferenceCategoryItem(title = stringResource(R.string.error_reporting))
+ PreferenceItem(title = stringResource(R.string.bug_report),
+ summary = stringResource(R.string.summary_preference_settings_bug_report),
+ onClick = {
+ Utils.openUrl(
+ context,
+ "https://github.com/D4rK7355608/${context.packageName}/issues/new"
+ )
+ })
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/cart/CartSettingsActivity.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/cart/CartSettingsActivity.kt
new file mode 100644
index 0000000..1d6366c
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/cart/CartSettingsActivity.kt
@@ -0,0 +1,27 @@
+package com.d4rk.cartcalculator.ui.settings.cart
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.ui.Modifier
+import com.d4rk.cartcalculator.ui.settings.display.theme.AppTheme
+
+class CartSettingsActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState : Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ AppTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize() , color = MaterialTheme.colorScheme.background
+ ) {
+ CartSettingsComposable(this@CartSettingsActivity)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/cart/CartSettingsComposable.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/cart/CartSettingsComposable.kt
new file mode 100644
index 0000000..bff40c3
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/cart/CartSettingsComposable.kt
@@ -0,0 +1,67 @@
+package com.d4rk.cartcalculator.ui.settings.cart
+
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import com.d4rk.cartcalculator.R
+import com.d4rk.cartcalculator.data.store.DataStore
+import com.d4rk.cartcalculator.dialogs.CurrencyDialog
+import com.d4rk.cartcalculator.utils.PreferenceCategoryItem
+import com.d4rk.cartcalculator.utils.PreferenceItem
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CartSettingsComposable(activity : CartSettingsActivity) {
+ val context = LocalContext.current
+ val dataStore = DataStore.getInstance(context)
+ val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
+ val showDialog = remember { mutableStateOf(false) }
+
+ Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) , topBar = {
+ LargeTopAppBar(title = { Text(stringResource(R.string.cart_settings)) } , navigationIcon = {
+ IconButton(onClick = {
+ activity.finish()
+ }) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack , contentDescription = null)
+ }
+ } , scrollBehavior = scrollBehavior)
+ }) { paddingValues ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxHeight()
+ .padding(paddingValues) ,
+ ) {
+ item {
+ PreferenceCategoryItem(title = stringResource(R.string.shopping_cart))
+ PreferenceItem(title = stringResource(R.string.currency) ,
+ summary = stringResource(id = R.string.summary_preference_settings_currency) ,
+ onClick = { showDialog.value = true })
+ }
+ }
+ }
+
+ if (showDialog.value) {
+ CurrencyDialog(dataStore = dataStore ,
+ onDismiss = { showDialog.value = false } ,
+ onCurrencySelected = {
+ showDialog.value = false
+ })
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/display/DisplaySettingsActivity.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/display/DisplaySettingsActivity.kt
new file mode 100644
index 0000000..ca965a4
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/display/DisplaySettingsActivity.kt
@@ -0,0 +1,27 @@
+package com.d4rk.cartcalculator.ui.settings.display
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.ui.Modifier
+import com.d4rk.cartcalculator.ui.settings.display.theme.AppTheme
+
+class DisplaySettingsActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ AppTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
+ ) {
+ DisplaySettingsComposable(this@DisplaySettingsActivity)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/display/DisplaySettingsComposable.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/display/DisplaySettingsComposable.kt
new file mode 100644
index 0000000..51ce788
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/display/DisplaySettingsComposable.kt
@@ -0,0 +1,122 @@
+package com.d4rk.cartcalculator.ui.settings.display
+
+import android.os.Build
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import com.d4rk.cartcalculator.R
+import com.d4rk.cartcalculator.data.store.DataStore
+import com.d4rk.cartcalculator.ui.settings.display.theme.ThemeSettingsActivity
+import com.d4rk.cartcalculator.utils.PreferenceCategoryItem
+import com.d4rk.cartcalculator.utils.PreferenceItem
+import com.d4rk.cartcalculator.utils.SwitchPreferenceItem
+import com.d4rk.cartcalculator.utils.SwitchPreferenceItemWithDivider
+import com.d4rk.cartcalculator.utils.Utils
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun DisplaySettingsComposable(activity: DisplaySettingsActivity) {
+ val context = LocalContext.current
+ val dataStore = DataStore.getInstance(context)
+ val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
+
+ val themeMode = dataStore.themeMode.collectAsState(initial = "follow_system").value
+ val darkModeString = stringResource(R.string.dark_mode)
+ val lightModeString = stringResource(R.string.light_mode)
+ val themeSummary = when (themeMode) {
+ darkModeString, lightModeString -> stringResource(R.string.will_never_turn_on_automatically)
+ else -> stringResource(R.string.will_turn_on_automatically_by_system)
+ }
+ val switchState = remember { mutableStateOf(themeMode == darkModeString) }
+
+ val isDynamicColors = dataStore.dynamicColors.collectAsState(initial = true)
+ val scope = rememberCoroutineScope()
+
+ Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {
+ LargeTopAppBar(title = { Text(stringResource(R.string.display)) }, navigationIcon = {
+ IconButton(onClick = {
+ activity.finish()
+ }) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
+ }
+ }, scrollBehavior = scrollBehavior)
+ }) { paddingValues ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxHeight()
+ .padding(paddingValues),
+ ) {
+ item {
+ PreferenceCategoryItem(title = stringResource(R.string.appearance))
+ SwitchPreferenceItemWithDivider(title = stringResource(R.string.dark_theme),
+ summary = themeSummary,
+ checked = switchState.value,
+ onCheckedChange = { isChecked ->
+ switchState.value = isChecked
+ },
+ onSwitchClick = { isChecked ->
+
+ // this code does not working
+ scope.launch(Dispatchers.IO) {
+ if (isChecked) {
+ dataStore.saveThemeMode(darkModeString)
+ dataStore.themeModeState.value =
+ darkModeString
+ } else {
+ dataStore.saveThemeMode(lightModeString)
+ dataStore.themeModeState.value =
+ lightModeString
+ }
+ }
+ },
+ onClick = {
+ Utils.openActivity(
+ context, ThemeSettingsActivity::class.java
+ )
+ })
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ SwitchPreferenceItem(
+ title = stringResource(R.string.dynamic_colors),
+ summary = stringResource(R.string.summary_preference_settings_dynamic_colors),
+ checked = isDynamicColors.value,
+ ) { isChecked ->
+ CoroutineScope(Dispatchers.IO).launch {
+ dataStore.saveDynamicColors(isChecked)
+ }
+ }
+ }
+ }
+ item {
+ PreferenceCategoryItem(title = stringResource(R.string.language))
+ PreferenceItem(title = stringResource(R.string.language),
+ summary = stringResource(id = R.string.summary_preference_settings_language),
+ onClick = {
+ Utils.openAppLocaleSettings(context)
+ })
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/display/theme/Color.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/display/theme/Color.kt
new file mode 100644
index 0000000..0efb3ab
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/display/theme/Color.kt
@@ -0,0 +1,75 @@
+package com.d4rk.cartcalculator.ui.settings.display.theme
+
+import androidx.compose.ui.graphics.Color
+
+val primaryLight = Color(0xFF6A5F00)
+val onPrimaryLight = Color(0xFFFFFFFF)
+val primaryContainerLight = Color(0xFFF9E00F)
+val onPrimaryContainerLight = Color(0xFF4F4600)
+val secondaryLight = Color(0xFF695F18)
+val onSecondaryLight = Color(0xFFFFFFFF)
+val secondaryContainerLight = Color(0xFFF7E892)
+val onSecondaryContainerLight = Color(0xFF544B02)
+val tertiaryLight = Color(0xFF496800)
+val onTertiaryLight = Color(0xFFFFFFFF)
+val tertiaryContainerLight = Color(0xFFBDF15A)
+val onTertiaryContainerLight = Color(0xFF354E00)
+val errorLight = Color(0xFFBA1A1A)
+val onErrorLight = Color(0xFFFFFFFF)
+val errorContainerLight = Color(0xFFFFDAD6)
+val onErrorContainerLight = Color(0xFF410002)
+val backgroundLight = Color(0xFFFFF9EA)
+val onBackgroundLight = Color(0xFF1E1C10)
+val surfaceLight = Color(0xFFFFF9EA)
+val onSurfaceLight = Color(0xFF1E1C10)
+val surfaceVariantLight = Color(0xFFEAE3C6)
+val onSurfaceVariantLight = Color(0xFF4B4732)
+val outlineLight = Color(0xFF7C775F)
+val outlineVariantLight = Color(0xFFCEC7AB)
+val scrimLight = Color(0xFF000000)
+val inverseSurfaceLight = Color(0xFF333124)
+val inverseOnSurfaceLight = Color(0xFFF7F1DD)
+val inversePrimaryLight = Color(0xFFDEC800)
+val surfaceDimLight = Color(0xFFE0DAC7)
+val surfaceBrightLight = Color(0xFFFFF9EA)
+val surfaceContainerLowestLight = Color(0xFFFFFFFF)
+val surfaceContainerLowLight = Color(0xFFFAF3E0)
+val surfaceContainerLight = Color(0xFFF4EEDA)
+val surfaceContainerHighLight = Color(0xFFEEE8D5)
+val surfaceContainerHighestLight = Color(0xFFE8E2CF)
+
+val primaryDark = Color(0xFFFFFFFF)
+val onPrimaryDark = Color(0xFF373100)
+val primaryContainerDark = Color(0xFFEED600)
+val onPrimaryContainerDark = Color(0xFF484000)
+val secondaryDark = Color(0xFFD5C875)
+val onSecondaryDark = Color(0xFF373100)
+val secondaryContainerDark = Color(0xFF483F00)
+val onSecondaryContainerDark = Color(0xFFE3D581)
+val tertiaryDark = Color(0xFFFFFFFF)
+val onTertiaryDark = Color(0xFF243600)
+val tertiaryContainerDark = Color(0xFFB1E550)
+val onTertiaryContainerDark = Color(0xFF304600)
+val errorDark = Color(0xFFFFB4AB)
+val onErrorDark = Color(0xFF690005)
+val errorContainerDark = Color(0xFF93000A)
+val onErrorContainerDark = Color(0xFFFFDAD6)
+val backgroundDark = Color(0xFF151309)
+val onBackgroundDark = Color(0xFFE8E2CF)
+val surfaceDark = Color(0xFF151309)
+val onSurfaceDark = Color(0xFFE8E2CF)
+val surfaceVariantDark = Color(0xFF4B4732)
+val onSurfaceVariantDark = Color(0xFFCEC7AB)
+val outlineDark = Color(0xFF979178)
+val outlineVariantDark = Color(0xFF4B4732)
+val scrimDark = Color(0xFF000000)
+val inverseSurfaceDark = Color(0xFFE8E2CF)
+val inverseOnSurfaceDark = Color(0xFF333124)
+val inversePrimaryDark = Color(0xFF6A5F00)
+val surfaceDimDark = Color(0xFF151309)
+val surfaceBrightDark = Color(0xFF3C392C)
+val surfaceContainerLowestDark = Color(0xFF100E05)
+val surfaceContainerLowDark = Color(0xFF1E1C10)
+val surfaceContainerDark = Color(0xFF222014)
+val surfaceContainerHighDark = Color(0xFF2D2A1E)
+val surfaceContainerHighestDark = Color(0xFF383528)
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/display/theme/Theme.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/display/theme/Theme.kt
new file mode 100644
index 0000000..4d3baad
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/display/theme/Theme.kt
@@ -0,0 +1,173 @@
+package com.d4rk.cartcalculator.ui.settings.display.theme
+
+import android.app.Activity
+import android.content.Context
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.stringResource
+import androidx.core.view.WindowCompat
+import com.d4rk.cartcalculator.R
+import com.d4rk.cartcalculator.data.store.DataStore
+
+private val lightScheme = lightColorScheme(
+ primary = primaryLight,
+ onPrimary = onPrimaryLight,
+ primaryContainer = primaryContainerLight,
+ onPrimaryContainer = onPrimaryContainerLight,
+ secondary = secondaryLight,
+ onSecondary = onSecondaryLight,
+ secondaryContainer = secondaryContainerLight,
+ onSecondaryContainer = onSecondaryContainerLight,
+ tertiary = tertiaryLight,
+ onTertiary = onTertiaryLight,
+ tertiaryContainer = tertiaryContainerLight,
+ onTertiaryContainer = onTertiaryContainerLight,
+ error = errorLight,
+ onError = onErrorLight,
+ errorContainer = errorContainerLight,
+ onErrorContainer = onErrorContainerLight,
+ background = backgroundLight,
+ onBackground = onBackgroundLight,
+ surface = surfaceLight,
+ onSurface = onSurfaceLight,
+ surfaceVariant = surfaceVariantLight,
+ onSurfaceVariant = onSurfaceVariantLight,
+ outline = outlineLight,
+ outlineVariant = outlineVariantLight,
+ scrim = scrimLight,
+ inverseSurface = inverseSurfaceLight,
+ inverseOnSurface = inverseOnSurfaceLight,
+ inversePrimary = inversePrimaryLight,
+ surfaceDim = surfaceDimLight,
+ surfaceBright = surfaceBrightLight,
+ surfaceContainerLowest = surfaceContainerLowestLight,
+ surfaceContainerLow = surfaceContainerLowLight,
+ surfaceContainer = surfaceContainerLight,
+ surfaceContainerHigh = surfaceContainerHighLight,
+ surfaceContainerHighest = surfaceContainerHighestLight,
+)
+
+private val darkScheme = darkColorScheme(
+ primary = primaryDark,
+ onPrimary = onPrimaryDark,
+ primaryContainer = primaryContainerDark,
+ onPrimaryContainer = onPrimaryContainerDark,
+ secondary = secondaryDark,
+ onSecondary = onSecondaryDark,
+ secondaryContainer = secondaryContainerDark,
+ onSecondaryContainer = onSecondaryContainerDark,
+ tertiary = tertiaryDark,
+ onTertiary = onTertiaryDark,
+ tertiaryContainer = tertiaryContainerDark,
+ onTertiaryContainer = onTertiaryContainerDark,
+ error = errorDark,
+ onError = onErrorDark,
+ errorContainer = errorContainerDark,
+ onErrorContainer = onErrorContainerDark,
+ background = backgroundDark,
+ onBackground = onBackgroundDark,
+ surface = surfaceDark,
+ onSurface = onSurfaceDark,
+ surfaceVariant = surfaceVariantDark,
+ onSurfaceVariant = onSurfaceVariantDark,
+ outline = outlineDark,
+ outlineVariant = outlineVariantDark,
+ scrim = scrimDark,
+ inverseSurface = inverseSurfaceDark,
+ inverseOnSurface = inverseOnSurfaceDark,
+ inversePrimary = inversePrimaryDark,
+ surfaceDim = surfaceDimDark,
+ surfaceBright = surfaceBrightDark,
+ surfaceContainerLowest = surfaceContainerLowestDark,
+ surfaceContainerLow = surfaceContainerLowDark,
+ surfaceContainer = surfaceContainerDark,
+ surfaceContainerHigh = surfaceContainerHighDark,
+ surfaceContainerHighest = surfaceContainerHighestDark,
+)
+
+/**
+ * Determines and returns the appropriate color scheme based on user preferences and system capabilities.
+ *
+ * This function considers the following factors:
+ * - **Dark Theme:** If enabled by the user (isDarkTheme).
+ * - **AMOLED Mode:** If enabled by the user (isAmoledMode).
+ * - **Dynamic Colors:** If supported by the device (Build Version) and enabled by the user (isDynamicColors).
+ * - **System Context:** Used to access dynamic color resources if available (context).
+ *
+ * @param isDarkTheme Whether the user prefers a dark theme.
+ * @param isAmoledMode Whether the user prefers AMOLED-optimized colors.
+ * @param isDynamicColors Whether the user has enabled dynamic colors (if supported).
+ * @param context The current application context, used for accessing system resources.
+ *
+ * @return The most suitable color scheme based on the provided parameters.
+ */
+private fun getColorScheme(
+ isDarkTheme: Boolean, isAmoledMode: Boolean, isDynamicColors: Boolean, context: Context
+): ColorScheme {
+ val dynamicDark =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) dynamicDarkColorScheme(context) else darkScheme
+ val dynamicLight =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) dynamicLightColorScheme(context) else lightScheme
+
+ return when {
+ isAmoledMode && isDarkTheme && isDynamicColors -> dynamicDark.copy(
+ surface = Color.Black,
+ background = Color.Black,
+ )
+
+ isAmoledMode && isDarkTheme -> darkScheme.copy(
+ surface = Color.Black,
+ background = Color.Black,
+ )
+
+ isDynamicColors -> if (isDarkTheme) dynamicDark else dynamicLight
+ else -> if (isDarkTheme) darkScheme else lightScheme
+ }
+}
+
+@Composable
+fun AppTheme(
+ content: @Composable () -> Unit
+) {
+ val context = LocalContext.current
+ val dataStore = DataStore.getInstance(context)
+ val themeMode = dataStore.themeMode.collectAsState(initial = "follow_system").value
+ val isDynamicColors = dataStore.dynamicColors.collectAsState(initial = true).value
+ val isAmoledMode = dataStore.amoledMode.collectAsState(initial = false).value
+
+ val isSystemDarkTheme = isSystemInDarkTheme()
+ val isDarkTheme = when (themeMode) {
+ stringResource(R.string.dark_mode) -> true
+ stringResource(R.string.light_mode) -> false
+ else -> isSystemDarkTheme
+ }
+
+ val colorScheme = getColorScheme(isDarkTheme, isAmoledMode, isDynamicColors, context)
+
+ val view = LocalView.current
+ if (!view.isInEditMode) {
+ SideEffect {
+ val window = (view.context as Activity).window
+ window.statusBarColor = Color.Transparent.toArgb()
+ WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars =
+ !isDarkTheme
+ }
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme, typography = Typography, content = content
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/display/theme/ThemeSettingsActivity.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/display/theme/ThemeSettingsActivity.kt
new file mode 100644
index 0000000..5d4a54c
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/display/theme/ThemeSettingsActivity.kt
@@ -0,0 +1,26 @@
+package com.d4rk.cartcalculator.ui.settings.display.theme
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.ui.Modifier
+
+class ThemeSettingsActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ AppTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
+ ) {
+ ThemeSettingsComposable(this@ThemeSettingsActivity)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/display/theme/ThemeSettingsComposable.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/display/theme/ThemeSettingsComposable.kt
new file mode 100644
index 0000000..f1ee85e
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/display/theme/ThemeSettingsComposable.kt
@@ -0,0 +1,126 @@
+package com.d4rk.cartcalculator.ui.settings.display.theme
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.outlined.Info
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.d4rk.cartcalculator.R
+import com.d4rk.cartcalculator.data.store.DataStore
+import com.d4rk.cartcalculator.utils.SwitchCardComposable
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ThemeSettingsComposable(activity: ThemeSettingsActivity) {
+ val context = LocalContext.current
+ val dataStore = DataStore.getInstance(context)
+ val scope = rememberCoroutineScope()
+ val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
+ val themeMode = dataStore.themeMode.collectAsState(initial = "follow_system").value
+ val isAmoledMode = dataStore.amoledMode.collectAsState(initial = false)
+
+ val themeOptions = listOf(
+ stringResource(R.string.follow_system),
+ stringResource(R.string.dark_mode),
+ stringResource(R.string.light_mode),
+ )
+ Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {
+ LargeTopAppBar(
+ title = { Text(stringResource(R.string.dark_theme)) },
+ navigationIcon = {
+ IconButton(onClick = {
+ activity.finish()
+ }) {
+ Icon(
+ Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = null
+ )
+ }
+ },
+ scrollBehavior = scrollBehavior
+ )
+ }) { paddingValues ->
+ Box(modifier = Modifier.fillMaxSize()) {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ ) {
+ item {
+ SwitchCardComposable(
+ title = stringResource(R.string.amoled_mode), switchState = isAmoledMode
+ ) { isChecked ->
+ scope.launch(Dispatchers.IO) {
+ dataStore.saveAmoledMode(isChecked)
+ }
+ }
+ }
+ item {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(24.dp)
+ ) {
+ themeOptions.forEach { text ->
+ Row(
+ Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(selected = (text == themeMode), onClick = {
+ scope.launch(Dispatchers.IO) {
+ dataStore.saveThemeMode(text)
+ dataStore.themeModeState.value = text
+ }
+ })
+ Text(
+ text = text,
+ style = MaterialTheme.typography.bodyMedium.merge(),
+ modifier = Modifier.padding(start = 16.dp)
+ )
+ }
+ }
+ }
+ }
+ item {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(24.dp)
+ ) {
+ Icon(imageVector = Icons.Outlined.Info, contentDescription = null)
+ Spacer(modifier = Modifier.height(24.dp))
+ Text(stringResource(R.string.summary_dark_theme))
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/display/theme/Type.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/display/theme/Type.kt
new file mode 100644
index 0000000..7d39b18
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/display/theme/Type.kt
@@ -0,0 +1,17 @@
+package com.d4rk.cartcalculator.ui.settings.display.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+)
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/permissions/PermissionsActivity.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/permissions/PermissionsActivity.kt
deleted file mode 100644
index 795471e..0000000
--- a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/permissions/PermissionsActivity.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.d4rk.cartcalculator.ui.settings.permissions
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-import androidx.preference.PreferenceFragmentCompat
-import com.d4rk.cartcalculator.R
-import com.d4rk.cartcalculator.databinding.ActivityPermissionsBinding
-class PermissionsActivity : AppCompatActivity() {
- private lateinit var binding: ActivityPermissionsBinding
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityPermissionsBinding.inflate(layoutInflater)
- setContentView(binding.root)
- supportFragmentManager.beginTransaction().replace(R.id.permissions, SettingsFragment()).commit()
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
- }
- class SettingsFragment : PreferenceFragmentCompat() {
- override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
- setPreferencesFromResource(R.xml.preferences_permissions, rootKey)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/PrivacySettingsActivity.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/PrivacySettingsActivity.kt
new file mode 100644
index 0000000..c58b71e
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/PrivacySettingsActivity.kt
@@ -0,0 +1,28 @@
+package com.d4rk.cartcalculator.ui.settings.privacy
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.ui.Modifier
+import com.d4rk.cartcalculator.ui.settings.display.theme.AppTheme
+
+class PrivacySettingsActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ AppTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ PrivacySettingsComposable(this@PrivacySettingsActivity)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/PrivacySettingsComposable.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/PrivacySettingsComposable.kt
new file mode 100644
index 0000000..37ee320
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/PrivacySettingsComposable.kt
@@ -0,0 +1,120 @@
+package com.d4rk.cartcalculator.ui.settings.privacy
+
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import com.d4rk.cartcalculator.R
+import com.d4rk.cartcalculator.ui.settings.privacy.ads.AdsSettingsActivity
+import com.d4rk.cartcalculator.ui.settings.privacy.permissions.PermissionsSettingsActivity
+import com.d4rk.cartcalculator.ui.settings.privacy.usage.UsageAndDiagnosticsActivity
+import com.d4rk.cartcalculator.utils.PreferenceCategoryItem
+import com.d4rk.cartcalculator.utils.PreferenceItem
+import com.d4rk.cartcalculator.utils.Utils
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun PrivacySettingsComposable(activity: PrivacySettingsActivity) {
+ val context = LocalContext.current
+ val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
+ Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {
+ LargeTopAppBar(
+ title = { Text(stringResource(R.string.security_and_privacy)) },
+ navigationIcon = {
+ IconButton(onClick = {
+ activity.finish()
+ }) {
+ Icon(
+ Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null
+ )
+ }
+ },
+ scrollBehavior = scrollBehavior
+ )
+ }) { paddingValues ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxHeight()
+ .padding(paddingValues),
+ ) {
+ item {
+ PreferenceCategoryItem(title = stringResource(R.string.privacy))
+ PreferenceItem(title = stringResource(R.string.privacy_policy),
+ summary = stringResource(id = R.string.summary_preference_settings_privacy_policy),
+ onClick = {
+ Utils.openUrl(
+ context,
+ "https://sites.google.com/view/d4rk7355608/more/apps/privacy-policy"
+ )
+ })
+ PreferenceItem(title = stringResource(R.string.terms_of_service),
+ summary = stringResource(id = R.string.summary_preference_settings_terms_of_service),
+ onClick = {
+ Utils.openUrl(
+ context,
+ "https://sites.google.com/view/d4rk7355608/more/apps/terms-of-service"
+ )
+ })
+ PreferenceItem(title = stringResource(R.string.code_of_conduct),
+ summary = stringResource(id = R.string.summary_preference_settings_code_of_conduct),
+ onClick = {
+ Utils.openUrl(
+ context,
+ "https://sites.google.com/view/d4rk7355608/more/code-of-conduct"
+ )
+ })
+ PreferenceItem(title = stringResource(R.string.permissions),
+ summary = stringResource(id = R.string.summary_preference_settings_permissions),
+ onClick = {
+ Utils.openActivity(
+ context, PermissionsSettingsActivity::class.java
+ )
+ })
+ PreferenceItem(title = stringResource(R.string.ads),
+ summary = "Manage the info to show you ads",
+ onClick = {
+ Utils.openActivity(
+ context, AdsSettingsActivity::class.java
+ )
+ })
+ PreferenceItem(title = stringResource(R.string.usage_and_diagnostics),
+ summary = stringResource(id = R.string.summary_preference_settings_usage_and_diagnostics),
+ onClick = {
+ Utils.openActivity(
+ context, UsageAndDiagnosticsActivity::class.java
+ )
+ })
+ }
+ item {
+ PreferenceCategoryItem(title = stringResource(R.string.legal))
+ PreferenceItem(title = stringResource(R.string.legal_notices),
+ summary = stringResource(id = R.string.summary_preference_settings_legal_notices),
+ onClick = {
+ Utils.openUrl(
+ context,
+ "https://sites.google.com/view/d4rk7355608/more/apps/legal-notices"
+ )
+ })
+ PreferenceItem(title = stringResource(R.string.license),
+ summary = stringResource(R.string.summary_preference_settings_license),
+ onClick = {
+ Utils.openUrl(context, "https://www.gnu.org/licenses/gpl-3.0")
+ })
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/ads/AdsSettingsActivity.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/ads/AdsSettingsActivity.kt
new file mode 100644
index 0000000..545bcb7
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/ads/AdsSettingsActivity.kt
@@ -0,0 +1,43 @@
+package com.d4rk.cartcalculator.ui.settings.privacy.ads
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.ui.Modifier
+import com.d4rk.cartcalculator.ui.settings.display.theme.AppTheme
+import com.google.android.ump.ConsentForm
+import com.google.android.ump.ConsentInformation
+import com.google.android.ump.UserMessagingPlatform
+
+class AdsSettingsActivity : ComponentActivity() {
+ private lateinit var consentInformation: ConsentInformation
+ private lateinit var consentForm: ConsentForm
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ AppTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
+ ) {
+ AdsSettingsComposable(this@AdsSettingsActivity)
+ }
+ }
+ }
+ }
+
+ fun loadForm() {
+ UserMessagingPlatform.loadConsentForm(this, { consentForm ->
+ this.consentForm = consentForm
+ if (consentInformation.consentStatus == ConsentInformation.ConsentStatus.OBTAINED) {
+ consentForm.show(this) {
+ loadForm()
+ }
+ }
+ }, {})
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/ads/AdsSettingsComposable.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/ads/AdsSettingsComposable.kt
new file mode 100644
index 0000000..f3b89ec
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/ads/AdsSettingsComposable.kt
@@ -0,0 +1,136 @@
+package com.d4rk.cartcalculator.ui.settings.privacy.ads
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.text.ClickableText
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.outlined.Info
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.dp
+import com.d4rk.cartcalculator.R
+import com.d4rk.cartcalculator.data.store.DataStore
+import com.d4rk.cartcalculator.utils.PreferenceItem
+import com.d4rk.cartcalculator.utils.SwitchCardComposable
+import com.d4rk.cartcalculator.utils.Utils
+import com.google.android.ump.ConsentRequestParameters
+import com.google.android.ump.UserMessagingPlatform
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AdsSettingsComposable(activity: AdsSettingsActivity) {
+ val context = LocalContext.current
+ val dataStore = DataStore.getInstance(context)
+ val switchState = dataStore.ads.collectAsState(initial = true)
+ val scope = rememberCoroutineScope()
+ val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
+ Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {
+ LargeTopAppBar(title = { Text(stringResource(R.string.ads)) }, navigationIcon = {
+ IconButton(onClick = {
+ activity.finish()
+ }) {
+ Icon(
+ Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null
+ )
+ }
+ }, scrollBehavior = scrollBehavior)
+ }) { innerPadding ->
+ Box(modifier = Modifier.fillMaxSize()) {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding),
+ ) {
+ item {
+ SwitchCardComposable(
+ title = stringResource(R.string.display_ads), switchState = switchState
+ ) { isChecked ->
+ scope.launch(Dispatchers.IO) {
+ dataStore.saveAds(isChecked)
+ }
+ }
+ }
+ item {
+ Box(modifier = Modifier.padding(horizontal = 8.dp)) {
+ PreferenceItem(title = stringResource(R.string.personalized_ads),
+ summary = "Manage the personalized ads consent for this app",
+ onClick = {
+ val params = ConsentRequestParameters.Builder()
+ .setTagForUnderAgeOfConsent(false).build()
+ val consentInformation =
+ UserMessagingPlatform.getConsentInformation(
+ context
+ )
+ consentInformation.requestConsentInfoUpdate(activity,
+ params,
+ {
+ activity.loadForm()
+ },
+ {})
+ })
+ }
+ }
+ item {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(24.dp)
+ ) {
+ Icon(imageVector = Icons.Outlined.Info, contentDescription = null)
+ Spacer(modifier = Modifier.height(24.dp))
+ Text(stringResource(R.string.summary_ads))
+ val annotatedString = buildAnnotatedString {
+ withStyle(
+ style = SpanStyle(
+ color = MaterialTheme.colorScheme.primary,
+ textDecoration = TextDecoration.Underline
+ )
+ ) {
+ append(stringResource(R.string.learn_more))
+ }
+ addStringAnnotation(
+ tag = "URL",
+ annotation = "https://www.example.com",
+ start = 0,
+ end = stringResource(R.string.learn_more).length
+ )
+ }
+ ClickableText(text = annotatedString, onClick = { offset ->
+ annotatedString.getStringAnnotations("URL", offset, offset)
+ .firstOrNull()?.let { annotation ->
+ Utils.openUrl(context, annotation.item)
+ }
+ })
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/permissions/PermissionsSettingsActivity.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/permissions/PermissionsSettingsActivity.kt
new file mode 100644
index 0000000..4bc8c3f
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/permissions/PermissionsSettingsActivity.kt
@@ -0,0 +1,28 @@
+package com.d4rk.cartcalculator.ui.settings.privacy.permissions
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.ui.Modifier
+import com.d4rk.cartcalculator.ui.settings.display.theme.AppTheme
+
+class PermissionsSettingsActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ AppTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ PermissionsSettingsComposable(this@PermissionsSettingsActivity)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/permissions/PermissionsSettingsComposable.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/permissions/PermissionsSettingsComposable.kt
new file mode 100644
index 0000000..8507b90
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/permissions/PermissionsSettingsComposable.kt
@@ -0,0 +1,89 @@
+package com.d4rk.cartcalculator.ui.settings.privacy.permissions
+
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.res.stringResource
+import com.d4rk.cartcalculator.R
+import com.d4rk.cartcalculator.utils.PreferenceCategoryItem
+import com.d4rk.cartcalculator.utils.PreferenceItem
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun PermissionsSettingsComposable(activity: PermissionsSettingsActivity) {
+ val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
+ Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {
+ LargeTopAppBar(
+ title = { Text(stringResource(R.string.permissions)) },
+ navigationIcon = {
+ IconButton(onClick = {
+ activity.finish()
+ }) {
+ Icon(
+ Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = null
+ )
+ }
+ },
+ scrollBehavior = scrollBehavior
+ )
+ }) { paddingValues ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxHeight()
+ .padding(paddingValues),
+ ) {
+ item {
+ PreferenceCategoryItem(title = stringResource(R.string.normal))
+ PreferenceItem(
+ title = stringResource(R.string.internet),
+ summary = stringResource(R.string.summary_preference_permissions_internet),
+ )
+ PreferenceItem(
+ title = stringResource(R.string.ad_id),
+ summary = stringResource(R.string.summary_preference_permissions_ad_id),
+ )
+ PreferenceItem(
+ title = stringResource(R.string.post_notifications),
+ summary = stringResource(R.string.summary_preference_permissions_post_notifications),
+ )
+ }
+ item {
+ PreferenceCategoryItem(title = stringResource(R.string.runtime))
+ PreferenceItem(
+ title = stringResource(R.string.access_network_state),
+ summary = stringResource(R.string.summary_preference_permissions_access_network_state),
+ )
+ PreferenceItem(
+ title = stringResource(R.string.access_notification_policy),
+ summary = stringResource(R.string.summary_preference_permissions_access_notification_policy),
+ )
+ PreferenceItem(
+ title = stringResource(R.string.billing),
+ summary = stringResource(R.string.summary_preference_permissions_billing),
+ )
+ PreferenceItem(
+ title = stringResource(R.string.check_license),
+ summary = stringResource(R.string.summary_preference_permissions_check_license),
+ )
+ PreferenceItem(
+ title = stringResource(R.string.foreground_service),
+ summary = stringResource(R.string.summary_preference_permissions_foreground_service),
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/usage/UsageAndDiagnosticsActivity.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/usage/UsageAndDiagnosticsActivity.kt
new file mode 100644
index 0000000..8701217
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/usage/UsageAndDiagnosticsActivity.kt
@@ -0,0 +1,28 @@
+package com.d4rk.cartcalculator.ui.settings.privacy.usage
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.ui.Modifier
+import com.d4rk.cartcalculator.ui.settings.display.theme.AppTheme
+
+class UsageAndDiagnosticsActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ AppTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ UsageAndDiagnosticsComposable(this@UsageAndDiagnosticsActivity)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/usage/UsageAndDiagnosticsComposable.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/usage/UsageAndDiagnosticsComposable.kt
new file mode 100644
index 0000000..78dca53
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/settings/privacy/usage/UsageAndDiagnosticsComposable.kt
@@ -0,0 +1,118 @@
+package com.d4rk.cartcalculator.ui.settings.privacy.usage
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.text.ClickableText
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.outlined.Info
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.dp
+import com.d4rk.cartcalculator.R
+import com.d4rk.cartcalculator.data.store.DataStore
+import com.d4rk.cartcalculator.utils.SwitchCardComposable
+import com.d4rk.cartcalculator.utils.Utils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun UsageAndDiagnosticsComposable(activity: UsageAndDiagnosticsActivity) {
+ val context = LocalContext.current
+ val dataStore = DataStore.getInstance(context)
+ val switchState = dataStore.usageAndDiagnostics.collectAsState(initial = true)
+ val scope = rememberCoroutineScope()
+ val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
+ Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {
+ LargeTopAppBar(
+ title = { Text(stringResource(R.string.usage_and_diagnostics)) },
+ navigationIcon = {
+ IconButton(onClick = {
+ activity.finish()
+ }) {
+ Icon(
+ Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null
+ )
+ }
+ },
+ scrollBehavior = scrollBehavior
+ )
+ }) { innerPadding ->
+ Box(modifier = Modifier.fillMaxSize()) {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding),
+ ) {
+ item {
+ SwitchCardComposable(
+ title = stringResource(R.string.usage_and_diagnostics),
+ switchState = switchState
+ ) { isChecked ->
+ scope.launch(Dispatchers.IO) {
+ dataStore.saveUsageAndDiagnostics(isChecked)
+ }
+ }
+ }
+ item {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(24.dp)
+ ) {
+ Icon(imageVector = Icons.Outlined.Info, contentDescription = null)
+ Spacer(modifier = Modifier.height(24.dp))
+ Text(stringResource(R.string.summary_usage_and_diagnostics))
+ val annotatedString = buildAnnotatedString {
+ withStyle(
+ style = SpanStyle(
+ color = MaterialTheme.colorScheme.primary,
+ textDecoration = TextDecoration.Underline
+ )
+ ) {
+ append(stringResource(R.string.learn_more))
+ }
+ addStringAnnotation(
+ tag = "URL",
+ annotation = "https://www.example.com",
+ start = 0,
+ end = stringResource(R.string.learn_more).length
+ )
+ }
+ ClickableText(text = annotatedString, onClick = { offset ->
+ annotatedString.getStringAnnotations("URL", offset, offset)
+ .firstOrNull()?.let { annotation ->
+ Utils.openUrl(context, annotation.item)
+ }
+ })
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/startup/StartupActivity.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/startup/StartupActivity.kt
index 6b26c86..241d582 100644
--- a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/startup/StartupActivity.kt
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/startup/StartupActivity.kt
@@ -1,60 +1,81 @@
package com.d4rk.cartcalculator.ui.startup
+
import android.Manifest
-import android.content.Intent
-import android.net.Uri
import android.os.Build
import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-import com.d4rk.cartcalculator.MainActivity
-import com.d4rk.cartcalculator.databinding.ActivityStartupBinding
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.ui.Modifier
+import com.d4rk.cartcalculator.ui.settings.display.theme.AppTheme
import com.google.android.ump.ConsentForm
import com.google.android.ump.ConsentInformation
import com.google.android.ump.ConsentRequestParameters
import com.google.android.ump.UserMessagingPlatform
-import me.zhanghai.android.fastscroll.FastScrollerBuilder
-class StartupActivity : AppCompatActivity() {
- private lateinit var binding: ActivityStartupBinding
+
+class StartupActivity : ComponentActivity() {
private lateinit var consentInformation: ConsentInformation
private lateinit var consentForm: ConsentForm
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- binding = ActivityStartupBinding.inflate(layoutInflater)
- setContentView(binding.root)
- val params = ConsentRequestParameters
- .Builder()
- .setTagForUnderAgeOfConsent(false)
- .build()
+ enableEdgeToEdge()
+ setContent {
+ AppTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
+ ) {
+ StartupComposable()
+ }
+ }
+ }
+ val params = ConsentRequestParameters.Builder().setTagForUnderAgeOfConsent(false).build()
consentInformation = UserMessagingPlatform.getConsentInformation(this)
consentInformation.requestConsentInfoUpdate(this, params, {
if (consentInformation.isConsentFormAvailable) {
loadForm()
}
- }, {
- })
- FastScrollerBuilder(binding.scrollView).useMd2Style().build()
- binding.buttonBrowsePrivacyPolicyAndTermsOfService.setOnClickListener {
- startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://sites.google.com/view/d4rk7355608/more/apps/privacy-policy")))
- }
- binding.floatingButtonAgree.setOnClickListener {
- startActivity(Intent(this, MainActivity::class.java))
- }
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1)
- }
+ }, {})
+ requestPermissions()
}
+
+ /**
+ * Loads the consent form for user messaging platform (UMP) based on consent status.
+ *
+ * This function initiates the loading of the consent form using UserMessagingPlatform (UMP) API.
+ * Upon successful loading of the consent form, it assigns the form to a local variable `consentForm`.
+ * If user consent is required (`ConsentStatus.REQUIRED`), the form is displayed to the user.
+ * If the consent status is not required or an error occurs during loading, the function handles this gracefully.
+ *
+ * @see com.google.android.gms.ads.UserMessagingPlatform
+ * @see com.google.ads.consent.ConsentInformation
+ */
private fun loadForm() {
- UserMessagingPlatform.loadConsentForm(
- this,
- { consentForm ->
- this.consentForm = consentForm
- if (consentInformation.consentStatus == ConsentInformation.ConsentStatus.REQUIRED) {
- consentForm.show(this) {
- loadForm()
- }
+ UserMessagingPlatform.loadConsentForm(this, { consentForm ->
+ this.consentForm = consentForm
+ if (consentInformation.consentStatus == ConsentInformation.ConsentStatus.REQUIRED) {
+ consentForm.show(this) {
+ loadForm()
}
- },
- {
}
- )
+ }, {})
+ }
+
+ /**
+ * Handles the application's permission requirements.
+ *
+ * This function is responsible for checking and requesting the necessary permissions for the application. It takes into account the Android version to manage specific permission scenarios.
+ * For Android versions Tiramisu or later, it requests the POST_NOTIFICATIONS permission.
+ *
+ * @see android.Manifest.permission.POST_NOTIFICATIONS
+ * @see android.os.Build.VERSION.SDK_INT
+ * @see android.os.Build.VERSION_CODES.TIRAMISU
+ */
+ private fun requestPermissions() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1)
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/startup/StartupComposable.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/startup/StartupComposable.kt
new file mode 100644
index 0000000..175a043
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/startup/StartupComposable.kt
@@ -0,0 +1,98 @@
+package com.d4rk.cartcalculator.ui.startup
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.text.ClickableText
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.CheckCircle
+import androidx.compose.material.icons.outlined.Info
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExtendedFloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.unit.dp
+import com.d4rk.cartcalculator.MainActivity
+import com.d4rk.cartcalculator.R
+import com.d4rk.cartcalculator.utils.Utils
+import com.d4rk.cartcalculator.utils.bounceClick
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun StartupComposable() {
+ val context = LocalContext.current
+ val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
+ Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {
+ LargeTopAppBar(
+ title = { Text(stringResource(R.string.welcome)) },
+ scrollBehavior = scrollBehavior
+ )
+ }) { innerPadding ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(24.dp)
+ ) {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding),
+ ) {
+ item {
+ Image(
+ painter = painterResource(id = R.drawable.il_startup),
+ contentDescription = null
+ )
+ Image(
+ Icons.Outlined.Info, contentDescription = null
+ )
+ }
+ item {
+ Text(
+ text = stringResource(R.string.summary_browse_terms_of_service_and_privacy_policy),
+ modifier = Modifier.padding(top = 24.dp, bottom = 24.dp)
+ )
+ ClickableText(
+ text = AnnotatedString(stringResource(R.string.browse_terms_of_service_and_privacy_policy)),
+ onClick = {
+ Utils.openUrl(
+ context,
+ "https://sites.google.com/view/d4rk7355608/more/apps/privacy-policy"
+ )
+
+ },
+ )
+ }
+ }
+ ExtendedFloatingActionButton(modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .bounceClick(),
+ text = { Text(stringResource(R.string.agree)) },
+ onClick = {
+ Utils.openActivity(
+ context, MainActivity::class.java
+ )
+ },
+ icon = {
+ Icon(
+ Icons.Outlined.CheckCircle,
+ contentDescription = null
+ )
+ })
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/support/SupportActivity.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/support/SupportActivity.kt
new file mode 100644
index 0000000..1b62e4a
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/support/SupportActivity.kt
@@ -0,0 +1,63 @@
+@file:Suppress("DEPRECATION")
+
+package com.d4rk.cartcalculator.ui.support
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.snapshots.SnapshotStateMap
+import androidx.compose.ui.Modifier
+import com.android.billingclient.api.BillingClient
+import com.android.billingclient.api.BillingFlowParams
+import com.android.billingclient.api.SkuDetails
+import com.android.billingclient.api.SkuDetailsParams
+import com.d4rk.cartcalculator.ui.settings.display.theme.AppTheme
+
+class SupportActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ AppTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
+ ) {
+ SupportComposable(this@SupportActivity)
+ }
+ }
+ }
+ }
+
+ fun initiatePurchase(
+ sku: String,
+ skuDetailsMap: Map,
+ billingClient: BillingClient
+ ) {
+ val skuDetails = skuDetailsMap[sku]
+ if (skuDetails != null) {
+ val flowParams = BillingFlowParams.newBuilder().setSkuDetails(skuDetails).build()
+ billingClient.launchBillingFlow(this, flowParams)
+ }
+ }
+
+ fun querySkuDetails(
+ billingClient: BillingClient,
+ skuDetailsMap: SnapshotStateMap
+ ) {
+ val skuList =
+ listOf("low_donation", "normal_donation", "high_donation", "extreme_donation")
+ val params = SkuDetailsParams.newBuilder().setSkusList(skuList)
+ .setType(BillingClient.SkuType.INAPP).build()
+ billingClient.querySkuDetailsAsync(params) { billingResult, skuDetailsList ->
+ if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && skuDetailsList != null) {
+ for (skuDetails in skuDetailsList) {
+ skuDetailsMap[skuDetails.sku] = skuDetails
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/support/SupportComposable.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/support/SupportComposable.kt
new file mode 100644
index 0000000..ab82c33
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/support/SupportComposable.kt
@@ -0,0 +1,251 @@
+@file:Suppress("DEPRECATION")
+
+package com.d4rk.cartcalculator.ui.support
+
+import android.content.Context
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.outlined.Paid
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedCard
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.mutableStateMapOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.snapshots.SnapshotStateMap
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.android.billingclient.api.BillingClient
+import com.android.billingclient.api.BillingClientStateListener
+import com.android.billingclient.api.BillingResult
+import com.android.billingclient.api.SkuDetails
+import com.d4rk.cartcalculator.R
+import com.d4rk.cartcalculator.utils.Utils
+import com.d4rk.cartcalculator.utils.bounceClick
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SupportComposable(activity: SupportActivity) {
+ val context = LocalContext.current
+ val coroutineScope = rememberCoroutineScope()
+ val skuDetailsMap = remember { mutableStateMapOf() }
+ val billingClient = rememberBillingClient(context, coroutineScope, activity, skuDetailsMap)
+ val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
+ Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {
+ LargeTopAppBar(title = { Text(stringResource(R.string.support_us)) }, navigationIcon = {
+ IconButton(onClick = {
+ activity.finish()
+ }) {
+ Icon(
+ Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null
+ )
+ }
+ }, scrollBehavior = scrollBehavior
+ )
+ }) { paddingValues ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxHeight()
+ .padding(paddingValues),
+ ) {
+ item {
+ Text(
+ text = stringResource(R.string.paid_support),
+ modifier = Modifier.padding(start = 16.dp, top = 16.dp),
+ style = MaterialTheme.typography.titleLarge,
+ )
+ }
+ item {
+ OutlinedCard(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ ) {
+ Column {
+ Text(
+ text = stringResource(R.string.summary_donations),
+ modifier = Modifier.padding(16.dp)
+ )
+ LazyRow(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ item {
+ FilledTonalButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .bounceClick(),
+ onClick = {
+ activity.initiatePurchase(
+ "low_donation", skuDetailsMap, billingClient
+ )
+ },
+ ) {
+ Icon(
+ Icons.Outlined.Paid,
+ contentDescription = null,
+ modifier = Modifier.size(ButtonDefaults.IconSize)
+ )
+ Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
+ Text(skuDetailsMap["low_donation"]?.price ?: "")
+ }
+ }
+ item {
+ FilledTonalButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .bounceClick(),
+ onClick = {
+ activity.initiatePurchase(
+ "normal_donation", skuDetailsMap, billingClient
+ )
+ },
+ ) {
+ Icon(
+ Icons.Outlined.Paid,
+ contentDescription = null,
+ modifier = Modifier.size(ButtonDefaults.IconSize)
+ )
+ Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
+ Text(skuDetailsMap["normal_donation"]?.price ?: "")
+ }
+ }
+ }
+ LazyRow(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ item {
+ FilledTonalButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .bounceClick(),
+ onClick = {
+ activity.initiatePurchase(
+ "high_donation", skuDetailsMap, billingClient
+ )
+ },
+ ) {
+ Icon(
+ Icons.Outlined.Paid,
+ contentDescription = null,
+ modifier = Modifier.size(ButtonDefaults.IconSize)
+ )
+ Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
+ Text(skuDetailsMap["high_donation"]?.price ?: "")
+ }
+ }
+ item {
+ FilledTonalButton(
+
+ modifier = Modifier
+ .fillMaxWidth()
+ .bounceClick(),
+ onClick = {
+ activity.initiatePurchase(
+ "extreme_donation", skuDetailsMap, billingClient
+ )
+ },
+ ) {
+ Icon(
+ Icons.Outlined.Paid,
+ contentDescription = null,
+ modifier = Modifier.size(ButtonDefaults.IconSize)
+ )
+ Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
+ Text(skuDetailsMap["extreme_donation"]?.price ?: "")
+ }
+ }
+ }
+ }
+ }
+ }
+ item {
+ Text(
+ text = stringResource(R.string.non_paid_support),
+ modifier = Modifier.padding(start = 16.dp),
+ style = MaterialTheme.typography.titleLarge,
+ )
+ }
+ item {
+ FilledTonalButton(
+ onClick = {
+ Utils.openUrl(context, "https://direct-link.net/548212/agOqI7123501341")
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .bounceClick()
+ .padding(16.dp),
+ ) {
+ Icon(
+ Icons.Outlined.Paid,
+ contentDescription = null,
+ modifier = Modifier.size(ButtonDefaults.IconSize)
+ )
+ Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
+ Text(text = stringResource(R.string.web_ad))
+ }
+ }
+ // TODO: Add ad view composable
+ }
+ }
+}
+
+@Composable
+fun rememberBillingClient(
+ context: Context,
+ coroutineScope: CoroutineScope,
+ activity: SupportActivity,
+ skuDetailsMap: SnapshotStateMap
+): BillingClient {
+ val billingClient = remember {
+ BillingClient.newBuilder(context).setListener { _, _ -> }.enablePendingPurchases().build()
+ }
+ DisposableEffect(billingClient) {
+ billingClient.startConnection(object : BillingClientStateListener {
+ override fun onBillingSetupFinished(billingResult: BillingResult) {
+ if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
+ coroutineScope.launch {
+ activity.querySkuDetails(billingClient, skuDetailsMap)
+ }
+ }
+ }
+
+ override fun onBillingServiceDisconnected() {}
+ })
+
+ onDispose {
+ billingClient.endConnection()
+ }
+ }
+ return billingClient
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/viewmodel/ViewModel.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/viewmodel/ViewModel.kt
deleted file mode 100644
index 0873519..0000000
--- a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/viewmodel/ViewModel.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package com.d4rk.cartcalculator.ui.viewmodel
-import androidx.lifecycle.ViewModel
-class ViewModel : ViewModel()
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/utils/ComposablesUtils.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/utils/ComposablesUtils.kt
new file mode 100644
index 0000000..8cb1a27
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/utils/ComposablesUtils.kt
@@ -0,0 +1,289 @@
+package com.d4rk.cartcalculator.utils
+
+import android.annotation.SuppressLint
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.waitForUpOrCancellation
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Switch
+import androidx.compose.material3.SwitchDefaults
+import androidx.compose.material3.Text
+import androidx.compose.material3.VerticalDivider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+
+/**
+ * Creates a clickable card with a title and a switch for app preference screens.
+ *
+ * This composable function displays a card with a title and a switch. The entire card is clickable and toggles the switch when clicked, calling the provided `onSwitchToggled` callback function with the new state.
+ * The switch displays a check icon when it's in the 'on' state.
+ *
+ * @param title The text displayed on the card's title.
+ * @param switchState A state variable holding the current on/off state of the switch. Set to true for on and false for off.
+ * @param onSwitchToggled A callback function that is called whenever the switch is toggled. This function receives the new state of the switch (boolean) as a parameter.
+ */
+@Composable
+fun SwitchCardComposable(
+ title: String, switchState: State, onSwitchToggled: (Boolean) -> Unit
+) {
+ Card(modifier = Modifier
+ .fillMaxWidth()
+ .padding(24.dp)
+ .clip(RoundedCornerShape(28.dp))
+ .clickable {
+ onSwitchToggled(!switchState.value)
+ }) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(text = title)
+ Switch(checked = switchState.value,
+ onCheckedChange = onSwitchToggled,
+ thumbContent = if (switchState.value) {
+ {
+ Icon(
+ Icons.Filled.Check,
+ contentDescription = null,
+ modifier = Modifier.size(SwitchDefaults.IconSize),
+ )
+ }
+ } else {
+ null
+ })
+ }
+ }
+}
+
+/**
+ * Displays a category header within your app's preference screens.
+ *
+ * This composable function is used to display a category header in your app's preference screens. It helps in separating different sections of your app's preferences with clear category titles. The title is displayed in a distinct style and color to differentiate it from other preference items.
+ *
+ * @param title The text to be displayed as the category header. This is typically the name of the category.
+ */
+@Composable
+fun PreferenceCategoryItem(
+ title: String
+) {
+ Text(
+ text = title,
+ color = MaterialTheme.colorScheme.primary,
+ style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
+ modifier = Modifier.padding(start = 16.dp, top = 16.dp)
+ )
+}
+
+/**
+ * Creates a clickable preference item for app preference screens.
+ *
+ * This composable function displays a preference item with an optional icon, title, and summary. The entire row is clickable and triggers the provided `onClick` callback function when clicked.
+ *
+ * @param icon An optional icon to be displayed at the start of the preference item. If provided, it should be an `ImageVector` object.
+ * @param title An optional main title text displayed for the preference item.
+ * @param summary An optional secondary text displayed below the title for additional information about the preference.
+ * @param onClick A callback function that is called when the entire preference item is clicked. If no action is needed on click, this can be left empty.
+ */
+@Composable
+fun PreferenceItem(
+ icon: ImageVector? = null,
+ title: String? = null,
+ summary: String? = null,
+ onClick: () -> Unit = {}
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(16.dp))
+ .clickable(onClick = onClick), verticalAlignment = Alignment.CenterVertically
+ ) {
+ icon?.let {
+ Spacer(modifier = Modifier.width(16.dp))
+ Icon(it, contentDescription = null)
+ Spacer(modifier = Modifier.width(16.dp))
+ }
+ Column(
+ modifier = Modifier.padding(16.dp)
+ ) {
+ title?.let {
+ Text(text = it, style = MaterialTheme.typography.titleLarge)
+ }
+ summary?.let {
+ Text(text = it, style = MaterialTheme.typography.bodyMedium)
+ }
+ }
+ }
+}
+
+/**
+ * Creates a clickable preference item with a switch for app preference screens.
+ *
+ * This composable function combines an optional icon, title, optional summary, and a switch into a single row.
+ * The entire row is clickable and toggles the switch when clicked, calling the provided `onCheckedChange` callback function with the new state.
+ *
+ * @param icon An optional icon to be displayed at the start of the preference item. If provided, it should be an `ImageVector` object.
+ * @param title The main title text displayed for the preference item.
+ * @param summary An optional secondary text displayed below the title for additional information about the preference.
+ * @param checked The initial state of the switch. Set to true for on and false for off.
+ * @param onCheckedChange A callback function that is called whenever the switch is toggled. This function receives the new state of the switch (boolean) as a parameter.
+ */
+@Composable
+fun SwitchPreferenceItem(
+ icon: ImageVector? = null,
+ title: String,
+ summary: String? = null,
+ checked: Boolean,
+ onCheckedChange: (Boolean) -> Unit
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(16.dp))
+ .clickable(onClick = { onCheckedChange(!checked) }),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ icon?.let {
+ Spacer(modifier = Modifier.width(16.dp))
+ Icon(it, contentDescription = null)
+ Spacer(modifier = Modifier.width(16.dp))
+ }
+ Column(
+ modifier = Modifier
+ .padding(16.dp)
+ .weight(1f)
+ ) {
+ Text(text = title, style = MaterialTheme.typography.titleLarge)
+ summary?.let {
+ Text(text = it, style = MaterialTheme.typography.bodyMedium)
+ }
+ }
+ Switch(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ modifier = Modifier.padding(16.dp)
+ )
+ }
+}
+
+/**
+ * Creates a clickable preference item with a switch and a divider for app preference screens.
+ *
+ * This composable function combines an optional icon, title, summary, switch, and a divider into a single row.
+ * The entire row is clickable and triggers the provided `onClick` callback function when clicked.
+ * The switch is toggled on or off based on the `checked` parameter, and any change in its state calls
+ * the `onCheckedChange` callback with the new state.
+ *
+ * @param icon An optional icon to be displayed at the start of the preference item. If provided, it should be an `ImageVector` object.
+ * @param title The main title text displayed for the preference item.
+ * @param summary A secondary text displayed below the title for additional information about the preference.
+ * @param checked The initial state of the switch. Set to true for on and false for off.
+ * @param onCheckedChange A callback function that is called whenever the switch is toggled. This function receives the new state of the switch (boolean) as a parameter.
+ * @param onClick A callback function that is called when the entire preference item is clicked. If no action is needed on click, this can be left empty.
+ */
+@Composable
+fun SwitchPreferenceItemWithDivider(
+ icon: ImageVector? = null,
+ title: String,
+ summary: String,
+ checked: Boolean,
+ onCheckedChange: (Boolean) -> Unit, onClick: () -> Unit, onSwitchClick: (Boolean) -> Unit
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(16.dp))
+ .clickable(onClick = onClick), verticalAlignment = Alignment.CenterVertically
+ ) {
+ icon?.let {
+ Spacer(modifier = Modifier.width(16.dp))
+ Icon(it, contentDescription = null)
+ Spacer(modifier = Modifier.width(16.dp))
+ }
+ Column(
+ modifier = Modifier
+ .padding(16.dp)
+ .weight(1f)
+ ) {
+ Text(text = title, style = MaterialTheme.typography.titleLarge)
+ Text(text = summary, style = MaterialTheme.typography.bodyMedium)
+ }
+
+ VerticalDivider(
+ modifier = Modifier
+ .height(32.dp)
+ .align(Alignment.CenterVertically),
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f),
+ thickness = 1.dp
+ )
+ Switch(
+ checked = checked, onCheckedChange = { isChecked ->
+ onCheckedChange(isChecked)
+ onSwitchClick(isChecked)
+ },
+ modifier = Modifier.padding(16.dp)
+ )
+
+ }
+}
+
+enum class ButtonState { Pressed, Idle }
+
+@SuppressLint("ReturnFromAwaitPointerEventScope")
+@Composable
+fun Modifier.bounceClick() = composed {
+ var buttonState by remember { mutableStateOf(ButtonState.Idle) }
+ val scale by animateFloatAsState(
+ if (buttonState == ButtonState.Pressed) 0.95f else 1f, label = ""
+ )
+ this
+ .graphicsLayer {
+ scaleX = scale
+ scaleY = scale
+ }
+ .clickable(interactionSource = remember { MutableInteractionSource() },
+ indication = null,
+ onClick = { })
+ .pointerInput(buttonState) {
+ awaitPointerEventScope {
+ buttonState = if (buttonState == ButtonState.Pressed) {
+ waitForUpOrCancellation()
+ ButtonState.Idle
+ } else {
+ awaitFirstDown(false)
+ ButtonState.Pressed
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/utils/Utils.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/utils/Utils.kt
new file mode 100644
index 0000000..332ad3c
--- /dev/null
+++ b/app/src/main/kotlin/com/d4rk/cartcalculator/utils/Utils.kt
@@ -0,0 +1,122 @@
+package com.d4rk.cartcalculator.utils
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.provider.Settings
+import com.d4rk.cartcalculator.R
+
+/**
+ * A utility object for performing common operations such as opening URLs, activities, and app notification settings.
+ *
+ * This object provides functions to open a URL in the default browser, open an activity, and open the app's notification settings.
+ * All operations are performed in the context of an Android application.
+ */
+object Utils {
+
+ /**
+ * Opens a specified URL in the default browser.
+ *
+ * This function creates an Intent with the ACTION_VIEW action and the specified URL, and starts an activity with this intent.
+ * The activity runs in a new task.
+ *
+ * @param context The Android context in which the URL should be opened.
+ * @param url The URL to open.
+ */
+ fun openUrl(context: Context, url: String) {
+ Intent(Intent.ACTION_VIEW, Uri.parse(url)).let { intent ->
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(intent)
+ }
+ }
+
+ /**
+ * Opens a specified activity.
+ *
+ * This function creates an Intent with the specified activity class, and starts an activity with this intent. The activity runs in a new task.
+ *
+ * @param context The Android context in which the activity should be opened.
+ * @param activityClass The class of the activity to open.
+ */
+ fun openActivity(context: Context, activityClass: Class<*>) {
+ Intent(context, activityClass).let { intent ->
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(intent)
+ }
+ }
+
+ /**
+ * Opens the app's notification settings.
+ *
+ * This function creates an Intent with the ACTION_APP_NOTIFICATION_SETTINGS action and the app's package name, and starts an activity with this intent.
+ * The activity runs in a new task.
+ *
+ * @param context The Android context in which the app's notification settings should be opened.
+ */
+ fun openAppNotificationSettings(context: Context) {
+ val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
+ putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ context.startActivity(intent)
+ }
+
+ /**
+ * Opens the app locale settings if available, otherwise opens the app details settings.
+ *
+ * @param context The Android context.
+ */
+ fun openAppLocaleSettings(context: Context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ val localeIntent = Intent(Settings.ACTION_APP_LOCALE_SETTINGS).setData(
+ Uri.fromParts("package", context.packageName, null)
+ )
+ val detailsIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(
+ Uri.fromParts("package", context.packageName, null)
+ )
+ when {
+ context.packageManager.resolveActivity(
+ localeIntent,
+ 0
+ ) != null -> context.startActivity(localeIntent)
+
+ context.packageManager.resolveActivity(
+ detailsIntent,
+ 0
+ ) != null -> context.startActivity(detailsIntent)
+
+ else -> {
+ // TODO: Handle the case where neither Intent can be resolved
+ }
+ }
+ } else {
+ // TODO: Handle the case for Android versions lower than 13
+ }
+ }
+
+ /**
+ * Opens the app's share sheet.
+ *
+ * This function creates an Intent with the ACTION_SEND action and a share message containing the app's name and a link to the app's Play Store listing.
+ * The intent is then wrapped in a chooser Intent to allow the user to select an app to share the message with.
+ *
+ * @param context The Android context in which the share sheet should be opened.
+ */
+ fun shareApp(context: Context) {
+ val shareMessage = context.getString(
+ R.string.summary_share_message,
+ "https://play.google.com/store/apps/details?id=${context.packageName}"
+ )
+ val shareIntent = Intent().apply {
+ action = Intent.ACTION_SEND
+ putExtra(Intent.EXTRA_TEXT, shareMessage)
+ type = "text/plain"
+ }
+ context.startActivity(
+ Intent.createChooser(
+ shareIntent, context.resources.getText(R.string.send_email_using)
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/play/keys/com.d4rk.cartcalculator.jks b/app/src/main/play/keys/com.d4rk.cartcalculator.jks
new file mode 100644
index 0000000..f4f02f7
Binary files /dev/null and b/app/src/main/play/keys/com.d4rk.cartcalculator.jks differ
diff --git a/app/src/main/play/keys/com.d4rk.cartcalculator_private_key.pepk b/app/src/main/play/keys/com.d4rk.cartcalculator_private_key.pepk
new file mode 100644
index 0000000..2dfa7c6
Binary files /dev/null and b/app/src/main/play/keys/com.d4rk.cartcalculator_private_key.pepk differ
diff --git a/app/src/main/play/listings/en-US/full-description.txt b/app/src/main/play/listings/en-US/full-description.txt
index 89df01f..6761d4c 100644
--- a/app/src/main/play/listings/en-US/full-description.txt
+++ b/app/src/main/play/listings/en-US/full-description.txt
@@ -1,11 +1,3 @@
-🛒 Cart Calculator 🛒
-
-╔╦╦╦═╦╗╔═╦═╦══╦═╗
-║║║║╩╣╚╣═╣║║║║║╩╣
-╚══╩═╩═╩═╩═╩╩╩╩═╝
-
-Let's go to shopping! 🛒
-
The Cart Calculator app is a convenient and simple tool that helps you to manage your shopping cart easily and efficiently. Whether you are grocery shopping or just buying household items, this app helps you to keep track of everything in one place.
With the Cart Calculator app, you can add items to the cart, set the quantity, and calculate the total cost of your purchase. You can also update the quantity of each item and see the updated total cost in real-time. The app is user-friendly and easy to navigate, making it a perfect choice for people of all ages.
@@ -18,50 +10,23 @@ Overall, the Cart Calculator app is an essential tool for anyone who wants to ma
Our app is designed to be simple and easy to use, while also being fast and lightweight. Plus, it's free and open-source software!
-⚠ Opening Issues!
-Bugs can be reported here: https://github.com/D4rK7355608/com.d4rk.cartcalculator/issues
-
-Create a calculator/currency/general bug. 🐞
+Features
+• Calculate the total amount of your cart
+• Add or remove items from cart
+• Create multiple carts
-🛠️ Features!
-⭐️ No internet required.
-⭐️ Adaptive themes + Material-You.
-⭐️ Simple and easy to use.
-⭐️ Fast and lightweight.
-⭐️ Free Open source & secure.
+Benefits
+• Efficient shopping
+• Budget management
+• Expense tracking
-👨💻 More about me!
-● Music:
-⇨ YouTube Music: https://music.youtube.com/channel/UCb2zXzO03OM7U9xeIsqqEQw
-⇨ Spotify: https://open.spotify.com/artist/5Q58DBSe2tpBb3qqq9WVfo
-⇨ Deezer: https://www.deezer.com/us/artist/408659
-⇨ SoundCloud: https://soundcloud.com/d4rk7355608
-⇨ BandLab: https://www.bandlab.com/d4rk7355608
+How it works
+Cart Calculator streamlines your shopping experience by allowing you to effortlessly manage your shopping cart. It provides a real-time calculation of your total purchase cost as you add or update item quantities. This user-friendly app is designed for shoppers of all ages, ensuring that managing your cart is a breeze.
-● Graphics:
-⇨ DeviantArt: https://www.deviantart.com/d4rk7355608
+Get started today
+Embark on a smarter shopping journey with Cart Calculator. Download it from the Google Play Store now and take the first step towards efficient shopping management. It’s free, open-source, and incredibly easy to use—perfect for enhancing your shopping efficiency. Enjoy a seamless shopping experience today!
-● Social:
-⇨ GameJolt: https://gamejolt.com/@D4rK7355608
-⇨ Twitter: https://twitter.com/D4rK7355608
-⇨ Skype: d4rk7355608
-⇨ Steam Profile: https://steamcommunity.com/id/d4rk7355608
-⇨ Steam Trade Link: https://steamcommunity.com/tradeoffer/new/?partner=892981294&token=pxsUtrm3
-
-● Developer stuff:
-⇨ GitHub: https://github.com/D4rK7355608
-⇨ Google Play Store: https://play.google.com/store/apps/dev?id=5390214922640123642
-⇨ Google Developers: https://g.dev/D4rK7355608
-
-🛑 Disclaimer!
-• Only use the GitHub Issues section if you discover issues with the code itself. Do not mistake the Issues page as a help desk. For support, information and requests, please contact d4rk7355608@gmail.com.
-
-💬 Feedback!
+Feedback
We are constantly updating and improving Cart Calculator to give you the best possible experience. If you have any suggested features or improvements, please leave a review. In case something is not working correctly please let me know. When posting a low rating please describe what is wrong to give the possibility to fix that issue.
-Thank you for choosing Cart Calculator. We hope you enjoy using our app as much as we enjoyed creating it for you! Rate us 5 stars ⭐⭐⭐⭐⭐ if you are happy with the app! ❤
-
-╔╦╦══╦══╦═╦╗
-║╔╣║║╠╗╔╣═╣║
-║║║╔╗║║║║═╬╣
-╚╝╚╝╚╝╚╝╚═╩╝
\ No newline at end of file
+Thank you for choosing Cart Calculator! We hope you enjoy using our app as much as we enjoyed creating it for you!
\ No newline at end of file
diff --git a/app/src/main/play/listings/en-US/graphics/feature-graphic/feature-graphic.png b/app/src/main/play/listings/en-US/graphics/feature-graphic/feature-graphic.png
deleted file mode 100644
index e6e29e4..0000000
Binary files a/app/src/main/play/listings/en-US/graphics/feature-graphic/feature-graphic.png and /dev/null differ
diff --git a/app/src/main/play/listings/en-US/graphics/feature-graphic/play_store_feature_graphic.png b/app/src/main/play/listings/en-US/graphics/feature-graphic/play_store_feature_graphic.png
new file mode 100644
index 0000000..b519725
Binary files /dev/null and b/app/src/main/play/listings/en-US/graphics/feature-graphic/play_store_feature_graphic.png differ
diff --git a/app/src/main/res/drawable-anydpi/ic_about.xml b/app/src/main/res/drawable-anydpi/ic_about.xml
deleted file mode 100644
index 282ec72..0000000
--- a/app/src/main/res/drawable-anydpi/ic_about.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_about_filled.xml b/app/src/main/res/drawable-anydpi/ic_about_filled.xml
deleted file mode 100644
index 88a0fa8..0000000
--- a/app/src/main/res/drawable-anydpi/ic_about_filled.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_add_circle_outline.xml b/app/src/main/res/drawable-anydpi/ic_add_circle_outline.xml
deleted file mode 100644
index 5a83988..0000000
--- a/app/src/main/res/drawable-anydpi/ic_add_circle_outline.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_add_to_cart.xml b/app/src/main/res/drawable-anydpi/ic_add_to_cart.xml
deleted file mode 100644
index c9ddc87..0000000
--- a/app/src/main/res/drawable-anydpi/ic_add_to_cart.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_album.xml b/app/src/main/res/drawable-anydpi/ic_album.xml
deleted file mode 100644
index 8169d8c..0000000
--- a/app/src/main/res/drawable-anydpi/ic_album.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_appearance_tinted.xml b/app/src/main/res/drawable-anydpi/ic_appearance_tinted.xml
deleted file mode 100644
index 6f8abc1..0000000
--- a/app/src/main/res/drawable-anydpi/ic_appearance_tinted.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_build_tinted.xml b/app/src/main/res/drawable-anydpi/ic_build_tinted.xml
deleted file mode 100644
index cf87f5e..0000000
--- a/app/src/main/res/drawable-anydpi/ic_build_tinted.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_changelog.xml b/app/src/main/res/drawable-anydpi/ic_changelog.xml
deleted file mode 100644
index 9e93a8b..0000000
--- a/app/src/main/res/drawable-anydpi/ic_changelog.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_check_circle.xml b/app/src/main/res/drawable-anydpi/ic_check_circle.xml
deleted file mode 100644
index b70e50b..0000000
--- a/app/src/main/res/drawable-anydpi/ic_check_circle.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_code_of_conduct.xml b/app/src/main/res/drawable-anydpi/ic_code_of_conduct.xml
deleted file mode 100644
index 3f0f115..0000000
--- a/app/src/main/res/drawable-anydpi/ic_code_of_conduct.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_data_object.xml b/app/src/main/res/drawable-anydpi/ic_data_object.xml
deleted file mode 100644
index ac8e669..0000000
--- a/app/src/main/res/drawable-anydpi/ic_data_object.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_edit_notifications.xml b/app/src/main/res/drawable-anydpi/ic_edit_notifications.xml
deleted file mode 100644
index 30c7973..0000000
--- a/app/src/main/res/drawable-anydpi/ic_edit_notifications.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_experimental.xml b/app/src/main/res/drawable-anydpi/ic_experimental.xml
deleted file mode 100644
index 15d0137..0000000
--- a/app/src/main/res/drawable-anydpi/ic_experimental.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_feedback.xml b/app/src/main/res/drawable-anydpi/ic_feedback.xml
deleted file mode 100644
index 3aca1ff..0000000
--- a/app/src/main/res/drawable-anydpi/ic_feedback.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_firebase.xml b/app/src/main/res/drawable-anydpi/ic_firebase.xml
deleted file mode 100644
index a003dbd..0000000
--- a/app/src/main/res/drawable-anydpi/ic_firebase.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_github.xml b/app/src/main/res/drawable-anydpi/ic_github.xml
deleted file mode 100644
index a400926..0000000
--- a/app/src/main/res/drawable-anydpi/ic_github.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_gmail.xml b/app/src/main/res/drawable-anydpi/ic_gmail.xml
deleted file mode 100644
index 82e0754..0000000
--- a/app/src/main/res/drawable-anydpi/ic_gmail.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_google_dev.xml b/app/src/main/res/drawable-anydpi/ic_google_dev.xml
deleted file mode 100644
index 27b4144..0000000
--- a/app/src/main/res/drawable-anydpi/ic_google_dev.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_help.xml b/app/src/main/res/drawable-anydpi/ic_help.xml
deleted file mode 100644
index 7876b10..0000000
--- a/app/src/main/res/drawable-anydpi/ic_help.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_help_center.xml b/app/src/main/res/drawable-anydpi/ic_help_center.xml
deleted file mode 100644
index b4917d0..0000000
--- a/app/src/main/res/drawable-anydpi/ic_help_center.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_help_selected.xml b/app/src/main/res/drawable-anydpi/ic_help_selected.xml
deleted file mode 100644
index 0dbcecc..0000000
--- a/app/src/main/res/drawable-anydpi/ic_help_selected.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_home.xml b/app/src/main/res/drawable-anydpi/ic_home.xml
deleted file mode 100644
index 0d938ac..0000000
--- a/app/src/main/res/drawable-anydpi/ic_home.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_home_filled.xml b/app/src/main/res/drawable-anydpi/ic_home_filled.xml
deleted file mode 100644
index 5a70d9c..0000000
--- a/app/src/main/res/drawable-anydpi/ic_home_filled.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_language.xml b/app/src/main/res/drawable-anydpi/ic_language.xml
deleted file mode 100644
index 806a18a..0000000
--- a/app/src/main/res/drawable-anydpi/ic_language.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_launcher_foreground.xml b/app/src/main/res/drawable-anydpi/ic_launcher_foreground.xml
index 859cf76..a8484eb 100644
--- a/app/src/main/res/drawable-anydpi/ic_launcher_foreground.xml
+++ b/app/src/main/res/drawable-anydpi/ic_launcher_foreground.xml
@@ -1,6 +1,20 @@
-
-
-
-
-
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_libraries.xml b/app/src/main/res/drawable-anydpi/ic_libraries.xml
deleted file mode 100644
index 5592a4d..0000000
--- a/app/src/main/res/drawable-anydpi/ic_libraries.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_license.xml b/app/src/main/res/drawable-anydpi/ic_license.xml
deleted file mode 100644
index 5554ff3..0000000
--- a/app/src/main/res/drawable-anydpi/ic_license.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_more.xml b/app/src/main/res/drawable-anydpi/ic_more.xml
deleted file mode 100644
index d602921..0000000
--- a/app/src/main/res/drawable-anydpi/ic_more.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_notification.xml b/app/src/main/res/drawable-anydpi/ic_notification.xml
deleted file mode 100644
index b0db147..0000000
--- a/app/src/main/res/drawable-anydpi/ic_notification.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_notification_important.xml b/app/src/main/res/drawable-anydpi/ic_notification_important.xml
index b0db147..e86870c 100644
--- a/app/src/main/res/drawable-anydpi/ic_notification_important.xml
+++ b/app/src/main/res/drawable-anydpi/ic_notification_important.xml
@@ -1,3 +1,9 @@
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_notification_update.xml b/app/src/main/res/drawable-anydpi/ic_notification_update.xml
index 522a6de..4905dbe 100644
--- a/app/src/main/res/drawable-anydpi/ic_notification_update.xml
+++ b/app/src/main/res/drawable-anydpi/ic_notification_update.xml
@@ -1,3 +1,9 @@
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_notifications_tinted.xml b/app/src/main/res/drawable-anydpi/ic_notifications_tinted.xml
deleted file mode 100644
index d286d11..0000000
--- a/app/src/main/res/drawable-anydpi/ic_notifications_tinted.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_perm_device_information.xml b/app/src/main/res/drawable-anydpi/ic_perm_device_information.xml
deleted file mode 100644
index a7662c2..0000000
--- a/app/src/main/res/drawable-anydpi/ic_perm_device_information.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_permissions.xml b/app/src/main/res/drawable-anydpi/ic_permissions.xml
deleted file mode 100644
index 8814183..0000000
--- a/app/src/main/res/drawable-anydpi/ic_permissions.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_play_store_tinted.xml b/app/src/main/res/drawable-anydpi/ic_play_store_tinted.xml
deleted file mode 100644
index 28bc505..0000000
--- a/app/src/main/res/drawable-anydpi/ic_play_store_tinted.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_privacy_policy.xml b/app/src/main/res/drawable-anydpi/ic_privacy_policy.xml
deleted file mode 100644
index c987356..0000000
--- a/app/src/main/res/drawable-anydpi/ic_privacy_policy.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_refresh.xml b/app/src/main/res/drawable-anydpi/ic_refresh.xml
deleted file mode 100644
index 1a18622..0000000
--- a/app/src/main/res/drawable-anydpi/ic_refresh.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_remove_circle_outline.xml b/app/src/main/res/drawable-anydpi/ic_remove_circle_outline.xml
deleted file mode 100644
index 9835cd9..0000000
--- a/app/src/main/res/drawable-anydpi/ic_remove_circle_outline.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_safety_check_tinted.xml b/app/src/main/res/drawable-anydpi/ic_safety_check_tinted.xml
deleted file mode 100644
index 0e5c6ad..0000000
--- a/app/src/main/res/drawable-anydpi/ic_safety_check_tinted.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_settings.xml b/app/src/main/res/drawable-anydpi/ic_settings.xml
deleted file mode 100644
index d39abee..0000000
--- a/app/src/main/res/drawable-anydpi/ic_settings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_settings_filled.xml b/app/src/main/res/drawable-anydpi/ic_settings_filled.xml
deleted file mode 100644
index 1bdb8cd..0000000
--- a/app/src/main/res/drawable-anydpi/ic_settings_filled.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_share.xml b/app/src/main/res/drawable-anydpi/ic_share.xml
deleted file mode 100644
index 8836402..0000000
--- a/app/src/main/res/drawable-anydpi/ic_share.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_shop.xml b/app/src/main/res/drawable-anydpi/ic_shop.xml
deleted file mode 100644
index 92e1126..0000000
--- a/app/src/main/res/drawable-anydpi/ic_shop.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_shopping_bag.xml b/app/src/main/res/drawable-anydpi/ic_shopping_bag.xml
deleted file mode 100644
index 82dd520..0000000
--- a/app/src/main/res/drawable-anydpi/ic_shopping_bag.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_shopping_cart_alert.xml b/app/src/main/res/drawable-anydpi/ic_shopping_cart_alert.xml
deleted file mode 100644
index 363936d..0000000
--- a/app/src/main/res/drawable-anydpi/ic_shopping_cart_alert.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_shortcut_settings_foreground.xml b/app/src/main/res/drawable-anydpi/ic_shortcut_settings_foreground.xml
index 1f3de29..8869dc4 100644
--- a/app/src/main/res/drawable-anydpi/ic_shortcut_settings_foreground.xml
+++ b/app/src/main/res/drawable-anydpi/ic_shortcut_settings_foreground.xml
@@ -1,4 +1,15 @@
-
-
-
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_support.xml b/app/src/main/res/drawable-anydpi/ic_support.xml
deleted file mode 100644
index 791f4cd..0000000
--- a/app/src/main/res/drawable-anydpi/ic_support.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_terms_of_service.xml b/app/src/main/res/drawable-anydpi/ic_terms_of_service.xml
deleted file mode 100644
index 84830de..0000000
--- a/app/src/main/res/drawable-anydpi/ic_terms_of_service.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_theme.xml b/app/src/main/res/drawable-anydpi/ic_theme.xml
deleted file mode 100644
index 37856ca..0000000
--- a/app/src/main/res/drawable-anydpi/ic_theme.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_twitter.xml b/app/src/main/res/drawable-anydpi/ic_twitter.xml
deleted file mode 100644
index f966ece..0000000
--- a/app/src/main/res/drawable-anydpi/ic_twitter.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_update.xml b/app/src/main/res/drawable-anydpi/ic_update.xml
deleted file mode 100644
index a8c50a0..0000000
--- a/app/src/main/res/drawable-anydpi/ic_update.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_xda.xml b/app/src/main/res/drawable-anydpi/ic_xda.xml
deleted file mode 100644
index 6e1322c..0000000
--- a/app/src/main/res/drawable-anydpi/ic_xda.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_youtube.xml b/app/src/main/res/drawable-anydpi/ic_youtube.xml
deleted file mode 100644
index 15dd284..0000000
--- a/app/src/main/res/drawable-anydpi/ic_youtube.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/il_about.xml b/app/src/main/res/drawable-anydpi/il_about.xml
deleted file mode 100644
index fa1ae15..0000000
--- a/app/src/main/res/drawable-anydpi/il_about.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/il_drawer.xml b/app/src/main/res/drawable-anydpi/il_drawer.xml
deleted file mode 100644
index 4c614dc..0000000
--- a/app/src/main/res/drawable-anydpi/il_drawer.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/il_startup.xml b/app/src/main/res/drawable-anydpi/il_startup.xml
index ea676c7..9af2bb5 100644
--- a/app/src/main/res/drawable-anydpi/il_startup.xml
+++ b/app/src/main/res/drawable-anydpi/il_startup.xml
@@ -1,38 +1,122 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/nav_about.xml b/app/src/main/res/drawable-anydpi/nav_about.xml
deleted file mode 100644
index f0249dd..0000000
--- a/app/src/main/res/drawable-anydpi/nav_about.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/nav_help.xml b/app/src/main/res/drawable-anydpi/nav_help.xml
deleted file mode 100644
index 8a14de8..0000000
--- a/app/src/main/res/drawable-anydpi/nav_help.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/nav_home.xml b/app/src/main/res/drawable-anydpi/nav_home.xml
deleted file mode 100644
index 4bf24e6..0000000
--- a/app/src/main/res/drawable-anydpi/nav_home.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/nav_settings.xml b/app/src/main/res/drawable-anydpi/nav_settings.xml
deleted file mode 100644
index afa67d9..0000000
--- a/app/src/main/res/drawable-anydpi/nav_settings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-xhdpi/tv_banner.png b/app/src/main/res/drawable-xhdpi/tv_banner.png
new file mode 100644
index 0000000..20cecc1
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/tv_banner.png differ
diff --git a/app/src/main/res/font/font_poppins.ttf b/app/src/main/res/font/font_poppins.ttf
deleted file mode 100644
index 6bcdcc2..0000000
Binary files a/app/src/main/res/font/font_poppins.ttf and /dev/null differ
diff --git a/app/src/main/res/layout/activity_help.xml b/app/src/main/res/layout/activity_help.xml
deleted file mode 100644
index 83fd5c9..0000000
--- a/app/src/main/res/layout/activity_help.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
deleted file mode 100644
index 027c4ea..0000000
--- a/app/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_permissions.xml b/app/src/main/res/layout/activity_permissions.xml
deleted file mode 100644
index 3500838..0000000
--- a/app/src/main/res/layout/activity_permissions.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml
deleted file mode 100644
index cab1029..0000000
--- a/app/src/main/res/layout/activity_settings.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_startup.xml b/app/src/main/res/layout/activity_startup.xml
deleted file mode 100644
index 8190b2d..0000000
--- a/app/src/main/res/layout/activity_startup.xml
+++ /dev/null
@@ -1,75 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/app_bar_main.xml b/app/src/main/res/layout/app_bar_main.xml
deleted file mode 100644
index 3a638a8..0000000
--- a/app/src/main/res/layout/app_bar_main.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml
deleted file mode 100644
index f749f03..0000000
--- a/app/src/main/res/layout/content_main.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_add_to_cart.xml b/app/src/main/res/layout/dialog_add_to_cart.xml
deleted file mode 100644
index 3617389..0000000
--- a/app/src/main/res/layout/dialog_add_to_cart.xml
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml
deleted file mode 100644
index f3e19b6..0000000
--- a/app/src/main/res/layout/fragment_about.xml
+++ /dev/null
@@ -1,206 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
deleted file mode 100644
index cea0465..0000000
--- a/app/src/main/res/layout/fragment_home.xml
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_list_cart.xml b/app/src/main/res/layout/item_list_cart.xml
deleted file mode 100644
index e4feb9a..0000000
--- a/app/src/main/res/layout/item_list_cart.xml
+++ /dev/null
@@ -1,74 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/nav_header_main.xml b/app/src/main/res/layout/nav_header_main.xml
deleted file mode 100644
index 38aed60..0000000
--- a/app/src/main/res/layout/nav_header_main.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/preference_switch_widget_layout.xml b/app/src/main/res/layout/preference_switch_widget_layout.xml
deleted file mode 100644
index 166055b..0000000
--- a/app/src/main/res/layout/preference_switch_widget_layout.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
\ No newline at end of file
diff --git a/app/src/main/res/menu/activity_main_drawer.xml b/app/src/main/res/menu/activity_main_drawer.xml
deleted file mode 100644
index 87e3626..0000000
--- a/app/src/main/res/menu/activity_main_drawer.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_feedback.xml b/app/src/main/res/menu/menu_feedback.xml
deleted file mode 100644
index cd565e2..0000000
--- a/app/src/main/res/menu/menu_feedback.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
deleted file mode 100644
index 7b15671..0000000
--- a/app/src/main/res/menu/menu_main.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
index 27e87f2..90d9271 100644
--- a/app/src/main/res/mipmap-anydpi/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -1,5 +1,5 @@
-
-
-
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/ic_shortcut_settings.xml b/app/src/main/res/mipmap-anydpi/ic_shortcut_settings.xml
index afef044..a06c741 100644
--- a/app/src/main/res/mipmap-anydpi/ic_shortcut_settings.xml
+++ b/app/src/main/res/mipmap-anydpi/ic_shortcut_settings.xml
@@ -1,4 +1,4 @@
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml
deleted file mode 100644
index a837223..0000000
--- a/app/src/main/res/navigation/mobile_navigation.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/raw/anim_about.lottie b/app/src/main/res/raw/anim_about.lottie
deleted file mode 100644
index 1b06406..0000000
Binary files a/app/src/main/res/raw/anim_about.lottie and /dev/null differ
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index a124c1e..119f373 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -1,5 +1,6 @@
- Verwalten und berechnen Sie Ihre Warenkorbartikel effizient mit Cart Calculator. 🛒 Startseite
+ Verwalten und berechnen Sie Ihre Warenkorbartikel effizient mit Cart Calculator. 🛒
+ Startseite
Aktualisieren
In den Warenkorb
Artikel hinzufügen
diff --git a/app/src/main/res/values-night-v27/themes.xml b/app/src/main/res/values-night-v27/themes.xml
index 9aba09d..3bb105a 100644
--- a/app/src/main/res/values-night-v27/themes.xml
+++ b/app/src/main/res/values-night-v27/themes.xml
@@ -1,4 +1,5 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night-v28/themes.xml b/app/src/main/res/values-night-v28/themes.xml
index b535341..a3d747c 100644
--- a/app/src/main/res/values-night-v28/themes.xml
+++ b/app/src/main/res/values-night-v28/themes.xml
@@ -1,4 +1,5 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index 5f95506..8304ed5 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -1,4 +1,5 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index 43e804b..b031821 100644
--- a/app/src/main/res/values-ro/strings.xml
+++ b/app/src/main/res/values-ro/strings.xml
@@ -1,5 +1,6 @@
- Gestionați și calculați eficient articolele din coșul dvs. cu Cart Calculator. 🛒 Acasă
+ Gestionați și calculați eficient articolele din coșul dvs. cu Cart Calculator. 🛒
+ Acasă
Reînnoiește
Adaugă în coș
Adaugă elementul tău
diff --git a/app/src/main/res/values-v27/themes.xml b/app/src/main/res/values-v27/themes.xml
index bb285e3..8597a9b 100644
--- a/app/src/main/res/values-v27/themes.xml
+++ b/app/src/main/res/values-v27/themes.xml
@@ -1,4 +1,5 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/values-v28/themes.xml b/app/src/main/res/values-v28/themes.xml
index 145c7ca..31e4a69 100644
--- a/app/src/main/res/values-v28/themes.xml
+++ b/app/src/main/res/values-v28/themes.xml
@@ -1,4 +1,5 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/values-v31/themes.xml b/app/src/main/res/values-v31/themes.xml
deleted file mode 100644
index a0ac73c..0000000
--- a/app/src/main/res/values-v31/themes.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index dd91a2d..41b18df 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -1,16 +1,13 @@
-
- - @string/follow_system
- - @string/light_mode
- - @string/dark_mode
- - @string/auto_battery_mode
-
-
- - MODE_NIGHT_FOLLOW_SYSTEM
- - MODE_NIGHT_NO
- - MODE_NIGHT_YES
- - MODE_NIGHT_AUTO_BATTERY
+
+ - CNY ¥
+ - EUR €
+ - GBP £
+ - JPY ¥
+ - RON
+ - USD $
+
- @string/bulgarian
- @string/english
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
deleted file mode 100644
index 6ffe31c..0000000
--- a/app/src/main/res/values/attrs.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml
deleted file mode 100644
index 5ec9553..0000000
--- a/app/src/main/res/values/keys.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
- theme
- MODE_NIGHT_FOLLOW_SYSTEM
- language
- en
- notifications_settings
- firebase
- changelog
- open_source_licenses
- share
- device_info
- feedback
-
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0315bac..e4834fe 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -5,29 +5,15 @@
Welcome
Browse the Terms of Service and Privacy Policy
Agree
- Home
- Refresh
- Add to Cart
Add your item
- Name
- Price
Quantity
- Total:\n%1$f $
Settings
Appearance
- Theme
Follow System Mode
Light Mode
Dark Mode
- Auto Battery Dark Mode
Language
Advanced
- Firebase
- App updates
- Check for updates…
- Subscribe to beta updates
- Changelog
- Privacy and security
Privacy Policy
Terms of Service
Code of Conduct
@@ -35,13 +21,9 @@
Normal
Runtime
License
- More
Share
Share using…
Try it now!!!
- More apps…
- Support me
- Info
Device Info
App Build: Release\n%1$s\n%2$s\n%3$s\n%4$s\n%5$s
Manufacturer:
@@ -50,15 +32,8 @@
API Level:
Help & feedback
Help
- Email developer 📧
FAQ
- Send feedback
About
- Version %1$s (%2$d)
- Music
- Made with ❤ in Romania!
- © Copyright %1$s, made by D4rK!
- Copied to clipboard!
Close?
Internet [INTERNET]
Ad id [AD_ID]
@@ -96,21 +71,93 @@
Allows the app to verify its compliance with the license agreement and enforce licensing terms to protect intellectual property.
Allows the app to access and modify the device\'s notification policy, controlling how and when notifications are displayed to the user and providing custom notification management features.
Allows the app to create and use services that run in the foreground, giving them priority over other background processes and improving performance and reliability.
- This let us collect information about app performance and crash logs.
- Click here if you want to support me without paying money. Just some time for viewing an ad.
- Set application language.
+
Let\'s manage your cart items efficiently!
A new version of the app is available. Click to update!
Read and agree to our Terms of Service and Privacy Policy to continue.
- Your shopping cart is empty.
+ Your shopping cart is empty
Are you sure you want to exit?
Unable to open Google Play Store.
Thanks for rating us!
- Enter name
- Enter price
+ Item name
+ Enter item name
+ Item price
+ Enter item price
Enter quantity
Accept the terms and conditions.
Subtract 1 item from the cart
Add 1 item to the cart
- Open me 🌐
+
+
+ Would you like to support our app? ❤️
+ The update process encountered an issue
+ The app has been successfully updated
+ Please select a valid file size
+ Try again
+ Updates
+ Check out this amazing app that I\'m using! It has some really cool features that you might find interesting. You can download it from the Play Store at: %1$s
+ Display
+ Dark theme
+ Will never turn on automatically
+ Will turn on automatically by the system
+ Changes the language used in the app
+
+ View the policy that governs how we handle your data
+ Review the terms you agree to when using our service
+ Understand the rules and guidelines for behavior within our service
+ Manage the permissions granted to our service
+ Share data to help improve Cart Calculator
+ View legal information about our service
+
+ Cart settings
+ Currency
+ Set your preferred currency for displaying prices
+ Manage how your cart behaves
+ This currency selection will be utilized across the platform for displaying product prices, processing transactions, and providing a more personalized shopping experience
+ To ensure a seamless checkout experience, please provide details about the items you\'re adding to your cart.
+ Select your preferred currency:
+ Personalize your app\'s look and feel
+ Experience enhanced visuals and deeper backgrounds
+ App info
+ Application build version
+ License details for open source software
+ Manage app notifications
+ Explore more advanced settings
+ Manage your privacy settings
+ Learn more about Cart Calculator
+ In-app ads
+ Change ad settings
+ AMOLED mode
+ Error reporting
+ Bug report
+ Dark theme uses a deep background to help keep your battery alive longer.
+ Dynamic colors
+ Apply colors from wallpapers to the app theme
+ Send bug reports and feature requests to the app\'s GitHub repository issues page
+ Ads
+ Display ads
+ Personalized ads
+ Turn on personalized ads
+ See ads that are relevant to you. Manage the information used to show personalized ads based on your app activity. You can always turn off personalization here.\n
+ Security & privacy
+ Learn more
+ Help improve your experience by automatically sending diagnostic, device, and app usage data to us. This will help improve app performance, stability, and other enhancements. Some aggregate data will also help our apps.\n\nThis is general information about your device and how you use our apps (such as battery level, system and app activity, and errors). The data will be used to improve our apps\n\nTurning off this feature doesn\'t affect your device\'s ability to send the information needed for essential services such as app updates and security.\n
+ Billing [BILLING]
+ Allows the app to use the Google Play Billing Library to handle in-app purchases and donations
+ Usage and diagnostics
+ Privacy
+ Legal
+ Legal notices
+ View in Google Play Store
+ Version info
+ Version %1$s
+ Beta program
+ Add new cart
+ Shopping cart
+ Support Us
+ Paid Support
+ Non-Paid Support
+ Web Ad
+ No matter how much you donate, you will help us keep our app running and improve our features. We appreciate your generosity and kindness!
+ We\'re committed to creating a personalized shopping experience for everyone. To make it easier for you to keep track of your selections and enhance your shopping journey, consider giving your cart a name before adding items.
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 8963ff2..d74646f 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,13 +1,5 @@
-
-
+
-
+ -
+ @style/ThemeOverlay.Material3.MaterialAlertDialog.Centered
+
+ -
+ @style/ThemeOverlay.Material3.MaterialAlertDialog.Centered
+
+ - @style/ThemeOverlay.Material3.MaterialAlertDialog.Centered
+
+ - @style/ThemeOverlay.Material3.MaterialAlertDialog.Centered
+
+
+
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values/untranslatable_strings.xml b/app/src/main/res/values/untranslatable_strings.xml
index 99e9eae..285d463 100644
--- a/app/src/main/res/values/untranslatable_strings.xml
+++ b/app/src/main/res/values/untranslatable_strings.xml
@@ -1,10 +1,5 @@
Cart Calculator
- Twitter
- YouTube
- GitHub
- XDA
- Google Dev
Arch:
Български
English
@@ -21,11 +16,11 @@
Español
Türkçe
Українська
- 0.00 $
General Public License-3.0
Feedback for
Dear developer,
Send mail using:
+ Copyright ©2023-2024, D4rK
Changelog - Version %1s
\n
• Made various under-the-hood improvements for a better overall app experience.
diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml
index d4228c1..db9a3cd 100644
--- a/app/src/main/res/xml/locales_config.xml
+++ b/app/src/main/res/xml/locales_config.xml
@@ -1,19 +1,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/preferences_faq.xml b/app/src/main/res/xml/preferences_faq.xml
deleted file mode 100644
index e309021..0000000
--- a/app/src/main/res/xml/preferences_faq.xml
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/xml/preferences_feedback.xml b/app/src/main/res/xml/preferences_feedback.xml
deleted file mode 100644
index b3724d1..0000000
--- a/app/src/main/res/xml/preferences_feedback.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/xml/preferences_permissions.xml b/app/src/main/res/xml/preferences_permissions.xml
deleted file mode 100644
index 88351f4..0000000
--- a/app/src/main/res/xml/preferences_permissions.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/xml/preferences_settings.xml b/app/src/main/res/xml/preferences_settings.xml
deleted file mode 100644
index deeecfe..0000000
--- a/app/src/main/res/xml/preferences_settings.xml
+++ /dev/null
@@ -1,140 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml
index 1a4c4b5..3273294 100644
--- a/app/src/main/res/xml/shortcuts.xml
+++ b/app/src/main/res/xml/shortcuts.xml
@@ -1,12 +1,12 @@
+ android:targetClass="com.d4rk.cartcalculator.ui.settings.SettingsActivity"
+ android:targetPackage="com.d4rk.cartcalculator" />
\ No newline at end of file
diff --git a/app/src/test/kotlin/com/d4rk/cartcalculator/ExampleUnitTest.kt b/app/src/test/kotlin/com/d4rk/cartcalculator/ExampleUnitTest.kt
index 070c9ed..4bfcc05 100644
--- a/app/src/test/kotlin/com/d4rk/cartcalculator/ExampleUnitTest.kt
+++ b/app/src/test/kotlin/com/d4rk/cartcalculator/ExampleUnitTest.kt
@@ -1,9 +1,8 @@
package com.d4rk.cartcalculator
+import org.junit.Assert.assertEquals
import org.junit.Test
-import org.junit.Assert.*
-
/**
* Example local unit test, which will execute on the development machine (host).
*
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index 42f1323..0000000
--- a/build.gradle
+++ /dev/null
@@ -1,8 +0,0 @@
-plugins {
- id("com.android.application") version "8.2.2" apply false
- id("com.android.library") version "8.2.2" apply false
- id("org.jetbrains.kotlin.android") version "1.9.22" apply false
- id("com.google.gms.google-services") version "4.4.0" apply false
- id 'com.google.android.gms.oss-licenses-plugin' version '0.10.6' apply false
- id 'com.google.firebase.crashlytics' version '2.9.2' apply false
-}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..d5aa4d2
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,9 @@
+plugins {
+ alias(libs.plugins.androidApplication) apply false
+ alias(libs.plugins.androidLibrary) apply false
+ alias(libs.plugins.jetbrainsKotlinAndroid) apply false
+ alias(libs.plugins.googlePlayServices) apply false
+ alias(libs.plugins.devToolsKsp) apply false
+ alias(libs.plugins.googleOssServices) apply false
+ alias(libs.plugins.googleFirebase) apply false
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..b90cfbd
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,83 @@
+[versions]
+agp = "8.4.0"
+appUpdateKtx = "2.1.0"
+composeBom = "2024.05.00"
+coreKtx = "1.13.1"
+billing = "7.0.0"
+datastoreCore = "1.1.1"
+lifecycleCommonJava8 = "2.8.0"
+lifecycleLivedataKtx = "2.8.0"
+lifecycleProcess = "2.8.0"
+lifecycleRuntimeKtx = "2.8.0"
+lifecycleViewmodelKtx = "2.8.0"
+multidex = "2.0.1"
+workRuntimeKtx = "2.9.0"
+coreSplashscreen = "1.0.1"
+firebaseBom = "32.8.1"
+kotlin = "1.9.23"
+junit = "4.13.2"
+activityCompose = "1.9.0"
+junitVersion = "1.1.5"
+navigationCompose = "2.7.7"
+constraintlayoutCompose = "1.0.1"
+espressoCore = "3.5.1"
+composeUi = "1.6.7"
+google-services = "4.4.1"
+google-oss-services = "0.10.6"
+google-firebase-crashlytics = "2.9.9"
+google-devtools-ksp = "1.9.23-1.0.20"
+roomKtx = "2.6.1"
+playServicesAds = "23.1.0"
+playServicesOssLicenses = "17.0.1"
+reviewKtx = "2.0.1"
+
+[libraries]
+androidx-animation-core = { module = "androidx.compose.animation:animation-core" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
+androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
+androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
+androidx-constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayoutCompose" }
+androidx-datastore-core = { module = "androidx.datastore:datastore-core", version.ref = "datastoreCore" }
+androidx-foundation = { module = "androidx.compose.foundation:foundation" }
+androidx-lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "lifecycleCommonJava8" }
+androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtx" }
+androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycleProcess" }
+androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
+androidx-multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" }
+androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
+androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
+androidx-material3 = { module = "androidx.compose.material3:material3" }
+androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" }
+androidx-runtime = { module = "androidx.compose.runtime:runtime" }
+androidx-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" }
+androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomKtx" }
+androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomKtx" }
+androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomKtx" }
+androidx-ui = { module = "androidx.compose.ui:ui" }
+androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+billing = { module = "com.android.billingclient:billing", version.ref = "billing" }
+app-update-ktx = { module = "com.google.android.play:app-update-ktx", version.ref = "appUpdateKtx" }
+firebase-analytics-ktx = { module = "com.google.firebase:firebase-analytics-ktx" }
+firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
+firebase-crashlytics-ktx = { module = "com.google.firebase:firebase-crashlytics-ktx" }
+firebase-perf = { module = "com.google.firebase:firebase-perf" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+play-services-ads = { module = "com.google.android.gms:play-services-ads", version.ref = "playServicesAds" }
+play-services-oss-licenses = { module = "com.google.android.gms:play-services-oss-licenses", version.ref = "playServicesOssLicenses" }
+review-ktx = { module = "com.google.android.play:review-ktx", version.ref = "reviewKtx" }
+ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "composeUi" }
+ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
+
+[plugins]
+androidApplication = { id = "com.android.application", version.ref = "agp" }
+androidLibrary = { id = "com.android.library", version.ref = "agp" }
+jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+googlePlayServices = { id = "com.google.gms.google-services", version.ref = "google-services" }
+googleOssServices = { id = "com.google.android.gms.oss-licenses-plugin", version.ref = "google-oss-services" }
+googleFirebase = { id = "com.google.firebase.crashlytics", version.ref = "google-firebase-crashlytics" }
+devToolsKsp = { id = "com.google.devtools.ksp", version.ref = "google-devtools-ksp" }
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle.kts
similarity index 60%
rename from settings.gradle
rename to settings.gradle.kts
index 623a80f..597e12a 100644
--- a/settings.gradle
+++ b/settings.gradle.kts
@@ -3,23 +3,27 @@ pluginManagement {
gradlePluginPortal()
google()
mavenCentral()
- maven { url 'https://jitpack.io' }
+ maven {
+ setUrl("https://jitpack.io")
+ }
}
resolutionStrategy {
eachPlugin {
- if (requested.id.id == 'com.google.android.gms.oss-licenses-plugin') {
+ if (requested.id.id == "com.google.android.gms.oss-licenses-plugin") {
useModule("com.google.android.gms:oss-licenses-plugin:${requested.version}")
}
}
}
}
-dependencyResolutionManagement {
+@Suppress("UnstableApiUsage") dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
- maven { url 'https://jitpack.io' }
+ maven {
+ setUrl("https://jitpack.io")
+ }
}
}
rootProject.name = "Cart Calculator"
-include ':app'
\ No newline at end of file
+include(":app")
\ No newline at end of file