This commit is contained in:
Rafal Wisniewski
2026-03-25 14:49:02 +01:00
parent 916481e4e3
commit 160dda3bff
15 changed files with 818 additions and 449 deletions

View File

@@ -102,4 +102,5 @@ dependencies {
implementation("io.ktor:ktor-client-core:3.4.1") implementation("io.ktor:ktor-client-core:3.4.1")
implementation("io.ktor:ktor-client-okhttp:3.4.1") implementation("io.ktor:ktor-client-okhttp:3.4.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.10.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.10.0")
implementation("org.apache.commons:commons-csv:1.5")
} }

View File

@@ -21,6 +21,16 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
</application> </application>
</manifest> </manifest>

View File

@@ -18,6 +18,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import cc.n0th1ng.tripmoney.R.string
import com.composables.icons.materialsymbols.outlined.R import com.composables.icons.materialsymbols.outlined.R
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -32,7 +33,7 @@ fun CustomNavigationDrawer(
Text("Trip Money", modifier = Modifier.padding(16.dp)) Text("Trip Money", modifier = Modifier.padding(16.dp))
HorizontalDivider() HorizontalDivider()
NavigationDrawerItem( NavigationDrawerItem(
label = { Text(text = "Pick trip") }, label = { Text(text = stringResource(string.pick_trip)) },
selected = false, selected = false,
onClick = { onClick = {
navController.navigate(Screens.TRIP_PICKER) navController.navigate(Screens.TRIP_PICKER)
@@ -48,7 +49,7 @@ fun CustomNavigationDrawer(
) )
}) })
NavigationDrawerItem( NavigationDrawerItem(
label = { Text(text = "List of expenses") }, label = { Text(text = stringResource(string.list_of_expenses)) },
selected = false, selected = false,
onClick = { onClick = {
navController.navigate(Screens.LIST_EXPENSE) navController.navigate(Screens.LIST_EXPENSE)
@@ -64,7 +65,7 @@ fun CustomNavigationDrawer(
) )
}) })
NavigationDrawerItem( NavigationDrawerItem(
label = { Text(text = "Statistics") }, label = { Text(text = stringResource(string.statistics)) },
selected = false, selected = false,
onClick = { onClick = {
navController.navigate(Screens.STATISTICS) navController.navigate(Screens.STATISTICS)
@@ -80,7 +81,7 @@ fun CustomNavigationDrawer(
) )
}) })
NavigationDrawerItem( NavigationDrawerItem(
label = { Text(text = "Settings") }, label = { Text(text = stringResource(string.settings)) },
selected = false, selected = false,
onClick = { onClick = {
navController.navigate(Screens.SETTINGS) navController.navigate(Screens.SETTINGS)
@@ -88,7 +89,7 @@ fun CustomNavigationDrawer(
drawerState.close() drawerState.close()
} }
}, },
icon = { Icon(Icons.Default.Settings, contentDescription = "settings") } icon = { Icon(Icons.Default.Settings, contentDescription = stringResource(string.settings)) }
) )
} }
}) { content() } }) { content() }

View File

