diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6e2da08..816387e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -33,7 +33,6 @@ android { create("benchmark") { initWith(getByName("release")) - // 🔑 Critical settings for Macrobenchmark isDebuggable = false isMinifyEnabled = false isShrinkResources = false @@ -111,6 +110,7 @@ dependencies { implementation(libs.androidx.datastore.preferences) implementation(libs.icons.material.symbols.outlined.android) + implementation(libs.icons.material.symbols.outlined.filled.android) implementation("com.google.dagger:hilt-android:2.57.1") ksp("com.google.dagger:hilt-android-compiler:2.57.1") diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt b/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt index 6d6f4cc..8adc5b1 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt @@ -150,4 +150,8 @@ data class Filter( fun without(category: Category): Filter { return this.copy(categories = categories - category) } + + fun isDefault(): Boolean { + return this.categories.isEmpty() && startAmount == 0.0 && endAmount == Double.MAX_VALUE + } } \ No newline at end of file diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt index bf57ea5..79c4b70 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt @@ -14,9 +14,7 @@ import cc.n0th1ng.tripmoney.data.dao.TripDao import cc.n0th1ng.tripmoney.data.entity.Category import cc.n0th1ng.tripmoney.data.entity.ExchangeRate import cc.n0th1ng.tripmoney.data.entity.Expense -import cc.n0th1ng.tripmoney.data.entity.ExpenseDto import cc.n0th1ng.tripmoney.data.entity.Trip -import cc.n0th1ng.tripmoney.screens.listexpense.toEpochMilli import cc.n0th1ng.tripmoney.utils.Currencies import cc.n0th1ng.tripmoney.utils.Icons import cc.n0th1ng.tripmoney.utils.colors @@ -26,9 +24,7 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Delay import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.time.Instant import java.time.LocalDate @@ -36,7 +32,6 @@ import java.time.LocalDateTime import java.time.ZoneOffset import javax.inject.Singleton import kotlin.random.Random -import kotlin.random.nextInt @Database( entities = [Trip::class, Expense::class, Category::class, ExchangeRate::class], @@ -176,6 +171,51 @@ private class DatabasePrepopulator( icon = Icons.GROCERIES, color = colors.random() ), + Category( + name = "Zakupy1", + icon = Icons.GROCERIES, + color = colors.random() + ), + Category( + name = "Zakupy2", + icon = Icons.GROCERIES, + color = colors.random() + ), + Category( + name = "Zakupy3", + icon = Icons.GROCERIES, + color = colors.random() + ), + Category( + name = "Zakupy4", + icon = Icons.GROCERIES, + color = colors.random() + ), + Category( + name = "Zakupy5", + icon = Icons.GROCERIES, + color = colors.random() + ), + Category( + name = "Zakupy6", + icon = Icons.GROCERIES, + color = colors.random() + ), + Category( + name = "Zakupy7", + icon = Icons.GROCERIES, + color = colors.random() + ), + Category( + name = "Zakupy8", + icon = Icons.GROCERIES, + color = colors.random() + ), + Category( + name = "Zakupy9 ", + icon = Icons.GROCERIES, + color = colors.random() + ), ) @RequiresApi(Build.VERSION_CODES.O) @@ -193,7 +233,7 @@ private class DatabasePrepopulator( val expense = Expense( - categoryId = Random.nextInt(1, 5), + categoryId = Random.nextInt(1, sampleCategories.size), tripId = 1, amount = Random.nextDouble(0.1, 300.0), currency = Currencies.entries.random().name, diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/ExpenseDao.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/ExpenseDao.kt index 601c5ac..97308f9 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/ExpenseDao.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/ExpenseDao.kt @@ -4,6 +4,7 @@ import androidx.paging.PagingSource import androidx.room.Dao import androidx.room.Delete import androidx.room.Query +import androidx.room.RewriteQueriesToDropUnusedColumns import androidx.room.Transaction import androidx.room.Upsert import cc.n0th1ng.tripmoney.data.entity.Category @@ -18,6 +19,8 @@ interface ExpenseDao { suspend fun insert(expense: Expense) + @Transaction + @RewriteQueriesToDropUnusedColumns @Query( """ SELECT * FROM expense @@ -87,13 +90,17 @@ interface ExpenseDao { @Query( """ - SELECT trip.budget - IFNULL(SUM(expense.amount * expense.rate), 0) + SELECT + CASE + WHEN trip.budget = 0 THEN NULL + ELSE trip.budget - IFNULL(SUM(expense.amount * expense.rate), 0) + END FROM trip LEFT JOIN expense ON expense.trip_id = trip.id WHERE trip.id = :tripId """ ) - fun budgetLeft(tripId: Int): Double + fun budgetLeft(tripId: Int): Double? @Delete suspend fun delete(expense: Expense) diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/entity/Expense.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/entity/Expense.kt index c4894ab..febd2d5 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/entity/Expense.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/entity/Expense.kt @@ -5,6 +5,7 @@ import androidx.room.ColumnInfo import androidx.room.Embedded import androidx.room.Entity import androidx.room.ForeignKey +import androidx.room.Index import androidx.room.PrimaryKey import androidx.room.Relation import java.time.LocalDateTime @@ -17,7 +18,8 @@ import java.time.LocalDateTime childColumns = arrayOf("category_id"), onUpdate = ForeignKey.CASCADE, onDelete = ForeignKey.CASCADE - )] + )], + indices = [Index(value = ["category_id"], unique = true)] ) @Immutable data class Expense( diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/ExpenseRepository.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/ExpenseRepository.kt index 5212e00..3e5aedf 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/ExpenseRepository.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/ExpenseRepository.kt @@ -13,6 +13,7 @@ import cc.n0th1ng.tripmoney.data.entity.ExpenseDto import cc.n0th1ng.tripmoney.utils.Currencies import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first +import java.util.OptionalDouble import javax.inject.Inject class ExpenseRepository @Inject constructor( @@ -20,7 +21,7 @@ class ExpenseRepository @Inject constructor( private val exchangeRateRepository: ExchangeRateRepository ) { - fun getBudgetLeft(tripId: Int): Double { + fun getBudgetLeft(tripId: Int): Double? { return expenseDao.budgetLeft(tripId) } diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/navigation/TopBar.kt b/app/src/main/java/cc/n0th1ng/tripmoney/navigation/TopBar.kt index b864b3f..c8b7c6d 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/navigation/TopBar.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/navigation/TopBar.kt @@ -14,6 +14,7 @@ import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Menu import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.Icon @@ -21,7 +22,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text -import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable @@ -38,7 +38,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import cc.n0th1ng.tripmoney.Filter @@ -121,7 +120,11 @@ fun TopBar( ) { Icon( tint = MaterialTheme.colorScheme.primary, - painter = painterResource(drawable.materialsymbols_ic_filter_alt_outlined), + painter = painterResource( + if (filter.isDefault()) + drawable.materialsymbols_ic_filter_alt_outlined + else com.composables.icons.materialsymbols.outlinedfilled.R.drawable.materialsymbols_ic_filter_alt_outlined_filled + ), contentDescription = null, modifier = Modifier.clickable(onClick = { showFilter = true @@ -150,7 +153,11 @@ fun TopBar( showFilter = false }, categories = categories, - filter = filter + filter = filter, + onClear = { + onFilterChange(Filter()) + showFilter = false + } ) @@ -161,6 +168,7 @@ fun TopBar( fun FilterDialog( onDismiss: () -> Unit, onSave: (Filter) -> Unit, + onClear: () -> Unit, categories: List, filter: Filter ) { @@ -168,20 +176,28 @@ fun FilterDialog( var fromAmountString by remember { mutableStateOf(filter.startAmount.toString()) } var toAmountString by remember { mutableStateOf(filter.endAmount.toString()) } AlertDialog( - onDismiss, { + onDismissRequest = onDismiss, + dismissButton = { + Button( + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary), + enabled = true, + onClick = onClear + ) { Text(stringResource(R.string.clear)) } + }, + confirmButton = { Button( enabled = true, onClick = { onSave( filter.withStartAmount(fromAmountString.safeToDouble()) - .withEndAmount( toAmountString.safeToDouble()) + .withEndAmount(toAmountString.safeToDouble()) ) }) { Text(stringResource(R.string.save)) } }, title = { Text("Filter") }, text = { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Text(text = "Categories") - FlowRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + FlowRow(horizontalArrangement = Arrangement.spacedBy(7.dp)) { categories.forEach { FilterChip(selected = filter.categories.contains(it), onClick = { filter = if (filter.categories.contains(it)) { @@ -189,7 +205,12 @@ fun FilterDialog( } else { filter.with(it) } - }, label = { Text(text = it.name) }) + }, label = { + Row(horizontalArrangement = Arrangement.spacedBy(5.dp)) { + Icon(painterResource(it.icon.resource), contentDescription = null) + Text(text = it.name) + } + }) } } AmountTextField(label = "from", onValueChange = { newText -> @@ -265,13 +286,14 @@ fun PreviewFilterDialog() { onDismiss = {}, onSave = {}, categories = categoriesToPreview, - filter = Filter() + filter = Filter(), + onClear = {} ) } } private fun String.safeToDouble(): Double { - if(this == "∞") return Double.MAX_VALUE - if(this.isEmpty()) return 0.0 + if (this == "∞") return Double.MAX_VALUE + if (this.isEmpty()) return 0.0 return this.toDouble() } \ No newline at end of file diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/addexpense/AddExpenseBottomSheet.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/addexpense/AddExpenseBottomSheet.kt index a006fe3..395b164 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/addexpense/AddExpenseBottomSheet.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/addexpense/AddExpenseBottomSheet.kt @@ -487,7 +487,7 @@ fun NumberKeyboard( onLongClick = onLongBackspaceClick ) - "+", "÷", "-", "×" -> KeyboardButton( + "+", "÷", "−", "×" -> KeyboardButton( text = key, onClick = { onOperatorClick(key) }, modifier = Modifier.weight(1f), @@ -541,7 +541,7 @@ fun KeyboardButton( } val keyboard = listOf( - listOf("+", "-", "×", "÷"), + listOf("+", "−", "×", "÷"), listOf("1", "2", "3"), listOf("4", "5", "6"), listOf("7", "8", "9"), diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt index d98b61e..f1e1bd7 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt @@ -363,7 +363,7 @@ fun ExpenseCard( colors = CardDefaults.elevatedCardColors() .copy(containerColor = MaterialTheme.colorScheme.surfaceContainer), modifier = Modifier - .fillMaxWidth(0.9f) + .fillMaxWidth(0.95f) .height(70.dp) .combinedClickable( enabled = true, diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/statistics/StatisticsScreen.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/statistics/StatisticsScreen.kt index e6ca67b..9ecbdd2 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/statistics/StatisticsScreen.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/statistics/StatisticsScreen.kt @@ -13,7 +13,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CardDefaults import androidx.compose.material3.ElevatedCard import androidx.compose.material3.Icon @@ -32,7 +34,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.graphics.toColorInt -import androidx.core.graphics.toColorLong import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory import cc.n0th1ng.tripmoney.data.entity.Category @@ -73,7 +74,7 @@ fun StatisticsScreen( summaryPerCategoryList: List, summaryAmount: Double, tripCurrency: Currencies, - moneyLeft: Double + moneyLeft: Double? ) { Column( modifier = Modifier @@ -104,7 +105,7 @@ fun StatisticsScreen( @Composable fun Summary( modifier: Modifier = Modifier, - amount: Double, + amount: Double?, currency: String, text: String, icon: Int, @@ -117,7 +118,8 @@ fun Summary( ) { Column( modifier = Modifier.padding(10.dp), - verticalArrangement = Arrangement.spacedBy(10.dp) + verticalArrangement = Arrangement.spacedBy(10.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, @@ -141,7 +143,8 @@ fun Summary( } Text( - "%.2f %s".format(amount, currency), + if (amount == null) "∞" else + "%.2f %s".format(amount, currency), style = MaterialTheme.typography.titleLarge, ) } @@ -156,7 +159,10 @@ fun SummaryPerCategoryCard(summaryPerCategoryList: List) { .copy(containerColor = MaterialTheme.colorScheme.surfaceContainer) ) { Column( - modifier = Modifier.padding(15.dp), + modifier = Modifier + .padding(15.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(5.dp) ) { summaryPerCategoryList.forEach { @@ -204,7 +210,7 @@ fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCa ) { Box( modifier = Modifier - .height(40.dp) + .height(30.dp) .fillMaxWidth(0.12f + (0.90f - 0.12f) * summaryPerCategory.percent) .clip(RoundedCornerShape(16.dp)) .background(MaterialTheme.colorScheme.primary) @@ -213,7 +219,7 @@ fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCa verticalArrangement = Arrangement.Center, modifier = Modifier .fillMaxSize() - .padding(11.dp) + .padding(vertical = 5.dp, horizontal = 10.dp) ) { Text( "%d%%".format((summaryPerCategory.percent * 100).toInt()), diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt index 3c5fba4..61fcf43 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt @@ -29,6 +29,7 @@ import org.apache.commons.csv.CSVFormat import org.apache.commons.csv.CSVPrinter import java.io.File import java.time.LocalDate +import java.util.OptionalDouble import javax.inject.Inject import kotlin.collections.mapValues @@ -41,7 +42,7 @@ open class ExpenseAndCategoryViewModel @Inject constructor( private val tripRepo: TripRepository ) : ViewModel() { - fun getBudgetLeft(tripId: Int): Double { + fun getBudgetLeft(tripId: Int): Double? { return expenseRepo.getBudgetLeft(tripId) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 81e4dca..182c496 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,4 +40,5 @@ Money left Open add expense form on startup Yesterday + Clear \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 543126a..e7d7fcb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,17 +1,26 @@ [versions] agp = "8.13.2" +commonsCsv = "1.14.1" datastorePreferences = "1.2.1" +desugar_jdk_libsVersion = "2.1.5" +hiltAndroid = "2.59.2" +hiltNavigationCompose = "1.3.0" iconsMaterialSymbolsOutlinedAndroid = "2.2.1" kotlin = "2.2.21" coreKtx = "1.10.1" junit = "4.13.2" junitVersion = "1.1.5" espressoCore = "3.5.1" +kotlinxSerializationJsonJvm = "1.11.0" +ktorClientCore = "3.4.3" +ktorClientOkhttp = "3.4.3" lifecycleRuntimeKtx = "2.6.1" activityCompose = "1.8.0" composeBom = "2024.09.00" navigationCompose = "2.9.7" foundationLayout = "1.10.5" +pagingCompose = "3.4.2" +roomCompiler = "2.8.4" uiautomator = "2.3.0" benchmarkMacroJunit4 = "1.2.4" baselineprofile = "1.2.4" @@ -21,7 +30,22 @@ uiTestJunit4 = "1.10.6" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } +#androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } +#androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pagingCompose" } +#androidx-paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "pagingCompose" } +#androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomCompiler" } +#androidx-room-guava = { module = "androidx.room:room-guava", version.ref = "roomCompiler" } +#androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomCompiler" } +#androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "roomCompiler" } +#androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomCompiler" } +#androidx-room-rxjava3 = { module = "androidx.room:room-rxjava3", version.ref = "roomCompiler" } +#androidx-room-rxjava2 = { module = "androidx.room:room-rxjava2", version.ref = "roomCompiler" } +#androidx-room-testing = { module = "androidx.room:room-testing", version.ref = "roomCompiler" } +#commons-csv = { module = "org.apache.commons:commons-csv", version.ref = "commonsCsv" } +#hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" } +#hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroid" } icons-material-symbols-outlined-android = { module = "com.composables:icons-material-symbols-outlined-android", version.ref = "iconsMaterialSymbolsOutlinedAndroid" } +icons-material-symbols-outlined-filled-android = { module = "com.composables:icons-material-symbols-outlined-filled-android", version.ref = "iconsMaterialSymbolsOutlinedAndroid" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } @@ -41,6 +65,10 @@ androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomato androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" } androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "uiTestJunit4" } +kotlinx-serialization-json-jvm = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm", version.ref = "kotlinxSerializationJsonJvm" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientCore" } +ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktorClientOkhttp" } +tools-desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libsVersion" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }