├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
├── schemas
│ └── com.kssidll.arru.data.database.AppDatabase
│ │ ├── 1.json
│ │ ├── 2.json
│ │ ├── 3.json
│ │ ├── 4.json
│ │ ├── 5.json
│ │ ├── 6.json
│ │ └── 7.json
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── java
│ └── com
│ │ └── kssidll
│ │ └── arru
│ │ ├── Arru.kt
│ │ ├── MainActivity.kt
│ │ ├── Navigation.kt
│ │ ├── broadcast
│ │ └── DataExportServiceStopActionReceiver.kt
│ │ ├── data
│ │ ├── dao
│ │ │ ├── CategoryDao.kt
│ │ │ ├── ImportDao.kt
│ │ │ ├── ItemDao.kt
│ │ │ ├── ProducerDao.kt
│ │ │ ├── ProductDao.kt
│ │ │ ├── ShopDao.kt
│ │ │ ├── TransactionBasketDao.kt
│ │ │ └── VariantDao.kt
│ │ ├── data
│ │ │ ├── DatabaseBackup.kt
│ │ │ ├── Item.kt
│ │ │ ├── Product.kt
│ │ │ ├── ProductCategory.kt
│ │ │ ├── ProductProducer.kt
│ │ │ ├── ProductVariant.kt
│ │ │ ├── Shop.kt
│ │ │ └── Transaction.kt
│ │ ├── database
│ │ │ ├── AppDatabase.kt
│ │ │ ├── Export.kt
│ │ │ └── Import.kt
│ │ ├── paging
│ │ │ └── FullItemPagingSource.kt
│ │ ├── preference
│ │ │ └── AppPreferences.kt
│ │ └── repository
│ │ │ ├── CategoryRepository.kt
│ │ │ ├── CategoryRepositorySource.kt
│ │ │ ├── ImportRepository.kt
│ │ │ ├── ImportRepositorySource.kt
│ │ │ ├── ItemRepository.kt
│ │ │ ├── ItemRepositorySource.kt
│ │ │ ├── ProducerRepository.kt
│ │ │ ├── ProducerRepositorySource.kt
│ │ │ ├── ProductRepository.kt
│ │ │ ├── ProductRepositorySource.kt
│ │ │ ├── ShopRepository.kt
│ │ │ ├── ShopRepositorySource.kt
│ │ │ ├── TransactionBasketRepository.kt
│ │ │ ├── TransactionBasketRepositorySource.kt
│ │ │ ├── VariantRepository.kt
│ │ │ └── VariantRepositorySource.kt
│ │ ├── di
│ │ └── module
│ │ │ ├── AppModule.kt
│ │ │ ├── ChangeDatabaseLocationUseCaseModule.kt
│ │ │ ├── DataStoreModule.kt
│ │ │ ├── DatabaseModule.kt
│ │ │ ├── ExportDataUseCaseModule.kt
│ │ │ └── ImportDataUseCaseModule.kt
│ │ ├── domain
│ │ ├── AppLocale.kt
│ │ ├── TimePeriodFlowHandler.kt
│ │ ├── data
│ │ │ ├── ChartSource.kt
│ │ │ ├── Data.kt
│ │ │ ├── Field.kt
│ │ │ ├── FloatSource.kt
│ │ │ ├── FuzzySearchSource.kt
│ │ │ ├── IdentitySource.kt
│ │ │ ├── NameSource.kt
│ │ │ ├── RankSource.kt
│ │ │ └── SortSource.kt
│ │ ├── usecase
│ │ │ ├── ChangeDatabaseLocationUseCase.kt
│ │ │ ├── ExportDataUIBlockingUseCase.kt
│ │ │ ├── ExportDataWithServiceUseCase.kt
│ │ │ └── ImportDataUIBlockingUseCase.kt
│ │ └── utils
│ │ │ └── Currency.kt
│ │ ├── helper
│ │ ├── DataGenerators.kt
│ │ ├── Locale.kt
│ │ ├── NavigationSuiteScaffold.kt
│ │ ├── Permission.kt
│ │ ├── RegexHelper.kt
│ │ ├── StringHelper.kt
│ │ └── Uri.kt
│ │ ├── service
│ │ ├── DataExportService.kt
│ │ └── State.kt
│ │ └── ui
│ │ ├── component
│ │ ├── SpendingSummaryComponent.kt
│ │ ├── TotalAverageAndMedianSpendingComponent.kt
│ │ ├── chart
│ │ │ ├── Marker.kt
│ │ │ ├── OneDimensionalColumnChart.kt
│ │ │ └── ShopPriceCompareChart.kt
│ │ ├── dialog
│ │ │ ├── DeleteWarningConfirmDialog.kt
│ │ │ ├── FuzzySearchableListDialog.kt
│ │ │ └── MergeConfirmDialog.kt
│ │ ├── field
│ │ │ ├── SearchField.kt
│ │ │ └── StyledOutlinedTextField.kt
│ │ ├── list
│ │ │ ├── BaseClickableListItem.kt
│ │ │ ├── FullItemCard.kt
│ │ │ ├── FullItemListContent.kt
│ │ │ ├── RankingList.kt
│ │ │ ├── SpendingComparisonList.kt
│ │ │ └── TransactionBasketCard.kt
│ │ └── other
│ │ │ ├── Loading.kt
│ │ │ ├── ProgressBar.kt
│ │ │ └── SecondaryAppBar.kt
│ │ ├── screen
│ │ ├── backups
│ │ │ ├── BackupsRoute.kt
│ │ │ ├── BackupsScreen.kt
│ │ │ └── BackupsViewModel.kt
│ │ ├── display
│ │ │ ├── category
│ │ │ │ ├── CategoryRoute.kt
│ │ │ │ ├── CategoryScreen.kt
│ │ │ │ └── CategoryViewModel.kt
│ │ │ ├── producer
│ │ │ │ ├── ProducerRoute.kt
│ │ │ │ ├── ProducerScreen.kt
│ │ │ │ └── ProducerViewModel.kt
│ │ │ ├── product
│ │ │ │ ├── ProductRoute.kt
│ │ │ │ ├── ProductScreen.kt
│ │ │ │ └── ProductViewModel.kt
│ │ │ ├── shop
│ │ │ │ ├── ShopRoute.kt
│ │ │ │ ├── ShopScreen.kt
│ │ │ │ └── ShopViewModel.kt
│ │ │ └── transaction
│ │ │ │ ├── TransactionRoute.kt
│ │ │ │ ├── TransactionScreen.kt
│ │ │ │ └── TransactionViewModel.kt
│ │ ├── home
│ │ │ ├── AnalysisScreen.kt
│ │ │ ├── DashboardScreen.kt
│ │ │ ├── HomeRoute.kt
│ │ │ ├── HomeScreen.kt
│ │ │ ├── HomeViewModel.kt
│ │ │ ├── TransactionsScreen.kt
│ │ │ └── component
│ │ │ │ ├── AnalysisDateHeader.kt
│ │ │ │ └── HomeScreenNothingToDisplayOverlay.kt
│ │ ├── modify
│ │ │ ├── ModifyScreen.kt
│ │ │ ├── category
│ │ │ │ ├── ModifyCategoryScreenImpl.kt
│ │ │ │ ├── ModifyCategoryViewModel.kt
│ │ │ │ ├── addcategory
│ │ │ │ │ ├── AddCategoryRoute.kt
│ │ │ │ │ └── AddCategoryViewModel.kt
│ │ │ │ └── editcategory
│ │ │ │ │ ├── EditCategoryRoute.kt
│ │ │ │ │ └── EditCategoryViewModel.kt
│ │ │ ├── item
│ │ │ │ ├── ModifyItemScreenImpl.kt
│ │ │ │ ├── ModifyItemViewModel.kt
│ │ │ │ ├── additem
│ │ │ │ │ ├── AddItemRoute.kt
│ │ │ │ │ └── AddItemViewModel.kt
│ │ │ │ └── edititem
│ │ │ │ │ ├── EditItemRoute.kt
│ │ │ │ │ └── EditItemViewModel.kt
│ │ │ ├── producer
│ │ │ │ ├── ModifyProducerScreenImpl.kt
│ │ │ │ ├── ModifyProducerViewModel.kt
│ │ │ │ ├── addproducer
│ │ │ │ │ ├── AddProducerRoute.kt
│ │ │ │ │ └── AddProducerViewModel.kt
│ │ │ │ └── editproducer
│ │ │ │ │ ├── EditProducerRoute.kt
│ │ │ │ │ └── EditProducerViewModel.kt
│ │ │ ├── product
│ │ │ │ ├── ModifyProductScreenImpl.kt
│ │ │ │ ├── ModifyProductViewModel.kt
│ │ │ │ ├── addproduct
│ │ │ │ │ ├── AddProductRoute.kt
│ │ │ │ │ └── AddProductViewModel.kt
│ │ │ │ └── editproduct
│ │ │ │ │ ├── EditProductRoute.kt
│ │ │ │ │ └── EditProductViewModel.kt
│ │ │ ├── shop
│ │ │ │ ├── ModifyShopScreenImpl.kt
│ │ │ │ ├── ModifyShopViewModel.kt
│ │ │ │ ├── addshop
│ │ │ │ │ ├── AddShopRoute.kt
│ │ │ │ │ └── AddShopViewModel.kt
│ │ │ │ └── editshop
│ │ │ │ │ ├── EditShopRoute.kt
│ │ │ │ │ └── EditShopViewModel.kt
│ │ │ ├── transaction
│ │ │ │ ├── ModifyTransactionScreenImpl.kt
│ │ │ │ ├── ModifyTransactionViewModel.kt
│ │ │ │ ├── addtransaction
│ │ │ │ │ ├── AddTransactionRoute.kt
│ │ │ │ │ └── AddTransactionViewModel.kt
│ │ │ │ └── edittransaction
│ │ │ │ │ ├── EditTransactionRoute.kt
│ │ │ │ │ └── EditTransactionViewModel.kt
│ │ │ └── variant
│ │ │ │ ├── ModifyVariantScreenImpl.kt
│ │ │ │ ├── ModifyVariantViewModel.kt
│ │ │ │ ├── addvariant
│ │ │ │ ├── AddVariantRoute.kt
│ │ │ │ └── AddVariantViewModel.kt
│ │ │ │ └── editvariant
│ │ │ │ ├── EditVariantRoute.kt
│ │ │ │ └── EditVariantViewModel.kt
│ │ ├── ranking
│ │ │ ├── RankingScreen.kt
│ │ │ ├── categoryranking
│ │ │ │ ├── CategoryRankingRoute.kt
│ │ │ │ └── CategoryRankingViewModel.kt
│ │ │ └── shopranking
│ │ │ │ ├── ShopRankingRoute.kt
│ │ │ │ └── ShopRankingViewModel.kt
│ │ ├── search
│ │ │ ├── SearchRoute.kt
│ │ │ ├── SearchScreen.kt
│ │ │ ├── SearchViewModel.kt
│ │ │ ├── categorylist
│ │ │ │ ├── CategoryListRoute.kt
│ │ │ │ └── CategoryListViewModel.kt
│ │ │ ├── component
│ │ │ │ └── SearchItem.kt
│ │ │ ├── producerlist
│ │ │ │ ├── ProducerListRoute.kt
│ │ │ │ └── ProducerListViewModel.kt
│ │ │ ├── productlist
│ │ │ │ ├── ProductListRoute.kt
│ │ │ │ └── ProductListViewModel.kt
│ │ │ ├── shared
│ │ │ │ └── SearchList.kt
│ │ │ ├── shoplist
│ │ │ │ ├── ShopListRoute.kt
│ │ │ │ └── ShopListViewModel.kt
│ │ │ └── start
│ │ │ │ ├── StartRoute.kt
│ │ │ │ └── StartScreen.kt
│ │ ├── settings
│ │ │ ├── SettingsRoute.kt
│ │ │ ├── SettingsScreen.kt
│ │ │ ├── SettingsViewModel.kt
│ │ │ └── component
│ │ │ │ ├── LanguageExposedDropdown.kt
│ │ │ │ └── ThemeExposedDropdown.kt
│ │ └── spendingcomparison
│ │ │ ├── SpendingComparisonScreen.kt
│ │ │ ├── categoryspendingcomparison
│ │ │ ├── CategorySpendingComparisonRoute.kt
│ │ │ └── CategorySpendingComparisonViewModel.kt
│ │ │ └── shopspendingcomparison
│ │ │ ├── ShopSpendingComparisonRoute.kt
│ │ │ └── ShopSpendingComparisonViewModel.kt
│ │ └── theme
│ │ ├── Theme.kt
│ │ ├── Type.kt
│ │ └── schema
│ │ ├── Dark.kt
│ │ └── Light.kt
│ └── res
│ ├── drawable
│ ├── close.xml
│ ├── ic_launcher_foreground.xml
│ └── ic_launcher_monochrome.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-mdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── resources.properties
│ ├── values-en
│ └── strings.xml
│ ├── values-notnight-v29
│ └── themes.xml
│ ├── values-pl
│ └── strings.xml
│ ├── values-tr
│ └── strings.xml
│ ├── values
│ ├── ic_launcher_background.xml
│ ├── strings.xml
│ └── themes.xml
│ └── xml
│ ├── backup_rules.xml
│ └── data_extraction_rules.xml
├── build.gradle.kts
├── crowdin.yml
├── docs
└── export.md
├── fastlane
└── metadata
│ └── android
│ └── en-US
│ ├── changelogs
│ ├── 37.txt
│ ├── 38.txt
│ ├── 39.txt
│ ├── 40.txt
│ ├── 41.txt
│ ├── 42.txt
│ ├── 43.txt
│ └── 44.txt
│ ├── full_description.txt
│ ├── images
│ ├── icon.png
│ └── phoneScreenshots
│ │ ├── 01-dashboard.jpg
│ │ ├── 02-analysis.jpg
│ │ ├── 03-transactions.jpg
│ │ ├── 04-product_top.jpg
│ │ ├── 05-categories_ranking.jpg
│ │ ├── 06-merge.jpg
│ │ ├── 07-backups.jpg
│ │ ├── 08-transaction_add_item.jpg
│ │ ├── 09-transaction_add_select.jpg
│ │ └── 10-transaction_add.jpg
│ └── short_description.txt
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── images
├── IzzyOnDroidButton.svg
├── analysis.jpg
├── backups.jpg
├── categories_ranking.jpg
├── dashboard.jpg
├── merge.jpg
├── product_top.jpg
├── transaction_add.jpg
├── transaction_add_item.jpg
├── transaction_add_select.jpg
└── transactions.jpg
├── processor
├── .gitignore
├── build.gradle.kts
└── src
│ └── main
│ ├── java
│ └── com
│ │ └── kssidll
│ │ └── processor
│ │ ├── CurrencyLocale.kt
│ │ └── CurrencyLocaleProvider.kt
│ └── resources
│ └── META-INF
│ └── services
│ └── com.google.devtools.ksp.processing.SymbolProcessorProvider
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | # Gradle files
2 | .gradle/
3 | build/
4 |
5 | # Local configuration file (sdk path, etc)
6 | local.properties
7 | signing.properties
8 |
9 | # Log/OS Files
10 | *.log
11 |
12 | # Android Studio generated files and folders
13 | captures/
14 | .externalNativeBuild/
15 | .cxx/
16 | *.apk
17 | output.json
18 | /app/release
19 | .kotlin
20 |
21 | # IntelliJ
22 | *.iml
23 | .idea/
24 | misc.xml
25 | deploymentTargetDropDown.xml
26 | render.experimental.xml
27 |
28 | # Google Services (e.g. APIs or Firebase)
29 | google-services.json
30 |
31 | # Android Profiling
32 | *.hprof
33 |
34 | /app/src/main/assets/database/arrugarq-test.db
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The Clear BSD License
2 |
3 | Copyright (c) 2024 Szymon Kolasa
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted (subject to the limitations in the disclaimer
8 | below) provided that the following conditions are met:
9 |
10 | * Redistributions of source code must retain the above copyright notice,
11 | this list of conditions and the following disclaimer.
12 |
13 | * Redistributions in binary form must reproduce the above copyright
14 | notice, this list of conditions and the following disclaimer in the
15 | documentation and/or other materials provided with the distribution.
16 |
17 | * Neither the name of the copyright holder nor the names of its
18 | contributors may be used to endorse or promote products derived from this
19 | software without specific prior written permission.
20 |
21 | NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
22 | THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
23 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
25 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
26 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
27 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
28 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
29 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
30 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 | POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Arru
2 | Your expenses tracker
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Arru is an app for expenditure tracking/analysis
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | # Features
46 |
47 | - Light/Dark mode
48 | - Wide screen support
49 | - Local backups
50 | - Data Export ([documentation](docs/export.md))
51 | - Transaction baskets tracking your total expenditure with optional product, category, shop and producer spending tracking
52 | - Comparisons between prices at different shops
53 | - Ranking of categories and shops based on total money spent
54 | - Merging capabilities for categories, shops, products and producers
55 |
56 | # Tech Stack & Libraries
57 |
58 | - [Kotlin](https://kotlinlang.org/) based
59 |
60 | - [Coroutines](https://github.com/Kotlin/kotlinx.coroutines) for asynchronous computing
61 |
62 | - [Flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/) to emit values from data layer reactively
63 |
64 | - [Hilt](https://dagger.dev/hilt/) for dependency injection
65 |
66 | - [Compose Navigation Reimagined](https://github.com/olshevski/compose-navigation-reimagined) for animated navigation
67 |
68 | - [Vico Compose](https://github.com/patrykandpatrick/vico) for graphs
69 |
70 | - [Fuzzywuzzy](https://github.com/xdrop/fuzzywuzzy) for fuzzy searching capabilities
71 |
72 | - Jetpack
73 |
74 | - [Compose](https://developer.android.com/jetpack/compose) - Modern Declarative UI style framework based on composable functions
75 |
76 | - [Room](https://developer.android.com/jetpack/androidx/releases/room) - Persistence library providing abstraction layer over SQLite
77 |
78 | - [Material You Kit](https://developer.android.com/jetpack/androidx/releases/compose-material3) - Material 3 powerful UI components
79 |
80 | - [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) - Manages UI-related data holder and lifecycle awareness. Allows data to survive configuration changes such as screen rotations
81 |
82 | - [Lifecycle](https://developer.android.com/jetpack/androidx/releases/lifecycle) - Observe Android lifecycles and handle UI states upon the lifecycle changes
83 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
10 |
14 |
18 |
19 |
30 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
45 |
49 |
50 |
51 |
57 |
58 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/Arru.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.res.Configuration.UI_MODE_NIGHT_NO
7 | import android.content.res.Configuration.UI_MODE_NIGHT_YES
8 | import androidx.compose.ui.tooling.preview.Devices
9 | import androidx.compose.ui.tooling.preview.Preview
10 | import dagger.hilt.android.HiltAndroidApp
11 | import kotlin.system.exitProcess
12 |
13 | @HiltAndroidApp
14 | class Arru: Application() {
15 |
16 | companion object {
17 | /**
18 | * restarts the app
19 | * @param context app context
20 | */
21 | fun restart(context: Context) {
22 | val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)
23 | intent!!.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
24 | context.startActivity(intent)
25 | exitProcess(0)
26 | }
27 | }
28 | }
29 |
30 | const val DAY_IN_MILIS: Long = 86400000
31 |
32 | const val APPLICATION_NAME = "Arru"
33 |
34 | @Preview(
35 | group = "Expanded",
36 | name = "Dark",
37 | showBackground = true,
38 | uiMode = UI_MODE_NIGHT_YES,
39 | device = Devices.PIXEL_FOLD
40 | )
41 | @Preview(
42 | group = "Expanded",
43 | name = "Light",
44 | showBackground = true,
45 | uiMode = UI_MODE_NIGHT_NO,
46 | device = Devices.PIXEL_FOLD
47 | )
48 | annotation class PreviewExpanded
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/broadcast/DataExportServiceStopActionReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.broadcast
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.BroadcastReceiver
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.util.Log
8 | import com.kssidll.arru.service.DataExportService
9 |
10 | /**
11 | * Receiver that stops the [DataExportService]
12 | */
13 | class DataExportServiceStopActionReceiver: BroadcastReceiver() {
14 | @SuppressLint("LongLogTag")
15 | override fun onReceive(
16 | context: Context?,
17 | intent: Intent?
18 | ) {
19 | val forced = intent?.extras?.getBoolean(DataExportService.FORCED_STOP_KEY)
20 |
21 | Log.d(
22 | TAG,
23 | "onReceive: received with forced = $forced"
24 | )
25 |
26 | context?.let {
27 | DataExportService.stop(
28 | it,
29 | forced ?: false
30 | )
31 | }
32 | }
33 |
34 | companion object {
35 | const val TAG = "EXPORT_SERVICE_STOP_ACTION"
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/data/dao/ImportDao.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.data.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 | import androidx.room.Transaction
8 | import com.kssidll.arru.data.data.Item
9 | import com.kssidll.arru.data.data.Product
10 | import com.kssidll.arru.data.data.ProductCategory
11 | import com.kssidll.arru.data.data.ProductProducer
12 | import com.kssidll.arru.data.data.ProductVariant
13 | import com.kssidll.arru.data.data.Shop
14 | import com.kssidll.arru.data.data.TransactionBasket
15 |
16 | @Dao
17 | interface ImportDao {
18 |
19 | @Insert(onConflict = OnConflictStrategy.REPLACE)
20 | suspend fun insertShops(entities: List)
21 |
22 | @Query("DELETE FROM Shop")
23 | suspend fun deleteShops()
24 |
25 |
26 | @Insert(onConflict = OnConflictStrategy.REPLACE)
27 | suspend fun insertProducers(entities: List)
28 |
29 | @Query("DELETE FROM ProductProducer")
30 | suspend fun deleteProducers()
31 |
32 |
33 | @Insert(onConflict = OnConflictStrategy.REPLACE)
34 | suspend fun insertCategories(entities: List)
35 |
36 | @Query("DELETE FROM ProductCategory")
37 | suspend fun deleteCategories()
38 |
39 |
40 | @Insert(onConflict = OnConflictStrategy.REPLACE)
41 | suspend fun insertTransactions(entities: List)
42 |
43 | @Query("DELETE FROM TransactionBasket")
44 | suspend fun deleteTransactions()
45 |
46 |
47 | @Insert(onConflict = OnConflictStrategy.REPLACE)
48 | suspend fun insertProducts(entities: List)
49 |
50 | @Query("DELETE FROM Product")
51 | suspend fun deleteProducts()
52 |
53 |
54 | @Insert(onConflict = OnConflictStrategy.REPLACE)
55 | suspend fun insertVariants(entities: List)
56 |
57 | @Query("DELETE FROM ProductVariant")
58 | suspend fun deleteVariants()
59 |
60 |
61 | @Insert(onConflict = OnConflictStrategy.REPLACE)
62 | suspend fun insertItems(entities: List- )
63 |
64 | @Query("DELETE FROM Item")
65 | suspend fun deleteItems()
66 |
67 |
68 | @Transaction
69 | suspend fun deleteAll() {
70 | deleteItems()
71 | deleteVariants()
72 | deleteProducts()
73 | deleteTransactions()
74 | deleteCategories()
75 | deleteProducers()
76 | deleteShops()
77 | }
78 |
79 | @Transaction
80 | suspend fun insertAll(
81 | shops: List,
82 | producers: List,
83 | categories: List,
84 | transactions: List,
85 | products: List,
86 | variants: List,
87 | items: List
-
88 | ) {
89 | deleteAll()
90 |
91 | insertShops(shops)
92 | insertProducers(producers)
93 | insertCategories(categories)
94 | insertTransactions(transactions)
95 | insertProducts(products)
96 | insertVariants(variants)
97 | insertItems(items)
98 | }
99 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/data/dao/ItemDao.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.data.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.Query
7 | import androidx.room.Update
8 | import com.kssidll.arru.data.data.Item
9 | import com.kssidll.arru.data.data.Product
10 | import com.kssidll.arru.data.data.ProductVariant
11 | import com.kssidll.arru.data.data.TransactionBasket
12 | import kotlinx.coroutines.flow.Flow
13 |
14 | @Dao
15 | interface ItemDao {
16 | // Create
17 |
18 | @Insert
19 | suspend fun insert(item: Item): Long
20 |
21 | // Update
22 |
23 | @Update
24 | suspend fun update(item: Item)
25 |
26 | // Delete
27 |
28 | @Delete
29 | suspend fun delete(item: Item)
30 |
31 | // Helper
32 |
33 | @Query("SELECT transactionbasket.* FROM transactionbasket WHERE transactionbasket.id = :transactionId")
34 | suspend fun getTransactionBasket(transactionId: Long): TransactionBasket?
35 |
36 | @Query("SELECT product.* FROM product WHERE product.id = :productId")
37 | suspend fun getProduct(productId: Long): Product?
38 |
39 | @Query("SELECT productvariant.* FROM productvariant WHERE productvariant.id = :variantId")
40 | suspend fun getVariant(variantId: Long): ProductVariant?
41 |
42 | // Read
43 |
44 | @Query("SELECT item.* FROM item WHERE item.id = :itemId")
45 | suspend fun get(itemId: Long): Item?
46 |
47 | @Query("SELECT item.* FROM item ORDER BY id DESC LIMIT 1")
48 | suspend fun newest(): Item?
49 |
50 | @Query("SELECT item.* FROM item ORDER BY id DESC LIMIT 1")
51 | fun newestFlow(): Flow
-
52 |
53 | @Query("SELECT COUNT(*) FROM item")
54 | suspend fun totalCount(): Int
55 |
56 | @Query("SELECT item.* FROM item ORDER BY id LIMIT :limit OFFSET :offset")
57 | suspend fun getPagedList(
58 | limit: Int,
59 | offset: Int
60 | ): List
-
61 |
62 | @Query("SELECT item.* FROM item WHERE item.transactionBasketId = :transactionId")
63 | suspend fun getByTransaction(transactionId: Long): List
-
64 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/data/dao/VariantDao.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.data.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.Query
7 | import androidx.room.Update
8 | import com.kssidll.arru.data.data.Item
9 | import com.kssidll.arru.data.data.Product
10 | import com.kssidll.arru.data.data.ProductVariant
11 | import kotlinx.coroutines.flow.Flow
12 |
13 | @Dao
14 | interface VariantDao {
15 | // Create
16 |
17 | @Insert
18 | suspend fun insert(variant: ProductVariant): Long
19 |
20 | // Update
21 |
22 | @Update
23 | suspend fun update(variant: ProductVariant)
24 |
25 | // Delete
26 |
27 | @Delete
28 | suspend fun delete(variant: ProductVariant)
29 |
30 | // Helper
31 |
32 | @Query("SELECT product.* FROM product WHERE product.id = :productId")
33 | suspend fun getProduct(productId: Long): Product?
34 |
35 | @Query("SELECT item.* FROM item WHERE item.variantId = :variantId")
36 | suspend fun getItems(variantId: Long): List
-
37 |
38 | @Delete
39 | suspend fun deleteItems(items: List
- )
40 |
41 | // Read
42 |
43 | @Query("SELECT productvariant.* FROM productvariant WHERE productvariant.id = :variantId")
44 | suspend fun get(variantId: Long): ProductVariant?
45 |
46 | @Query("SELECT productvariant.* FROM productvariant WHERE productvariant.id = :variantId")
47 | fun getFlow(variantId: Long): Flow
48 |
49 | @Query("SELECT productvariant.* FROM productvariant WHERE productvariant.productId = :productId AND productvariant.name = :name")
50 | suspend fun byProductAndName(
51 | productId: Long,
52 | name: String
53 | ): ProductVariant?
54 |
55 | @Query("SELECT productvariant.* FROM productvariant WHERE productvariant.productId == :productId")
56 | fun byProductFlow(productId: Long): Flow
>
57 |
58 | @Query("SELECT COUNT(*) FROM productvariant")
59 | suspend fun totalCount(): Int
60 |
61 | @Query("SELECT productvariant.* FROM productvariant ORDER BY id LIMIT :limit OFFSET :offset")
62 | suspend fun getPagedList(
63 | limit: Int,
64 | offset: Int
65 | ): List
66 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/data/data/ProductProducer.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.data.data
2 |
3 | import androidx.room.Entity
4 | import androidx.room.Ignore
5 | import androidx.room.Index
6 | import androidx.room.PrimaryKey
7 | import com.kssidll.arru.domain.data.FuzzySearchSource
8 | import com.kssidll.arru.domain.data.NameSource
9 | import com.kssidll.arru.helper.generateRandomStringValue
10 | import me.xdrop.fuzzywuzzy.FuzzySearch
11 |
12 | @Entity(
13 | indices = [
14 | Index(
15 | value = ["name"],
16 | unique = true
17 | )
18 | ]
19 | )
20 | data class ProductProducer(
21 | @PrimaryKey(autoGenerate = true) val id: Long,
22 | var name: String,
23 | ): FuzzySearchSource, NameSource {
24 | /**
25 | * Converts the [ProductProducer] data to a string with csv format
26 | *
27 | * Doesn't include the csv headers
28 | * @return [ProductProducer] data as [String] with csv format
29 | */
30 | @Ignore
31 | fun formatAsCsvString(): String {
32 | return "${id};${name}"
33 | }
34 |
35 | companion object {
36 | /**
37 | * Returns the [String] representing the [ProductProducer] csv format headers
38 | * @return [String] representing the [ProductProducer] csv format headers
39 | */
40 | @Ignore
41 | fun csvHeaders(): String {
42 | return "id;name"
43 | }
44 |
45 | @Ignore
46 | fun generate(producerId: Long = 0): ProductProducer {
47 | return ProductProducer(
48 | id = producerId,
49 | name = generateRandomStringValue(),
50 | )
51 | }
52 |
53 | @Ignore
54 | fun generateList(amount: Int = 10): List {
55 | return List(amount) {
56 | generate(it.toLong())
57 | }
58 | }
59 | }
60 |
61 | @Ignore
62 | constructor(
63 | name: String,
64 | ): this(
65 | 0,
66 | name.trim()
67 | )
68 |
69 | @Ignore
70 | override fun fuzzyScore(query: String): Int {
71 | return FuzzySearch.extractOne(
72 | query,
73 | listOf(name)
74 | ).score
75 | }
76 |
77 | @Ignore
78 | override fun name(): String {
79 | return name
80 | }
81 |
82 | /**
83 | * @return true if name is valid, false otherwise
84 | */
85 | @Ignore
86 | fun validName(): Boolean {
87 | return name.isNotBlank()
88 | }
89 |
90 | }
91 |
92 | /**
93 | * Converts a list of [ProductProducer] data to a list of strings with csv format
94 | * @param includeHeaders whether to include the csv headers
95 | * @return [ProductProducer] data as list of string with csv format
96 | */
97 | fun List.asCsvList(includeHeaders: Boolean = false): List = buildList {
98 | // Add headers
99 | if (includeHeaders) {
100 | add(ProductProducer.csvHeaders() + "\n")
101 | }
102 |
103 | // Add rows
104 | this@asCsvList.forEach {
105 | add(it.formatAsCsvString() + "\n")
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/data/data/ProductVariant.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.data.data
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.ForeignKey
6 | import androidx.room.Ignore
7 | import androidx.room.PrimaryKey
8 | import com.kssidll.arru.domain.data.FuzzySearchSource
9 | import com.kssidll.arru.helper.generateRandomLongValue
10 | import com.kssidll.arru.helper.generateRandomStringValue
11 | import me.xdrop.fuzzywuzzy.FuzzySearch
12 |
13 | @Entity(
14 | foreignKeys = [
15 | ForeignKey(
16 | entity = Product::class,
17 | parentColumns = ["id"],
18 | childColumns = ["productId"],
19 | onDelete = ForeignKey.RESTRICT,
20 | onUpdate = ForeignKey.RESTRICT,
21 | ),
22 | ]
23 | )
24 | data class ProductVariant(
25 | @PrimaryKey(autoGenerate = true) val id: Long,
26 | @ColumnInfo(index = true) var productId: Long,
27 | var name: String,
28 | ): FuzzySearchSource {
29 |
30 | @Ignore
31 | constructor(
32 | productId: Long,
33 | name: String,
34 | ): this(
35 | 0,
36 | productId,
37 | name.trim()
38 | )
39 |
40 | /**
41 | * Converts the [ProductVariant] data to a string with csv format
42 | *
43 | * Doesn't include the csv headers
44 | * @return [ProductVariant] data as [String] with csv format
45 | */
46 | @Ignore
47 | fun formatAsCsvString(): String {
48 | return "${id};${productId};${name}"
49 | }
50 |
51 |
52 | companion object {
53 | /**
54 | * Returns the [String] representing the [ProductVariant] csv format headers
55 | * @return [String] representing the [ProductVariant] csv format headers
56 | */
57 | @Ignore
58 | fun csvHeaders(): String {
59 | return "id;productId;name"
60 | }
61 |
62 | @Ignore
63 | fun generate(variantId: Long = 0): ProductVariant {
64 | return ProductVariant(
65 | id = variantId,
66 | productId = generateRandomLongValue(),
67 | name = generateRandomStringValue(),
68 | )
69 | }
70 |
71 | @Ignore
72 | fun generateList(amount: Int = 10): List {
73 | return List(amount) {
74 | generate(it.toLong())
75 | }
76 | }
77 | }
78 |
79 | @Ignore
80 | override fun fuzzyScore(query: String): Int {
81 | return FuzzySearch.extractOne(
82 | query,
83 | listOf(name)
84 | ).score
85 | }
86 |
87 | /**
88 | * @return true if name is valid, false otherwise
89 | */
90 | @Ignore
91 | fun validName(): Boolean {
92 | return name.isNotBlank()
93 | }
94 | }
95 |
96 | /**
97 | * Converts a list of [ProductVariant] data to a list of strings with csv format
98 | * @param includeHeaders whether to include the csv headers
99 | * @return [ProductVariant] data as list of string with csv format
100 | */
101 | fun List.asCsvList(includeHeaders: Boolean = false): List = buildList {
102 | // Add headers
103 | if (includeHeaders) {
104 | add(ProductVariant.csvHeaders() + "\n")
105 | }
106 |
107 | // Add rows
108 | this@asCsvList.forEach {
109 | add(it.formatAsCsvString() + "\n")
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/data/data/Shop.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.data.data
2 |
3 | import androidx.room.Entity
4 | import androidx.room.Ignore
5 | import androidx.room.Index
6 | import androidx.room.PrimaryKey
7 | import com.kssidll.arru.domain.data.FuzzySearchSource
8 | import com.kssidll.arru.domain.data.NameSource
9 | import com.kssidll.arru.helper.generateRandomStringValue
10 | import me.xdrop.fuzzywuzzy.FuzzySearch
11 |
12 | @Entity(
13 | indices = [
14 | Index(
15 | value = ["name"],
16 | unique = true
17 | )
18 | ]
19 | )
20 | data class Shop(
21 | @PrimaryKey(autoGenerate = true) val id: Long,
22 | val name: String,
23 | ): FuzzySearchSource, NameSource {
24 | /**
25 | * Converts the [Shop] data to a string with csv format
26 | *
27 | * Doesn't include the csv headers
28 | * @return [Shop] data as [String] with csv format
29 | */
30 | @Ignore
31 | fun formatAsCsvString(): String {
32 | return "${id};${name}"
33 | }
34 |
35 | companion object {
36 | /**
37 | * Returns the [String] representing the [Shop] csv format headers
38 | * @return [String] representing the [Shop] csv format headers
39 | */
40 | @Ignore
41 | fun csvHeaders(): String {
42 | return "id;name"
43 | }
44 |
45 | @Ignore
46 | fun generate(shopId: Long = 0): Shop {
47 | return Shop(
48 | id = shopId,
49 | name = generateRandomStringValue(),
50 | )
51 | }
52 |
53 | @Ignore
54 | fun generateList(amount: Int = 10): List {
55 | return List(amount) {
56 | generate(it.toLong())
57 | }
58 | }
59 | }
60 |
61 | @Ignore
62 | constructor(
63 | name: String
64 | ): this(
65 | 0,
66 | name.trim()
67 | )
68 |
69 | @Ignore
70 | override fun fuzzyScore(query: String): Int {
71 | return FuzzySearch.extractOne(
72 | query,
73 | listOf(name)
74 | ).score
75 | }
76 |
77 | /**
78 | * @return true if name is valid, false otherwise
79 | */
80 | @Ignore
81 | fun validName(): Boolean {
82 | return name.isNotBlank()
83 | }
84 |
85 | @Ignore
86 | override fun name(): String {
87 | return name
88 | }
89 | }
90 |
91 | /**
92 | * Converts a list of [Shop] data to a list of strings with csv format
93 | * @param includeHeaders whether to include the csv headers
94 | * @return [Shop] data as list of string with csv format
95 | */
96 | fun List.asCsvList(includeHeaders: Boolean = false): List = buildList {
97 | // Add headers
98 | if (includeHeaders) {
99 | add(Shop.csvHeaders() + "\n")
100 | }
101 |
102 | // Add rows
103 | this@asCsvList.forEach {
104 | add(it.formatAsCsvString() + "\n")
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/data/paging/FullItemPagingSource.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.data.paging
2 |
3 | import androidx.paging.PagingSource
4 | import androidx.paging.PagingState
5 | import com.kssidll.arru.data.data.FullItem
6 |
7 | class FullItemPagingSource(
8 | private val query: suspend (start: Int, loadSize: Int) -> List,
9 | private val itemsBefore: suspend (id: Long) -> Int,
10 | private val itemsAfter: suspend (id: Long) -> Int,
11 | ): PagingSource() {
12 | override suspend fun load(params: LoadParams): LoadResult {
13 | val pageIndex = params.key ?: 0
14 |
15 | val page = query(
16 | pageIndex,
17 | params.loadSize
18 | )
19 |
20 | val itemsBefore =
21 | if (pageIndex == 0) 0
22 | else page.firstOrNull()?.id?.let { itemsAfter(it) } ?: 0 // reversed since newest first
23 |
24 | val itemsAfter =
25 | page.lastOrNull()?.id?.let { itemsBefore(it) } ?: 0 // reversed since newest first
26 |
27 | val prevKey = if (pageIndex == 0) null else pageIndex - params.loadSize
28 | val nextKey = if (itemsAfter == 0) null else pageIndex + params.loadSize
29 |
30 | return LoadResult.Page(
31 | data = page,
32 | prevKey = prevKey,
33 | nextKey = nextKey,
34 | itemsBefore = itemsBefore,
35 | itemsAfter = itemsAfter
36 | )
37 | }
38 |
39 | override fun getRefreshKey(state: PagingState): Int? {
40 | return state.anchorPosition?.let {
41 | state.closestPageToPosition(it)?.prevKey
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/data/repository/ImportRepository.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.data.repository
2 |
3 | import com.kssidll.arru.data.dao.ImportDao
4 | import com.kssidll.arru.data.data.Item
5 | import com.kssidll.arru.data.data.Product
6 | import com.kssidll.arru.data.data.ProductCategory
7 | import com.kssidll.arru.data.data.ProductProducer
8 | import com.kssidll.arru.data.data.ProductVariant
9 | import com.kssidll.arru.data.data.Shop
10 | import com.kssidll.arru.data.data.TransactionBasket
11 |
12 | class ImportRepository(private val dao: ImportDao): ImportRepositorySource {
13 | override suspend fun insertAll(
14 | shops: List,
15 | producers: List,
16 | categories: List,
17 | transactions: List,
18 | products: List,
19 | variants: List,
20 | items: List-
21 | ) {
22 | dao.insertAll(
23 | shops = shops,
24 | producers = producers,
25 | categories = categories,
26 | transactions = transactions,
27 | products = products,
28 | variants = variants,
29 | items = items
30 | )
31 | }
32 |
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/data/repository/ImportRepositorySource.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.data.repository
2 |
3 | import com.kssidll.arru.data.data.Item
4 | import com.kssidll.arru.data.data.Product
5 | import com.kssidll.arru.data.data.ProductCategory
6 | import com.kssidll.arru.data.data.ProductProducer
7 | import com.kssidll.arru.data.data.ProductVariant
8 | import com.kssidll.arru.data.data.Shop
9 | import com.kssidll.arru.data.data.TransactionBasket
10 |
11 | interface ImportRepositorySource {
12 | suspend fun insertAll(
13 | shops: List,
14 | producers: List,
15 | categories: List,
16 | transactions: List,
17 | products: List,
18 | variants: List,
19 | items: List
-
20 | )
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/di/module/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.di.module
2 |
3 | import dagger.Module
4 | import dagger.hilt.InstallIn
5 | import dagger.hilt.components.SingletonComponent
6 |
7 | @Module
8 | @InstallIn(SingletonComponent::class)
9 | class AppModule
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/di/module/ChangeDatabaseLocationUseCaseModule.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.di.module
2 |
3 | import android.content.Context
4 | import com.kssidll.arru.domain.usecase.ChangeDatabaseLocationUseCase
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.android.qualifiers.ApplicationContext
9 | import dagger.hilt.components.SingletonComponent
10 | import javax.inject.Singleton
11 |
12 | @Module
13 | @InstallIn(SingletonComponent::class)
14 | class ChangeDatabaseLocationUseCaseModule {
15 | @Provides
16 | @Singleton
17 | fun provideChangeDatabaseLocationUseCase(@ApplicationContext context: Context): ChangeDatabaseLocationUseCase {
18 | return ChangeDatabaseLocationUseCase(context)
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/di/module/DataStoreModule.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.di.module
2 |
3 | import android.content.Context
4 | import androidx.datastore.core.DataStore
5 | import androidx.datastore.preferences.core.Preferences
6 | import androidx.datastore.preferences.preferencesDataStore
7 | import com.kssidll.arru.data.preference.AppPreferences
8 | import dagger.Module
9 | import dagger.Provides
10 | import dagger.hilt.InstallIn
11 | import dagger.hilt.android.qualifiers.ApplicationContext
12 | import dagger.hilt.components.SingletonComponent
13 | import kotlinx.coroutines.flow.first
14 | import kotlinx.coroutines.runBlocking
15 | import javax.inject.Singleton
16 |
17 | val Context.dataStore: DataStore by preferencesDataStore(name = AppPreferences.DATASTORENAME)
18 |
19 | fun getPreferencesDataStore(context: Context): DataStore {
20 | return context.dataStore
21 | }
22 |
23 | suspend fun getPreferences(context: Context): Preferences {
24 | return getPreferencesDataStore(context).data.first()
25 | }
26 |
27 | suspend fun Preferences.from(context: Context): Preferences {
28 | return getPreferences(context)
29 | }
30 |
31 | suspend fun Context.preferences(): Preferences {
32 | return getPreferences(this)
33 | }
34 |
35 | @Module
36 | @InstallIn(SingletonComponent::class)
37 | class DataStoreModule {
38 | @Provides
39 | @Singleton
40 | fun provideDataStore(@ApplicationContext context: Context): DataStore {
41 | return getPreferencesDataStore(context)
42 | }
43 |
44 | @Provides
45 | fun providePreferences(@ApplicationContext context: Context): Preferences {
46 | return runBlocking {
47 | return@runBlocking getPreferences(context)
48 | }
49 | }
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/di/module/ExportDataUseCaseModule.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.di.module
2 |
3 | import android.content.Context
4 | import com.kssidll.arru.data.repository.CategoryRepositorySource
5 | import com.kssidll.arru.data.repository.ItemRepositorySource
6 | import com.kssidll.arru.data.repository.ProducerRepositorySource
7 | import com.kssidll.arru.data.repository.ProductRepositorySource
8 | import com.kssidll.arru.data.repository.ShopRepositorySource
9 | import com.kssidll.arru.data.repository.TransactionBasketRepositorySource
10 | import com.kssidll.arru.data.repository.VariantRepositorySource
11 | import com.kssidll.arru.domain.usecase.ExportDataUIBlockingUseCase
12 | import com.kssidll.arru.domain.usecase.ExportDataWithServiceUseCase
13 | import dagger.Module
14 | import dagger.Provides
15 | import dagger.hilt.InstallIn
16 | import dagger.hilt.android.qualifiers.ApplicationContext
17 | import dagger.hilt.components.SingletonComponent
18 | import javax.inject.Singleton
19 |
20 | @Module
21 | @InstallIn(SingletonComponent::class)
22 | class ExportDataUseCaseModule {
23 | @Provides
24 | @Singleton
25 | fun provideExportDataWithServiceUseCase(@ApplicationContext context: Context): ExportDataWithServiceUseCase {
26 | return ExportDataWithServiceUseCase(context)
27 | }
28 |
29 | @Provides
30 | @Singleton
31 | fun provideExportDataUIBlockingUseCase(
32 | @ApplicationContext context: Context,
33 | categoryRepository: CategoryRepositorySource,
34 | itemRepository: ItemRepositorySource,
35 | producerRepository: ProducerRepositorySource,
36 | productRepository: ProductRepositorySource,
37 | shopRepository: ShopRepositorySource,
38 | transactionRepository: TransactionBasketRepositorySource,
39 | variantRepository: VariantRepositorySource,
40 | ): ExportDataUIBlockingUseCase {
41 | return ExportDataUIBlockingUseCase(
42 | context,
43 | categoryRepository,
44 | itemRepository,
45 | producerRepository,
46 | productRepository,
47 | shopRepository,
48 | transactionRepository,
49 | variantRepository
50 | )
51 | }
52 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/di/module/ImportDataUseCaseModule.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.di.module
2 |
3 | import android.content.Context
4 | import com.kssidll.arru.data.repository.ImportRepositorySource
5 | import com.kssidll.arru.domain.usecase.ImportDataUIBlockingUseCase
6 | import dagger.Module
7 | import dagger.Provides
8 | import dagger.hilt.InstallIn
9 | import dagger.hilt.android.qualifiers.ApplicationContext
10 | import dagger.hilt.components.SingletonComponent
11 | import javax.inject.Singleton
12 |
13 | @Module
14 | @InstallIn(SingletonComponent::class)
15 | class ImportDataUseCaseModule {
16 | @Provides
17 | @Singleton
18 | fun provideImportDataUIBlockingUseCase(
19 | @ApplicationContext context: Context,
20 | importRepository: ImportRepositorySource,
21 | ): ImportDataUIBlockingUseCase {
22 | return ImportDataUIBlockingUseCase(
23 | context,
24 | importRepository
25 | )
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/domain/AppLocale.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.domain
2 |
3 | // TODO Remove when reading application supported locales is possible on API < 34
4 | // for same reason [Locale] doesn't work as intended
5 | /**
6 | * Application supported locales
7 | * @param code ISO 639-2 language code of the locale
8 | * @param tag Language tag of the locale
9 | */
10 | enum class AppLocale(
11 | val code: String,
12 | val tag: String
13 | ) {
14 | PL(
15 | "pol",
16 | "pl-PL"
17 | ),
18 | EN_US(
19 | "eng",
20 | "en-US"
21 | ),
22 | TR(
23 | "tur",
24 | "tr-TR"
25 | )
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/domain/TimePeriodFlowHandler.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.domain
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.MutableState
5 | import androidx.compose.runtime.ReadOnlyComposable
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.ui.res.stringResource
9 | import com.kssidll.arru.R
10 | import kotlinx.coroutines.CoroutineScope
11 | import kotlinx.coroutines.Job
12 | import kotlinx.coroutines.flow.Flow
13 | import kotlinx.coroutines.flow.cancellable
14 | import kotlinx.coroutines.flow.distinctUntilChanged
15 | import kotlinx.coroutines.flow.flowOf
16 | import kotlinx.coroutines.launch
17 |
18 | /**
19 | * Defines available time periods and handles flow managment when changing time periods
20 | */
21 | class TimePeriodFlowHandler(
22 | private val scope: CoroutineScope,
23 | private val dayFlow: () -> Flow,
24 | private val weekFlow: () -> Flow,
25 | private val monthFlow: () -> Flow,
26 | private val yearFlow: () -> Flow,
27 | startPeriod: Periods = Periods.Month,
28 | ) {
29 | private var mCurrentPeriod: MutableState = mutableStateOf(startPeriod)
30 | val currentPeriod get() = mCurrentPeriod.value
31 |
32 | private var mSpentByTimeQuery: Job? = null
33 | private var mSpentByTimeData: MutableState> = mutableStateOf(flowOf())
34 | val spentByTimeData by mSpentByTimeData
35 |
36 | init {
37 | handlePeriodSwitch()
38 | }
39 |
40 | fun switchPeriod(newPeriod: Periods) {
41 | mCurrentPeriod.value = newPeriod
42 | handlePeriodSwitch()
43 | }
44 |
45 | private fun handlePeriodSwitch() {
46 | mSpentByTimeQuery?.cancel()
47 |
48 | mSpentByTimeQuery = scope.launch {
49 | when (currentPeriod) {
50 | Periods.Day -> mSpentByTimeData.value = dayFlow().distinctUntilChanged()
51 | .cancellable()
52 |
53 | Periods.Week -> mSpentByTimeData.value = weekFlow().distinctUntilChanged()
54 | .cancellable()
55 |
56 | Periods.Month -> mSpentByTimeData.value = monthFlow().distinctUntilChanged()
57 | .cancellable()
58 |
59 | Periods.Year -> mSpentByTimeData.value = yearFlow().distinctUntilChanged()
60 | .cancellable()
61 | }
62 | }
63 | }
64 |
65 | /**
66 | * Ordinal signifies which order they appear in on the UI
67 | */
68 | enum class Periods {
69 | Day,
70 | Week,
71 | Month,
72 | Year,
73 | }
74 | }
75 |
76 | @Composable
77 | @ReadOnlyComposable
78 | fun TimePeriodFlowHandler.Periods.getTranslation(): String {
79 | return when (this) {
80 | TimePeriodFlowHandler.Periods.Day -> stringResource(R.string.day)
81 | TimePeriodFlowHandler.Periods.Week -> stringResource(R.string.week)
82 | TimePeriodFlowHandler.Periods.Month -> stringResource(R.string.month)
83 | TimePeriodFlowHandler.Periods.Year -> stringResource(R.string.year)
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/domain/data/ChartSource.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.domain.data
2 |
3 | import com.patrykandpatrick.vico.core.entry.ChartEntry
4 | import java.util.Locale
5 |
6 | interface ChartSource: FloatSource, SortSource {
7 | fun chartEntry(x: Int): ChartEntry
8 | fun startAxisLabel(locale: Locale): String?
9 | fun topAxisLabel(locale: Locale): String?
10 | fun bottomAxisLabel(locale: Locale): String?
11 | fun endAxisLabel(locale: Locale): String?
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/domain/data/Data.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.domain.data
2 |
3 | import androidx.paging.LoadState
4 | import androidx.paging.compose.LazyPagingItems
5 |
6 | /**
7 | * A generic abstraction for repository data with loaded and loading states
8 | */
9 | sealed class Data {
10 | /**
11 | * Signifies loaded state with some contained data value
12 | */
13 | data class Loaded(val data: T): Data()
14 |
15 | /**
16 | * Signifies loading state without any contained data
17 | */
18 | class Loading(): Data()
19 | }
20 |
21 | /**
22 | * Checks whether the [Data] reported loaded state but is empty
23 | * @return true if [Data] reported loaded state but has no items, false otherwise
24 | */
25 | fun Data.loadedEmpty(): Boolean where T: List {
26 | return this is Data.Loaded && data.isEmpty()
27 | }
28 |
29 | /**
30 | * Checks whether the [Data] reported loaded state and is not empty
31 | * @return true if [Data] reported loaded state and has items, false otherwise
32 | */
33 | fun Data.loadedData(): Boolean where T: List {
34 | return this is Data.Loaded && data.isNotEmpty()
35 | }
36 |
37 | /**
38 | * Checks whether the [LazyPagingItems] reported loaded state but is empty
39 | * @return true if [LazyPagingItems] reported loaded state but has no items, false otherwise
40 | */
41 | fun LazyPagingItems.loadedEmpty(): Boolean where T: Any {
42 | return loadState.refresh is LoadState.NotLoading && itemCount == 0
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/domain/data/Field.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.domain.data
2 |
3 | import androidx.compose.material3.Text
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.res.stringResource
6 | import com.kssidll.arru.R
7 | import com.kssidll.arru.ui.theme.Typography
8 |
9 | /**
10 | * Generic abstraction for data and its status
11 | */
12 | sealed class Field(
13 | val data: T? = null,
14 | val error: FieldError? = null,
15 | ) {
16 | /**
17 | * Field status signifying that it's loaded correctly and can display and use [data]
18 | */
19 | class Loaded(data: T? = null): Field(data)
20 |
21 | /**
22 | * Field status signifying a loading process taking place, the field shouldn't be user changeable in this status
23 | */
24 | class Loading(data: T? = null): Field(data)
25 |
26 | /**
27 | * Field status signifying an error
28 | * @param error [FieldError] error to set the field to
29 | */
30 | class Error(
31 | error: FieldError? = null,
32 | data: T? = null,
33 | ): Field(
34 | data,
35 | error,
36 | )
37 |
38 | /**
39 | * @return Whether the field should be user changeable
40 | */
41 | fun isEnabled(): Boolean {
42 | return this !is Loading
43 | }
44 |
45 | /**
46 | * @return Whether the field is in an error state
47 | */
48 | fun isError(): Boolean {
49 | return this is Error
50 | }
51 |
52 | /**
53 | * @return Negation of [isError]
54 | */
55 | fun isNotError(): Boolean {
56 | return isError().not()
57 | }
58 |
59 | /**
60 | * Tries to change this field status to [Loaded]
61 | * @return This field as [Loaded] status
62 | */
63 | fun toLoaded(): Field {
64 | return Loaded(data)
65 | }
66 |
67 | /**
68 | * Tries to change this field status to [Loaded]
69 | * @return This field as [Loaded] status or as [FieldError.NoValueError] [Error] if the data is null
70 | */
71 | fun toLoadedOrError(): Field {
72 | return this.data?.let { Loaded(it) } ?: Error(FieldError.NoValueError)
73 | }
74 |
75 | /**
76 | * Changes this field status to [Loading]
77 | * @return This field as [Loading]
78 | */
79 | fun toLoading(): Loading {
80 | return Loading(this.data)
81 | }
82 |
83 | /**
84 | * Changes this field status to [Error]
85 | * @param error Error to set the field to
86 | * @return This field as [Error]
87 | */
88 | fun toError(error: FieldError? = null): Error {
89 | return Error(
90 | error,
91 | this.data
92 | )
93 | }
94 | }
95 |
96 | /**
97 | * Possible [Field] errors
98 | */
99 | sealed class FieldError {
100 | @Composable
101 | fun ErrorText() {
102 | Text(
103 | text = errorString(),
104 | style = Typography.bodySmall,
105 | )
106 | }
107 |
108 | @Composable
109 | abstract fun errorString(): String
110 |
111 | /**
112 | * Error signifying lack of value in the field
113 | */
114 | data object NoValueError: FieldError() {
115 | @Composable
116 | override fun errorString(): String {
117 | return stringResource(id = R.string.no_value_error_text)
118 | }
119 | }
120 |
121 | /**
122 | * Error signifying correct, but duplicate value that can't be used
123 | */
124 | data object DuplicateValueError: FieldError() {
125 | @Composable
126 | override fun errorString(): String {
127 | return stringResource(id = R.string.duplicate_value_error_text)
128 | }
129 | }
130 |
131 | /**
132 | * Error signifying incorrect value
133 | */
134 | data object InvalidValueError: FieldError() {
135 | @Composable
136 | override fun errorString(): String {
137 | return stringResource(id = R.string.invalid_value_error_text)
138 | }
139 | }
140 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/domain/data/FloatSource.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.domain.data
2 |
3 | import com.patrykandpatrick.vico.core.entry.ChartEntry
4 | import com.patrykandpatrick.vico.core.entry.FloatEntry
5 |
6 | interface FloatSource {
7 | fun value(): Float
8 | }
9 |
10 | fun List.avg(): Float where E: FloatSource {
11 | if (isEmpty()) return 0f
12 |
13 | var sum = 0f
14 | forEach {
15 | sum += it.value()
16 | }
17 | return sum / size
18 | }
19 |
20 | /**
21 | * assumes the list being ordered by time and creates a list representing the moving average in that time
22 | * @return list representing the moving average
23 | */
24 | fun List.movingAverage(): List where E: FloatSource {
25 | if (isEmpty()) return emptyList()
26 |
27 | val results = mutableListOf()
28 | var sum = 0f
29 | forEach { item ->
30 | sum += item.value()
31 | results.add(sum / (results.size + 1))
32 | }
33 |
34 | return results
35 |
36 | }
37 |
38 | /**
39 | * assumes the list being ordered by time and creates a list representing the moving average in that time
40 | * @return list representing the moving average
41 | */
42 | fun List.movingAverageChartData(): List where E: FloatSource {
43 | if (isEmpty()) return emptyList()
44 |
45 | return movingAverage().mapIndexed { index, median ->
46 | FloatEntry(
47 | index.toFloat(),
48 | median
49 | )
50 | }
51 | }
52 |
53 | /**
54 | * assumes the list being ordered by time and creates a list representing the moving total in that time
55 | * @return list representing the moving total
56 | */
57 | fun List.movingTotal(): List where E: FloatSource {
58 | if (isEmpty()) return emptyList()
59 |
60 | val results = mutableListOf()
61 | var sum = 0f
62 | forEach { item ->
63 | sum += item.value()
64 | results.add(sum)
65 | }
66 |
67 | return results
68 |
69 | }
70 |
71 | /**
72 | * assumes the list being ordered by time and creates a list representing the moving total in that time
73 | * @return list representing the moving total
74 | */
75 | fun List.movingTotalChartData(): List where E: FloatSource {
76 | if (isEmpty()) return emptyList()
77 |
78 | return movingTotal().mapIndexed { index, median ->
79 | FloatEntry(
80 | index.toFloat(),
81 | median
82 | )
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/domain/data/FuzzySearchSource.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.domain.data
2 |
3 | interface FuzzySearchSource {
4 | fun fuzzyScore(query: String): Int
5 | }
6 |
7 | fun List.searchSort(query: String): List where E: FuzzySearchSource {
8 | if (query.isBlank()) return this
9 |
10 | return searchSort { it.fuzzyScore(query) }
11 | }
12 |
13 | fun List.searchSort(
14 | calculateScore: (E) -> Int
15 | ): List {
16 | if (this.isEmpty()) return this
17 |
18 | return this.map {
19 | it to calculateScore(it)
20 | }
21 | .sortedByDescending { (_, score) -> score }
22 | .map { (element, _) -> element }
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/domain/data/IdentitySource.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.domain.data
2 |
3 | interface IdentitySource {
4 | fun identificator(): Long
5 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/domain/data/NameSource.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.domain.data
2 |
3 | interface NameSource {
4 | fun name(): String
5 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/domain/data/RankSource.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.domain.data
2 |
3 | import java.util.Locale
4 |
5 | interface RankSource: SortSource, FloatSource, IdentitySource {
6 | fun displayName(): String
7 | fun displayValue(locale: Locale): String
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/domain/data/SortSource.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.domain.data
2 |
3 | import com.patrykandpatrick.vico.core.entry.ChartEntry
4 | import com.patrykandpatrick.vico.core.entry.FloatEntry
5 |
6 | interface SortSource {
7 | fun sortValue(): Long
8 | }
9 |
10 | fun List.median(): Float where E: SortSource, E: FloatSource {
11 | if (isEmpty()) return 0f
12 |
13 | val sorted = sortedBy { it.sortValue() }
14 | val middle = sorted.size.div(2) - 1
15 |
16 | if (sorted.size % 2 == 1) return sorted[middle + 1].value()
17 |
18 | return (sorted[middle].value() + sorted[middle + 1].value()).div(2)
19 | }
20 |
21 | /**
22 | * assumes the list being ordered by time and creates a list representing the moving median in that time
23 | * @return list representing the moving median
24 | */
25 | fun List.movingMedian(): List where E: SortSource, E: FloatSource {
26 | if (isEmpty()) return emptyList()
27 |
28 | val results = mutableListOf()
29 | val calculationList = mutableListOf()
30 | forEach { item ->
31 | calculationList.add(item)
32 |
33 | val sorted = calculationList.sortedBy { it.sortValue() }
34 | val middle = sorted.size.div(2) - 1
35 |
36 | if (sorted.size % 2 == 1) results.add(sorted[middle + 1].value())
37 | else results.add((sorted[middle].value() + sorted[middle + 1].value()).div(2))
38 | }
39 |
40 | return results
41 |
42 | }
43 |
44 | /**
45 | * assumes the list being ordered by time and creates a list representing the moving median in that time
46 | * @return list representing the moving median
47 | */
48 | fun List.movingMedianChartData(): List where E: SortSource, E: FloatSource {
49 | if (isEmpty()) return emptyList()
50 |
51 | return movingMedian().mapIndexed { index, median ->
52 | FloatEntry(
53 | index.toFloat(),
54 | median
55 | )
56 | }
57 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/domain/usecase/ChangeDatabaseLocationUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.domain.usecase
2 |
3 | import android.content.Context
4 | import androidx.annotation.RequiresApi
5 | import com.kssidll.arru.data.preference.AppPreferences
6 | import com.kssidll.arru.data.preference.setDatabaseLocation
7 | import dagger.hilt.android.qualifiers.ApplicationContext
8 | import kotlinx.coroutines.CoroutineDispatcher
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.withContext
11 |
12 | enum class DatabaseMoveResult {
13 | SUCCESS,
14 | FAILED,
15 | REQUEST_CONFIRMATION
16 | }
17 |
18 | class ChangeDatabaseLocationUseCase(
19 | @ApplicationContext val appContext: Context,
20 | private val dispatcher: CoroutineDispatcher = Dispatchers.Default,
21 | ) {
22 | @RequiresApi(30)
23 | suspend operator fun invoke(
24 | newDatabaseLocation: AppPreferences.Database.Location.Values,
25 | force: Boolean = false
26 | ) = withContext(dispatcher) {
27 | if (newDatabaseLocation == AppPreferences.Database.Location.Values.DOWNLOADS && !force) {
28 | return@withContext DatabaseMoveResult.REQUEST_CONFIRMATION
29 | }
30 |
31 | val setLocation = AppPreferences.setDatabaseLocation(appContext, newDatabaseLocation)
32 |
33 | if (setLocation != newDatabaseLocation) {
34 | return@withContext DatabaseMoveResult.FAILED
35 | }
36 |
37 | return@withContext DatabaseMoveResult.SUCCESS
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/domain/usecase/ExportDataUIBlockingUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.domain.usecase
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import com.kssidll.arru.data.database.exportDataAsCompactCsv
6 | import com.kssidll.arru.data.database.exportDataAsJson
7 | import com.kssidll.arru.data.database.exportDataAsRawCsv
8 | import com.kssidll.arru.data.preference.AppPreferences
9 | import com.kssidll.arru.data.preference.getExportType
10 | import com.kssidll.arru.data.repository.CategoryRepositorySource
11 | import com.kssidll.arru.data.repository.ItemRepositorySource
12 | import com.kssidll.arru.data.repository.ProducerRepositorySource
13 | import com.kssidll.arru.data.repository.ProductRepositorySource
14 | import com.kssidll.arru.data.repository.ShopRepositorySource
15 | import com.kssidll.arru.data.repository.TransactionBasketRepositorySource
16 | import com.kssidll.arru.data.repository.VariantRepositorySource
17 | import dagger.hilt.android.qualifiers.ApplicationContext
18 | import kotlinx.coroutines.CoroutineDispatcher
19 | import kotlinx.coroutines.Dispatchers
20 | import kotlinx.coroutines.flow.first
21 | import kotlinx.coroutines.withContext
22 |
23 | class ExportDataUIBlockingUseCase(
24 | @ApplicationContext val appContext: Context,
25 | private val categoryRepository: CategoryRepositorySource,
26 | private val itemRepository: ItemRepositorySource,
27 | private val producerRepository: ProducerRepositorySource,
28 | private val productRepository: ProductRepositorySource,
29 | private val shopRepository: ShopRepositorySource,
30 | private val transactionRepository: TransactionBasketRepositorySource,
31 | private val variantRepository: VariantRepositorySource,
32 | private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
33 | ) {
34 | suspend operator fun invoke(
35 | uri: Uri,
36 | onMaxProgressChange: (newMaxProgress: Int) -> Unit,
37 | onProgressChange: (newProgress: Int) -> Unit,
38 | onFinished: () -> Unit,
39 | ) = withContext(dispatcher) {
40 | when (AppPreferences.getExportType(appContext).first()) {
41 | AppPreferences.Export.Type.Values.CompactCSV -> {
42 | exportDataAsCompactCsv(
43 | appContext,
44 | uri,
45 | categoryRepository,
46 | itemRepository,
47 | producerRepository,
48 | productRepository,
49 | shopRepository,
50 | transactionRepository,
51 | variantRepository,
52 | onMaxProgressChange,
53 | onProgressChange,
54 | onFinished,
55 | )
56 | }
57 |
58 | AppPreferences.Export.Type.Values.RawCSV -> {
59 | exportDataAsRawCsv(
60 | appContext,
61 | uri,
62 | categoryRepository,
63 | itemRepository,
64 | producerRepository,
65 | productRepository,
66 | shopRepository,
67 | transactionRepository,
68 | variantRepository,
69 | onMaxProgressChange,
70 | onProgressChange,
71 | onFinished,
72 | )
73 | }
74 |
75 | AppPreferences.Export.Type.Values.JSON -> {
76 | exportDataAsJson(
77 | appContext,
78 | uri,
79 | categoryRepository,
80 | itemRepository,
81 | producerRepository,
82 | productRepository,
83 | shopRepository,
84 | transactionRepository,
85 | variantRepository,
86 | onMaxProgressChange,
87 | onProgressChange,
88 | onFinished,
89 | )
90 | }
91 | }
92 | }
93 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/domain/usecase/ExportDataWithServiceUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.domain.usecase
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import com.kssidll.arru.data.preference.AppPreferences
6 | import com.kssidll.arru.data.preference.getExportType
7 | import com.kssidll.arru.service.DataExportService
8 | import dagger.hilt.android.qualifiers.ApplicationContext
9 | import kotlinx.coroutines.CoroutineDispatcher
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.flow.first
12 | import kotlinx.coroutines.withContext
13 |
14 | class ExportDataWithServiceUseCase(
15 | @ApplicationContext val appContext: Context,
16 | private val dispatcher: CoroutineDispatcher = Dispatchers.Default,
17 | ) {
18 | suspend operator fun invoke(uri: Uri) = withContext(dispatcher) {
19 | when (AppPreferences.getExportType(appContext).first()) {
20 | AppPreferences.Export.Type.Values.CompactCSV -> {
21 | DataExportService.startExportCsvCompact(
22 | appContext,
23 | uri
24 | )
25 | }
26 |
27 | AppPreferences.Export.Type.Values.RawCSV -> {
28 | DataExportService.startExportCsvRaw(
29 | appContext,
30 | uri
31 | )
32 | }
33 |
34 | AppPreferences.Export.Type.Values.JSON -> {
35 | DataExportService.startExportJson(
36 | appContext,
37 | uri
38 | )
39 | }
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/domain/usecase/ImportDataUIBlockingUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.domain.usecase
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import com.kssidll.arru.data.database.ImportError
6 | import com.kssidll.arru.data.database.importDataFromUris
7 | import com.kssidll.arru.data.repository.ImportRepositorySource
8 | import dagger.hilt.android.qualifiers.ApplicationContext
9 | import kotlinx.coroutines.CoroutineDispatcher
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.withContext
12 |
13 | class ImportDataUIBlockingUseCase(
14 | @ApplicationContext val appContext: Context,
15 | private val importRepository: ImportRepositorySource,
16 | private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
17 | ) {
18 | suspend operator fun invoke(
19 | uri: Uri,
20 | onMaxProgressChange: (newMaxProgress: Int) -> Unit,
21 | onProgressChange: (newProgress: Int) -> Unit,
22 | onFinished: () -> Unit,
23 | onError: (error: ImportError) -> Unit,
24 | ) = withContext(dispatcher) {
25 | importDataFromUris(
26 | context = appContext,
27 | uri = uri,
28 | importRepository = importRepository,
29 | onMaxProgressChange = onMaxProgressChange,
30 | onProgressChange = onProgressChange,
31 | onFinished = onFinished,
32 | onError = onError
33 | )
34 | }
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/domain/utils/Currency.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("unused")
2 |
3 | package com.kssidll.arru.domain.utils
4 |
5 | import java.text.NumberFormat
6 | import java.util.Locale
7 |
8 | fun Float.formatToCurrency(
9 | locale: Locale,
10 | dropDecimal: Boolean = false,
11 | ): String {
12 | val numberFormat = NumberFormat.getCurrencyInstance(locale)
13 |
14 | if (dropDecimal) numberFormat.maximumFractionDigits = 0
15 |
16 | return numberFormat.format(this)
17 | }
18 |
19 | fun Long.formatToCurrency(
20 | locale: Locale,
21 | ): String {
22 | return toFloat().formatToCurrency(
23 | locale,
24 | dropDecimal = true
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/helper/DataGenerators.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.helper
2 |
3 | import android.annotation.SuppressLint
4 | import java.sql.Date
5 | import java.text.SimpleDateFormat
6 | import java.util.Locale
7 | import kotlin.random.Random
8 |
9 | private val defaultTimeFrom: Long = Date.valueOf("2020-01-01").time
10 | private val defaultTimeUntil: Long = Date.valueOf("2025-12-31").time
11 | private const val defaultDateStringFormatting: String = "yyyy-MM-dd"
12 |
13 | @SuppressLint("ConstantLocale")
14 | private val defaultLocale: Locale = Locale.getDefault()
15 | private const val defaultStringLength: Int = 10
16 | private const val defaultStringLengthFrom: Int = 4
17 | private const val defaultStringLengthUntil: Int = 12
18 | private const val defaultStringAllowedCharacters: String = "pyfgcrlaoeuidhtnsqjkxbmwvz"
19 | private const val defaultLongValueFrom: Long = 10000
20 | private const val defaultLongValueUntil: Long = 100000
21 | private const val defaultFloatDivisionFactor: Long = 100
22 |
23 | fun generateRandomTime(
24 | timeFrom: Long = defaultTimeFrom,
25 | timeUntil: Long = defaultTimeUntil,
26 | ): Long {
27 | return Random.nextLong(
28 | from = (timeFrom / 86400000),
29 | until = (timeUntil / 86400000),
30 | ) * 86400000
31 | }
32 |
33 | fun generateRandomDate(
34 | timeFrom: Long = defaultTimeFrom,
35 | timeUntil: Long = defaultTimeUntil,
36 | ): Date {
37 | return Date(
38 | generateRandomTime(
39 | timeFrom,
40 | timeUntil
41 | )
42 | )
43 | }
44 |
45 | fun generateRandomDateString(
46 | timeFrom: Long = defaultTimeFrom,
47 | timeUntil: Long = defaultTimeUntil,
48 | dateFormatting: String = defaultDateStringFormatting,
49 | dateLocale: Locale = defaultLocale,
50 | ): String {
51 | return SimpleDateFormat(
52 | dateFormatting,
53 | dateLocale
54 | ).format(
55 | generateRandomDate(
56 | timeFrom,
57 | timeUntil
58 | )
59 | )
60 | }
61 |
62 | fun generateRandomStringValue(
63 | stringLength: Int = defaultStringLength,
64 | allowedCharacters: String = defaultStringAllowedCharacters,
65 | ): String {
66 | return List(stringLength) {
67 | allowedCharacters[Random.nextInt(allowedCharacters.length)]
68 | }.toCharArray()
69 | .concatToString()
70 | }
71 |
72 | fun generateRandomStringValue(
73 | stringLengthFrom: Int = defaultStringLengthFrom,
74 | stringLengthUntil: Int = defaultStringLengthUntil,
75 | allowedCharacters: String = defaultStringAllowedCharacters,
76 | ): String {
77 | return generateRandomStringValue(
78 | stringLength = Random.nextInt(
79 | stringLengthFrom,
80 | stringLengthUntil
81 | ),
82 | allowedCharacters = allowedCharacters,
83 | )
84 | }
85 |
86 | fun generateRandomLongValue(
87 | valueFrom: Long = defaultLongValueFrom,
88 | valueUntil: Long = defaultLongValueUntil,
89 | ): Long {
90 | return Random.nextLong(
91 | from = valueFrom,
92 | until = valueUntil,
93 | )
94 | }
95 |
96 | fun generateRandomFloatValue(
97 | valueFrom: Long = defaultLongValueFrom,
98 | valueUntil: Long = defaultLongValueUntil,
99 | divisionFactor: Long = defaultFloatDivisionFactor,
100 | ): Float {
101 | return Random.nextLong(
102 | from = valueFrom,
103 | until = valueUntil,
104 | )
105 | .toFloat()
106 | .div(divisionFactor)
107 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/helper/Locale.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.helper
2 |
3 | import android.content.Context
4 | import android.content.res.Configuration
5 |
6 | /**
7 | * Helper function to get localized string when getString for some reason doesn't localize it
8 | */
9 | fun Context.getLocalizedString(resourseId: Int): String {
10 | val config = resources.configuration
11 | val locale = java.util.Locale.getDefault()
12 | val newConfig = Configuration(config)
13 | newConfig.setLocale(locale)
14 | return createConfigurationContext(newConfig).getString(resourseId)
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/helper/NavigationSuiteScaffold.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.helper
2 |
3 | import androidx.compose.material3.adaptive.WindowAdaptiveInfo
4 | import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
5 | import androidx.window.core.layout.WindowWidthSizeClass
6 |
7 | object BetterNavigationSuiteScaffoldDefaults {
8 | fun calculateFromAdaptiveInfo(adaptiveInfo: WindowAdaptiveInfo): NavigationSuiteType {
9 | return with(adaptiveInfo) {
10 | if (
11 | windowPosture.isTabletop ||
12 | windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT
13 | ) {
14 | NavigationSuiteType.NavigationBar
15 | } else if (
16 | windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED ||
17 | windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.MEDIUM
18 | ) {
19 | NavigationSuiteType.NavigationRail
20 | } else {
21 | NavigationSuiteType.NavigationBar
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/helper/Permission.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.helper
2 |
3 | import android.content.Context
4 | import android.content.pm.PackageManager
5 | import androidx.core.app.ActivityCompat
6 |
7 | /**
8 | * Checks whether [permission] is granted
9 | * @param context application context
10 | * @param permission permission to check
11 | * @return whether permission is granted
12 | */
13 | fun checkPermission(
14 | context: Context,
15 | permission: String
16 | ): Boolean {
17 | return ActivityCompat.checkSelfPermission(
18 | context,
19 | permission
20 | ) == PackageManager.PERMISSION_GRANTED
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/helper/RegexHelper.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.helper
2 |
3 | sealed class RegexHelper {
4 | companion object {
5 | /**
6 | * Checks whether [value] is a representation of digits optionally divided by some other character
7 | * @param value Value to check
8 | * @param decimalPoints Optional max amount of digits after the non digit character, null means no limit
9 | * @return True if [value] is a representation of digits optionally divided by some other character, False otherwise
10 | */
11 | fun isFloat(
12 | value: String,
13 | decimalPoints: Int? = null
14 | ): Boolean {
15 | if (decimalPoints == null) {
16 | return value.matches(Regex("\\d+?\\D?\\d*"))
17 | }
18 |
19 | return value.matches(Regex("\\d+?\\D?\\d{0,${decimalPoints}}"))
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/helper/StringHelper.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.helper
2 |
3 | class StringHelper {
4 | companion object {
5 | fun toDoubleOrNull(value: String): Double? {
6 | return if (RegexHelper.isFloat(value)) {
7 | value.replace(
8 | Regex("\\D"),
9 | "."
10 | )
11 | .toDoubleOrNull()
12 | } else null
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/helper/Uri.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.helper
2 |
3 | import android.net.Uri
4 |
5 | fun getReadablePathFromUri(uri: Uri): String {
6 | val path = uri.path
7 | if (path != null) {
8 | // Extract the part after "/tree/"
9 | val treePath = path.substringAfter("/tree/", "")
10 | if (treePath.isNotEmpty()) {
11 | return treePath.replace(':', '/').substringAfterLast("//")
12 | }
13 | }
14 |
15 | // If all else fails, return the URI as a string
16 | return uri.toString()
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/service/State.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.service
2 |
3 | import android.app.ActivityManager
4 | import android.content.Context
5 | import android.content.SharedPreferences
6 | import androidx.core.content.ContextCompat
7 |
8 | /**
9 | * Helper function to remove the need to define the [Context.getSharedPreferences] mode
10 | * as the only non deprecated mode is [Context.MODE_PRIVATE]
11 | * @return [SharedPreferences]
12 | */
13 | fun Context.getSharedPreferences(name: String): SharedPreferences {
14 | return getSharedPreferences(
15 | name,
16 | Context.MODE_PRIVATE
17 | )
18 | }
19 |
20 | /**
21 | * Possible states that a service can be in
22 | */
23 | enum class ServiceState {
24 | STOPPED,
25 | STARTED
26 | }
27 |
28 | /**
29 | * Transforms the name of the service into its state key used in [SharedPreferences]
30 | */
31 | private fun serviceNameToStateKey(serviceName: String): String = "${serviceName}_STATE"
32 |
33 | /**
34 | * Sets the service state to provided [state]
35 | * @param serviceName name of the service
36 | * @param state state to set the service to
37 | */
38 | fun Context.setServiceState(
39 | serviceName: String,
40 | state: ServiceState
41 | ) {
42 | getSharedPreferences(serviceName).edit()
43 | .let {
44 | it.putString(
45 | serviceNameToStateKey(serviceName),
46 | state.name
47 | )
48 | it.commit()
49 | }
50 | }
51 |
52 | /**
53 | * Gets the service state of the service with provided [serviceName]
54 | * @param serviceName name of the service
55 | * @return [ServiceState] of the service
56 | */
57 | fun Context.getServiceState(serviceName: String): ServiceState {
58 | val value = getSharedPreferences(serviceName).getString(
59 | serviceNameToStateKey(serviceName),
60 | ServiceState.STOPPED.name
61 | )!! // it's impossible for this to be null, but kotlin LSP thinks otherwise? i'm confused
62 |
63 | return ServiceState.valueOf(value)
64 | }
65 |
66 | /**
67 | * Gets the service state of the service with provided [serviceClass]
68 | *
69 | * Checks the system instead of the shared preferences so it can't be listened on
70 | * and isn't efficient, it also relies on a deprecated method that exists only for compatibility
71 | * @param serviceClass class of the service
72 | * @return [ServiceState] of the service
73 | */
74 | // we use the deprecated method for the purpose that it's backwards compatible for
75 | @Suppress("DEPRECATION")
76 | fun Context.getServiceStateCold(serviceClass: Class<*>): ServiceState {
77 | val manager = ContextCompat.getSystemService(
78 | this,
79 | ActivityManager::class.java
80 | )
81 | for (service in manager!!.getRunningServices(Int.MAX_VALUE)) {
82 | if (serviceClass.name == service.service.className) {
83 | return ServiceState.STARTED
84 | }
85 | }
86 | return ServiceState.STOPPED
87 | }
88 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/component/dialog/MergeConfirmDialog.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.component.dialog
2 |
3 | import androidx.compose.foundation.layout.Row
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.heightIn
6 | import androidx.compose.foundation.layout.width
7 | import androidx.compose.material3.AlertDialog
8 | import androidx.compose.material3.Button
9 | import androidx.compose.material3.ButtonDefaults
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Surface
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.graphics.Color
16 | import androidx.compose.ui.res.stringResource
17 | import androidx.compose.ui.text.style.TextAlign
18 | import androidx.compose.ui.tooling.preview.PreviewLightDark
19 | import androidx.compose.ui.unit.dp
20 | import androidx.compose.ui.window.DialogProperties
21 | import com.kssidll.arru.R
22 | import com.kssidll.arru.ui.theme.ArrugarqTheme
23 | import com.kssidll.arru.ui.theme.Typography
24 |
25 | @Composable
26 | fun MergeConfirmDialog(
27 | message: String,
28 | onCancel: () -> Unit,
29 | onConfirm: () -> Unit,
30 | ) {
31 | AlertDialog(
32 | onDismissRequest = onCancel,
33 | confirmButton = {
34 | Row {
35 | Button(
36 | onClick = onCancel,
37 | colors = ButtonDefaults.buttonColors(
38 | containerColor = Color.Transparent,
39 | contentColor = MaterialTheme.colorScheme.tertiary,
40 | ),
41 | modifier = Modifier.weight(1F)
42 | ) {
43 | Text(
44 | text = stringResource(id = R.string.merge_action_cancel),
45 | style = Typography.bodyLarge
46 | )
47 | }
48 |
49 | Button(
50 | onClick = onConfirm,
51 | colors = ButtonDefaults.buttonColors(
52 | containerColor = Color.Transparent,
53 | contentColor = MaterialTheme.colorScheme.tertiary,
54 | ),
55 | modifier = Modifier.weight(1F)
56 | ) {
57 | Text(
58 | text = stringResource(id = R.string.merge_action_confirm),
59 | style = Typography.bodyLarge
60 | )
61 | }
62 |
63 | }
64 | },
65 | title = {
66 | Text(
67 | text = stringResource(id = R.string.merge_action),
68 | textAlign = TextAlign.Center,
69 | modifier = Modifier.fillMaxWidth()
70 | )
71 | },
72 | text = {
73 | Text(
74 | text = message,
75 | )
76 | },
77 | properties = DialogProperties(
78 | dismissOnClickOutside = false,
79 | ),
80 | modifier = Modifier
81 | .width(360.dp)
82 | .heightIn(min = 200.dp)
83 | )
84 | }
85 |
86 | @PreviewLightDark
87 | @Composable
88 | private fun MergeConfirmDialogPreview() {
89 | ArrugarqTheme {
90 | Surface {
91 | MergeConfirmDialog(
92 | message = "test",
93 | onCancel = {},
94 | onConfirm = {},
95 | )
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/component/list/BaseClickableListItem.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.component.list
2 |
3 | import androidx.compose.foundation.ExperimentalFoundationApi
4 | import androidx.compose.foundation.combinedClickable
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.material3.Surface
9 | import androidx.compose.material3.Text
10 | import androidx.compose.material3.minimumInteractiveComponentSize
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.semantics.Role
15 | import androidx.compose.ui.text.style.TextAlign
16 | import androidx.compose.ui.tooling.preview.PreviewLightDark
17 | import androidx.compose.ui.unit.dp
18 | import com.kssidll.arru.ui.theme.ArrugarqTheme
19 | import com.kssidll.arru.ui.theme.Typography
20 |
21 | @OptIn(ExperimentalFoundationApi::class)
22 | @Composable
23 | fun BaseClickableListItem(
24 | text: String,
25 | onClick: (() -> Unit)? = null,
26 | onClickLabel: String? = null,
27 | onLongClick: (() -> Unit)? = null,
28 | onLongClickLabel: String? = null,
29 | ) {
30 | Box(
31 | modifier = Modifier
32 | .fillMaxWidth()
33 | .minimumInteractiveComponentSize()
34 | .combinedClickable(
35 | role = Role.Button,
36 | onClick = {
37 | onClick?.invoke()
38 | },
39 | onClickLabel = onClickLabel,
40 | onLongClick = {
41 | onLongClick?.invoke()
42 | },
43 | onLongClickLabel = onLongClickLabel,
44 | )
45 | ) {
46 | Text(
47 | text = text,
48 | style = Typography.titleLarge,
49 | textAlign = TextAlign.Center,
50 | modifier = Modifier
51 | .padding(
52 | vertical = 16.dp,
53 | horizontal = 4.dp
54 | )
55 | .align(Alignment.Center)
56 | )
57 | }
58 | }
59 |
60 | @PreviewLightDark
61 | @Composable
62 | private fun BaseClickableListItemPreview() {
63 | ArrugarqTheme {
64 | Surface {
65 | BaseClickableListItem(
66 | text = "test"
67 | )
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/component/other/Loading.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.component.other
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.size
7 | import androidx.compose.material3.CircularProgressIndicator
8 | import androidx.compose.material3.MaterialTheme
9 | import androidx.compose.material3.Surface
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.tooling.preview.PreviewLightDark
14 | import androidx.compose.ui.unit.dp
15 | import com.kssidll.arru.ui.theme.ArrugarqTheme
16 |
17 | @Composable
18 | fun Loading() {
19 | Row(
20 | modifier = Modifier.fillMaxSize(),
21 | verticalAlignment = Alignment.CenterVertically,
22 | horizontalArrangement = Arrangement.Center
23 | ) {
24 | CircularProgressIndicator(
25 | color = MaterialTheme.colorScheme.onBackground,
26 | modifier = Modifier.size(150.dp),
27 | strokeWidth = 12.dp
28 | )
29 | }
30 | }
31 |
32 | @PreviewLightDark
33 | @Composable
34 | private fun LoadingPreview() {
35 | ArrugarqTheme {
36 | Surface(modifier = Modifier.fillMaxSize()) {
37 | Loading()
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/component/other/ProgressBar.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.component.other
2 |
3 |
4 | import androidx.compose.animation.core.AnimationSpec
5 | import androidx.compose.animation.core.animateFloatAsState
6 | import androidx.compose.animation.core.tween
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.layout.Box
9 | import androidx.compose.foundation.layout.fillMaxHeight
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.ShapeDefaults
13 | import androidx.compose.material3.Surface
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.LaunchedEffect
16 | import androidx.compose.runtime.getValue
17 | import androidx.compose.runtime.mutableFloatStateOf
18 | import androidx.compose.runtime.remember
19 | import androidx.compose.runtime.setValue
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.draw.clip
22 | import androidx.compose.ui.graphics.Color
23 | import androidx.compose.ui.graphics.Shape
24 | import androidx.compose.ui.tooling.preview.PreviewLightDark
25 | import com.kssidll.arru.ui.theme.ArrugarqTheme
26 |
27 | /**
28 | * A progress bar component
29 | * @param progressValue Percentage, from 0.0 to 1.0, of the progress bar to fill
30 | * @param modifier Container modifier
31 | * @param color Color of the progress bar
32 | * @param shape Shape of the progress bar
33 | * @param animationSpec Animation to use to change the progress percentage value throught time. Tween will be used as default
34 | */
35 | @Composable
36 | fun ProgressBar(
37 | progressValue: Float,
38 | modifier: Modifier = Modifier,
39 | color: Color = MaterialTheme.colorScheme.primary,
40 | shape: Shape = ShapeDefaults.Medium,
41 | animationSpec: AnimationSpec = tween(1200),
42 | ) {
43 | var targetValue by remember { mutableFloatStateOf(0F) }
44 |
45 | LaunchedEffect(progressValue) {
46 | targetValue = progressValue
47 | }
48 |
49 | val animatedValue by animateFloatAsState(
50 | targetValue = targetValue,
51 | animationSpec = animationSpec,
52 | label = "Progress bar value animation"
53 | )
54 |
55 | Box(
56 | modifier = modifier
57 | .clip(shape)
58 | ) {
59 | Box(
60 | modifier = Modifier
61 | .fillMaxHeight()
62 | .fillMaxWidth(animatedValue)
63 | .clip(shape)
64 | .background(color)
65 | )
66 | }
67 | }
68 |
69 |
70 | @PreviewLightDark
71 | @Composable
72 | private fun ProgressBarPreview() {
73 | ArrugarqTheme {
74 | Surface {
75 | ProgressBar(
76 | progressValue = 0.7f,
77 | )
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/component/other/SecondaryAppBar.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.component.other
2 |
3 | import androidx.compose.foundation.layout.RowScope
4 | import androidx.compose.foundation.layout.WindowInsets
5 | import androidx.compose.foundation.layout.size
6 | import androidx.compose.material.icons.Icons
7 | import androidx.compose.material.icons.automirrored.rounded.ArrowBack
8 | import androidx.compose.material3.CenterAlignedTopAppBar
9 | import androidx.compose.material3.ExperimentalMaterial3Api
10 | import androidx.compose.material3.Icon
11 | import androidx.compose.material3.IconButton
12 | import androidx.compose.material3.MaterialTheme
13 | import androidx.compose.material3.Surface
14 | import androidx.compose.material3.Text
15 | import androidx.compose.material3.TopAppBarDefaults
16 | import androidx.compose.material3.TopAppBarScrollBehavior
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.res.stringResource
20 | import androidx.compose.ui.tooling.preview.PreviewLightDark
21 | import androidx.compose.ui.unit.dp
22 | import com.kssidll.arru.ui.theme.ArrugarqTheme
23 |
24 | @OptIn(ExperimentalMaterial3Api::class)
25 | @Composable
26 | fun SecondaryAppBar(
27 | onBack: () -> Unit,
28 | title: @Composable () -> Unit,
29 | modifier: Modifier = Modifier,
30 | actions: @Composable RowScope.() -> Unit = {},
31 | windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
32 | scrollBehavior: TopAppBarScrollBehavior? = null,
33 | ) {
34 | CenterAlignedTopAppBar(
35 | title = title,
36 | modifier = modifier,
37 | navigationIcon = {
38 | IconButton(
39 | onClick = onBack
40 | ) {
41 | Icon(
42 | imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
43 | contentDescription = stringResource(com.kssidll.arru.R.string.navigate_to_previous_screen),
44 | modifier = Modifier.size(30.dp),
45 | )
46 | }
47 | },
48 | actions = actions,
49 | windowInsets = windowInsets,
50 | colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
51 | containerColor = MaterialTheme.colorScheme.surfaceContainer,
52 | scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer,
53 | navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
54 | titleContentColor = MaterialTheme.colorScheme.onSurface,
55 | actionIconContentColor = MaterialTheme.colorScheme.onSurface,
56 | ),
57 | scrollBehavior = scrollBehavior,
58 | )
59 | }
60 |
61 | @OptIn(ExperimentalMaterial3Api::class)
62 | @PreviewLightDark
63 | @Composable
64 | private fun SecondaryAppBarPreview() {
65 | ArrugarqTheme {
66 | Surface {
67 | SecondaryAppBar(
68 | onBack = {},
69 | title = {
70 | Text(text = "test")
71 | },
72 | )
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/backups/BackupsRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.backups
2 |
3 | import androidx.compose.runtime.Composable
4 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
5 |
6 | @Composable
7 | fun BackupsRoute(
8 | navigateBack: () -> Unit,
9 | viewModel: BackupsViewModel = hiltViewModel()
10 | ) {
11 | BackupsScreen(
12 | createBackup = {
13 | viewModel.createDbBackup()
14 | },
15 | loadBackup = {
16 | viewModel.loadDbBackup(it)
17 | },
18 | deleteBackup = {
19 | viewModel.deleteDbBackup(it)
20 | },
21 | toggleLockBackup = {
22 | viewModel.toggleLockDbBackup(it)
23 | },
24 | availableBackups = viewModel.availableBackups.toList(),
25 | onBack = navigateBack,
26 | )
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/backups/BackupsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.backups
2 |
3 | import android.content.Context
4 | import androidx.compose.runtime.mutableStateListOf
5 | import androidx.compose.runtime.snapshots.SnapshotStateList
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import com.kssidll.arru.data.data.DatabaseBackup
9 | import com.kssidll.arru.data.database.AppDatabase
10 | import com.kssidll.arru.data.repository.TransactionBasketRepositorySource
11 | import com.kssidll.arru.domain.data.Data
12 | import dagger.hilt.android.lifecycle.HiltViewModel
13 | import dagger.hilt.android.qualifiers.ApplicationContext
14 | import kotlinx.coroutines.Dispatchers
15 | import kotlinx.coroutines.invoke
16 | import kotlinx.coroutines.launch
17 | import javax.inject.Inject
18 |
19 | @HiltViewModel
20 | class BackupsViewModel @Inject constructor(
21 | @ApplicationContext private val context: Context,
22 | private val transactionBasketRepository: TransactionBasketRepositorySource,
23 | ): ViewModel() {
24 | val availableBackups: SnapshotStateList = mutableStateListOf()
25 |
26 | init {
27 | viewModelScope.launch {
28 | refreshAvailableBackups()
29 | }
30 | }
31 |
32 | /**
33 | * Refreshes the available backups list to represent currently available backups
34 | */
35 | private suspend fun refreshAvailableBackups() {
36 | val newAvailableBackups = AppDatabase.availableBackups(context)
37 |
38 | availableBackups.clear()
39 | availableBackups.addAll(newAvailableBackups)
40 | }
41 |
42 | private fun lockDbBackup(dbBackup: DatabaseBackup) = viewModelScope.launch {
43 | AppDatabase.lockDbBackup(dbBackup)
44 |
45 | refreshAvailableBackups()
46 | }
47 |
48 | private fun unlockDbBackup(dbBackup: DatabaseBackup) = viewModelScope.launch {
49 | AppDatabase.unlockDbBackup(dbBackup)
50 |
51 | refreshAvailableBackups()
52 | }
53 |
54 | fun toggleLockDbBackup(dbBackup: DatabaseBackup) = viewModelScope.launch {
55 | if (dbBackup.locked) {
56 | unlockDbBackup(dbBackup)
57 | } else {
58 | lockDbBackup(dbBackup)
59 | }
60 | }
61 |
62 | /**
63 | * Creates a backup of current database
64 | */
65 | fun createDbBackup() = viewModelScope.launch {
66 | Dispatchers.IO.invoke {
67 | // TODO add notification when you create maybe?
68 | val totalTransactions = transactionBasketRepository.count()
69 | val totalSpending = transactionBasketRepository.totalSpentLong()
70 |
71 | if (totalSpending is Data.Loaded) {
72 | AppDatabase.saveDbBackup(
73 | context = context,
74 | totalTransactions = totalTransactions,
75 | totalSpending = totalSpending.data ?: 0,
76 | )
77 | }
78 | refreshAvailableBackups()
79 | }
80 | }
81 |
82 | /**
83 | * Loads a backup of the database
84 | * @param dbFile Database file to load
85 | */
86 | fun loadDbBackup(dbFile: DatabaseBackup) = viewModelScope.launch {
87 | AppDatabase.loadDbBackup(
88 | context,
89 | dbFile,
90 | )
91 | }
92 |
93 | /**
94 | * Removes a backup of the database
95 | * @param dbFile Database file to remove
96 | */
97 | fun deleteDbBackup(dbFile: DatabaseBackup) = viewModelScope.launch {
98 | AppDatabase.deleteDbBackup(dbFile)
99 | refreshAvailableBackups()
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/display/category/CategoryRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.display.category
2 |
3 |
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.LaunchedEffect
6 | import androidx.compose.runtime.collectAsState
7 | import androidx.paging.compose.collectAsLazyPagingItems
8 | import com.kssidll.arru.domain.data.Data
9 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
10 |
11 | @Composable
12 | fun CategoryRoute(
13 | categoryId: Long,
14 | navigateBack: () -> Unit,
15 | navigateCategoryEdit: () -> Unit,
16 | navigateProduct: (productId: Long) -> Unit,
17 | navigateItemEdit: (itemId: Long) -> Unit,
18 | navigateProducer: (producerId: Long) -> Unit,
19 | navigateShop: (shopId: Long) -> Unit,
20 | ) {
21 | val viewModel: CategoryViewModel = hiltViewModel()
22 |
23 | LaunchedEffect(categoryId) {
24 | if (!viewModel.performDataUpdate(categoryId)) {
25 | navigateBack()
26 | }
27 | }
28 |
29 | CategoryScreen(
30 | onBack = navigateBack,
31 | category = viewModel.category,
32 | transactionItems = viewModel.transactions()
33 | .collectAsLazyPagingItems(),
34 | spentByTimeData = viewModel.spentByTimeData?.collectAsState(initial = Data.Loading())?.value
35 | ?: Data.Loaded(emptyList()),
36 | totalSpentData = viewModel.categoryTotalSpent()
37 | ?.collectAsState(initial = Data.Loading())?.value ?: Data.Loading(),
38 | spentByTimePeriod = viewModel.spentByTimePeriod,
39 | onSpentByTimePeriodSwitch = {
40 | viewModel.switchPeriod(it)
41 | },
42 | chartEntryModelProducer = viewModel.chartEntryModelProducer,
43 | onItemClick = navigateProduct,
44 | onItemProducerClick = navigateProducer,
45 | onItemShopClick = navigateShop,
46 | onItemLongClick = navigateItemEdit,
47 | onEditAction = navigateCategoryEdit,
48 | )
49 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/display/producer/ProducerRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.display.producer
2 |
3 |
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.LaunchedEffect
6 | import androidx.compose.runtime.collectAsState
7 | import androidx.paging.compose.collectAsLazyPagingItems
8 | import com.kssidll.arru.domain.data.Data
9 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
10 |
11 | @Composable
12 | fun ProducerRoute(
13 | producerId: Long,
14 | navigateBack: () -> Unit,
15 | navigateProduct: (productId: Long) -> Unit,
16 | navigateCategory: (categoryId: Long) -> Unit,
17 | navigateShop: (shopId: Long) -> Unit,
18 | navigateItemEdit: (itemId: Long) -> Unit,
19 | navigateProducerEdit: () -> Unit,
20 | ) {
21 | val viewModel: ProducerViewModel = hiltViewModel()
22 |
23 | LaunchedEffect(producerId) {
24 | if (!viewModel.performDataUpdate(producerId)) {
25 | navigateBack()
26 | }
27 | }
28 |
29 | ProducerScreen(
30 | onBack = navigateBack,
31 | producer = viewModel.producer,
32 | transactionItems = viewModel.transactions()
33 | .collectAsLazyPagingItems(),
34 | spentByTimeData = viewModel.spentByTimeData?.collectAsState(initial = Data.Loading())?.value
35 | ?: Data.Loaded(emptyList()),
36 | totalSpentData = viewModel.producerTotalSpent()
37 | ?.collectAsState(initial = Data.Loading())?.value ?: Data.Loaded(0f),
38 | spentByTimePeriod = viewModel.spentByTimePeriod,
39 | onSpentByTimePeriodSwitch = {
40 | viewModel.switchPeriod(it)
41 | },
42 | chartEntryModelProducer = viewModel.chartEntryModelProducer,
43 | onItemClick = navigateProduct,
44 | onItemCategoryClick = navigateCategory,
45 | onItemShopClick = navigateShop,
46 | onItemLongClick = navigateItemEdit,
47 | onEditAction = navigateProducerEdit,
48 | )
49 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/display/product/ProductRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.display.product
2 |
3 |
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.LaunchedEffect
6 | import androidx.compose.runtime.collectAsState
7 | import androidx.paging.compose.collectAsLazyPagingItems
8 | import com.kssidll.arru.domain.data.Data
9 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
10 |
11 | @Composable
12 | fun ProductRoute(
13 | productId: Long,
14 | navigateBack: () -> Unit,
15 | navigateCategory: (categoryId: Long) -> Unit,
16 | navigateProducer: (producerId: Long) -> Unit,
17 | navigateShop: (shopId: Long) -> Unit,
18 | navigateItemEdit: (itemId: Long) -> Unit,
19 | navigateProductEdit: () -> Unit,
20 | ) {
21 | val viewModel: ProductViewModel = hiltViewModel()
22 |
23 | LaunchedEffect(productId) {
24 | if (!viewModel.performDataUpdate(productId)) {
25 | navigateBack()
26 | }
27 | }
28 |
29 | ProductScreen(
30 | onBack = navigateBack,
31 | product = viewModel.product,
32 | transactionItems = viewModel.transactions()
33 | .collectAsLazyPagingItems(),
34 | spentByTimeData = viewModel.spentByTimeData?.collectAsState(initial = Data.Loading())?.value
35 | ?: Data.Loaded(emptyList()),
36 | productPriceByShopByTimeData = viewModel.productPriceByShop()
37 | ?.collectAsState(initial = Data.Loading())?.value ?: Data.Loaded(emptyList()),
38 | totalSpentData = viewModel.productTotalSpent()
39 | ?.collectAsState(initial = Data.Loading())?.value ?: Data.Loaded(0F),
40 | spentByTimePeriod = viewModel.spentByTimePeriod,
41 | onSpentByTimePeriodSwitch = {
42 | viewModel.switchPeriod(it)
43 | },
44 | chartEntryModelProducer = viewModel.chartEntryModelProducer,
45 | onItemCategoryClick = navigateCategory,
46 | onItemProducerClick = navigateProducer,
47 | onItemShopClick = navigateShop,
48 | onItemLongClick = navigateItemEdit,
49 | onEditAction = navigateProductEdit,
50 | )
51 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/display/shop/ShopRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.display.shop
2 |
3 |
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.LaunchedEffect
6 | import androidx.compose.runtime.collectAsState
7 | import androidx.paging.compose.collectAsLazyPagingItems
8 | import com.kssidll.arru.domain.data.Data
9 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
10 |
11 | @Composable
12 | fun ShopRoute(
13 | shopId: Long,
14 | navigateBack: () -> Unit,
15 | navigateProduct: (productId: Long) -> Unit,
16 | navigateCategory: (categoryId: Long) -> Unit,
17 | navigateProducer: (producerId: Long) -> Unit,
18 | navigateItemEdit: (itemId: Long) -> Unit,
19 | navigateShopEdit: () -> Unit,
20 | ) {
21 | val viewModel: ShopViewModel = hiltViewModel()
22 |
23 | LaunchedEffect(shopId) {
24 | if (!viewModel.performDataUpdate(shopId)) {
25 | navigateBack()
26 | }
27 | }
28 |
29 | ShopScreen(
30 | onBack = navigateBack,
31 | shop = viewModel.shop,
32 | transactionItems = viewModel.transactions()
33 | .collectAsLazyPagingItems(),
34 | spentByTimeData = viewModel.spentByTimeData?.collectAsState(initial = Data.Loading())?.value
35 | ?: Data.Loaded(emptyList()),
36 | totalSpentData = viewModel.shopTotalSpent()
37 | ?.collectAsState(initial = Data.Loading())?.value ?: Data.Loaded(0F),
38 | spentByTimePeriod = viewModel.spentByTimePeriod,
39 | onSpentByTimePeriodSwitch = {
40 | viewModel.switchPeriod(it)
41 | },
42 | chartEntryModelProducer = viewModel.chartEntryModelProducer,
43 | onItemClick = navigateProduct,
44 | onItemCategoryClick = navigateCategory,
45 | onItemProducerClick = navigateProducer,
46 | onItemLongClick = navigateItemEdit,
47 | onEditAction = navigateShopEdit,
48 | )
49 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/display/transaction/TransactionRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.display.transaction
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.collectAsState
5 | import com.kssidll.arru.domain.data.Data
6 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
7 |
8 | @Composable
9 | fun TransactionRoute(
10 | transactionId: Long,
11 | navigateBack: () -> Unit,
12 | navigateTransactionEdit: (transactionId: Long) -> Unit,
13 | navigateItemAdd: (transactionId: Long) -> Unit,
14 | navigateProduct: (productId: Long) -> Unit,
15 | navigateItemEdit: (itemId: Long) -> Unit,
16 | navigateCategory: (categoryId: Long) -> Unit,
17 | navigateProducer: (producerId: Long) -> Unit,
18 | navigateShop: (shopId: Long) -> Unit,
19 | ) {
20 | val viewModel: TransactionViewModel = hiltViewModel()
21 |
22 | TransactionScreen(
23 | onBack = navigateBack,
24 | transaction = viewModel.transaction(transactionId)
25 | .collectAsState(initial = Data.Loading()).value,
26 | onEditAction = {
27 | navigateTransactionEdit(transactionId)
28 | },
29 | onItemAddClick = navigateItemAdd,
30 | onItemClick = navigateProduct,
31 | onItemLongClick = navigateItemEdit,
32 | onItemCategoryClick = navigateCategory,
33 | onItemProducerClick = navigateProducer,
34 | onItemShopClick = navigateShop,
35 | )
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/display/transaction/TransactionViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.display.transaction
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.kssidll.arru.data.data.TransactionBasketWithItems
5 | import com.kssidll.arru.data.repository.TransactionBasketRepositorySource
6 | import com.kssidll.arru.domain.data.Data
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import kotlinx.coroutines.flow.Flow
9 | import javax.inject.Inject
10 |
11 | @HiltViewModel
12 | class TransactionViewModel @Inject constructor(
13 | private val transactionRepository: TransactionBasketRepositorySource,
14 | ): ViewModel() {
15 | fun transaction(transactionId: Long): Flow> {
16 | return transactionRepository.transactionBasketWithItemsFlow(transactionId)
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/home/component/AnalysisDateHeader.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.home.component
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.size
8 | import androidx.compose.foundation.layout.widthIn
9 | import androidx.compose.material.icons.Icons
10 | import androidx.compose.material.icons.filled.KeyboardDoubleArrowLeft
11 | import androidx.compose.material.icons.filled.KeyboardDoubleArrowRight
12 | import androidx.compose.material3.Icon
13 | import androidx.compose.material3.IconButton
14 | import androidx.compose.material3.Surface
15 | import androidx.compose.material3.Text
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.tooling.preview.PreviewLightDark
20 | import androidx.compose.ui.unit.dp
21 | import com.kssidll.arru.ui.theme.ArrugarqTheme
22 | import com.kssidll.arru.ui.theme.Typography
23 | import java.text.SimpleDateFormat
24 | import java.util.Calendar
25 | import java.util.Locale
26 |
27 | /**
28 | * @param year Year for which the main data is fetched
29 | * @param month Month for which the main data is fetched, in range of 1 - 12
30 | * @param onMonthIncrement Callback called to request [month] increment, should handle overflow and increase year
31 | * @param onMonthDecrement Callback called to request [month] decrement, should handle underflow and decrease year
32 | */
33 | @Composable
34 | fun AnalysisDateHeader(
35 | year: Int,
36 | month: Int,
37 | onMonthIncrement: () -> Unit,
38 | onMonthDecrement: () -> Unit,
39 | modifier: Modifier = Modifier
40 | ) {
41 | Row(
42 | horizontalArrangement = Arrangement.Center,
43 | verticalAlignment = Alignment.CenterVertically,
44 | modifier = modifier.fillMaxWidth()
45 | ) {
46 | val cal = Calendar.getInstance()
47 | cal.clear()
48 | cal.set(
49 | Calendar.MONTH,
50 | month - 1
51 | ) // calendar has 0 - 11 months
52 |
53 | IconButton(onClick = onMonthDecrement) {
54 | Icon(
55 | imageVector = Icons.Default.KeyboardDoubleArrowLeft,
56 | contentDescription = null,
57 | modifier = Modifier.size(30.dp)
58 | )
59 | }
60 |
61 | Column(
62 | horizontalAlignment = Alignment.CenterHorizontally,
63 | modifier = Modifier.widthIn(min = 188.dp)
64 | ) {
65 | Text(
66 | text = SimpleDateFormat(
67 | "LLLL",
68 | Locale.getDefault()
69 | ).format(cal.time)
70 | .replaceFirstChar { it.titlecase() },
71 | style = Typography.headlineLarge,
72 | )
73 | Text(
74 | text = year.toString(),
75 | style = Typography.titleMedium,
76 | )
77 | }
78 |
79 | IconButton(onClick = onMonthIncrement) {
80 | Icon(
81 | imageVector = Icons.Default.KeyboardDoubleArrowRight,
82 | contentDescription = null,
83 | modifier = Modifier.size(30.dp)
84 | )
85 | }
86 | }
87 | }
88 |
89 | @PreviewLightDark
90 | @Composable
91 | private fun AnalysisDateHeaderPreview() {
92 | ArrugarqTheme {
93 | Surface {
94 | AnalysisDateHeader(
95 | year = 2021,
96 | month = 12,
97 | onMonthIncrement = {},
98 | onMonthDecrement = {},
99 | )
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/category/ModifyCategoryViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.category
2 |
3 | import androidx.compose.runtime.MutableState
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.lifecycle.ViewModel
6 | import com.kssidll.arru.data.repository.CategoryRepositorySource
7 | import com.kssidll.arru.domain.data.Field
8 | import com.kssidll.arru.ui.screen.modify.ModifyScreenState
9 |
10 | /**
11 | * Base [ViewModel] class for Category modification view models
12 | * @property screenState A [ModifyCategoryScreenState] instance to use as screen state representation
13 | */
14 | abstract class ModifyCategoryViewModel: ViewModel() {
15 | protected abstract val categoryRepository: CategoryRepositorySource
16 | internal val screenState: ModifyCategoryScreenState = ModifyCategoryScreenState()
17 | }
18 |
19 | /**
20 | * Data representing [ModifyCategoryScreenImpl] screen state
21 | */
22 | data class ModifyCategoryScreenState(
23 | val name: MutableState> = mutableStateOf(Field.Loaded(String())),
24 | ): ModifyScreenState()
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/category/addcategory/AddCategoryRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.category.addcategory
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.compose.runtime.rememberCoroutineScope
6 | import com.kssidll.arru.domain.data.Field
7 | import com.kssidll.arru.ui.screen.modify.category.ModifyCategoryScreenImpl
8 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
9 | import kotlinx.coroutines.launch
10 |
11 | @Composable
12 | fun AddCategoryRoute(
13 | defaultName: String?,
14 | navigateBack: (categoryId: Long?) -> Unit,
15 | ) {
16 | val scope = rememberCoroutineScope()
17 | val viewModel: AddCategoryViewModel = hiltViewModel()
18 |
19 | LaunchedEffect(Unit) {
20 | viewModel.screenState.name.value = Field.Loaded(defaultName)
21 | }
22 |
23 | ModifyCategoryScreenImpl(
24 | onBack = {
25 | navigateBack(null)
26 | },
27 | state = viewModel.screenState,
28 | onSubmit = {
29 | scope.launch {
30 | val result = viewModel.addCategory()
31 | if (result.isNotError()) {
32 | navigateBack(result.id)
33 | }
34 | }
35 | },
36 | )
37 | }
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/category/addcategory/AddCategoryViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.category.addcategory
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import com.kssidll.arru.data.repository.CategoryRepositorySource
5 | import com.kssidll.arru.data.repository.CategoryRepositorySource.Companion.InsertResult
6 | import com.kssidll.arru.domain.data.FieldError
7 | import com.kssidll.arru.ui.screen.modify.category.ModifyCategoryViewModel
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import kotlinx.coroutines.async
10 | import javax.inject.Inject
11 |
12 | @HiltViewModel
13 | class AddCategoryViewModel @Inject constructor(
14 | override val categoryRepository: CategoryRepositorySource,
15 | ): ModifyCategoryViewModel() {
16 |
17 | /**
18 | * Tries to add a product category to the repository
19 | * @return resulting [InsertResult]
20 | */
21 | suspend fun addCategory(): InsertResult = viewModelScope.async {
22 | screenState.attemptedToSubmit.value = true
23 |
24 | val result = categoryRepository.insert(screenState.name.value.data.orEmpty())
25 |
26 | if (result.isError()) {
27 | when (result.error!!) {
28 | is InsertResult.InvalidName -> {
29 | screenState.name.apply {
30 | value = value.toError(FieldError.InvalidValueError)
31 | }
32 | }
33 |
34 | is InsertResult.DuplicateName -> {
35 | screenState.name.apply {
36 | value = value.toError(FieldError.DuplicateValueError)
37 | }
38 | }
39 | }
40 | }
41 |
42 | return@async result
43 | }
44 | .await()
45 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/category/editcategory/EditCategoryRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.category.editcategory
2 |
3 |
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.LaunchedEffect
6 | import androidx.compose.runtime.rememberCoroutineScope
7 | import androidx.compose.ui.res.stringResource
8 | import com.kssidll.arru.R
9 | import com.kssidll.arru.ui.screen.modify.category.ModifyCategoryScreenImpl
10 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
11 | import kotlinx.coroutines.launch
12 |
13 | @Composable
14 | fun EditCategoryRoute(
15 | categoryId: Long,
16 | navigateBack: () -> Unit,
17 | navigateBackDelete: () -> Unit,
18 | ) {
19 | val scope = rememberCoroutineScope()
20 |
21 | val viewModel: EditCategoryViewModel = hiltViewModel()
22 |
23 | LaunchedEffect(categoryId) {
24 | if (!viewModel.updateState(categoryId)) {
25 | navigateBack()
26 | }
27 | }
28 |
29 | ModifyCategoryScreenImpl(
30 | onBack = navigateBack,
31 | state = viewModel.screenState,
32 | onSubmit = {
33 | scope.launch {
34 | if (viewModel.updateCategory(categoryId)
35 | .isNotError()
36 | ) {
37 | navigateBack()
38 | }
39 | }
40 | },
41 | onDelete = {
42 | scope.launch {
43 | if (viewModel.deleteCategory(categoryId)
44 | .isNotError()
45 | ) {
46 | navigateBackDelete()
47 | }
48 | }
49 | },
50 | onMerge = {
51 | scope.launch {
52 | if (viewModel.mergeWith(it)
53 | .isNotError()
54 | ) {
55 | navigateBackDelete()
56 | }
57 | }
58 | },
59 | mergeCandidates = viewModel.allMergeCandidates(categoryId),
60 | mergeConfirmMessageTemplate = stringResource(id = R.string.merge_action_message_template)
61 | .replace(
62 | "{value_1}",
63 | viewModel.mergeMessageCategoryName
64 | ),
65 |
66 | chosenMergeCandidate = viewModel.chosenMergeCandidate.value,
67 | onChosenMergeCandidateChange = {
68 | viewModel.chosenMergeCandidate.apply { value = it }
69 | },
70 | showMergeConfirmDialog = viewModel.showMergeConfirmDialog.value,
71 | onShowMergeConfirmDialogChange = {
72 | viewModel.showMergeConfirmDialog.apply { value = it }
73 | },
74 | submitButtonText = stringResource(id = R.string.item_product_category_edit),
75 | )
76 | }
77 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/item/additem/AddItemRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.item.additem
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.compose.runtime.collectAsState
6 | import androidx.compose.runtime.rememberCoroutineScope
7 | import com.kssidll.arru.domain.data.Data
8 | import com.kssidll.arru.ui.screen.modify.item.ModifyItemScreenImpl
9 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
10 | import kotlinx.coroutines.launch
11 |
12 | @Composable
13 | fun AddItemRoute(
14 | transactionId: Long,
15 | navigateBack: () -> Unit,
16 | navigateProductAdd: (query: String?) -> Unit,
17 | navigateVariantAdd: (productId: Long, query: String?) -> Unit,
18 | navigateProductEdit: (productId: Long) -> Unit,
19 | navigateVariantEdit: (variantId: Long) -> Unit,
20 | providedProductId: Long?,
21 | providedVariantId: Long?,
22 | ) {
23 | val scope = rememberCoroutineScope()
24 | val viewModel: AddItemViewModel = hiltViewModel()
25 |
26 | LaunchedEffect(
27 | providedProductId,
28 | providedVariantId
29 | ) {
30 | viewModel.setSelectedProductToProvided(
31 | providedProductId,
32 | providedVariantId
33 | )
34 | }
35 |
36 | ModifyItemScreenImpl(
37 | onBack = navigateBack,
38 | state = viewModel.screenState,
39 | products = viewModel.allProducts()
40 | .collectAsState(initial = Data.Loading()).value,
41 | variants = viewModel.productVariants.collectAsState(initial = Data.Loading()).value,
42 | onNewProductSelected = {
43 | scope.launch {
44 | viewModel.onNewProductSelected(it)
45 | }
46 | },
47 | onNewVariantSelected = {
48 | viewModel.onNewVariantSelected(it)
49 | },
50 | onSubmit = {
51 | scope.launch {
52 | if (viewModel.addItem(transactionId)
53 | .isNotError()
54 | ) {
55 | navigateBack()
56 | }
57 | }
58 | },
59 | onProductAddButtonClick = navigateProductAdd,
60 | onVariantAddButtonClick = navigateVariantAdd,
61 | onItemLongClick = navigateProductEdit,
62 | onItemVariantLongClick = navigateVariantEdit,
63 | )
64 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/item/additem/AddItemViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.item.additem
2 |
3 | import android.util.Log
4 | import androidx.lifecycle.viewModelScope
5 | import com.kssidll.arru.data.data.Item
6 | import com.kssidll.arru.data.data.TransactionBasket
7 | import com.kssidll.arru.data.repository.ItemRepositorySource
8 | import com.kssidll.arru.data.repository.ItemRepositorySource.Companion.InsertResult
9 | import com.kssidll.arru.data.repository.ProductRepositorySource
10 | import com.kssidll.arru.data.repository.VariantRepositorySource
11 | import com.kssidll.arru.domain.data.FieldError
12 | import com.kssidll.arru.ui.screen.modify.item.ModifyItemViewModel
13 | import dagger.hilt.android.lifecycle.HiltViewModel
14 | import kotlinx.coroutines.async
15 | import javax.inject.Inject
16 |
17 | @HiltViewModel
18 | class AddItemViewModel @Inject constructor(
19 | override val itemRepository: ItemRepositorySource,
20 | override val productRepository: ProductRepositorySource,
21 | override val variantsRepository: VariantRepositorySource,
22 | ): ModifyItemViewModel() {
23 |
24 | init {
25 | loadLastItem()
26 | }
27 |
28 | /**
29 | * Tries to add an item to the repository
30 | * @param transactionId id of the [TransactionBasket] to add the item to
31 | * @return resulting [InsertResult]
32 | */
33 | suspend fun addItem(transactionId: Long) = viewModelScope.async {
34 | screenState.attemptedToSubmit.value = true
35 |
36 | val result = itemRepository.insert(
37 | transactionId = transactionId,
38 | productId = screenState.selectedProduct.value.data?.id ?: Item.INVALID_PRODUCT_ID,
39 | variantId = screenState.selectedVariant.value.data?.id,
40 | quantity = screenState.quantity.value.data?.let { Item.quantityFromString(it) }
41 | ?: Item.INVALID_QUANTITY,
42 | price = screenState.price.value.data?.let { Item.priceFromString(it) }
43 | ?: Item.INVALID_PRICE,
44 | )
45 |
46 | if (result.isError()) {
47 | when (result.error!!) {
48 | is InsertResult.InvalidTransactionId -> {
49 | Log.e(
50 | "InvalidId",
51 | "Tried inserting an item to a transaction that doesn't exist in AddItemViewModel"
52 | )
53 | return@async InsertResult.Success(-1)
54 | }
55 |
56 | is InsertResult.InvalidProductId -> {
57 | screenState.selectedProduct.apply {
58 | value = value.toError(FieldError.InvalidValueError)
59 | }
60 | }
61 |
62 | is InsertResult.InvalidVariantId -> {
63 | screenState.selectedVariant.apply {
64 | value = value.toError(FieldError.InvalidValueError)
65 | }
66 | }
67 |
68 | is InsertResult.InvalidQuantity -> {
69 | screenState.quantity.apply {
70 | value = value.toError(FieldError.InvalidValueError)
71 | }
72 | }
73 |
74 | is InsertResult.InvalidPrice -> {
75 | screenState.price.apply {
76 | value = value.toError(FieldError.InvalidValueError)
77 | }
78 | }
79 | }
80 | }
81 |
82 | return@async result
83 | }
84 | .await()
85 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/item/edititem/EditItemRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.item.edititem
2 |
3 |
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.LaunchedEffect
6 | import androidx.compose.runtime.collectAsState
7 | import androidx.compose.runtime.rememberCoroutineScope
8 | import androidx.compose.ui.res.stringResource
9 | import com.kssidll.arru.R
10 | import com.kssidll.arru.domain.data.Data
11 | import com.kssidll.arru.ui.screen.modify.item.ModifyItemScreenImpl
12 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
13 | import kotlinx.coroutines.launch
14 |
15 | @Composable
16 | fun EditItemRoute(
17 | itemId: Long,
18 | navigateBack: () -> Unit,
19 | navigateBackDelete: () -> Unit,
20 | navigateProductAdd: (query: String?) -> Unit,
21 | navigateVariantAdd: (productId: Long, query: String?) -> Unit,
22 | navigateProductEdit: (productId: Long) -> Unit,
23 | navigateVariantEdit: (variantId: Long) -> Unit,
24 | providedProductId: Long?,
25 | providedVariantId: Long?,
26 | ) {
27 | val scope = rememberCoroutineScope()
28 |
29 | val viewModel: EditItemViewModel = hiltViewModel()
30 |
31 | LaunchedEffect(itemId) {
32 | if (!viewModel.updateState(itemId)) {
33 | navigateBack()
34 | }
35 | }
36 |
37 | LaunchedEffect(
38 | providedProductId,
39 | providedVariantId
40 | ) {
41 | viewModel.setSelectedProductToProvided(
42 | providedProductId,
43 | providedVariantId
44 | )
45 | }
46 |
47 | ModifyItemScreenImpl(
48 | onBack = navigateBack,
49 | state = viewModel.screenState,
50 | products = viewModel.allProducts()
51 | .collectAsState(initial = Data.Loading()).value,
52 | variants = viewModel.productVariants.collectAsState(initial = Data.Loading()).value,
53 | onNewProductSelected = {
54 | scope.launch {
55 | viewModel.onNewProductSelected(it)
56 | }
57 | },
58 | onNewVariantSelected = {
59 | viewModel.onNewVariantSelected(it)
60 | },
61 | onSubmit = {
62 | scope.launch {
63 | if (viewModel.updateItem(itemId)
64 | .isNotError()
65 | ) {
66 | navigateBack()
67 | }
68 | }
69 | },
70 | onDelete = {
71 | scope.launch {
72 | if (viewModel.deleteItem(itemId)
73 | .isNotError()
74 | ) {
75 | navigateBackDelete()
76 | }
77 | }
78 | },
79 | submitButtonText = stringResource(id = R.string.item_edit),
80 | onProductAddButtonClick = navigateProductAdd,
81 | onVariantAddButtonClick = navigateVariantAdd,
82 | onItemLongClick = navigateProductEdit,
83 | onItemVariantLongClick = navigateVariantEdit,
84 | )
85 | }
86 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/producer/ModifyProducerViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.producer
2 |
3 | import androidx.compose.runtime.MutableState
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.lifecycle.ViewModel
6 | import com.kssidll.arru.data.repository.ProducerRepositorySource
7 | import com.kssidll.arru.domain.data.Field
8 | import com.kssidll.arru.ui.screen.modify.ModifyScreenState
9 |
10 | /**
11 | * Base [ViewModel] class for Producer modification view models
12 | * @property screenState A [ModifyProducerScreenState] instance to use as screen state representation
13 | */
14 | abstract class ModifyProducerViewModel: ViewModel() {
15 | protected abstract val producerRepository: ProducerRepositorySource
16 | internal val screenState: ModifyProducerScreenState = ModifyProducerScreenState()
17 | }
18 |
19 | /**
20 | * Data representing [ModifyProducerScreenImpl] screen state
21 | */
22 | data class ModifyProducerScreenState(
23 | val name: MutableState> = mutableStateOf(Field.Loaded())
24 | ): ModifyScreenState()
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/producer/addproducer/AddProducerRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.producer.addproducer
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.compose.runtime.rememberCoroutineScope
6 | import com.kssidll.arru.domain.data.Field
7 | import com.kssidll.arru.ui.screen.modify.producer.ModifyProducerScreenImpl
8 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
9 | import kotlinx.coroutines.launch
10 |
11 | @Composable
12 | fun AddProducerRoute(
13 | defaultName: String?,
14 | navigateBack: (producerId: Long?) -> Unit,
15 | ) {
16 | val scope = rememberCoroutineScope()
17 | val viewModel: AddProducerViewModel = hiltViewModel()
18 |
19 | LaunchedEffect(Unit) {
20 | viewModel.screenState.name.value = Field.Loaded(defaultName)
21 | }
22 |
23 | ModifyProducerScreenImpl(
24 | onBack = {
25 | navigateBack(null)
26 | },
27 | state = viewModel.screenState,
28 | onSubmit = {
29 | scope.launch {
30 | val result = viewModel.addProducer()
31 | if (result.isNotError()) {
32 | navigateBack(result.id)
33 | }
34 | }
35 | }
36 | )
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/producer/addproducer/AddProducerViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.producer.addproducer
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import com.kssidll.arru.data.repository.ProducerRepositorySource
5 | import com.kssidll.arru.data.repository.ProducerRepositorySource.Companion.InsertResult
6 | import com.kssidll.arru.domain.data.FieldError
7 | import com.kssidll.arru.ui.screen.modify.producer.ModifyProducerViewModel
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import kotlinx.coroutines.async
10 | import javax.inject.Inject
11 |
12 | @HiltViewModel
13 | class AddProducerViewModel @Inject constructor(
14 | override val producerRepository: ProducerRepositorySource,
15 | ): ModifyProducerViewModel() {
16 |
17 | /**
18 | * Tries to add a product producer to the repository
19 | * @return resulting [InsertResult]
20 | */
21 | suspend fun addProducer() = viewModelScope.async {
22 | screenState.attemptedToSubmit.value = true
23 |
24 | val result = producerRepository.insert(screenState.name.value.data.orEmpty())
25 |
26 | if (result.isError()) {
27 | when (result.error!!) {
28 | InsertResult.InvalidName -> {
29 | screenState.name.apply {
30 | value = value.toError(FieldError.InvalidValueError)
31 | }
32 | }
33 |
34 | InsertResult.DuplicateName -> {
35 | screenState.name.apply {
36 | value = value.toError(FieldError.DuplicateValueError)
37 | }
38 | }
39 | }
40 | }
41 |
42 | return@async result
43 | }
44 | .await()
45 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/producer/editproducer/EditProducerRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.producer.editproducer
2 |
3 |
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.LaunchedEffect
6 | import androidx.compose.runtime.rememberCoroutineScope
7 | import androidx.compose.ui.res.stringResource
8 | import com.kssidll.arru.R
9 | import com.kssidll.arru.ui.screen.modify.producer.ModifyProducerScreenImpl
10 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
11 | import kotlinx.coroutines.launch
12 |
13 | @Composable
14 | fun EditProducerRoute(
15 | producerId: Long,
16 | navigateBack: () -> Unit,
17 | navigateBackDelete: () -> Unit,
18 | ) {
19 | val scope = rememberCoroutineScope()
20 |
21 | val viewModel: EditProducerViewModel = hiltViewModel()
22 |
23 | LaunchedEffect(producerId) {
24 | if (!viewModel.updateState(producerId)) {
25 | navigateBack()
26 | }
27 | }
28 |
29 | ModifyProducerScreenImpl(
30 | onBack = navigateBack,
31 | state = viewModel.screenState,
32 | onSubmit = {
33 | scope.launch {
34 | if (viewModel.updateProducer(producerId)
35 | .isNotError()
36 | ) {
37 | navigateBack()
38 | }
39 | }
40 | },
41 | onDelete = {
42 | scope.launch {
43 | if (viewModel.deleteProducer(producerId)
44 | .isNotError()
45 | ) {
46 | navigateBackDelete()
47 | }
48 | }
49 | },
50 | onMerge = {
51 | scope.launch {
52 | if (viewModel.mergeWith(it)
53 | .isNotError()
54 | ) {
55 | navigateBackDelete()
56 | }
57 | }
58 | },
59 | mergeCandidates = viewModel.allMergeCandidates(producerId),
60 | mergeConfirmMessageTemplate = stringResource(id = R.string.merge_action_message_template)
61 | .replace(
62 | "{value_1}",
63 | viewModel.mergeMessageProducerName
64 | ),
65 |
66 | chosenMergeCandidate = viewModel.chosenMergeCandidate.value,
67 | onChosenMergeCandidateChange = {
68 | viewModel.chosenMergeCandidate.apply { value = it }
69 | },
70 | showMergeConfirmDialog = viewModel.showMergeConfirmDialog.value,
71 | onShowMergeConfirmDialogChange = {
72 | viewModel.showMergeConfirmDialog.apply { value = it }
73 | },
74 | submitButtonText = stringResource(id = R.string.item_product_producer_edit),
75 | )
76 | }
77 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/product/addproduct/AddProductRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.product.addproduct
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.compose.runtime.collectAsState
6 | import androidx.compose.runtime.rememberCoroutineScope
7 | import com.kssidll.arru.domain.data.Data
8 | import com.kssidll.arru.domain.data.Field
9 | import com.kssidll.arru.ui.screen.modify.product.ModifyProductScreenImpl
10 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
11 | import kotlinx.coroutines.launch
12 |
13 | @Composable
14 | fun AddProductRoute(
15 | defaultName: String?,
16 | navigateBack: (productId: Long?) -> Unit,
17 | navigateCategoryAdd: (query: String?) -> Unit,
18 | navigateProducerAdd: (query: String?) -> Unit,
19 | navigateCategoryEdit: (categoryId: Long) -> Unit,
20 | navigateProducerEdit: (producerId: Long) -> Unit,
21 | providedProducerId: Long?,
22 | providedCategoryId: Long?,
23 | ) {
24 | val scope = rememberCoroutineScope()
25 | val viewModel: AddProductViewModel = hiltViewModel()
26 |
27 | LaunchedEffect(Unit) {
28 | viewModel.screenState.name.value = Field.Loaded(defaultName)
29 | }
30 |
31 | LaunchedEffect(providedProducerId) {
32 | viewModel.setSelectedProducer(providedProducerId)
33 | }
34 |
35 | LaunchedEffect(providedCategoryId) {
36 | viewModel.setSelectedCategory(providedCategoryId)
37 | }
38 |
39 | ModifyProductScreenImpl(
40 | onBack = {
41 | navigateBack(null)
42 | },
43 | state = viewModel.screenState,
44 | categories = viewModel.allCategories()
45 | .collectAsState(initial = Data.Loading()).value,
46 | producers = viewModel.allProducers()
47 | .collectAsState(initial = Data.Loading()).value,
48 | onNewProducerSelected = {
49 | viewModel.onNewProducerSelected(it)
50 | },
51 | onNewCategorySelected = {
52 | viewModel.onNewCategorySelected(it)
53 | },
54 | onSubmit = {
55 | scope.launch {
56 | val result = viewModel.addProduct()
57 | if (result.isNotError()) {
58 | navigateBack(result.id)
59 | }
60 | }
61 | },
62 | onCategoryAddButtonClick = navigateCategoryAdd,
63 | onProducerAddButtonClick = navigateProducerAdd,
64 | onItemCategoryLongClick = navigateCategoryEdit,
65 | onItemProducerLongClick = navigateProducerEdit,
66 | )
67 | }
68 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/product/addproduct/AddProductViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.product.addproduct
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import com.kssidll.arru.data.data.Product
5 | import com.kssidll.arru.data.repository.CategoryRepositorySource
6 | import com.kssidll.arru.data.repository.ProducerRepositorySource
7 | import com.kssidll.arru.data.repository.ProductRepositorySource
8 | import com.kssidll.arru.data.repository.ProductRepositorySource.Companion.InsertResult
9 | import com.kssidll.arru.domain.data.FieldError
10 | import com.kssidll.arru.ui.screen.modify.product.ModifyProductViewModel
11 | import dagger.hilt.android.lifecycle.HiltViewModel
12 | import kotlinx.coroutines.async
13 | import javax.inject.Inject
14 |
15 | @HiltViewModel
16 | class AddProductViewModel @Inject constructor(
17 | override val productRepository: ProductRepositorySource,
18 | override val categoryRepository: CategoryRepositorySource,
19 | override val producerRepository: ProducerRepositorySource,
20 | ): ModifyProductViewModel() {
21 |
22 | /**
23 | * Tries to add a product to the repository
24 | * @return resulting [InsertResult]
25 | */
26 | suspend fun addProduct() = viewModelScope.async {
27 | screenState.attemptedToSubmit.value = true
28 |
29 | val result = productRepository.insert(
30 | name = screenState.name.value.data.orEmpty(),
31 | categoryId = screenState.selectedProductCategory.value.data?.id
32 | ?: Product.INVALID_CATEGORY_ID,
33 | producerId = screenState.selectedProductProducer.value.data?.id
34 | )
35 |
36 | if (result.isError()) {
37 | when (result.error!!) {
38 | InsertResult.InvalidName -> {
39 | screenState.name.apply {
40 | value = value.toError(FieldError.InvalidValueError)
41 | }
42 | }
43 |
44 | InsertResult.DuplicateName -> {
45 | screenState.name.apply {
46 | value = value.toError(FieldError.DuplicateValueError)
47 | }
48 | }
49 |
50 | InsertResult.InvalidCategoryId -> {
51 | screenState.selectedProductCategory.apply {
52 | value = value.toError(FieldError.InvalidValueError)
53 | }
54 | }
55 |
56 | InsertResult.InvalidProducerId -> {
57 | screenState.selectedProductProducer.apply {
58 | value = value.toError(FieldError.InvalidValueError)
59 | }
60 | }
61 | }
62 | }
63 |
64 | return@async result
65 | }
66 | .await()
67 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/shop/ModifyShopViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.shop
2 |
3 | import androidx.compose.runtime.MutableState
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.lifecycle.ViewModel
6 | import com.kssidll.arru.data.repository.ShopRepositorySource
7 | import com.kssidll.arru.domain.data.Field
8 | import com.kssidll.arru.ui.screen.modify.ModifyScreenState
9 |
10 | /**
11 | * Base [ViewModel] class for Shop modification view models
12 | * @property screenState A [ModifyShopScreenState] instance to use as screen state representation
13 | */
14 | abstract class ModifyShopViewModel: ViewModel() {
15 | protected abstract val shopRepository: ShopRepositorySource
16 | internal val screenState: ModifyShopScreenState = ModifyShopScreenState()
17 | }
18 |
19 | /**
20 | * Data representing [ModifyShopScreenImpl] screen state
21 | */
22 | data class ModifyShopScreenState(
23 | val name: MutableState> = mutableStateOf(Field.Loaded()),
24 | ): ModifyScreenState()
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/shop/addshop/AddShopRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.shop.addshop
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.compose.runtime.rememberCoroutineScope
6 | import com.kssidll.arru.domain.data.Field
7 | import com.kssidll.arru.ui.screen.modify.shop.ModifyShopScreenImpl
8 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
9 | import kotlinx.coroutines.launch
10 |
11 | @Composable
12 | fun AddShopRoute(
13 | defaultName: String?,
14 | navigateBack: (shopId: Long?) -> Unit,
15 | ) {
16 | val scope = rememberCoroutineScope()
17 | val viewModel: AddShopViewModel = hiltViewModel()
18 |
19 | LaunchedEffect(Unit) {
20 | viewModel.screenState.name.value = Field.Loaded(defaultName)
21 | }
22 |
23 | ModifyShopScreenImpl(
24 | onBack = {
25 | navigateBack(null)
26 | },
27 | state = viewModel.screenState,
28 | onSubmit = {
29 | scope.launch {
30 | val result = viewModel.addShop()
31 | if (result.isNotError()) {
32 | navigateBack(result.id)
33 | }
34 | }
35 | },
36 | )
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/shop/addshop/AddShopViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.shop.addshop
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import com.kssidll.arru.data.repository.ShopRepositorySource
5 | import com.kssidll.arru.data.repository.ShopRepositorySource.Companion.InsertResult
6 | import com.kssidll.arru.domain.data.FieldError
7 | import com.kssidll.arru.ui.screen.modify.shop.ModifyShopViewModel
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import kotlinx.coroutines.async
10 | import javax.inject.Inject
11 |
12 | @HiltViewModel
13 | class AddShopViewModel @Inject constructor(
14 | override val shopRepository: ShopRepositorySource,
15 | ): ModifyShopViewModel() {
16 |
17 | /**
18 | * Tries to add a shop to the repository
19 | * @return resulting [InsertResult]
20 | */
21 | suspend fun addShop() = viewModelScope.async {
22 | screenState.attemptedToSubmit.value = true
23 |
24 | val result = shopRepository.insert(screenState.name.value.data.orEmpty())
25 |
26 | if (result.isError()) {
27 | when (result.error!!) {
28 | InsertResult.InvalidName -> {
29 | screenState.name.apply {
30 | value = value.toError(FieldError.InvalidValueError)
31 | }
32 | }
33 |
34 | InsertResult.DuplicateName -> {
35 | screenState.name.apply {
36 | value = value.toError(FieldError.DuplicateValueError)
37 | }
38 | }
39 | }
40 | }
41 |
42 | return@async result
43 | }
44 | .await()
45 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/shop/editshop/EditShopRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.shop.editshop
2 |
3 |
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.LaunchedEffect
6 | import androidx.compose.runtime.rememberCoroutineScope
7 | import androidx.compose.ui.res.stringResource
8 | import com.kssidll.arru.R
9 | import com.kssidll.arru.ui.screen.modify.shop.ModifyShopScreenImpl
10 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
11 | import kotlinx.coroutines.launch
12 |
13 | @Composable
14 | fun EditShopRoute(
15 | shopId: Long,
16 | navigateBack: () -> Unit,
17 | navigateBackDelete: () -> Unit,
18 | ) {
19 | val scope = rememberCoroutineScope()
20 |
21 | val viewModel: EditShopViewModel = hiltViewModel()
22 |
23 | LaunchedEffect(shopId) {
24 | if (!viewModel.updateState(shopId)) {
25 | navigateBack()
26 | }
27 | }
28 |
29 | ModifyShopScreenImpl(
30 | onBack = navigateBack,
31 | state = viewModel.screenState,
32 | onSubmit = {
33 | scope.launch {
34 | if (viewModel.updateShop(shopId)
35 | .isNotError()
36 | ) {
37 | navigateBack()
38 | }
39 | }
40 | },
41 | onDelete = {
42 | scope.launch {
43 | if (viewModel.deleteShop(shopId)
44 | .isNotError()
45 | ) {
46 | navigateBackDelete()
47 | }
48 | }
49 | },
50 | onMerge = {
51 | scope.launch {
52 | if (viewModel.mergeWith(it)
53 | .isNotError()
54 | ) {
55 | navigateBackDelete()
56 | }
57 | }
58 | },
59 | mergeCandidates = viewModel.allMergeCandidates(shopId),
60 | mergeConfirmMessageTemplate = stringResource(id = R.string.merge_action_message_template)
61 | .replace(
62 | "{value_1}",
63 | viewModel.mergeMessageShopName
64 | ),
65 |
66 | chosenMergeCandidate = viewModel.chosenMergeCandidate.value,
67 | onChosenMergeCandidateChange = {
68 | viewModel.chosenMergeCandidate.apply { value = it }
69 | },
70 | showMergeConfirmDialog = viewModel.showMergeConfirmDialog.value,
71 | onShowMergeConfirmDialogChange = {
72 | viewModel.showMergeConfirmDialog.apply { value = it }
73 | },
74 | submitButtonText = stringResource(id = R.string.item_shop_edit),
75 | )
76 | }
77 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/transaction/ModifyTransactionViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.transaction
2 |
3 | import androidx.compose.runtime.MutableState
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import com.kssidll.arru.data.data.Shop
8 | import com.kssidll.arru.data.repository.ShopRepositorySource
9 | import com.kssidll.arru.domain.data.Data
10 | import com.kssidll.arru.domain.data.Field
11 | import com.kssidll.arru.ui.screen.modify.ModifyScreenState
12 | import kotlinx.coroutines.Job
13 | import kotlinx.coroutines.flow.Flow
14 | import kotlinx.coroutines.flow.collectLatest
15 | import kotlinx.coroutines.launch
16 |
17 | abstract class ModifyTransactionViewModel: ViewModel() {
18 | protected abstract val shopRepository: ShopRepositorySource
19 | internal val screenState: ModifyTransactionScreenState = ModifyTransactionScreenState()
20 |
21 | private var mShopListener: Job? = null
22 |
23 | /**
24 | * @return List of all shops
25 | */
26 | fun allShops(): Flow>> {
27 | return shopRepository.allFlow()
28 | }
29 |
30 | suspend fun setSelectedShopToProvided(providedShopId: Long?) {
31 | if (providedShopId != null) {
32 | screenState.selectedShop.apply { value = value.toLoading() }
33 | onNewShopSelected(shopRepository.get(providedShopId))
34 | }
35 | }
36 |
37 | fun onNewShopSelected(shop: Shop?) {
38 | // Don't do anything if the shop is the same as already selected
39 | if (screenState.selectedShop.value.data == shop) {
40 | screenState.selectedShop.apply { value = value.toLoaded() }
41 | return
42 | }
43 |
44 | screenState.selectedShop.value = Field.Loaded(shop)
45 |
46 | mShopListener?.cancel()
47 | if (shop != null) {
48 | mShopListener = viewModelScope.launch {
49 | shopRepository.getFlow(shop.id)
50 | .collectLatest {
51 | if (it is Data.Loaded) {
52 | screenState.selectedShop.value = Field.Loaded(it.data)
53 | }
54 | }
55 | }
56 | }
57 | }
58 | }
59 |
60 | data class ModifyTransactionScreenState(
61 | val date: MutableState> = mutableStateOf(Field.Loaded()),
62 | val totalCost: MutableState> = mutableStateOf(Field.Loaded()),
63 | val selectedShop: MutableState> = mutableStateOf(Field.Loaded()),
64 |
65 | var isDatePickerDialogExpanded: MutableState = mutableStateOf(false),
66 | var isShopSearchDialogExpanded: MutableState = mutableStateOf(false),
67 | ): ModifyScreenState() {
68 |
69 | /**
70 | * Sets all fields to Loading status
71 | */
72 | fun allToLoading() {
73 | date.apply { value = value.toLoading() }
74 | totalCost.apply { value = value.toLoading() }
75 | selectedShop.apply { value = value.toLoading() }
76 | }
77 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/transaction/addtransaction/AddTransactionRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.transaction.addtransaction
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.compose.runtime.collectAsState
6 | import androidx.compose.runtime.rememberCoroutineScope
7 | import com.kssidll.arru.domain.data.Data
8 | import com.kssidll.arru.ui.screen.modify.transaction.ModifyTransactionScreenImpl
9 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
10 | import kotlinx.coroutines.launch
11 |
12 | @Composable
13 | fun AddTransactionRoute(
14 | isExpandedScreen: Boolean,
15 | navigateBack: () -> Unit,
16 | navigateTransaction: (transactionId: Long) -> Unit,
17 | navigateShopAdd: (query: String?) -> Unit,
18 | navigateShopEdit: (shopId: Long) -> Unit,
19 | providedShopId: Long?,
20 | ) {
21 | val scope = rememberCoroutineScope()
22 | val viewModel: AddTransactionViewModel = hiltViewModel()
23 |
24 | LaunchedEffect(providedShopId) {
25 | viewModel.setSelectedShopToProvided(providedShopId)
26 | }
27 |
28 | ModifyTransactionScreenImpl(
29 | isExpandedScreen = isExpandedScreen,
30 | onBack = navigateBack,
31 | state = viewModel.screenState,
32 | shops = viewModel.allShops()
33 | .collectAsState(initial = Data.Loading()).value,
34 | onNewShopSelected = {
35 | viewModel.onNewShopSelected(it)
36 | },
37 | onSubmit = {
38 | scope.launch {
39 | val result = viewModel.addTransaction()
40 | if (result.isNotError() && result.id != null) {
41 | navigateBack()
42 | navigateTransaction(result.id)
43 | }
44 | }
45 | },
46 | onShopAddButtonClick = navigateShopAdd,
47 | onTransactionShopLongClick = navigateShopEdit,
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/transaction/edittransaction/EditTransactionRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.transaction.edittransaction
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.compose.runtime.collectAsState
6 | import androidx.compose.runtime.rememberCoroutineScope
7 | import androidx.compose.ui.res.stringResource
8 | import com.kssidll.arru.R
9 | import com.kssidll.arru.domain.data.Data
10 | import com.kssidll.arru.ui.screen.modify.transaction.ModifyTransactionScreenImpl
11 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
12 | import kotlinx.coroutines.launch
13 |
14 | @Composable
15 | fun EditTransactionRoute(
16 | isExpandedScreen: Boolean,
17 | transactionId: Long,
18 | navigateBack: () -> Unit,
19 | navigateBackDelete: (transactionId: Long) -> Unit,
20 | navigateShopAdd: (query: String?) -> Unit,
21 | navigateShopEdit: (shopId: Long) -> Unit,
22 | providedShopId: Long?,
23 | ) {
24 | val scope = rememberCoroutineScope()
25 | val viewModel: EditTransactionViewModel = hiltViewModel()
26 |
27 | LaunchedEffect(transactionId) {
28 | if (!viewModel.updateState(transactionId)) {
29 | navigateBack()
30 | }
31 | }
32 |
33 | LaunchedEffect(providedShopId) {
34 | viewModel.setSelectedShopToProvided(providedShopId)
35 | }
36 |
37 | ModifyTransactionScreenImpl(
38 | isExpandedScreen = isExpandedScreen,
39 | onBack = navigateBack,
40 | state = viewModel.screenState,
41 | shops = viewModel.allShops()
42 | .collectAsState(initial = Data.Loading()).value,
43 | onNewShopSelected = {
44 | viewModel.onNewShopSelected(it)
45 | },
46 | onSubmit = {
47 | scope.launch {
48 | if (viewModel.updateTransaction(transactionId)
49 | .isNotError()
50 | ) {
51 | navigateBack()
52 | }
53 | }
54 | },
55 | onDelete = {
56 | scope.launch {
57 | if (viewModel.deleteTransaction(transactionId)
58 | .isNotError()
59 | ) {
60 | navigateBackDelete(transactionId)
61 | }
62 | }
63 | },
64 | submitButtonText = stringResource(id = R.string.transaction_edit),
65 | onShopAddButtonClick = navigateShopAdd,
66 | onTransactionShopLongClick = navigateShopEdit,
67 | )
68 | }
69 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/variant/ModifyVariantViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.variant
2 |
3 | import androidx.compose.runtime.MutableState
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.lifecycle.ViewModel
6 | import com.kssidll.arru.data.repository.VariantRepositorySource
7 | import com.kssidll.arru.domain.data.Field
8 | import com.kssidll.arru.ui.screen.modify.ModifyScreenState
9 |
10 | /**
11 | * Base [ViewModel] class for Variant modification view models
12 | * @property screenState A [ModifyVariantScreenState] instance to use as screen state representation
13 | */
14 | abstract class ModifyVariantViewModel: ViewModel() {
15 | protected abstract val variantRepository: VariantRepositorySource
16 | internal val screenState: ModifyVariantScreenState = ModifyVariantScreenState()
17 | }
18 |
19 | /**
20 | * Data representing [ModifyVariantScreenImpl] screen state
21 | */
22 | data class ModifyVariantScreenState(
23 | val name: MutableState> = mutableStateOf(Field.Loaded()),
24 | ): ModifyScreenState()
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/variant/addvariant/AddVariantRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.variant.addvariant
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.compose.runtime.rememberCoroutineScope
6 | import com.kssidll.arru.domain.data.Field
7 | import com.kssidll.arru.ui.screen.modify.variant.ModifyVariantScreenImpl
8 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
9 | import kotlinx.coroutines.launch
10 |
11 | @Composable
12 | fun AddVariantRoute(
13 | productId: Long,
14 | defaultName: String?,
15 | navigateBack: (variantId: Long?) -> Unit,
16 | ) {
17 | val scope = rememberCoroutineScope()
18 | val viewModel: AddVariantViewModel = hiltViewModel()
19 |
20 | LaunchedEffect(Unit) {
21 | viewModel.screenState.name.value = Field.Loaded(defaultName)
22 | }
23 |
24 | ModifyVariantScreenImpl(
25 | onBack = {
26 | navigateBack(null)
27 | },
28 | state = viewModel.screenState,
29 | onSubmit = {
30 | scope.launch {
31 | val result = viewModel.addVariant(productId)
32 | if (result.isNotError()) {
33 | navigateBack(result.id)
34 | }
35 | }
36 | }
37 | )
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/variant/addvariant/AddVariantViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.variant.addvariant
2 |
3 | import android.util.Log
4 | import androidx.lifecycle.viewModelScope
5 | import com.kssidll.arru.data.repository.VariantRepositorySource
6 | import com.kssidll.arru.data.repository.VariantRepositorySource.Companion.InsertResult
7 | import com.kssidll.arru.domain.data.FieldError
8 | import com.kssidll.arru.ui.screen.modify.variant.ModifyVariantViewModel
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import kotlinx.coroutines.async
11 | import javax.inject.Inject
12 |
13 | @HiltViewModel
14 | class AddVariantViewModel @Inject constructor(
15 | override val variantRepository: VariantRepositorySource,
16 | ): ModifyVariantViewModel() {
17 |
18 | /**
19 | * Tries to add a product variant to the repository
20 | * @param productId: Id of the product that the variant is being created for
21 | * @return resulting [InsertResult]
22 | */
23 | suspend fun addVariant(productId: Long) = viewModelScope.async {
24 | screenState.attemptedToSubmit.value = true
25 |
26 | val result = variantRepository.insert(
27 | productId,
28 | screenState.name.value.data.orEmpty()
29 | )
30 |
31 | if (result.isError()) {
32 | when (result.error!!) {
33 | is InsertResult.InvalidName -> {
34 | screenState.name.apply {
35 | value = value.toError(FieldError.InvalidValueError)
36 | }
37 | }
38 |
39 | is InsertResult.DuplicateName -> {
40 | screenState.name.apply {
41 | value = value.toError(FieldError.DuplicateValueError)
42 | }
43 | }
44 |
45 | is InsertResult.InvalidProductId -> {
46 | Log.e(
47 | "InvalidId",
48 | "Tried to insert variant with invalid productId in AddVariantViewModel"
49 | )
50 |
51 | return@async InsertResult.Success(0)
52 | }
53 | }
54 | }
55 |
56 | return@async result
57 | }
58 | .await()
59 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/modify/variant/editvariant/EditVariantRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.modify.variant.editvariant
2 |
3 |
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.LaunchedEffect
6 | import androidx.compose.runtime.rememberCoroutineScope
7 | import androidx.compose.ui.res.stringResource
8 | import com.kssidll.arru.R
9 | import com.kssidll.arru.ui.screen.modify.variant.ModifyVariantScreenImpl
10 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
11 | import kotlinx.coroutines.launch
12 |
13 | @Composable
14 | fun EditVariantRoute(
15 | variantId: Long,
16 | navigateBack: () -> Unit,
17 | navigateBackDelete: () -> Unit,
18 | ) {
19 | val scope = rememberCoroutineScope()
20 |
21 | val viewModel: EditVariantViewModel = hiltViewModel()
22 |
23 | LaunchedEffect(variantId) {
24 | if (!viewModel.updateState(variantId)) {
25 | navigateBack()
26 | }
27 | }
28 |
29 | ModifyVariantScreenImpl(
30 | onBack = navigateBack,
31 | state = viewModel.screenState,
32 | onSubmit = {
33 | scope.launch {
34 | if (viewModel.updateVariant(variantId)
35 | .isNotError()
36 | ) {
37 | navigateBack()
38 | }
39 | }
40 | },
41 | onDelete = {
42 | scope.launch {
43 | if (viewModel.deleteVariant(variantId)
44 | .isNotError()
45 | ) {
46 | navigateBackDelete()
47 | }
48 | }
49 | },
50 | submitButtonText = stringResource(id = R.string.item_product_variant_edit),
51 | )
52 | }
53 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/ranking/categoryranking/CategoryRankingRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.ranking.categoryranking
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.collectAsState
5 | import androidx.compose.ui.res.stringResource
6 | import com.kssidll.arru.R
7 | import com.kssidll.arru.ui.screen.ranking.RankingScreen
8 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
9 |
10 | @Composable
11 | fun CategoryRankingRoute(
12 | navigateBack: () -> Unit,
13 | navigateCategory: (categoryId: Long) -> Unit,
14 | navigateCategoryEdit: (categoryId: Long) -> Unit,
15 | ) {
16 | val viewModel: CategoryRankingViewModel = hiltViewModel()
17 |
18 | RankingScreen(
19 | onBack = navigateBack,
20 | title = stringResource(R.string.categories),
21 | data = viewModel.categoryTotalSpentFlow()
22 | .collectAsState(emptyList()).value,
23 | onItemClick = {
24 | navigateCategory(it.category.id)
25 | },
26 | onItemClickLabel = stringResource(id = R.string.select),
27 | onItemLongClick = {
28 | navigateCategoryEdit(it.category.id)
29 | },
30 | onItemLongClickLabel = stringResource(id = R.string.edit),
31 | )
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/ranking/categoryranking/CategoryRankingViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.ranking.categoryranking
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.kssidll.arru.data.data.ItemSpentByCategory
5 | import com.kssidll.arru.data.repository.CategoryRepositorySource
6 | import dagger.hilt.android.lifecycle.HiltViewModel
7 | import kotlinx.coroutines.flow.Flow
8 | import javax.inject.Inject
9 |
10 | @HiltViewModel
11 | class CategoryRankingViewModel @Inject constructor(
12 | private val categoryRepository: CategoryRepositorySource,
13 | ): ViewModel() {
14 |
15 | /**
16 | * @return List of data points representing shop spending in time as flow
17 | */
18 | fun categoryTotalSpentFlow(): Flow
> {
19 | return categoryRepository.totalSpentByCategoryFlow()
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/ranking/shopranking/ShopRankingRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.ranking.shopranking
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.collectAsState
5 | import androidx.compose.ui.res.stringResource
6 | import com.kssidll.arru.R
7 | import com.kssidll.arru.ui.screen.ranking.RankingScreen
8 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
9 |
10 | @Composable
11 | fun ShopRankingRoute(
12 | navigateBack: () -> Unit,
13 | navigateShop: (shopId: Long) -> Unit,
14 | navigateShopEdit: (shopId: Long) -> Unit,
15 | ) {
16 | val viewModel: ShopRankingViewModel = hiltViewModel()
17 |
18 | RankingScreen(
19 | onBack = navigateBack,
20 | title = stringResource(R.string.shops),
21 | data = viewModel.shopTotalSpentFlow()
22 | .collectAsState(emptyList()).value,
23 | onItemClick = {
24 | navigateShop(it.shop.id)
25 | },
26 | onItemClickLabel = stringResource(id = R.string.select),
27 | onItemLongClick = {
28 | navigateShopEdit(it.shop.id)
29 | },
30 | onItemLongClickLabel = stringResource(id = R.string.edit),
31 | )
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/ranking/shopranking/ShopRankingViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.ranking.shopranking
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.kssidll.arru.data.data.TransactionTotalSpentByShop
5 | import com.kssidll.arru.data.repository.ShopRepositorySource
6 | import dagger.hilt.android.lifecycle.HiltViewModel
7 | import kotlinx.coroutines.flow.Flow
8 | import javax.inject.Inject
9 |
10 | @HiltViewModel
11 | class ShopRankingViewModel @Inject constructor(
12 | private val shopRepository: ShopRepositorySource
13 | ): ViewModel() {
14 |
15 | /**
16 | * @return List of data points representing shop spending in time as flow
17 | */
18 | fun shopTotalSpentFlow(): Flow> {
19 | return shopRepository.totalSpentByShopFlow()
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/search/SearchRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.search
2 |
3 |
4 | import androidx.compose.runtime.Composable
5 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
6 |
7 | @Composable
8 | fun SearchRoute(
9 | navigateBack: () -> Unit,
10 | navigateProduct: (productId: Long) -> Unit,
11 | navigateCategory: (categoryId: Long) -> Unit,
12 | navigateProducer: (producerId: Long) -> Unit,
13 | navigateShop: (shopId: Long) -> Unit,
14 | navigateProductEdit: (productId: Long) -> Unit,
15 | navigateCategoryEdit: (categoryId: Long) -> Unit,
16 | navigateProducerEdit: (producerId: Long) -> Unit,
17 | navigateShopEdit: (shopId: Long) -> Unit,
18 | ) {
19 | val viewModel: SearchViewModel = hiltViewModel()
20 |
21 | SearchScreen(
22 | onBack = navigateBack,
23 | state = viewModel.screenState,
24 | onProductClick = navigateProduct,
25 | onCategoryClick = navigateCategory,
26 | onProducerClick = navigateProducer,
27 | onShopClick = navigateShop,
28 | onProductLongClick = navigateProductEdit,
29 | onCategoryLongClick = navigateCategoryEdit,
30 | onProducerLongClick = navigateProducerEdit,
31 | onShopLongClick = navigateShopEdit,
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/search/SearchViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.search
2 |
3 |
4 | import androidx.lifecycle.ViewModel
5 | import dagger.hilt.android.lifecycle.HiltViewModel
6 | import javax.inject.Inject
7 |
8 | @HiltViewModel
9 | class SearchViewModel @Inject constructor(
10 |
11 | ): ViewModel() {
12 | internal val screenState: SearchScreenState = SearchScreenState()
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/search/categorylist/CategoryListRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.search.categorylist
2 |
3 |
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.collectAsState
6 | import com.kssidll.arru.domain.data.Data
7 | import com.kssidll.arru.ui.screen.search.shared.SearchList
8 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
9 |
10 | @Composable
11 | fun CategoryListRoute(
12 | onCategoryClick: (categoryId: Long) -> Unit,
13 | onCategoryLongClick: (categoryId: Long) -> Unit,
14 | ) {
15 | val viewModel: CategoryListViewModel = hiltViewModel()
16 |
17 | SearchList(
18 | filter = viewModel.filter,
19 | onFilterChange = {
20 | viewModel.filter = it
21 | },
22 | items = viewModel.items()
23 | .collectAsState(initial = Data.Loading()).value,
24 | onItemClick = {
25 | onCategoryClick(it.category.id)
26 | },
27 | onItemLongClick = {
28 | onCategoryLongClick(it.category.id)
29 | },
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/search/categorylist/CategoryListViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.search.categorylist
2 |
3 |
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.setValue
7 | import androidx.lifecycle.ViewModel
8 | import com.kssidll.arru.data.repository.CategoryRepositorySource
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import javax.inject.Inject
11 |
12 | @HiltViewModel
13 | class CategoryListViewModel @Inject constructor(
14 | private val categoryRepository: CategoryRepositorySource,
15 | ): ViewModel() {
16 | private val _filter = mutableStateOf(String())
17 | var filter by _filter
18 |
19 | fun items() = categoryRepository.allWithAltNamesFlow()
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/search/component/SearchItem.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.search.component
2 |
3 |
4 | import androidx.compose.foundation.ExperimentalFoundationApi
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.combinedClickable
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.material3.Surface
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.res.stringResource
16 | import androidx.compose.ui.semantics.Role
17 | import androidx.compose.ui.text.style.TextAlign
18 | import androidx.compose.ui.tooling.preview.PreviewLightDark
19 | import androidx.compose.ui.unit.Dp
20 | import androidx.compose.ui.unit.dp
21 | import com.kssidll.arru.R
22 | import com.kssidll.arru.ui.theme.ArrugarqTheme
23 | import com.kssidll.arru.ui.theme.Typography
24 |
25 | private val DefaultItemHeight: Dp = 80.dp
26 |
27 | @OptIn(ExperimentalFoundationApi::class)
28 | @Composable
29 | internal fun SearchItem(
30 | text: String,
31 | onItemClick: () -> Unit,
32 | onItemLongClick: (() -> Unit)? = null,
33 | itemHeight: Dp = DefaultItemHeight,
34 | ) {
35 | val containerModifier =
36 | if (onItemLongClick == null)
37 | Modifier.clickable(
38 | role = Role.Button,
39 | onClickLabel = stringResource(id = R.string.select)
40 | ) {
41 | onItemClick()
42 | }
43 | else Modifier
44 | .combinedClickable(
45 | role = Role.Button,
46 | onClick = {
47 | onItemClick()
48 | },
49 | onClickLabel = stringResource(id = R.string.select),
50 | onLongClick = {
51 | onItemLongClick()
52 | },
53 | onLongClickLabel = stringResource(id = R.string.edit)
54 | )
55 |
56 | Box(
57 | modifier = containerModifier
58 | .fillMaxWidth()
59 | .height(itemHeight)
60 | ) {
61 | Text(
62 | text = text,
63 | style = Typography.titleLarge,
64 | textAlign = TextAlign.Center,
65 | modifier = Modifier.align(Alignment.Center)
66 | )
67 | }
68 | }
69 |
70 | @PreviewLightDark
71 | @Composable
72 | private fun SearchItemPreview() {
73 | ArrugarqTheme {
74 | Surface {
75 | SearchItem(
76 | text = "test",
77 | onItemClick = {},
78 | onItemLongClick = {},
79 | )
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/search/producerlist/ProducerListRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.search.producerlist
2 |
3 |
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.collectAsState
6 | import com.kssidll.arru.domain.data.Data
7 | import com.kssidll.arru.ui.screen.search.shared.SearchList
8 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
9 |
10 | @Composable
11 | fun ProducerListRoute(
12 | onProducerClick: (producerId: Long) -> Unit,
13 | onProducerLongClick: (producerId: Long) -> Unit,
14 | ) {
15 | val viewModel: ProducerListViewModel = hiltViewModel()
16 |
17 | SearchList(
18 | filter = viewModel.filter,
19 | onFilterChange = {
20 | viewModel.filter = it
21 | },
22 | items = viewModel.items()
23 | .collectAsState(initial = Data.Loading()).value,
24 | onItemClick = {
25 | onProducerClick(it.id)
26 | },
27 | onItemLongClick = {
28 | onProducerLongClick(it.id)
29 | },
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/search/producerlist/ProducerListViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.search.producerlist
2 |
3 |
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.setValue
7 | import androidx.lifecycle.ViewModel
8 | import com.kssidll.arru.data.repository.ProducerRepositorySource
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import javax.inject.Inject
11 |
12 | @HiltViewModel
13 | class ProducerListViewModel @Inject constructor(
14 | private val producerRepository: ProducerRepositorySource,
15 | ): ViewModel() {
16 | private val _filter = mutableStateOf(String())
17 | var filter by _filter
18 |
19 | fun items() = producerRepository.allFlow()
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/search/productlist/ProductListRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.search.productlist
2 |
3 |
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.collectAsState
6 | import com.kssidll.arru.domain.data.Data
7 | import com.kssidll.arru.ui.screen.search.shared.SearchList
8 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
9 |
10 | @Composable
11 | internal fun ProductListRoute(
12 | onProductClick: (productId: Long) -> Unit,
13 | onProductLongClick: (productId: Long) -> Unit,
14 | ) {
15 | val viewModel: ProductListViewModel = hiltViewModel()
16 |
17 | SearchList(
18 | filter = viewModel.filter,
19 | onFilterChange = {
20 | viewModel.filter = it
21 | },
22 | items = viewModel.items()
23 | .collectAsState(initial = Data.Loading()).value,
24 | onItemClick = {
25 | onProductClick(it.product.id)
26 | },
27 | onItemLongClick = {
28 | onProductLongClick(it.product.id)
29 | },
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/search/productlist/ProductListViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.search.productlist
2 |
3 |
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.setValue
7 | import androidx.lifecycle.ViewModel
8 | import com.kssidll.arru.data.repository.ProductRepositorySource
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import javax.inject.Inject
11 |
12 | @HiltViewModel
13 | class ProductListViewModel @Inject constructor(
14 | private val productRepository: ProductRepositorySource,
15 | ): ViewModel() {
16 | private val _filter = mutableStateOf(String())
17 | var filter by _filter
18 |
19 | fun items() = productRepository.allWithAltNamesFlow()
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/search/shoplist/ShopListRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.search.shoplist
2 |
3 |
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.collectAsState
6 | import com.kssidll.arru.domain.data.Data
7 | import com.kssidll.arru.ui.screen.search.shared.SearchList
8 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
9 |
10 | @Composable
11 | internal fun ShopListRoute(
12 | onShopClick: (shopId: Long) -> Unit,
13 | onShopLongClick: (shopId: Long) -> Unit,
14 | ) {
15 | val viewModel: ShopListViewModel = hiltViewModel()
16 |
17 | SearchList(
18 | filter = viewModel.filter,
19 | onFilterChange = {
20 | viewModel.filter = it
21 | },
22 | items = viewModel.items()
23 | .collectAsState(initial = Data.Loading()).value,
24 | onItemClick = {
25 | onShopClick(it.id)
26 | },
27 | onItemLongClick = {
28 | onShopLongClick(it.id)
29 | },
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/search/shoplist/ShopListViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.search.shoplist
2 |
3 |
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.setValue
7 | import androidx.lifecycle.ViewModel
8 | import com.kssidll.arru.data.repository.ShopRepositorySource
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import javax.inject.Inject
11 |
12 | @HiltViewModel
13 | class ShopListViewModel @Inject constructor(
14 | private val shopRepository: ShopRepositorySource,
15 | ): ViewModel() {
16 | private val _filter = mutableStateOf(String())
17 | var filter by _filter
18 |
19 | fun items() = shopRepository.allFlow()
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/search/start/StartRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.search.start
2 |
3 |
4 | import androidx.compose.runtime.Composable
5 |
6 | @Composable
7 | internal fun StartRoute(
8 | onProductClick: () -> Unit,
9 | onCategoryClick: () -> Unit,
10 | onShopClick: () -> Unit,
11 | onProducerClick: () -> Unit,
12 | ) {
13 | StartScreen(
14 | onProductClick = onProductClick,
15 | onCategoryClick = onCategoryClick,
16 | onShopClick = onShopClick,
17 | onProducerClick = onProducerClick,
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/spendingcomparison/categoryspendingcomparison/CategorySpendingComparisonRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.spendingcomparison.categoryspendingcomparison
2 |
3 |
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.collectAsState
6 | import androidx.compose.ui.res.stringResource
7 | import com.kssidll.arru.R
8 | import com.kssidll.arru.ui.screen.spendingcomparison.SpendingComparisonScreen
9 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
10 | import java.text.SimpleDateFormat
11 | import java.util.Calendar
12 | import java.util.Locale
13 |
14 | @Composable
15 | fun CategorySpendingComparisonRoute(
16 | navigateBack: () -> Unit,
17 | year: Int,
18 | month: Int,
19 | ) {
20 | val viewModel: CategorySpendingComparisonViewModel = hiltViewModel()
21 |
22 | val calendar = Calendar.getInstance()
23 | calendar.clear()
24 | calendar.set(
25 | Calendar.MONTH,
26 | month - 1
27 | ) // calendar has 0 - 11 month indexes
28 |
29 | val formatter = SimpleDateFormat(
30 | "LLLL",
31 | Locale.getDefault()
32 | )
33 |
34 | SpendingComparisonScreen(
35 | onBack = navigateBack,
36 | title = "${
37 | formatter.format(calendar.time)
38 | .replaceFirstChar { it.titlecase() }
39 | } $year",
40 | leftSideItems = viewModel.categoryTotalSpentPreviousMonth(
41 | year,
42 | month
43 | )
44 | .collectAsState(initial = emptyList()).value,
45 | leftSideHeader = stringResource(id = R.string.previous),
46 | rightSideItems = viewModel.categoryTotalSpentCurrentMonth(
47 | year,
48 | month
49 | )
50 | .collectAsState(initial = emptyList()).value,
51 | rightSideHeader = stringResource(id = R.string.current),
52 | )
53 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/spendingcomparison/categoryspendingcomparison/CategorySpendingComparisonViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.spendingcomparison.categoryspendingcomparison
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.kssidll.arru.data.data.ItemSpentByCategory
5 | import com.kssidll.arru.data.repository.CategoryRepositorySource
6 | import dagger.hilt.android.lifecycle.HiltViewModel
7 | import kotlinx.coroutines.flow.Flow
8 | import javax.inject.Inject
9 |
10 | @HiltViewModel
11 | class CategorySpendingComparisonViewModel @Inject constructor(
12 | private val categoryRepository: CategoryRepositorySource
13 | ): ViewModel() {
14 |
15 | /**
16 | * @return List of data points representing category spending in [year] and [month]
17 | */
18 | fun categoryTotalSpentCurrentMonth(
19 | year: Int,
20 | month: Int
21 | ): Flow> {
22 | return categoryRepository.totalSpentByCategoryByMonthFlow(
23 | year,
24 | month
25 | )
26 | }
27 |
28 | /**
29 | * @return List of data points representing category spending in previous month for [year] and [month]
30 | */
31 | fun categoryTotalSpentPreviousMonth(
32 | year: Int,
33 | month: Int
34 | ): Flow> {
35 | var localYear: Int = year
36 | var localMonth: Int = month
37 |
38 | if (localMonth == 1) {
39 | localYear -= 1
40 | localMonth = 12
41 | } else {
42 | localMonth -= 1
43 | }
44 |
45 | return categoryRepository.totalSpentByCategoryByMonthFlow(
46 | localYear,
47 | localMonth
48 | )
49 | }
50 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/spendingcomparison/shopspendingcomparison/ShopSpendingComparisonRoute.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.spendingcomparison.shopspendingcomparison
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.collectAsState
5 | import androidx.compose.ui.res.stringResource
6 | import com.kssidll.arru.R
7 | import com.kssidll.arru.ui.screen.spendingcomparison.SpendingComparisonScreen
8 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
9 | import java.text.SimpleDateFormat
10 | import java.util.Calendar
11 | import java.util.Locale
12 |
13 | @Composable
14 | fun ShopSpendingComparisonRoute(
15 | navigateBack: () -> Unit,
16 | year: Int,
17 | month: Int,
18 | ) {
19 | val viewModel: ShopSpendingComparisonViewModel = hiltViewModel()
20 |
21 | val calendar = Calendar.getInstance()
22 | calendar.clear()
23 | calendar.set(
24 | Calendar.MONTH,
25 | month - 1
26 | ) // calendar has 0 - 11 month indexes
27 |
28 | val formatter = SimpleDateFormat(
29 | "LLLL",
30 | Locale.getDefault()
31 | )
32 |
33 | SpendingComparisonScreen(
34 | onBack = navigateBack,
35 | title = "${
36 | formatter.format(calendar.time)
37 | .replaceFirstChar { it.titlecase() }
38 | } $year",
39 | leftSideItems = viewModel.shopTotalSpentPreviousMonth(
40 | year,
41 | month
42 | )
43 | .collectAsState(initial = emptyList()).value,
44 | leftSideHeader = stringResource(id = R.string.previous),
45 | rightSideItems = viewModel.shopTotalSpentCurrentMonth(
46 | year,
47 | month
48 | )
49 | .collectAsState(initial = emptyList()).value,
50 | rightSideHeader = stringResource(id = R.string.current),
51 | )
52 | }
53 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/screen/spendingcomparison/shopspendingcomparison/ShopSpendingComparisonViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.screen.spendingcomparison.shopspendingcomparison
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.kssidll.arru.data.data.TransactionTotalSpentByShop
5 | import com.kssidll.arru.data.repository.ShopRepositorySource
6 | import dagger.hilt.android.lifecycle.HiltViewModel
7 | import kotlinx.coroutines.flow.Flow
8 | import javax.inject.Inject
9 |
10 | @HiltViewModel
11 | class ShopSpendingComparisonViewModel @Inject constructor(
12 | private val shopRepository: ShopRepositorySource
13 | ): ViewModel() {
14 |
15 | /**
16 | * @return List of data points representing category spending in [year] and [month]
17 | */
18 | fun shopTotalSpentCurrentMonth(
19 | year: Int,
20 | month: Int
21 | ): Flow> {
22 | return shopRepository.totalSpentByShopByMonthFlow(
23 | year,
24 | month
25 | )
26 | }
27 |
28 | /**
29 | * @return List of data points representing category spending in previous month for [year] and [month]
30 | */
31 | fun shopTotalSpentPreviousMonth(
32 | year: Int,
33 | month: Int
34 | ): Flow> {
35 | var localYear: Int = year
36 | var localMonth: Int = month
37 |
38 | if (localMonth == 1) {
39 | localYear -= 1
40 | localMonth = 12
41 | } else {
42 | localMonth -= 1
43 | }
44 |
45 | return shopRepository.totalSpentByShopByMonthFlow(
46 | localYear,
47 | localMonth
48 | )
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/theme/schema/Dark.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.theme.schema
2 |
3 | import androidx.compose.material3.darkColorScheme
4 | import androidx.compose.ui.graphics.Color
5 |
6 | /**
7 | * Default application dark color scheme
8 | */
9 | val DarkColorScheme = darkColorScheme(
10 | primary = Color(0xFFDABAFA),
11 | onPrimary = Color(0xFF3D2459),
12 | primaryContainer = Color(0xFF553B71),
13 | onPrimaryContainer = Color(0xFFEFDBFF),
14 | secondary = Color(0xFFD0C1DA),
15 | onSecondary = Color(0xFF362D3F),
16 | secondaryContainer = Color(0xFF4D4357),
17 | onSecondaryContainer = Color(0xFFECDDF6),
18 | tertiary = Color(0xFFF3B7BF),
19 | onTertiary = Color(0xFF4B252B),
20 | tertiaryContainer = Color(0xFF653A41),
21 | onTertiaryContainer = Color(0xFFFFD9DD),
22 | error = Color(0xFFFFB4AB),
23 | onError = Color(0xFF690005),
24 | errorContainer = Color(0xFF93000A),
25 | onErrorContainer = Color(0xFFFFDAD6),
26 | background = Color(0xFF151218),
27 | onBackground = Color(0xFFE8E0E8),
28 | surface = Color(0xFF151218),
29 | onSurface = Color(0xFFE8E0E8),
30 | surfaceVariant = Color(0xFF4A454E),
31 | onSurfaceVariant = Color(0xFFCCC4CF),
32 | outline = Color(0xFF958E98),
33 | outlineVariant = Color(0xFF4A454E),
34 | scrim = Color(0xFF000000),
35 | inverseSurface = Color(0xFFE8E0E8),
36 | inverseOnSurface = Color(0xFF332F35),
37 | inversePrimary = Color(0xFF6E528B),
38 | surfaceDim = Color(0xFF151218),
39 | surfaceBright = Color(0xFF3C383E),
40 | surfaceContainerLowest = Color(0xFF100D12),
41 | surfaceContainerLow = Color(0xFF1E1A20),
42 | surfaceContainer = Color(0xFF221E24),
43 | surfaceContainerHigh = Color(0xFF2C292E),
44 | surfaceContainerHighest = Color(0xFF373339)
45 | )
46 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kssidll/arru/ui/theme/schema/Light.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.arru.ui.theme.schema
2 |
3 | import androidx.compose.material3.lightColorScheme
4 | import androidx.compose.ui.graphics.Color
5 |
6 | /**
7 | * Default application light color scheme
8 | */
9 | val LightColorScheme = lightColorScheme(
10 | primary = Color(0xFF6E528B),
11 | onPrimary = Color(0xFFFFFFFF),
12 | primaryContainer = Color(0xFFEFDBFF),
13 | onPrimaryContainer = Color(0xFF270D43),
14 | secondary = Color(0xFF655A6F),
15 | onSecondary = Color(0xFFFFFFFF),
16 | secondaryContainer = Color(0xFFECDDF6),
17 | onSecondaryContainer = Color(0xFF21182A),
18 | tertiary = Color(0xFF805158),
19 | onTertiary = Color(0xFFFFFFFF),
20 | tertiaryContainer = Color(0xFFFFD9DD),
21 | onTertiaryContainer = Color(0xFF321017),
22 | error = Color(0xFFBA1A1A),
23 | onError = Color(0xFFFFFFFF),
24 | errorContainer = Color(0xFFFFDAD6),
25 | onErrorContainer = Color(0xFF410002),
26 | background = Color(0xFFEEE6EE),
27 | onBackground = Color(0xFF1E1A20),
28 | surface = Color(0xFFEEE6EE),
29 | onSurface = Color(0xFF1E1A20),
30 | surfaceVariant = Color(0xFFE9E0EB),
31 | onSurfaceVariant = Color(0xFF4A454E),
32 | outline = Color(0xFF7B757E),
33 | outlineVariant = Color(0xFFCCC4CF),
34 | scrim = Color(0xFF000000),
35 | inverseSurface = Color(0xFF332F35),
36 | inverseOnSurface = Color(0xFFF6EEF6),
37 | inversePrimary = Color(0xFFDABAFA),
38 | surfaceDim = Color(0xFFDFD8DF),
39 | surfaceBright = Color(0xFFFFF7FF),
40 | surfaceContainerLowest = Color(0xFFFFFFFF),
41 | surfaceContainerLow = Color(0xFFF9F1F9),
42 | surfaceContainer = Color(0xFFEFE6FA),
43 | surfaceContainerHigh = Color(0xFFEEE6EE),
44 | surfaceContainerHighest = Color(0xFFE8E0E8)
45 | )
46 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/close.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
8 |
14 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_monochrome.xml:
--------------------------------------------------------------------------------
1 |
8 |
14 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/resources.properties:
--------------------------------------------------------------------------------
1 | unqualifiedResLocale=en-US
--------------------------------------------------------------------------------
/app/src/main/res/values-notnight-v29/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #260F3D
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.kotlin.android) apply false
5 | alias(libs.plugins.compose) apply false
6 | alias(libs.plugins.hilt) apply false
7 | alias(libs.plugins.ksp) apply false
8 | alias(libs.plugins.jvm) apply false
9 | }
10 |
11 | tasks.register("clean", Delete::class) {
12 | delete(rootProject.layout.buildDirectory)
13 | }
14 |
--------------------------------------------------------------------------------
/crowdin.yml:
--------------------------------------------------------------------------------
1 | bundles:
2 | - 1
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/37.txt:
--------------------------------------------------------------------------------
1 | - Fixed transaction screen not taking up the full available space
2 | - Improved transaction date auto selection logic
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/38.txt:
--------------------------------------------------------------------------------
1 | - Add data export
2 | - Add backup locking to prevent accidental deletions
3 | - Fix crash when attempting to delete a variant used in an existing item
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/39.txt:
--------------------------------------------------------------------------------
1 | - Add Turkish locale support
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40.txt:
--------------------------------------------------------------------------------
1 | - Add in-app theme picking
2 | - Improve theme compatibility on older devices
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/41.txt:
--------------------------------------------------------------------------------
1 | - Add currency locale selection to settings menu
2 | - Add option to change database location (Supported on Android 11 and newer)
3 | - Fix transactions screen state loss on screen change
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/42.txt:
--------------------------------------------------------------------------------
1 | - Fix settings screen text locale
2 | - Fix outdated currency locale data
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/43.txt:
--------------------------------------------------------------------------------
1 | - Add warning when trying to change database location to Downloads as it is extremely dangerous
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/44.txt:
--------------------------------------------------------------------------------
1 | - Implement data import for Arru export formats
2 | - Fix export without notification permission
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | Arru is an app to help you to track and analyze your expenses.
Features:
- Light/Dark mode
- Wide screen support
- Local backups
- Data export
- Transaction baskets tracking your total expenditure with optional product, category, shop and producer spending tracking
- Comparisons between prices at different shops
- Ranking of categories and shops based on total money spent
- Merging capabilities for categories, shops, products and producers
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/01-dashboard.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/fastlane/metadata/android/en-US/images/phoneScreenshots/01-dashboard.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/02-analysis.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/fastlane/metadata/android/en-US/images/phoneScreenshots/02-analysis.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/03-transactions.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/fastlane/metadata/android/en-US/images/phoneScreenshots/03-transactions.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/04-product_top.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/fastlane/metadata/android/en-US/images/phoneScreenshots/04-product_top.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/05-categories_ranking.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/fastlane/metadata/android/en-US/images/phoneScreenshots/05-categories_ranking.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/06-merge.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/fastlane/metadata/android/en-US/images/phoneScreenshots/06-merge.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/07-backups.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/fastlane/metadata/android/en-US/images/phoneScreenshots/07-backups.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/08-transaction_add_item.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/fastlane/metadata/android/en-US/images/phoneScreenshots/08-transaction_add_item.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/09-transaction_add_select.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/fastlane/metadata/android/en-US/images/phoneScreenshots/09-transaction_add_select.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/10-transaction_add.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/fastlane/metadata/android/en-US/images/phoneScreenshots/10-transaction_add.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Track and analyze your expenses
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Aug 13 17:07:29 CEST 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/images/analysis.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/images/analysis.jpg
--------------------------------------------------------------------------------
/images/backups.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/images/backups.jpg
--------------------------------------------------------------------------------
/images/categories_ranking.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/images/categories_ranking.jpg
--------------------------------------------------------------------------------
/images/dashboard.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/images/dashboard.jpg
--------------------------------------------------------------------------------
/images/merge.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/images/merge.jpg
--------------------------------------------------------------------------------
/images/product_top.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/images/product_top.jpg
--------------------------------------------------------------------------------
/images/transaction_add.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/images/transaction_add.jpg
--------------------------------------------------------------------------------
/images/transaction_add_item.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/images/transaction_add_item.jpg
--------------------------------------------------------------------------------
/images/transaction_add_select.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/images/transaction_add_select.jpg
--------------------------------------------------------------------------------
/images/transactions.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KSSidll/Arru/cefef347dd6bbca0d701f380bf903474633f329b/images/transactions.jpg
--------------------------------------------------------------------------------
/processor/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/processor/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.jvm)
3 | }
4 |
5 | dependencies {
6 | implementation(libs.google.ksp.spa)
7 | }
--------------------------------------------------------------------------------
/processor/src/main/java/com/kssidll/processor/CurrencyLocale.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.processor
2 |
3 | import com.google.devtools.ksp.processing.CodeGenerator
4 | import com.google.devtools.ksp.processing.Dependencies
5 | import com.google.devtools.ksp.processing.KSPLogger
6 | import com.google.devtools.ksp.processing.Resolver
7 | import com.google.devtools.ksp.processing.SymbolProcessor
8 | import com.google.devtools.ksp.symbol.KSAnnotated
9 | import java.text.NumberFormat
10 | import java.util.Locale
11 |
12 | class CurrencyLocaleDataGenerator(
13 | private val codeGenerator: CodeGenerator,
14 | private val logger: KSPLogger
15 | ) : SymbolProcessor {
16 |
17 | override fun process(resolver: Resolver): List {
18 | val fileName = "CurrencyLocaleData"
19 | val packageName = "com.kssidll.compiled"
20 | val fileContent = """
21 | package $packageName
22 |
23 | object $fileName {
24 | val items = listOf(
25 | ${generateCachedItems()}
26 | )
27 | }
28 | """.trimIndent()
29 |
30 | try {
31 | codeGenerator.createNewFile(
32 | dependencies = Dependencies(false),
33 | packageName = packageName,
34 | fileName = fileName,
35 | extensionName = "kt"
36 | ).use { outputStream ->
37 | outputStream.write(fileContent.toByteArray())
38 | }
39 | } catch (e: FileAlreadyExistsException) {
40 | logger.info("File $fileName.kt already exists. Skipping generation.")
41 | }
42 |
43 | return emptyList()
44 | }
45 |
46 | private fun generateCachedItems(): String {
47 | return NumberFormat.getAvailableLocales().map { it.toLanguageTag() }
48 | .groupBy { 1.0f.formatToCurrency(Locale.forLanguageTag(it)) }
49 | .toSortedMap { a, b -> b.compareTo(a) }
50 | .map { (currency, tags) ->
51 | "Pair(\"$currency\", listOf(${tags.joinToString { "\"$it\"" }}))"
52 | }
53 | .joinToString(",\n")
54 | }
55 |
56 | private fun Float.formatToCurrency(locale: Locale): String {
57 | return NumberFormat.getCurrencyInstance(locale).format(this)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/processor/src/main/java/com/kssidll/processor/CurrencyLocaleProvider.kt:
--------------------------------------------------------------------------------
1 | package com.kssidll.processor
2 |
3 | import com.google.devtools.ksp.processing.SymbolProcessor
4 | import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
5 | import com.google.devtools.ksp.processing.SymbolProcessorProvider
6 |
7 | class LocaleDataGeneratorProvider : SymbolProcessorProvider {
8 | override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
9 | return CurrencyLocaleDataGenerator(
10 | codeGenerator = environment.codeGenerator,
11 | logger = environment.logger
12 | )
13 | }
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider:
--------------------------------------------------------------------------------
1 | com.kssidll.processor.LocaleDataGeneratorProvider
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | pluginManagement {
4 | repositories {
5 | gradlePluginPortal()
6 | google()
7 | mavenCentral()
8 | }
9 | }
10 |
11 | dependencyResolutionManagement {
12 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
13 | repositories {
14 | google()
15 | mavenCentral()
16 | }
17 | }
18 |
19 | rootProject.name = "Arru"
20 | include(":app")
21 | include(":processor")
--------------------------------------------------------------------------------