This commit is contained in:
Rafal Wisniewski
2026-04-27 19:29:19 +02:00
parent 795ce9812a
commit 0518da44d7
13 changed files with 146 additions and 34 deletions

View File

@@ -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")

View File

@@ -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
}
}

View File

@@ -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,

View File

@@ -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)

View File

@@ -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(

View File

@@ -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)
}

View File

@@ -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<Category>,
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()
}

View File

@@ -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"),

View File

@@ -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,

View File

@@ -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<SummaryPerCategory>,
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,6 +143,7 @@ fun Summary(
}
Text(
if (amount == null) "" else
"%.2f %s".format(amount, currency),
style = MaterialTheme.typography.titleLarge,
)
@@ -156,7 +159,10 @@ fun SummaryPerCategoryCard(summaryPerCategoryList: List<SummaryPerCategory>) {
.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()),

View File

@@ -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)
}

View File

@@ -40,4 +40,5 @@
<string name="money_left">Money left</string>
<string name="add_expense_settings">Open add expense form on startup</string>
<string name="yesterday">Yesterday</string>
<string name="clear">Clear</string>
</resources>

View File

@@ -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" }