@@ -11,6 +11,7 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import cc.n0th1ng.tripmoney.R
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -31,7 +32,7 @@ fun TopBar(onClick: () -> Unit, title: String = "") {
fun TopBarSettings(navController: NavHostController) { fun TopBarSettings(navController: NavHostController) {
TopAppBar( TopAppBar(
title = { Text("Settings") }, title = { Text(stringResource(R.string.settings)) },
navigationIcon = { navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) { IconButton(onClick = { navController.popBackStack() }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")

View File

@@ -1,6 +1,7 @@
package cc.n0th1ng.tripmoney.screens.addexpense package cc.n0th1ng.tripmoney.screens.addexpense
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.graphics.drawable.PaintDrawable
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -21,9 +22,14 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
@@ -34,6 +40,7 @@ import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -42,10 +49,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
import androidx.compose.ui.modifier.modifierLocalMapOf
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.sensitiveContent
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
@@ -59,21 +69,24 @@ import cc.n0th1ng.tripmoney.R
import cc.n0th1ng.tripmoney.data.entity.Category import cc.n0th1ng.tripmoney.data.entity.Category
import cc.n0th1ng.tripmoney.data.entity.Expense import cc.n0th1ng.tripmoney.data.entity.Expense
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
import cc.n0th1ng.tripmoney.data.entity.Trip
import cc.n0th1ng.tripmoney.screens.listexpense.CategorySelectionDialog import cc.n0th1ng.tripmoney.screens.listexpense.CategorySelectionDialog
import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog
import cc.n0th1ng.tripmoney.screens.listexpense.DateTimePicker import cc.n0th1ng.tripmoney.screens.listexpense.DateTimePicker
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
import cc.n0th1ng.tripmoney.utils.AllPreviews
import cc.n0th1ng.tripmoney.utils.Currencies import cc.n0th1ng.tripmoney.utils.Currencies
import cc.n0th1ng.tripmoney.utils.colors
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
import com.composables.icons.materialsymbols.outlined.R.drawable
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import kotlin.collections.listOf
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
@@ -88,23 +101,48 @@ fun AddExpenseBottomSheet(
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel() val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
val settingsViewModel: SettingsViewModel = hiltViewModel() val settingsViewModel: SettingsViewModel = hiltViewModel()
val currentTripId by settingsViewModel.currentTrip.collectAsState() val currentTripId by settingsViewModel.currentTrip.collectAsState()
val currentTrip = tripViewModel.getTrip(currentTripId) val currentTrip = tripViewModel.getTrip(currentTripId)!!
val categories by expenseAndCategoryViewModel.getCategories().collectAsState(emptyList()) val categories by expenseAndCategoryViewModel.getCategories().collectAsState(emptyList())
AddExpenseBottomSheet(
onSave = onSave,
onDismiss = onDismiss,
expenseDtoToEdit = expenseDtoToEdit,
state = state,
currentTrip = currentTrip,
categories = categories
)
}
@OptIn(ExperimentalMaterial3Api::class)
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun AddExpenseBottomSheet(
onSave: (Expense) -> Unit,
onDismiss: () -> Unit,
expenseDtoToEdit: ExpenseDto?,
state: SheetState,
currentTrip: Trip,
categories: List<Category>
) {
val currentTripId = currentTrip.id
if (categories.isEmpty()) { if (categories.isEmpty()) {
return return
} }
var amount by remember { var amount by remember {
mutableStateOf( mutableStateOf(
expenseDtoToEdit?.expense?.amount?.toString() ?: "0.00" expenseDtoToEdit?.expense?.amount?.toString() ?: "0.00"
) )
} }
var equationResult by remember { mutableDoubleStateOf(0.0) }
val dummyFocusRequester = remember { FocusRequester() } val dummyFocusRequester = remember { FocusRequester() }
var showCurrencyDialog by remember { mutableStateOf(false) } var showCurrencyDialog by remember { mutableStateOf(false) }
var showCategoryDialog by remember { mutableStateOf(false) } var showCategoryDialog by remember { mutableStateOf(false) }
var showDateTimePicker by remember { mutableStateOf(false) } var showDateTimePicker by remember { mutableStateOf(false) }
var currency by remember { var currency by remember {
mutableStateOf( mutableStateOf(
expenseDtoToEdit?.expense?.currency ?: currentTrip?.currency ?: Currencies.default().name expenseDtoToEdit?.expense?.currency ?: currentTrip.currency
) )
} }
var category by remember { mutableStateOf(expenseDtoToEdit?.category ?: categories[0]) } var category by remember { mutableStateOf(expenseDtoToEdit?.category ?: categories[0]) }
@@ -122,6 +160,8 @@ fun AddExpenseBottomSheet(
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
sheetState = state, sheetState = state,
containerColor = MaterialTheme.colorScheme.surfaceContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -135,31 +175,58 @@ fun AddExpenseBottomSheet(
.fillMaxWidth() .fillMaxWidth()
.padding(start = 15.dp), .padding(start = 15.dp),
text = stringResource(if (expenseDtoToEdit == null) R.string.add_expense else R.string.edit_expense), text = stringResource(if (expenseDtoToEdit == null) R.string.add_expense else R.string.edit_expense),
fontWeight = FontWeight.Bold, style = MaterialTheme.typography.displaySmall,
fontSize = 35.sp,
textAlign = TextAlign.Start textAlign = TextAlign.Start
) )
HorizontalDivider(modifier = Modifier.fillMaxWidth()) HorizontalDivider(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.onSecondaryContainer
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.padding(10.dp)
) {
Row( Row(
modifier = Modifier.fillMaxWidth(0.9f), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Column{
Text( Text(
text = amount.ifEmpty { "0.00" }, text = amount.ifEmpty { "0.00" },
fontSize = 25.sp, fontSize = 25.sp,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Text(
text = if(amount.contains(Regex("[+\\/*-]\\d+"))) "%.2f".format(equationResult) else "",
fontSize = 14.sp,
)
}
CurrencyButton(onClick = { showCurrencyDialog = true }, text = currency) CurrencyButton(onClick = { showCurrencyDialog = true }, text = currency)
} }
Box(
modifier = Modifier
.size(0.dp)
.focusRequester(dummyFocusRequester)
.focusable()
)
NoteInput(
note = note,
onTextChange = { newNote -> note = newNote },
modifier = Modifier.fillMaxWidth(),
focusRequester = dummyFocusRequester
)
Row( Row(
modifier = Modifier.fillMaxWidth(0.9f), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp) horizontalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
OutlinedButton( Button(
onClick = { showDateTimePicker = true }, onClick = { showDateTimePicker = true },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
shape = MaterialTheme.shapes.medium,
) { ) {
Text( Text(
text = datetime.format(DateTimeFormatter.ofPattern("dd.MM HH:mm")), text = datetime.format(DateTimeFormatter.ofPattern("dd.MM HH:mm")),
@@ -173,29 +240,26 @@ fun AddExpenseBottomSheet(
) )
} }
Row(
verticalAlignment = Alignment.CenterVertically,
) {
NoteInput(
note = note,
onTextChange = { newNote -> note = newNote },
modifier = Modifier.fillMaxWidth(0.9f),
focusRequester = dummyFocusRequester
)
}
Box(
modifier = Modifier
.size(0.dp)
.focusRequester(dummyFocusRequester)
.focusable()
)
NumberKeyboard( NumberKeyboard(
modifier = Modifier.fillMaxWidth(),
onOperatorClick = { operator ->
if(amount.isDoubleTwoDigitsOrEquation() && amount.contains(Regex("[+\\/*-]\\d+"))) {
amount = evaluate(amount).toString()
// equationResult = 0.0
}
val newText = amount + operator
if(newText.isDoubleTwoDigitsOrEquation()) {
amount = newText
enableSave = false
}
},
onNumberClick = { number -> onNumberClick = { number ->
val newText = (if (amount == "0.00") "" else amount) + number val newText = (if (amount == "0.00") "" else amount) + number
if (newText.isDoubleTwoDigitsAboveZero()) { if (newText.isDoubleTwoDigitsOrEquation()) {
amount = newText amount = newText
enableSave = true equationResult = evaluate(amount)
enableSave = equationResult > 0
} else if (amount == "0.00") { } else if (amount == "0.00") {
enableSave = false enableSave = false
} }
@@ -204,11 +268,18 @@ fun AddExpenseBottomSheet(
onBackspaceClick = { onBackspaceClick = {
if (amount == "0.00") return@NumberKeyboard if (amount == "0.00") return@NumberKeyboard
amount = amount.safeSubstring(0, amount.length - 1) amount = amount.safeSubstring(0, amount.length - 1)
enableSave = amount.isDoubleTwoDigitsAboveZero() enableSave = amount.isDoubleTwoDigitsOrEquation()
equationResult = evaluate(amount)
enableSave = amount.isDoubleTwoDigitsOrEquation() && equationResult > 0
}, },
onSave = { )
SaveButton(
modifier = Modifier.fillMaxWidth(),
enabled = enableSave,
onClick = {
val expenseToSave = Expense( val expenseToSave = Expense(
amount = amount.toDouble(), amount = equationResult,
currency = currency, currency = currency,
note = note, note = note,
datetime = datetime.toString(), datetime = datetime.toString(),
@@ -219,11 +290,11 @@ fun AddExpenseBottomSheet(
if (expenseDtoToEdit == null) expenseToSave if (expenseDtoToEdit == null) expenseToSave
else expenseToSave.copy(id = expenseDtoToEdit.expense.id) else expenseToSave.copy(id = expenseDtoToEdit.expense.id)
) )
}, enableSave = enableSave })
) }
}
}
}
}
if (showDateTimePicker) { if (showDateTimePicker) {
DateTimePicker(datetime, onChange = { newDateTime -> DateTimePicker(datetime, onChange = { newDateTime ->
@@ -264,12 +335,50 @@ fun String.safeSubstring(start: Int, end: Int): String {
} }
} }
fun String.isDoubleTwoDigitsAboveZero(): Boolean { private fun evaluate(equation: String): Double {
return this.toDoubleOrNull() != null && this.matches(Regex("^\\d*(\\.\\d{0,2})?$")) && this.toDouble() > 0 if (equation.isEmpty()) return 0.0
val operatorIndex = equation.indexOfFirstIndexed { i, c ->
i != 0 && c in "+-*/"
}
if (operatorIndex == -1) return equation.toDouble()
val leftString = equation.substring(0, operatorIndex)
val rightString = equation.substring(operatorIndex + 1)
if (leftString.isEmpty() || rightString.isEmpty()) return 0.0
val left = leftString.toDouble()
val right = rightString.toDouble()
return when (equation[operatorIndex]) {
'+' -> left + right
'-' -> left - right
'*' -> left * right
'/' -> left / right
else -> 0.0
}
}
private inline fun String.indexOfFirstIndexed(predicate: (index: Int, Char) -> Boolean): Int {
for (i in indices) {
if (predicate(i, this[i])) return i
}
return -1
}
private fun String.isDoubleTwoDigitsOrEquation(): Boolean {
return this != "0.00" && this.matches(Regex("^(-?(0\\.?|0\\.\\d{1,2}|[1-9]\\d*(\\.\\d{0,2})?))([+\\/*-](0\\.?|0\\.\\d{1,2}|[1-9]\\d*(\\.\\d{0,2})?)?)?$"))
} }
@Composable @Composable
fun NoteInput(note: String, onTextChange: (String) -> Unit, modifier: Modifier = Modifier, focusRequester: FocusRequester) { fun NoteInput(
note: String,
onTextChange: (String) -> Unit,
modifier: Modifier = Modifier,
focusRequester: FocusRequester
) {
var text by remember { mutableStateOf(note) } var text by remember { mutableStateOf(note) }
OutlinedTextField( OutlinedTextField(
@@ -292,33 +401,36 @@ fun NoteInput(note: String, onTextChange: (String) -> Unit, modifier: Modifier =
@Composable @Composable
fun CurrencyButton(modifier: Modifier = Modifier, onClick: () -> Unit, text: String) { fun CurrencyButton(modifier: Modifier = Modifier, onClick: () -> Unit, text: String) {
OutlinedButton(onClick = onClick, modifier = modifier) { Button(onClick = onClick, modifier = modifier, shape = MaterialTheme.shapes.medium) {
Text(text) Text(text)
} }
} }
@Composable @Composable
fun CategoryButton(onClick: () -> Unit, category: Category, modifier: Modifier = Modifier) { fun CategoryButton(onClick: () -> Unit, category: Category, modifier: Modifier = Modifier) {
OutlinedButton( Button(
onClick = onClick, onClick = onClick,
modifier = modifier modifier = modifier,
shape = MaterialTheme.shapes.medium,
colors = ButtonDefaults.buttonColors()
.copy(containerColor = Color(category.color.toColorInt()), contentColor = Color.Black)
) { ) {
Icon( Icon(
modifier = Modifier.padding(end = 10.dp), modifier = Modifier.padding(end = 10.dp),
painter = painterResource(category.icon.resource), painter = painterResource(category.icon.resource),
contentDescription = stringResource(R.string.category), contentDescription = stringResource(R.string.category),
tint = Color(category.color.toColorInt())
) )
Text(category.name, color = Color(category.color.toColorInt())) Text(category.name)
} }
} }
@Composable @Composable
fun SaveButton(enabled: Boolean, onClick: () -> Unit) { fun SaveButton(modifier: Modifier = Modifier, enabled: Boolean, onClick: () -> Unit) {
OutlinedButton( Button(
onClick = onClick, onClick = onClick,
enabled = enabled, enabled = enabled,
modifier = Modifier modifier = modifier,
shape = MaterialTheme.shapes.medium
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Check, imageVector = Icons.Filled.Check,
@@ -332,273 +444,171 @@ fun NumberKeyboard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onNumberClick: (String) -> Unit, onNumberClick: (String) -> Unit,
onBackspaceClick: () -> Unit, onBackspaceClick: () -> Unit,
onSave: () -> Unit, onOperatorClick: (String) -> Unit
enableSave: Boolean
) { ) {
val buttonModifier = Modifier
.padding(4.dp)
.aspectRatio(2f)
Column( Column(
modifier = modifier, modifier = modifier,
verticalArrangement = Arrangement.spacedBy(4.dp) verticalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
keyboard.forEach { row ->
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp) horizontalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
TextButton( row.forEach { key ->
onClick = { onNumberClick("1") }, when (key) {
modifier = buttonModifier.weight(1f) "backspace" -> KeyboardButton(
) { icon = painterResource(drawable.materialsymbols_ic_arrow_left_alt_outlined),
Text("1", fontSize = 20.sp)
}
TextButton(
onClick = { onNumberClick("2") },
modifier = buttonModifier.weight(1f)
) {
Text("2", fontSize = 20.sp)
}
TextButton(
onClick = { onNumberClick("3") },
modifier = buttonModifier.weight(1f)
) {
Text("3", fontSize = 20.sp)
}
TextButton(
onClick = { onNumberClick("") },
modifier = buttonModifier.weight(1f)
) {
Text("+", fontSize = 20.sp)
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
TextButton(
onClick = { onNumberClick("4") },
modifier = buttonModifier.weight(1f)
) {
Text("4", fontSize = 20.sp)
}
TextButton(
onClick = { onNumberClick("5") },
modifier = buttonModifier.weight(1f)
) {
Text("5", fontSize = 20.sp)
}
TextButton(
onClick = { onNumberClick("6") },
modifier = buttonModifier.weight(1f)
) {
Text("6", fontSize = 20.sp)
}
TextButton(
onClick = { onNumberClick("") },
modifier = buttonModifier.weight(1f)
) {
Text("-", fontSize = 20.sp)
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
TextButton(
onClick = { onNumberClick("7") },
modifier = buttonModifier.weight(1f)
) {
Text("7", fontSize = 20.sp)
}
TextButton(
onClick = { onNumberClick("8") },
modifier = buttonModifier.weight(1f)
) {
Text("8", fontSize = 20.sp)
}
TextButton(
onClick = { onNumberClick("9") },
modifier = buttonModifier.weight(1f)
) {
Text("9", fontSize = 20.sp)
}
TextButton(
onClick = { onNumberClick("") },
modifier = buttonModifier.weight(1f)
) {
Text("*", fontSize = 20.sp)
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
TextButton(
onClick = { onNumberClick(".") },
modifier = buttonModifier.weight(1f)
) {
Text(".", fontSize = 20.sp)
}
TextButton(
onClick = { onNumberClick("0") },
modifier = buttonModifier.weight(1f)
) {
Text("0", fontSize = 20.sp)
}
TextButton(
onClick = onBackspaceClick, onClick = onBackspaceClick,
modifier = buttonModifier.weight(1f) modifier = Modifier.weight(1f),
) { containerColor = MaterialTheme.colorScheme.primary
Icon( )
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.backspace) "+", "/", "-", "*" -> KeyboardButton(
text = key,
onClick = { onOperatorClick(key) },
modifier = Modifier.weight(1f),
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
)
else -> KeyboardButton(
text = key,
onClick = { onNumberClick(key) },
modifier = Modifier.weight(1f),
containerColor = MaterialTheme.colorScheme.secondary,
contentColor = MaterialTheme.colorScheme.onSecondary
) )
} }
TextButton( }
onClick = onSave,
modifier = buttonModifier.weight(1f),
enabled = enableSave
) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = stringResource(R.string.backspace)
)
} }
} }
} }
} }
@Composable
fun KeyboardButton(
text: String? = null,
icon: Painter? = null,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
containerColor: Color = MaterialTheme.colorScheme.primary,
contentColor: Color = MaterialTheme.colorScheme.onPrimary
) {
Button(
onClick = onClick,
shape = MaterialTheme.shapes.medium,
modifier = modifier
.padding(4.dp)
.aspectRatio(2.5f),
enabled = enabled,
colors = ButtonDefaults.buttonColors(
containerColor = containerColor,
contentColor = contentColor
)
) {
when {
text != null -> Text(
text,
style = MaterialTheme.typography.titleMedium
)
//@SuppressLint("CoroutineCreationDuringComposition") icon != null -> Icon(painter = icon, contentDescription = null)
//@RequiresApi(Build.VERSION_CODES.O) }
//@OptIn(ExperimentalMaterial3Api::class) }
//@Preview }
//@Composable
//fun PreviewLight() { val keyboard = listOf(
// val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) listOf("+", "-", "*", "/"),
// CoroutineScope(Dispatchers.IO).launch { listOf("1", "2", "3"),
// sheetState.show() listOf("4", "5", "6"),
// } listOf("7", "8", "9"),
// listOf(".", "0", "backspace")
// TripMoneyTheme { )
// AddExpenseBottomSheet(
// {}, {}, null, sheetState,
// categories = listOf( @SuppressLint("CoroutineCreationDuringComposition")
// Category( @RequiresApi(Build.VERSION_CODES.O)
// name = "Hotel", @OptIn(ExperimentalMaterial3Api::class)
// icon = cc.n0th1ng.tripmoney.utils.Icons.HOTEL, @AllPreviews
// color = "#B3E5FC" @Composable
// ), fun PreviewAddExpenseDisabled() {
// Category( val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
// name = "Jedzenie", CoroutineScope(Dispatchers.IO).launch {
// icon = cc.n0th1ng.tripmoney.utils.Icons.RESTAURANT, sheetState.show()
// color = "#C8E6C9" }
// ),
// Category( TripMoneyTheme {
// name = "Transport", AddExpenseBottomSheet(
// icon = cc.n0th1ng.tripmoney.utils.Icons.FLIGHT, onSave = {},
// color = "#FFCDD2" onDismiss = {},
// ), expenseDtoToEdit = null,
// Category( state = sheetState,
// name = "Rozrywka", currentTrip = Trip(1, "Trip", "2020-01-01", Currencies.entries.random().name),
// icon = cc.n0th1ng.tripmoney.utils.Icons.ATTRACTION, categories = categoriesToPreview
// color = "#FFF9C4" )
// ), }
// Category(
// name = "Zakupy", }
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
// color = "#E1BEE7" @SuppressLint("CoroutineCreationDuringComposition")
// ), @RequiresApi(Build.VERSION_CODES.O)
// Category( @OptIn(ExperimentalMaterial3Api::class)
// name = "Zakupy1", @AllPreviews
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES, @Composable
// color = "#D7CCC8" fun PreviewAddExpenseEnabled() {
// ), val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
// Category( CoroutineScope(Dispatchers.IO).launch {
// name = "Zakupy2", sheetState.show()
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES, }
// color = "#BBDEFB"
// ), TripMoneyTheme {
// Category( AddExpenseBottomSheet(
// name = "Zakupy3", onSave = {},
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES, onDismiss = {},
// color = "#D1C4E9" expenseDtoToEdit = ExpenseDto(
// ), Expense(
// Category( amount = 10.31,
// name = "Zakupy4", currency = "PLN",
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES, note = "some note",
// color = "#DCEDC8" datetime = "2025-11-30T10:16:26.939",
// ), categoryId = 1,
// ) tripId = 1
// ) ), category = categoriesToPreview.get(0), Trip(1, "Włochy", "2025-01-02", "PLN")
// } ),
//} state = sheetState,
// currentTrip = Trip(1, "Trip", "2020-01-01", Currencies.entries.random().name),
//@SuppressLint("CoroutineCreationDuringComposition") categories = categoriesToPreview
//@RequiresApi(Build.VERSION_CODES.O) )
//@OptIn(ExperimentalMaterial3Api::class) }
//@Preview
//@Composable }
//fun PreviewDark() {
// val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val categoriesToPreview = listOf(
// CoroutineScope(Dispatchers.IO).launch { Category(
// sheetState.show() name = "Hotel",
// } icon = cc.n0th1ng.tripmoney.utils.Icons.HOTEL,
// color = colors.random()
// TripMoneyTheme(darkTheme = true) { ),
// AddExpenseBottomSheet( Category(
// {}, {}, null, sheetState, name = "Jedzenie",
// categories = listOf( icon = cc.n0th1ng.tripmoney.utils.Icons.RESTAURANT,
// Category( color = colors.random()
// name = "Hotel", ),
// icon = cc.n0th1ng.tripmoney.utils.Icons.HOTEL, Category(
// color = "#B3E5FC" name = "Transport",
// ), icon = cc.n0th1ng.tripmoney.utils.Icons.FLIGHT,
// Category( color = colors.random()
// name = "Jedzenie", ),
// icon = cc.n0th1ng.tripmoney.utils.Icons.RESTAURANT, Category(
// color = "#C8E6C9" name = "Rozrywka",
// ), icon = cc.n0th1ng.tripmoney.utils.Icons.ATTRACTION,
// Category( color = colors.random()
// name = "Transport", ),
// icon = cc.n0th1ng.tripmoney.utils.Icons.FLIGHT, Category(
// color = "#FFCDD2" name = "Zakupy",
// ), icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
// Category( color = colors.random()
// name = "Rozrywka", ),
// icon = cc.n0th1ng.tripmoney.utils.Icons.ATTRACTION, )
// color = "#FFF9C4"
// ),
// Category(
// name = "Zakupy",
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
// color = "#E1BEE7"
// ),
// Category(
// name = "Zakupy1",
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
// color = "#D7CCC8"
// ),
// Category(
// name = "Zakupy2",
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
// color = "#BBDEFB"
// ),
// Category(
// name = "Zakupy3",
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
// color = "#D1C4E9"
// ),
// Category(
// name = "Zakupy4",
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
// color = "#DCEDC8"
// ),
// )
// )
// }
//}

View File

@@ -50,9 +50,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.paging.PagingData import androidx.paging.PagingData
@@ -126,7 +124,6 @@ fun ListExpenseScreen(
sumMap.clear() sumMap.clear()
sumMap.putAll(newSums) sumMap.putAll(newSums)
} }
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,

View File

@@ -1,5 +1,6 @@
package cc.n0th1ng.tripmoney.screens.settings package cc.n0th1ng.tripmoney.screens.settings
import android.content.Intent
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.annotation.StringRes import androidx.annotation.StringRes
@@ -12,9 +13,12 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -27,15 +31,31 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.content.FileProvider
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import cc.n0th1ng.tripmoney.R.* import cc.n0th1ng.tripmoney.R.*
import cc.n0th1ng.tripmoney.data.repository.AppTheme import cc.n0th1ng.tripmoney.data.repository.AppTheme
import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
import cc.n0th1ng.tripmoney.utils.AllPreviews
import cc.n0th1ng.tripmoney.utils.Currencies import cc.n0th1ng.tripmoney.utils.Currencies
import cc.n0th1ng.tripmoney.utils.Icons
import cc.n0th1ng.tripmoney.utils.saveCsv
import cc.n0th1ng.tripmoney.utils.shareCsv
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
import com.composables.icons.materialsymbols.outlined.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.nio.file.Files
@RequiresApi(Build.VERSION_CODES.S) @RequiresApi(Build.VERSION_CODES.S)
@Composable @Composable
@@ -43,6 +63,48 @@ fun SettingsScreen() {
val settingsViewModel: SettingsViewModel = hiltViewModel() val settingsViewModel: SettingsViewModel = hiltViewModel()
val currentTheme by settingsViewModel.theme.collectAsState() val currentTheme by settingsViewModel.theme.collectAsState()
val currentDefaultCurrency by settingsViewModel.defaultCurrency.collectAsState() val currentDefaultCurrency by settingsViewModel.defaultCurrency.collectAsState()
val currentTripId by settingsViewModel.currentTrip.collectAsState()
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
val tripViewModel: TripViewModel = hiltViewModel()
val currentTrip = tripViewModel.getTrip(currentTripId)
val context = LocalContext.current
val tripName = currentTrip?.name ?: ""
val scope = rememberCoroutineScope()
SettingsScreen(
currentDefaultCurrency = currentDefaultCurrency,
currentTheme = currentTheme,
onThemeSave = { settingsViewModel.setTheme(it) },
onCurrencySave = { settingsViewModel.setDefaultCurrency(it) },
tripName = tripName,
onExportToCsv = {
scope.launch {
try {
val safeTripName = tripName.replace(Regex("[^a-zA-Z0-9_]"), "_")
val file = File(context.cacheDir, "$safeTripName.csv")
expenseAndCategoryViewModel.generateCSVToFile(currentTripId, file)
shareCsv(context, file)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
)
}
@RequiresApi(Build.VERSION_CODES.S)
@Composable
fun SettingsScreen(
currentDefaultCurrency: Currencies,
currentTheme: AppTheme,
onThemeSave: (AppTheme) -> Unit,
onCurrencySave: (Currencies) -> Unit,
tripName: String,
onExportToCsv: () -> Unit,
) {
Scaffold { padding ->
var showThemeDialog by remember { mutableStateOf(false) } var showThemeDialog by remember { mutableStateOf(false) }
var showCurrencyDialog by remember { mutableStateOf(false) } var showCurrencyDialog by remember { mutableStateOf(false) }
Column( Column(
@@ -51,30 +113,43 @@ fun SettingsScreen() {
.padding(15.dp), .padding(15.dp),
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
Card {
SettingsListItem(onClick = { showThemeDialog = true }, stringResource(string.theme)) {
Text(
if (isSystemInDarkTheme()) stringResource(string.dark_theme) else stringResource(
string.light_theme
)
)
}
}
Card {
SettingsListItem( SettingsListItem(
onClick = { showCurrencyDialog = true }, onClick = { showCurrencyDialog = true },
stringResource(string.default_currency) headlineText = stringResource(string.default_currency),
) { supportingText = currentDefaultCurrency.name,
Text(currentDefaultCurrency.name) iconResource = R.drawable.materialsymbols_ic_currency_yen_outlined
} )
SettingsCard(string.theme) {
SettingsListItem(
onClick = { showThemeDialog = true },
stringResource(string.theme),
supportingText = if (isSystemInDarkTheme()) stringResource(string.dark_theme) else stringResource(
string.light_theme
),
iconResource = R.drawable.materialsymbols_ic_format_paint_outlined
)
SettingsListItem(
onClick = { },
"Pallete",
supportingText = if (isSystemInDarkTheme()) stringResource(string.dark_theme) else stringResource(
string.light_theme
),
iconResource = R.drawable.materialsymbols_ic_palette_outlined
)
} }
SettingsListItem(
onClick = onExportToCsv,
stringResource(string.export_to_csv),
supportingText = "Save expenses from %s to a file".format(tripName),
iconResource = R.drawable.materialsymbols_ic_csv_outlined
)
if (showThemeDialog) { if (showThemeDialog) {
ThemeSelectionDialog( ThemeSelectionDialog(
onDismiss = { showThemeDialog = false }, onDismiss = { showThemeDialog = false },
onThemeSelected = { theme -> onThemeSelected = { theme ->
settingsViewModel.setTheme(theme) onThemeSave(theme)
showThemeDialog = false showThemeDialog = false
}, },
selected = currentTheme selected = currentTheme
@@ -82,11 +157,15 @@ fun SettingsScreen() {
} }
if (showCurrencyDialog) { if (showCurrencyDialog) {
CurrencySelectionDialog(onDismiss = {showCurrencyDialog = false}, onCurrencySelected = { CurrencySelectionDialog(
currencyString -> onDismiss = { showCurrencyDialog = false },
settingsViewModel.setDefaultCurrency(Currencies.valueOf(currencyString)) onCurrencySelected = { currencyString ->
onCurrencySave(Currencies.valueOf(currencyString))
showCurrencyDialog = false showCurrencyDialog = false
}, currentDefaultCurrency.name) },
currentDefaultCurrency.name
)
}
} }
} }
} }
@@ -97,7 +176,7 @@ fun SettingsCard(@StringRes title: Int = -1, content: @Composable () -> Unit) {
if (title != -1) { if (title != -1) {
Text( Text(
text = stringResource(title), text = stringResource(title),
fontSize = 13.sp, style = MaterialTheme.typography.titleSmall,
modifier = Modifier modifier = Modifier
.padding(start = 15.dp, top = 15.dp, end = 15.dp) .padding(start = 15.dp, top = 15.dp, end = 15.dp)
.alpha(0.6f) .alpha(0.6f)
@@ -112,16 +191,22 @@ fun SettingsListItem(
onClick: () -> Unit, onClick: () -> Unit,
headlineText: String, headlineText: String,
trailingContent: @Composable () -> Unit = {}, trailingContent: @Composable () -> Unit = {},
supportingContent: @Composable () -> Unit supportingText: String,
iconResource: Int
) { ) {
Card {
ListItem( ListItem(
leadingContent = {
Icon(painter = painterResource(iconResource), contentDescription = null)
},
colors = ListItemDefaults.colors(containerColor = Color.Transparent), colors = ListItemDefaults.colors(containerColor = Color.Transparent),
headlineContent = { Text(headlineText) }, headlineContent = { Text(headlineText) },
supportingContent = supportingContent, supportingContent = { Text(supportingText) },
trailingContent = trailingContent, trailingContent = trailingContent,
modifier = Modifier modifier = Modifier
.clickable(true, onClick = onClick) .clickable(true, onClick = onClick)
) )
}
} }
@Composable @Composable
@@ -166,3 +251,34 @@ fun ThemeSelectionDialog(
confirmButton = {} confirmButton = {}
) )
} }
@RequiresApi(Build.VERSION_CODES.S)
@AllPreviews
@Composable
fun PreviewSettingsScreen() {
TripMoneyTheme {
SettingsScreen(Currencies.entries.random(), AppTheme.entries.random(), {}, {}, "Włochy", {})
}
}
@RequiresApi(Build.VERSION_CODES.S)
@AllPreviews
@Composable
fun PreviewThemeSelectionDialog() {
TripMoneyTheme {
ThemeSelectionDialog(onDismiss = {}, onThemeSelected = {}, AppTheme.SYSTEM)
}
}
@RequiresApi(Build.VERSION_CODES.S)
@AllPreviews
@Composable
fun PreviewCurrencySelectionDialog() {
TripMoneyTheme {
CurrencySelectionDialog(
onDismiss = {},
onCurrencySelected = {},
selected = Currencies.entries.random().name
)
}
}

View File

@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -25,38 +24,97 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory
import cc.n0th1ng.tripmoney.data.entity.Category import cc.n0th1ng.tripmoney.data.entity.Category
import cc.n0th1ng.tripmoney.data.entity.Trip
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
import cc.n0th1ng.tripmoney.utils.AllPreviews
import cc.n0th1ng.tripmoney.utils.Currencies import cc.n0th1ng.tripmoney.utils.Currencies
import cc.n0th1ng.tripmoney.utils.Icons import cc.n0th1ng.tripmoney.utils.Icons
import cc.n0th1ng.tripmoney.utils.colors import cc.n0th1ng.tripmoney.utils.colors
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
import com.composables.icons.materialsymbols.outlined.R
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
@Composable @Composable
fun StatisticsScreen() { fun StatisticsScreen() {
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel() val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
val settingsViewModel: SettingsViewModel = hiltViewModel() val settingsViewModel: SettingsViewModel = hiltViewModel()
val currentTrip by settingsViewModel.currentTrip.collectAsState() val tripViewModel: TripViewModel = hiltViewModel()
val summaryPerCategoryList by expenseAndCategoryViewModel.getSummaryPerCategory(currentTrip) val currentTripId by settingsViewModel.currentTrip.collectAsState()
val currentTrip = tripViewModel.getTrip(currentTripId)
val summaryPerCategoryList by expenseAndCategoryViewModel.getSummaryPerCategory(currentTripId)
.collectAsState(emptyList()) .collectAsState(emptyList())
val summaryAmount by expenseAndCategoryViewModel.getSummaryAmount(currentTripId)
.collectAsState(0.0)
StatisticsScreen(
summaryPerCategoryList,
summaryAmount,
Currencies.valueOf(currentTrip?.currency ?: Currencies.default().name)
)
}
Column(modifier = Modifier.padding(10.dp)) { @RequiresApi(Build.VERSION_CODES.O)
@Composable
fun StatisticsScreen(
summaryPerCategoryList: List<SummaryPerCategory>,
summaryAmount: Double,
tripCurrency: Currencies
) {
Column(
modifier = Modifier
.padding(10.dp)
.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Summary(summaryAmount, tripCurrency.name)
SummaryPerCategoryCard(summaryPerCategoryList) SummaryPerCategoryCard(summaryPerCategoryList)
} }
} }
@Composable
fun Summary(summaryAmount: Double, currency: String) {
Card(
modifier = Modifier
.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(15.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column {
Text(stringResource(cc.n0th1ng.tripmoney.R.string.total_expenses), style = MaterialTheme.typography.titleSmall)
Text(
"%.2f %s".format(summaryAmount, currency),
style = MaterialTheme.typography.headlineLarge
)
}
Row(
horizontalArrangement = Arrangement.Center
)
{
Icon(
painter = painterResource(R.drawable.materialsymbols_ic_payment_arrow_down_outlined),
contentDescription = null,
modifier = Modifier.size(45.dp)
)
}
}
}
}
@Composable @Composable
fun SummaryPerCategoryCard(summaryPerCategoryList: List<SummaryPerCategory>) { fun SummaryPerCategoryCard(summaryPerCategoryList: List<SummaryPerCategory>) {
@@ -65,7 +123,6 @@ fun SummaryPerCategoryCard(summaryPerCategoryList: List<SummaryPerCategory>) {
modifier = Modifier.padding(15.dp), modifier = Modifier.padding(15.dp),
verticalArrangement = Arrangement.spacedBy(5.dp) verticalArrangement = Arrangement.spacedBy(5.dp)
) { ) {
// Text(text = "Summary", fontWeight = FontWeight.Bold, fontSize = 25.sp)
summaryPerCategoryList.forEach { summaryPerCategoryList.forEach {
CategoryCard( CategoryCard(
summaryPerCategory = it, modifier = Modifier summaryPerCategory = it, modifier = Modifier
@@ -80,21 +137,35 @@ fun SummaryPerCategoryCard(summaryPerCategoryList: List<SummaryPerCategory>) {
fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCategory) { fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCategory) {
Column(modifier = modifier) { Column(modifier = modifier) {
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) { Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(5.dp)
) {
Icon( Icon(
painter = painterResource(summaryPerCategory.category.icon.resource), painter = painterResource(summaryPerCategory.category.icon.resource),
contentDescription = null, contentDescription = null,
modifier = Modifier.size(MaterialTheme.typography.bodyLarge.fontSize.value.dp), modifier = Modifier.size(MaterialTheme.typography.bodyLarge.fontSize.value.dp),
tint = Color(summaryPerCategory.category.color.toColorInt()) tint = Color(summaryPerCategory.category.color.toColorInt())
) )
Text("%s".format(summaryPerCategory.category.name, (summaryPerCategory.percent * 100).toInt()), Text(
style = MaterialTheme.typography.bodyLarge, color = Color(summaryPerCategory.category.color.toColorInt())) "%s".format(
summaryPerCategory.category.name,
(summaryPerCategory.percent * 100).toInt()
),
style = MaterialTheme.typography.bodyLarge,
color = Color(summaryPerCategory.category.color.toColorInt())
)
} }
Text("%.2f ${summaryPerCategory.currency}".format(summaryPerCategory.amount), Text(
style = MaterialTheme.typography.bodyMedium) "%.2f ${summaryPerCategory.currency}".format(summaryPerCategory.amount),
style = MaterialTheme.typography.bodyMedium
)
} }
Row(horizontalArrangement = Arrangement.spacedBy(5.dp), verticalAlignment = Alignment.CenterVertically){ Row(
horizontalArrangement = Arrangement.spacedBy(5.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box( Box(
modifier = Modifier modifier = Modifier
.height(40.dp) .height(40.dp)
@@ -102,31 +173,34 @@ fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCa
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.primary) .background(MaterialTheme.colorScheme.primary)
) { ) {
Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxSize().padding(11.dp)) { Column(
Text("%d%%".format((summaryPerCategory.percent * 100).toInt()), verticalArrangement = Arrangement.Center,
style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onPrimary) modifier = Modifier
.fillMaxSize()
.padding(11.dp)
) {
Text(
"%d%%".format((summaryPerCategory.percent * 100).toInt()),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onPrimary
)
} }
} }
// Text("%d%%".format((summaryPerCategory.percent * 100).toInt()),
// style = MaterialTheme.typography.labelSmall)
} }
} }
} }
@Preview @RequiresApi(Build.VERSION_CODES.O)
@AllPreviews
@Composable @Composable
fun previewLight() { fun Preview() {
TripMoneyTheme { TripMoneyTheme {
SummaryPerCategoryCard(summaryPerCategoryList) StatisticsScreen(
} summaryPerCategoryList,
} summaryAmount = 125.24,
Currencies.entries.random()
@Preview )
@Composable
fun previewDark() {
TripMoneyTheme(darkTheme = true) {
SummaryPerCategoryCard(summaryPerCategoryList)
} }
} }
@@ -143,8 +217,8 @@ val categories = listOf(
val summaryPerCategoryList = listOf( val summaryPerCategoryList = listOf(
SummaryPerCategory(categories[0], 50.0, 1f, Currencies.PLN), SummaryPerCategory(categories[0], 50.0, 1f, Currencies.PLN),
SummaryPerCategory(categories[1], 120.0, 0.3f, Currencies.PLN), SummaryPerCategory(categories[1], 120.0, 0.3f, Currencies.PLN),
SummaryPerCategory(categories[4], 120.0, 0.3f, Currencies.PLN),
SummaryPerCategory(categories[2], 80.0, 0.2f, Currencies.PLN), SummaryPerCategory(categories[2], 80.0, 0.2f, Currencies.PLN),
SummaryPerCategory(categories[3], 50.0, 0.1f, Currencies.PLN), SummaryPerCategory(categories[3], 50.0, 0.1f, Currencies.PLN),
SummaryPerCategory(categories[4], 120.0, 0.3f, Currencies.PLN),
SummaryPerCategory(categories[5], 50.0, 0.0001f, Currencies.PLN), SummaryPerCategory(categories[5], 50.0, 0.0001f, Currencies.PLN),
) )

View File

@@ -1,5 +1,6 @@
package cc.n0th1ng.tripmoney.screens.trippicker package cc.n0th1ng.tripmoney.screens.trippicker
import android.annotation.SuppressLint
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -17,12 +18,14 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Shapes import androidx.compose.material3.Shapes
import androidx.compose.material3.SheetState import androidx.compose.material3.SheetState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -44,9 +47,14 @@ import cc.n0th1ng.tripmoney.data.entity.Trip
import cc.n0th1ng.tripmoney.screens.addexpense.CurrencyButton import cc.n0th1ng.tripmoney.screens.addexpense.CurrencyButton
import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog
import cc.n0th1ng.tripmoney.screens.listexpense.DatePicker import cc.n0th1ng.tripmoney.screens.listexpense.DatePicker
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
import cc.n0th1ng.tripmoney.utils.AllPreviews
import cc.n0th1ng.tripmoney.utils.Currencies import cc.n0th1ng.tripmoney.utils.Currencies
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
import io.ktor.http.hostIsIp import io.ktor.http.hostIsIp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.time.LocalDate import java.time.LocalDate
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@@ -59,6 +67,29 @@ fun AddTripBottomSheet(
tripToEdit: Trip?, tripToEdit: Trip?,
sheetState: SheetState sheetState: SheetState
) { ) {
val settingsViewModel: SettingsViewModel = hiltViewModel()
val defaultCurrency by settingsViewModel.defaultCurrency.collectAsState()
AddTripBottomSheet(
onDismiss = onDismiss,
onSave = onSave,
tripToEdit = tripToEdit,
sheetState = sheetState,
defaultCurrency = defaultCurrency
)
}
@RequiresApi(Build.VERSION_CODES.O)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddTripBottomSheet(
onDismiss: () -> Unit,
onSave: (Trip) -> Unit,
tripToEdit: Trip?,
sheetState: SheetState,
defaultCurrency: Currencies
) {
var name by remember { mutableStateOf(tripToEdit?.name ?: "") } var name by remember { mutableStateOf(tripToEdit?.name ?: "") }
var startDate by remember { var startDate by remember {
@@ -66,8 +97,7 @@ fun AddTripBottomSheet(
LocalDate.parse(tripToEdit?.startDate ?: LocalDate.now().toString()) LocalDate.parse(tripToEdit?.startDate ?: LocalDate.now().toString())
) )
} }
val settingsViewModel: SettingsViewModel = hiltViewModel()
val defaultCurrency by settingsViewModel.defaultCurrency.collectAsState()
var showCurrencyDialog by remember { mutableStateOf(false) } var showCurrencyDialog by remember { mutableStateOf(false) }
var showDatePicker by remember { mutableStateOf(false) } var showDatePicker by remember { mutableStateOf(false) }
var currency by remember { mutableStateOf(tripToEdit?.currency ?: defaultCurrency.name) } var currency by remember { mutableStateOf(tripToEdit?.currency ?: defaultCurrency.name) }
@@ -87,7 +117,7 @@ fun AddTripBottomSheet(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(start = 15.dp), .padding(start = 15.dp),
text = stringResource(if(tripToEdit == null) R.string.add_trip else R.string.edit_trip), text = stringResource(if (tripToEdit == null) R.string.add_trip else R.string.edit_trip),
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 35.sp, fontSize = 35.sp,
textAlign = TextAlign.Start textAlign = TextAlign.Start
@@ -101,32 +131,35 @@ fun AddTripBottomSheet(
modifier = Modifier.fillMaxWidth(0.9f), modifier = Modifier.fillMaxWidth(0.9f),
horizontalArrangement = Arrangement.spacedBy(10.dp) horizontalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
CurrencyButton( Button(
modifier = Modifier
.weight(1f)
.fillMaxWidth(1f),
onClick = { showCurrencyDialog = true }, text = currency
)
OutlinedButton(
modifier = Modifier modifier = Modifier
.fillMaxWidth(1f) .fillMaxWidth(1f)
.weight(1f), .weight(1f),
shape = MaterialTheme.shapes.medium,
onClick = { showDatePicker = true }) { onClick = { showDatePicker = true }) {
Text( Text(
text = startDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")), text = startDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")),
fontSize = 17.sp fontSize = 17.sp
) )
} }
CurrencyButton(
modifier = Modifier
.weight(1f)
.fillMaxWidth(1f),
onClick = { showCurrencyDialog = true }, text = currency
)
} }
Button( Button(
modifier = Modifier.fillMaxWidth(0.9f), modifier = Modifier.fillMaxWidth(0.9f),
enabled = enableSave, enabled = enableSave,
shape = MaterialTheme.shapes.medium,
onClick = { onClick = {
val trip = Trip(name = name, startDate = startDate.toString(), currency = currency) val trip =
Trip(name = name, startDate = startDate.toString(), currency = currency)
onSave(if(tripToEdit == null) trip else trip.copy(id = tripToEdit.id)) onSave(if (tripToEdit == null) trip else trip.copy(id = tripToEdit.id))
}) { }) {
Icon( Icon(
imageVector = Icons.Filled.Check, imageVector = Icons.Filled.Check,
@@ -167,3 +200,40 @@ fun NameInput(name: String, onTextChange: (String) -> Unit) {
}, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
) )
} }
@RequiresApi(Build.VERSION_CODES.O)
@SuppressLint("CoroutineCreationDuringComposition")
@OptIn(ExperimentalMaterial3Api::class)
@AllPreviews
@Composable
fun PreviewAddTripBottomSheet() {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
CoroutineScope(Dispatchers.IO).launch {
sheetState.show()
}
TripMoneyTheme {
AddTripBottomSheet({}, {}, null, sheetState, defaultCurrency = Currencies.entries.random())
}
}
@RequiresApi(Build.VERSION_CODES.O)
@SuppressLint("CoroutineCreationDuringComposition")
@OptIn(ExperimentalMaterial3Api::class)
@AllPreviews
@Composable
fun PreviewAddTripBottomSheetEditTrip() {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
CoroutineScope(Dispatchers.IO).launch {
sheetState.show()
}
TripMoneyTheme {
AddTripBottomSheet(
{},
{},
Trip(1, "Włochy", "2025-01-02", "PLN"),
sheetState,
defaultCurrency = Currencies.entries.random()
)
}
}

View File

@@ -2,7 +2,18 @@ package cc.n0th1ng.tripmoney.utils
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.content.res.Configuration.UI_MODE_TYPE_NORMAL import android.content.res.Configuration.UI_MODE_TYPE_NORMAL
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import cc.n0th1ng.tripmoney.screens.addexpense.PreviewAddExpenseDisabled
import cc.n0th1ng.tripmoney.screens.addexpense.PreviewAddExpenseEnabled
import cc.n0th1ng.tripmoney.screens.settings.PreviewSettingsScreen
import cc.n0th1ng.tripmoney.screens.settings.SettingsScreen
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
@Preview(name = "Light") @Preview(name = "Light")
@Preview(name = "Dark", uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL) @Preview(name = "Dark", uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL)

View File

@@ -0,0 +1,30 @@
package cc.n0th1ng.tripmoney.utils
import android.content.Context
import android.content.Intent
import androidx.core.content.FileProvider
import java.io.File
fun saveCsv(context: Context, fileName: String, content: String): File {
val file = File(context.cacheDir, "$fileName.csv")
file.writeText(content)
return file
}
fun shareCsv(context: Context, file: File) {
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.provider",
file
)
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/csv"
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(
Intent.createChooser(intent, "Share CSV")
)
}

View File

@@ -18,12 +18,17 @@ import cc.n0th1ng.tripmoney.data.repository.TripRepository
import cc.n0th1ng.tripmoney.utils.Currencies import cc.n0th1ng.tripmoney.utils.Currencies
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.apache.commons.csv.CSVFormat
import org.apache.commons.csv.CSVPrinter
import java.io.File
import java.time.LocalDateTime import java.time.LocalDateTime
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
open class ExpenseAndCategoryViewModel @Inject constructor( open class ExpenseAndCategoryViewModel @Inject constructor(
private val expenseRepo: ExpenseRepository, private val expenseRepo: ExpenseRepository,
@@ -55,12 +60,39 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
} }
} }
@RequiresApi(Build.VERSION_CODES.O)
suspend fun generateCSVToFile(tripId: Int, file: File) {
file.writer().use { writer ->
CSVPrinter(
writer,
CSVFormat.DEFAULT.withHeader("date", "category", "currency", "amount")
).use { printer ->
expenseRepo.getExpenses(tripId).first().forEach { expenseDto ->
printer.printRecord(
expenseDto.expense.datetime,
expenseDto.category.name,
expenseDto.expense.currency,
expenseDto.expense.amount
)
}
}
}
}
@RequiresApi(Build.VERSION_CODES.O)
fun getSummaryAmount(tripId: Int): Flow<Double> {
return getExpensesWithConvertedAmounts(tripId).map { list ->
list.sumOf { it.convertedAmount }
}
}
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
fun getSummaryPerCategory(tripId: Int): Flow<List<SummaryPerCategory>> { fun getSummaryPerCategory(tripId: Int): Flow<List<SummaryPerCategory>> {
val tripCurrency = tripRepo.getTrip(tripId)?.currency ?: Currencies.default().name val tripCurrency = tripRepo.getTrip(tripId)?.currency ?: Currencies.default().name
return getExpensesWithConvertedAmounts(tripId) return getExpensesWithConvertedAmounts(tripId)
.map { list -> .map { list ->
// Compute summary
val sumOfAll = list.sumOf { it.convertedAmount } val sumOfAll = list.sumOf { it.convertedAmount }
list.groupBy { it.expenseDto.category } list.groupBy { it.expenseDto.category }
.map { (category, expenses) -> .map { (category, expenses) ->
@@ -84,7 +116,7 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
val convertedAmount = val convertedAmount =
if (expenseDto.expense.currency != expenseDto.trip.currency) { if (expenseDto.expense.currency != expenseDto.trip.currency) {
runBlocking { runBlocking {
expenseDto.toExpenseDtoWithConvertedAmount() expenseDto.convertedAmount()
} }
} else { } else {
expenseDto.expense.amount expenseDto.expense.amount
@@ -102,7 +134,7 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
val convertedAmount = val convertedAmount =
if (expenseDto.expense.currency != expenseDto.trip.currency) { if (expenseDto.expense.currency != expenseDto.trip.currency) {
runBlocking { runBlocking {
expenseDto.toExpenseDtoWithConvertedAmount() expenseDto.convertedAmount()
} }
} else { } else {
expenseDto.expense.amount expenseDto.expense.amount
@@ -120,7 +152,7 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
} }
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
suspend fun ExpenseDto.toExpenseDtoWithConvertedAmount(): Double { suspend fun ExpenseDto.convertedAmount(): Double {
return exchangeRateRepository.getRate( return exchangeRateRepository.getRate(
Currencies.valueOf(this.expense.currency), Currencies.valueOf(this.expense.currency),
Currencies.valueOf(this.trip.currency), Currencies.valueOf(this.trip.currency),

View File

@@ -21,4 +21,10 @@
<string name="edit_trip">Edytuj wycieczkę</string> <string name="edit_trip">Edytuj wycieczkę</string>
<string name="edit_expense">Edytuj wydatek</string> <string name="edit_expense">Edytuj wydatek</string>
<string name="default_currency">Domyślna waluta</string> <string name="default_currency">Domyślna waluta</string>
<string name="total_expenses">Suma wydatków</string>
<string name="settings">Ustawienia</string>
<string name="pick_trip">Wybierz wycieczkę</string>
<string name="list_of_expenses">Lista wydatków</string>
<string name="statistics">Statystyki</string>
<string name="export_to_csv">Eksport do CSV</string>
</resources> </resources>

View File

@@ -21,4 +21,10 @@
<string name="edit_trip">Edit trip</string> <string name="edit_trip">Edit trip</string>
<string name="edit_expense">Edit expense</string> <string name="edit_expense">Edit expense</string>
<string name="default_currency">Default currency</string> <string name="default_currency">Default currency</string>
<string name="total_expenses">Total expenses</string>
<string name="settings">Settings</string>
<string name="pick_trip">Pick trip</string>
<string name="list_of_expenses">List of expenses</string>
<string name="statistics">Statistics</string>
<string name="export_to_csv">Export to CSV</string>
</resources> </resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="cache" path="." />
</paths>