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